Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: test

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
defaults:
run:
working-directory: plugins/session-intelligence
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
cache-dependency-path: plugins/session-intelligence/package-lock.json
- run: npm ci
- run: npm test
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- gitnexus:start -->
# GitNexus MCP

This project is indexed by GitNexus as **claude-session-intelligence** (668 symbols, 2033 relationships, 55 execution flows).
This project is indexed by GitNexus as **claude-session-intelligence** (698 symbols, 2165 relationships, 57 execution flows).

## Always Start Here

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Always update `MEMORY.md` with a one-line pointer. Don't duplicate prior memory
Disable either surface with `compact.memoryOffload: false` in `~/.claude/session-intelligence.json` or `CLAUDE_COMPACT_MEMORY_OFFLOAD=0`.

#### Tool-Response Archive (post-compact retrieval)
`si-tool-archive.js` snapshots any tool response larger than `toolArchive.thresholdChars` (default 4096) to `${TMPDIR}/claude-tool-archive-<sid>/<tool_use_id>.json`. After `/compact` erases the body from context, replay it with `/si expand <tool_use_id>` instead of re-running the tool.
`si-tool-archive.js` snapshots any tool response larger than `toolArchive.thresholdChars` (default 4096) to `~/.claude/state/claude-tool-archive-<sid>/<tool_use_id>.json`. After `/compact` erases the body from context, replay it with `/si expand <tool_use_id>` instead of re-running the tool.

When to reach for this:
- After `/compact`, when you need the full body of a `Read`/`Bash`/`Grep` result that just got summarised away.
Expand All @@ -71,7 +71,7 @@ Disable with `toolArchive.enabled=false` or `CLAUDE_TOOL_ARCHIVE=0` if you don't
<!-- gitnexus:start -->
# GitNexus MCP

This project is indexed by GitNexus as **claude-session-intelligence** (668 symbols, 2033 relationships, 55 execution flows).
This project is indexed by GitNexus as **claude-session-intelligence** (698 symbols, 2165 relationships, 57 execution flows).

## Always Start Here

Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ Opus 4.7 · claude-session-intelligence · main · (+235,-16) · feat — status

### Quota & reset timers (line 3)

`blockUsage` and `weekUsage` surface Claude's 5-hour block and 7-day rolling quota with reset countdowns, fed from the same `api.anthropic.com/api/oauth/usage` endpoint ccstatusline uses. A detached background worker (`lib/usage-refresh.js`) refreshes a disk cache every 180 s — the statusline hot path only reads the cache, so redraws stay sub-100 ms. On macOS the OAuth token is read from the `Claude Code-credentials` keychain service; elsewhere (or as fallback) from `~/.claude/.credentials.json`. Any failure (no creds, network down, API error) silently renders an empty cell — never blocks.
`blockUsage` and `weekUsage` surface Claude's 5-hour block and 7-day rolling quota with reset countdowns. On Claude Code ≥2.1.90 the data comes straight from the statusline stdin JSON (`rate_limits.five_hour` / `.seven_day`) — fresh every redraw, zero API requests. Older builds fall back to the `api.anthropic.com/api/oauth/usage` poller: a detached background worker (`lib/usage-refresh.js`) refreshes a disk cache (180 s TTL, honoring 429 `Retry-After` with a 5-minute error backoff), and while the endpoint errors the cache carries the last good values, rendered with a `~` stale marker. On macOS the OAuth token is read from the `Claude Code-credentials` keychain service; elsewhere (or as fallback) from `~/.claude/.credentials.json`. Any hard failure silently renders an empty cell — never blocks.

### Real token count

Expand Down Expand Up @@ -291,8 +291,11 @@ Set `fields` in `~/.claude/statusline-intel.json` to the list + order you want.
| `cacheSaved` | `saved:$2.83` | **Cumulative** USD saved across the session by cache hits vs. paying the uncached input rate for the same tokens. Hidden when savings are under $0.10 (not worth the field) (dim) |
| `compactAge` | `compact:2hr 13m ago` | Time since last `/compact` event. Dim when <2h, **red** when ≥2h — the only line-2 field that escalates, because it's the one line-2 signal that says "you should act" |
| `compactCost` | `c$12.34 / s$68.20` | Cost + cache-savings accrued **since last /compact**. Incrementally cached at `/tmp/claude-cost-sincecompact-<sid>`, auto-invalidated when compact mtime advances. Shows `c$` only when ≥$0.01, `s$` only when ≥$0.10; hidden when both trivial (dim) |
| `blockUsage` | `b:47% r:1hr 12m` | Claude's 5-hour block quota utilisation + time until reset. Data from cached `/api/oauth/usage` response (180s TTL, refreshed in background). % zone-coloured: green <60%, yellow 60-85%, orange 85-95%, red ≥95%. Empty cell when the cache is absent or errored |
| `weekUsage` | `w:31% r:4d 12hr` | Weekly (7-day) quota utilisation + time until reset. Same colour escalation as `blockUsage` |
| `blockUsage` | `b:47% r:1hr 12m` | Claude's 5-hour block quota used + reset countdown; leads line 2 next to session duration + time until reset. Sourced from statusline stdin `rate_limits` (CC ≥2.1.90); cached `/api/oauth/usage` fallback for older builds, rendering last-good values with a `~` marker during API outages |
| `weekUsage` | `w:31% r:4d 12hr` | Weekly (7-day) quota used + time until reset. Same sourcing as `blockUsage`; % zone-coloured: green <60%, yellow 60-85%, orange 85-95%, red ≥95% |
| `modelSplit` | `fable-5:sc$80·sp99%·wr:~85%` | Per model: session cost (`sc$`), share of session spend (`sp%`), and estimated weekly window remaining (`wr:~%`). `wr` attributes the account weekly used% across models by their share of this week's tracked spend — an SI estimate (hence `~`), since Anthropic exposes no per-model weekly quota. Parent-session splits are recorded to `session_model_costs` on each redraw, so the weekly attribution gets richer as sessions run |
| `tokenSpeed` | `tps:238` | Output-token generation speed (tokens/sec) — median of the last few streamed turns, measured from per-message chunk-timestamp spans (pure generation time). Falls back to session average (total output / total API time) when the tail has no streamed turn |
| `modelQuota` | `opus:12/88%` | Per-model QUOTA used/available — dormant until Anthropic populates per-model limits (stdin extra `rate_limits` keys or `seven_day_<model>` buckets in the usage payload; both are null on plans without per-model limits) |
| `cwd` | `~/DWS/claude-session-intelligence` | Full working directory, ccstatusline-style. `$HOME` collapses to `~`; middle-ellipsis when longer than 60 chars (keeps the leaf) (dim) |
| `activeRoot` | `→plugins/session-intelligence` | "Where Claude is touching files now" — latest non-blank `root` from the per-session shape log, complements `cwd`. Hidden when the latest root collapses to `.` (Claude is parked at cwd root) or matches `basename(cwd)` — those add no signal beyond `cwd`. To force a `→.` "still alive" indicator on flat-layout repos where everything is at the root, set `statusline.perProject["<abs cwd>"].activeRootShowAtRoot: true` (dim) |
| `siHealth` | `⚠ si-off (run /si doctor)` | **Self-diagnosis tag.** Empty when SI is healthy on this project (silence = success), red warning when the project's `enabledPlugins` whitelist is suppressing SI. Without this the dark state is invisible — the bar still renders (statusline is wired separately) but every SI-fed field stays blank forever and there's no way to distinguish "nothing happened yet" from "hooks are off." The tag points users at `/si doctor` for the verbose remediation block |
Expand All @@ -312,7 +315,7 @@ Set `fields` in `~/.claude/statusline-intel.json` to the list + order you want.
|---|---|
| `minimal` | `tokens` |
| `standard` | `model`, `project`, `tokens`, `newline`, `task` |
| `verbose` (default) | 4 lines. L1: `tokens`, `compactAge`, `compactCost`, `cacheHit`, `siHealth` — colour-escalating warning row (`siHealth` is silent when SI is wired up correctly; flags `⚠ si-off` only when the project's `enabledPlugins` whitelist is suppressing the plugin) · L2: `session`, `blockUsage`, `sessionId`, `costSaved`, `tools`, `tokenFlow` — live activity · L3: `branch`, `diffstat`, `cwd`, `activeRoot` — git + working dir delta · L4: `model`, `weekUsage`, `outputStyle`, `thinking`, `task` — dim reference context |
| `verbose` (default) | 5 lines. L1: `tokens`, `compactAge`, `compactCost`, `cacheHit`, `siHealth` — colour-escalating warning row (`siHealth` is silent when SI is wired up correctly; flags `⚠ si-off` only when the project's `enabledPlugins` whitelist is suppressing the plugin) · L2: `blockUsage`, `session`, `sessionId`, `costSaved`, `tools`, `tokenFlow`, `tokenSpeed` — live activity · L3: `branch`, `diffstat`, `cwd`, `activeRoot` — git + working dir delta · L4: `model`, `outputStyle`, `thinking`, `task` — dim reference context · L5: `weekUsage`, `modelQuota`, `modelSplit` — weekly quota, per-model quota (dormant until plans have per-model limits), per-model spend |
| `verbose-cache` | 4 lines, token-economics-focused. Same shape as `verbose` with `cacheTokens` appended to L2; L4 trimmed to model/project/task |

Switch via `/si set statusline.preset minimal` or override one session with `CLAUDE_STATUSLINE_PRESET=minimal`.
Expand Down Expand Up @@ -856,7 +859,7 @@ Large tool responses (Read on a big file, Bash log dump, Grep with many hits) si
`si-tool-archive.js` is a **PostToolUse** hook that snapshots tool responses larger than `toolArchive.thresholdChars` (default 4096) to:

```
${TMPDIR}/claude-tool-archive-<sid>/<tool_use_id>.json
~/.claude/state/claude-tool-archive-<sid>/<tool_use_id>.json
```

After `/compact` wipes the body from context, replay it with:
Expand Down
2 changes: 1 addition & 1 deletion plugins/session-intelligence/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Always update `MEMORY.md` with a one-line pointer. Don't duplicate prior memory
Disable either surface with `compact.memoryOffload: false` in `~/.claude/session-intelligence.json` or `CLAUDE_COMPACT_MEMORY_OFFLOAD=0`.

#### Tool-Response Archive (post-compact retrieval)
`si-tool-archive.js` snapshots any tool response larger than `toolArchive.thresholdChars` (default 4096) to `${TMPDIR}/claude-tool-archive-<sid>/<tool_use_id>.json`. After `/compact` erases the body from context, replay it with `/si expand <tool_use_id>` instead of re-running the tool.
`si-tool-archive.js` snapshots any tool response larger than `toolArchive.thresholdChars` (default 4096) to `~/.claude/state/claude-tool-archive-<sid>/<tool_use_id>.json`. After `/compact` erases the body from context, replay it with `/si expand <tool_use_id>` instead of re-running the tool.

When to reach for this:
- After `/compact`, when you need the full body of a `Read`/`Bash`/`Grep` result that just got summarised away.
Expand Down
8 changes: 8 additions & 0 deletions plugins/session-intelligence/hooks/si-bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,14 @@ function main() {
const wfAgents = require(path.join(SI_LIB, 'events')).reconcileWorkflowAgents({ cwd });
if (wfAgents > 0) intelLog('bootstrap', 'info', 'workflow agents reconciled', { recorded: wfAgents });
} catch { /* best effort */ }
// Backfill usage on rows the live tracker recorded blind — background-run
// Agent/Task calls fire PostToolUse before the subagent transcript has
// flushed usage, so model/tokens/cost land NULL. The transcripts are
// complete by now; fill the gaps from them.
try {
const filled = require(path.join(SI_LIB, 'events')).reconcileSubagentUsage({ cwd });
if (filled > 0) intelLog('bootstrap', 'info', 'subagent usage backfilled', { updated: filled });
} catch { /* best effort */ }
const locked = acquireStateLock();
try {
const state = loadState();
Expand Down
92 changes: 92 additions & 0 deletions plugins/session-intelligence/lib/agent-usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,95 @@ function listWorkflowAgentTranscripts({ cwd, projectsRoot: projectsRootOverride
return out;
}

/**
* List every plain (non-workflow) subagent transcript for a project:
* ~/.claude/projects/<encoded-cwd>/<parent-sid>/subagents/agent-<id>.jsonl
*
* Companion to listWorkflowAgentTranscripts, which only walks the
* `subagents/workflows/` layer below this one. Needed because Agent/Task
* calls now run in the background by default: at PostToolUse time the
* transcript usually hasn't flushed any usage yet, so si-agent-tracker
* records the row with NULL model/tokens/cost. A SessionStart reconcile
* re-reads these completed transcripts to fill the gaps.
*
* `sinceMs` (ms epoch) skips files whose mtime is older — the reconcile
* only targets recent unfilled rows, so there's no reason to re-parse
* months of transcript history every session. Returns `[{ path, sid,
* mtimeMs }]`, newest first. Same cwd-encoding caveat as the other walkers.
*/
function listPlainSubagentTranscripts({ cwd, projectsRoot: projectsRootOverride, sinceMs = 0 } = {}) {
const enc = encodeProjectPath(cwd);
if (!enc) return [];
const projDir = path.join(projectsRoot(projectsRootOverride), enc);
let sidEntries;
try { sidEntries = fs.readdirSync(projDir, { withFileTypes: true }); } catch { return []; }

const out = [];
for (const sidEnt of sidEntries) {
if (!sidEnt.isDirectory()) continue;
const dir = path.join(projDir, sidEnt.name, 'subagents');
for (const c of listCandidates(dir)) {
if (c.mtimeMs < sinceMs) continue;
out.push({ path: c.path, sid: sidEnt.name, mtimeMs: c.mtimeMs });
}
}
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
return out;
}

/** Path of the PARENT session's transcript for a sid under this project. */
function parentTranscriptPathFor(cwd, sid, projectsRootOverride) {
const enc = encodeProjectPath(cwd);
if (!enc || !sid) return null;
return path.join(projectsRoot(projectsRootOverride), enc, `${sid}.jsonl`);
}

function _blockText(block) {
if (typeof block.content === 'string') return block.content;
if (Array.isArray(block.content)) {
return block.content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join('\n');
}
return '';
}

/**
* Map subagent agentId → launching Agent-tool tool_use_id by scanning the
* PARENT session transcript. Newer Claude Code builds (2.1.x) stopped
* writing `parentToolUseId` into subagent transcript lines, so the pairing
* has to come from the parent side, where it appears twice per agent:
* - the launch ack — a tool_result block for the tool_use_id whose text
* carries "agentId: <id>"
* - the completion notification — "<task-id><id></task-id>" and
* "<tool-use-id><toolu_...></tool-use-id>" inside one message
* A cheap substring prefilter keeps this fast on multi-MB transcripts.
*/
function mapAgentToolUseIds(parentTranscriptPath) {
const map = new Map();
if (!parentTranscriptPath) return map;
let raw;
try { raw = fs.readFileSync(parentTranscriptPath, 'utf8'); } catch { return map; }
for (const line of raw.split('\n')) {
if (!line) continue;
if (!line.includes('agentId') && !line.includes('task-id')) continue;
let d;
try { d = JSON.parse(line); } catch { continue; }
const content = d && d.message && d.message.content;
if (!Array.isArray(content)) continue;
for (const block of content) {
if (!block) continue;
if (block.type === 'tool_result' && block.tool_use_id) {
const m = /\bagentId:\s*([A-Za-z0-9_-]+)/.exec(_blockText(block));
if (m) map.set(m[1], block.tool_use_id);
} else if (block.type === 'text' && typeof block.text === 'string') {
const tid = /<task-id>([A-Za-z0-9_-]+)<\/task-id>/.exec(block.text);
const tuid = /<tool-use-id>(toolu_[A-Za-z0-9_-]+)<\/tool-use-id>/.exec(block.text);
if (tid && tuid) map.set(tid[1], tuid[1]);
}
}
}
return map;
}

/**
* Find the subagent transcript most likely to belong to the Task call we
* just observed. `windowMs` is how far back to look; the default is wide
Expand Down Expand Up @@ -219,4 +308,7 @@ module.exports = {
readSubagentTranscript,
findUsageForTask,
listWorkflowAgentTranscripts,
listPlainSubagentTranscripts,
parentTranscriptPathFor,
mapAgentToolUseIds,
};
24 changes: 15 additions & 9 deletions plugins/session-intelligence/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,34 @@ const STATUSLINE_PRESETS = {
// `93.0%` signal so we can retire the 1.5s npx spend.
// Line 2: session activity + token economics — live "what's happening
// right now" row at eye level, just below the warning bar.
// Line 3: quota + cwd — Claude Code 5h / 7d usage plus working dir.
// Data comes from the cached usage API (180 s TTL, refreshed
// by a detached worker so the hot path never blocks on HTTPS).
// Line 4: identity / repo / task — dim reference context at the bottom.
// Line 3: git + working dir delta.
// Line 4: identity / repo / task — dim reference context.
// Line 5: usage & models row — 5h/7d quota as used/available, per-model
// quota (dormant until plans have per-model limits), and the
// per-model spend split. All the "how much have I used / what
// did it go to" signals in one bottom line.
verbose: [
'tokens', 'compactAge', 'compactCost', 'cacheHit', 'siHealth',
'newline',
'session', 'blockUsage', 'sessionId', 'costSaved', 'tools', 'tokenFlow',
'blockUsage', 'session', 'sessionId', 'costSaved', 'tools', 'tokenFlow', 'tokenSpeed',
'newline',
'branch', 'diffstat', 'cwd', 'activeRoot',
'newline',
'model', 'weekUsage', 'outputStyle', 'thinking', 'task',
'model', 'outputStyle', 'thinking', 'task',
'newline',
'weekUsage', 'modelQuota', 'modelSplit',
],
// Token-economics focus — same 4-line skeleton, adds cacheTokens.
// Token-economics focus — same skeleton, adds cacheTokens.
'verbose-cache': [
'tokens', 'compactAge', 'compactCost',
'newline',
'session', 'sessionId', 'tools', 'costSaved', 'tokenFlow', 'cacheHit', 'cacheTokens',
'blockUsage', 'session', 'sessionId', 'tools', 'costSaved', 'tokenFlow', 'cacheHit', 'cacheTokens', 'tokenSpeed',
'newline',
'weekUsage', 'blockUsage', 'cwd',
'cwd',
'newline',
'model', 'project', 'task',
'newline',
'weekUsage', 'modelQuota', 'modelSplit',
],
};

Expand Down
Loading
Loading