opencode-context-indicator 1.5.2 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,78 @@ Notable user-facing changes per release. Dates are commit days (UTC).
4
4
  The package also ships a dual V1/V2 plugin — see the README for the surface
5
5
  matrix; everything below concerns the V2 path unless stated otherwise.
6
6
 
7
+ ## 1.5.4 — 2026-10-27
8
+
9
+ Fix-and-hygiene release after a full-project review. No new commands.
10
+
11
+ - **Subagent roll-up no longer collapses on a child's idle.** `session.idle`
12
+ wiped the child's parent attribution and accumulated totals, so the very next
13
+ live line reported `subagents 0 session(s)` right after any subagent
14
+ finished, even though its parent was still running. V2 now keeps a finished
15
+ child's entries until the parent itself goes idle or is deleted
16
+ (`clearChildSubagentTracking`, breadth-first, so sub-subagents are collected
17
+ too); `/context-breakdown` and `/cx` were never affected (they read
18
+ `state.json`). T29 covers the lifetime.
19
+ - **TUI panel no longer re-measures every message once per second.** The
20
+ measured-contributors memo was keyed on the 1000 ms snapshot tick and the host
21
+ rebuilds the message array on every poll, so the full walk (per-character
22
+ token estimates plus `JSON.stringify` of tool args) ran on the render thread
23
+ every second. It is now keyed on a cheap signature (list length, last message
24
+ id, last message size including tool results and status), so a static session
25
+ costs almost nothing while streaming text and completing tool calls still
26
+ refresh.
27
+ - **A summary-only log write keeps the live snapshot.** `pushFinalSummary` used
28
+ to rewrite `context-breakdown.log` with an empty live section, so every idle /
29
+ compaction / command payload blanked it until the next measured write. The
30
+ module now remembers the last rendered snapshot and re-renders it.
31
+ - Doc fixes: the log is rewritten atomically rather than "appended" with
32
+ `appendFileSync`, and both its sections are process-local (survive rewrites,
33
+ not a restart); the `state.json` field list now includes `toolOutByTool`,
34
+ `reclaim`, `cacheRead`/`cacheWrite` and `compactedAt`/`compactStale`; "tested
35
+ against opencode" now says 2.1.4 (was a stale v2.0.28); the gate table now
36
+ states the actual scan scope.
37
+ - Packaging: `docs/` is published, so the README screenshot resolves on the npm
38
+ page. Gate (a) expands `docs/` the same way it expands `lib/` and rejects any
39
+ non-image file there.
40
+ - Tests: new `OPENCODE_CONTEXT_INDICATOR_LOG_FILE` override (test isolation -
41
+ idle/compacted handlers no longer rewrite the user's real
42
+ `context-breakdown.log`), T29 added (child / grand-child lifetime). 75 unit
43
+ checks.
44
+
45
+ ## 1.5.3 — 2026-10-26
46
+
47
+ - **Compaction ceiling aligned with opencode 2.1.4 (#51271).** `lib/limits.js`
48
+ now mirrors `calculateCeiling()` exactly: the reserve is
49
+ `max(floor(window * 0.15), window >= 2 * 16_000 ? 16_000 : 0)` (was 10%),
50
+ and an override buffer is honored **only while it is smaller than the
51
+ window** — a buffer configured for a larger model is ignored instead of
52
+ leaving zero room. The `compact at` line and the `%` denominator now match the
53
+ host's actual auto-compaction trigger.
54
+ - **Top context consumers.** `/context-breakdown` adds a
55
+ `### Top context consumers (main)` section listing the 5 heaviest single items
56
+ still in the assembled request (one tool result, one message, the system
57
+ prompt, one tool-call's args) with their share of `input`. Tool targets are
58
+ shown live; the `state.json` copy (`top`) is path-free, like the waste
59
+ detector.
60
+ - **`/context-diff`.** New slash command: the previous distinct measurement vs
61
+ the current one (ctx total + per-category deltas, unchanged rows omitted).
62
+ `state.json` persists a `previous` snapshot; an unchanged / mirror write
63
+ inherits it, so the diff always spans two different observations. Main
64
+ session only, cache + `state.json` only (no RPC), delivered under the
65
+ `<context-diff>` tag with `resume:true` like the other commands.
66
+ - Dev: `@opencode/plugin` devDependency pinned to `2.1.4`; `@opentui/solid`
67
+ and `@opentui/core` aligned with the 2.1.4 host (`0.6.8`). The 2.1.4 type
68
+ changes are internal (`DeepMutable` re-exported from effect; an unused
69
+ `permission.descending` brand); the `./tui` surface we import is unchanged.
70
+ `@opentui/solid` 0.6.8 is byte-identical to 0.6.6 in `dist/` and
71
+ `scripts/`; only its `@opentui/core` dependency moved to 0.6.8.
72
+ - Tests: T26 (2.1.4 reserve table + buffer-over-window guard), T27/T27a/T27b
73
+ (top consumers: estimate → path-free persistence → rendered section),
74
+ T28/T28a/T28b/T28c/T28d/T28e (`previous` inheritance → diff rendering →
75
+ honest empty state → unmeasured-write guard (known + unknown model) →
76
+ null-safe delta); existing
77
+ T2/T4/T4b/T7/T9/T10/T12/T16* updated for the 0.15 reserve. 74 unit checks.
78
+
7
79
  ## 1.5.2 — 2026-10-21
8
80
 
9
81
  Maintenance release: no runtime behavior change. It aligns the dev toolchain
package/README.md CHANGED
@@ -13,7 +13,7 @@ arguments, tool results (the outputs fed back into context) and the residual
13
13
  The package ships a **dual V1 + V2 plugin** (one entry works in both OpenCode
14
14
  generations) plus an optional TUI module (`./tui` entry) that renders a **live
15
15
  sidebar** and a **zero-LLM `/cx` breakdown panel** with measured contributors
16
- and a redacted export. The V2 path is **tested against opencode v2.0.28**; the
16
+ and a redacted export. The V2 path is **tested against opencode 2.1.4**; the
17
17
  V1 path against 1.18.29+.
18
18
 
19
19
  > **V1 is feature-frozen.** It is kept working for compatibility (bug fixes
@@ -70,14 +70,16 @@ For every served request the plugin estimates how the context window is split:
70
70
  duplicate group), reported honestly as *"N redundant tool outputs ≈ X tokens"*
71
71
  — never as "the model wasted effort". Findings are per-session and **path-free
72
72
  in the persisted/exported form**.
73
- * The denominator is the **opencode v2.0.23 compaction ceiling**: `window =
74
- limit.input || limit.context`; `reserve = max(floor(window * 0.1), window >=
75
- 32_000 ? 16_000 : 0)`; the ceiling is `window - reserve`. When opencode
73
+ * The denominator is the **opencode 2.1.4 compaction ceiling**: `window =
74
+ limit.input || limit.context`; `reserve = max(floor(window * 0.15), window >=
75
+ 2 * 16_000 ? 16_000 : 0)`; the ceiling is `window - reserve`. This mirrors
76
+ `calculateCeiling()` in `packages/core/src/session/compaction.ts`. When opencode
76
77
  carries a `compaction.buffer` configured in its own settings, the plugin
77
78
  cannot read that value (it is not exposed to plugins), so the default
78
79
  reserve formula is used — set `OPENCODE_CONTEXT_INDICATOR_COMPACT_BUFFER`
79
80
  to override the reserve for testing or custom setups (non-negative integer <=
80
- 1e9; invalid values are silently ignored).
81
+ 1e9; invalid values are silently ignored; a buffer that is **not smaller than
82
+ the window** is ignored, exactly like core, so it can never leave zero room).
81
83
 
82
84
  ### Toast (V1 only)
83
85
 
@@ -115,13 +117,14 @@ updated 12:30:01
115
117
 
116
118
  ### Slash commands (V2)
117
119
 
118
- On OpenCode 2.x (Desktop **and** terminal TUI) the plugin registers two slash
120
+ On OpenCode 2.x (Desktop **and** terminal TUI) the plugin registers three slash
119
121
  commands:
120
122
 
121
123
  | Command | Scope | Delivery | Agent turn? |
122
124
  | --- | --- | --- | :---: |
123
125
  | `/context` | **this session only** (no subagents); cache + `state.json` only | `resume:true` — the model echoes the 3-line summary into the transcript | yes |
124
126
  | `/context-breakdown` | this session **+ all subagent descendants** | `resume:true` — the model echoes the table into the transcript | yes |
127
+ | `/context-diff` | **this session only**; previous measurement vs current | `resume:true` — the model echoes the delta table into the transcript | yes |
125
128
 
126
129
  #### `/context` — quick summary (main session only)
127
130
 
@@ -179,6 +182,14 @@ unavailable._
179
182
  ### Tool results by tool (main)
180
183
  webfetch 242.1k · read 18.4k · bash 4.9k · +3 more
181
184
 
185
+ ### Top context consumers (main)
186
+ 1. tool result · webfetch https://example.com/large-page — ~242.1k (61% of input)
187
+ 2. tool result · read src/a.ts — ~18.4k (5% of input)
188
+ 3. system prompt — ~8.5k (2% of input)
189
+ 4. reasoning — ~4.5k (1% of input)
190
+ 5. tool args · bash npm test — ~1.2k (0% of input)
191
+ _Heaviest single items still in the request; one item per line, tokens are estimates._
192
+
182
193
  ### Prompt cache (main, provider-reported)
183
194
  read 12.0k (27% of input) · write 1.1k
184
195
 
@@ -203,6 +214,34 @@ instructions to follow.
203
214
  </context-breakdown>
204
215
  ```
205
216
 
217
+ The top-5 heaviest **single items** still in the assembled request (one tool
218
+ result, one message, the system prompt) are listed separately, with their share
219
+ of `input`. Tool targets (file paths / URLs) are shown in the live section only;
220
+ the `state.json` copy is path-free, like the waste detector.
221
+
222
+ #### `/context-diff` — what changed since the previous measurement
223
+
224
+ `state.json` keeps one `previous` measurement per session: the record that was
225
+ **replaced** by the most recent distinct write. `/context-diff` compares it with
226
+ the current one and renders only the rows that actually moved:
227
+
228
+ ```
229
+ **Context diff** — your-model
230
+ total 41.5k -> 45.2k (+3.7k tokens, +9%)
231
+
232
+ | category | previous | now | delta |
233
+ | --- | --- | --- | --- |
234
+ | toolOut | 24.7k | 28.4k | +3.7k |
235
+
236
+ _previous 12:36:16 · current 12:40:11 · tokens are estimates._
237
+ ```
238
+
239
+ Collection is instant (cache + `state.json`, no subagent traversal, no RPC) and
240
+ delivery uses the same `resume:true` + `<context-diff>` tag protocol as the other
241
+ commands. An unchanged (mirror / repeated) write does **not** shift `previous`,
242
+ so the diff always spans two different observations. A session with only one
243
+ measurement reports `No previous measurement yet`.
244
+
206
245
  #### Delivery semantics (`resume`)
207
246
 
208
247
  `ctx.session.synthetic({ sessionID, text, description, resume })` durably *admits*
@@ -215,8 +254,8 @@ session is woken (opencode `Session.synthetic`:
215
254
  | `false` | durable inbox notice; **no** LLM request; session context unchanged. A synthetic message is rendered as a compact **Notice chip** whose label is `description ?? text` (the client row-builder maps `synthetic` → Notice) — so the chip shows the description, **not** the table. |
216
255
  | `true` | the same inbox item **plus** a wake → a real agent turn runs. The model sees the payload as input and its answer is a **normal assistant message**, rendered in full. |
217
256
 
218
- Both commands now use `resume:true` with the payload wrapped in explicit tags
219
- (`<context-summary>` / `<context-breakdown>`). A real agent turn runs and the
257
+ All three commands use `resume:true` with the payload wrapped in explicit tags
258
+ (`<context-summary>` / `<context-breakdown>` / `<context-diff>`). A real agent turn runs and the
220
259
  model renders the tagged content as a normal assistant message — the only way
221
260
  the content is visible in the Desktop (the chip only shows `description`).
222
261
  `CONTEXT_BREAKDOWN_RESUME` controls `/context-breakdown` back to a plain
@@ -231,10 +270,10 @@ notice if needed.
231
270
  > exist). Treat `/context-breakdown` as *"ask the agent to show the table"*, not
232
271
  > as a pure UI action.
233
272
 
234
- * **Both commands run an agent turn.** `/context` collects from the in-memory
235
- cache and `state.json` only (no RPC, no subagent traversal) and
236
- `/context-breakdown` collects the full tree, but both deliver with
237
- `resume:true` — so each issues a real LLM request and the session context
273
+ * **All three commands run an agent turn.** `/context` and `/context-diff`
274
+ collect from the in-memory cache and `state.json` only (no RPC, no subagent
275
+ traversal) and `/context-breakdown` collects the full tree, but all deliver
276
+ with `resume:true` — so each issues a real LLM request and the session context
238
277
  grows by that turn.
239
278
  * Subagents are discovered by walking the **parentID chain persisted in
240
279
  `state.json`** (breadth-first from the current session, every level — subagents
@@ -263,15 +302,16 @@ notice if needed.
263
302
  are also seeded from the snapshot at startup, and the retry backs off
264
303
  (30 s → 5 min, capped). A known `usable`/`limit`/`agent` is never overwritten
265
304
  with `null` when another instance rewrites the snapshot.
266
- * Both commands append their payload to `context-breakdown.log` (final-summaries
267
- section), so it stays recoverable even if delivery fails.
305
+ * All three commands copy their payload into `context-breakdown.log` (final-summaries
306
+ section) while the plugin instance lives, so it stays recoverable even if
307
+ delivery fails (see the log lifetime note under "Log / state file locations").
268
308
  * If `ctx.session.synthetic` throws, the command logs an actionable error and the
269
309
  payload is preserved in the log — there is no other delivery channel in this
270
310
  runtime (no `noReply`/`prompt` option exists).
271
311
  * **Instruction echo.** Delivery is an agent turn: the model renders the
272
312
  payload, and with the previous instruction wording it occasionally echoed
273
- the instruction line itself. Both instructions now delimit the payload with
274
- explicit tags (`<context-summary>` / `<context-breakdown>`) and instruct the
313
+ the instruction line itself. All instructions now delimit the payload with
314
+ explicit tags (`<context-summary>` / `<context-breakdown>` / `<context-diff>`) and instruct the
275
315
  model to output only the tagged content — an unambiguous payload boundary
276
316
  instead of a "reproduce exactly" phrasing. Residual risk: a model may still
277
317
  add light commentary or include the tags themselves.
@@ -304,8 +344,8 @@ session view (the palette itself is only reachable outside the composer).
304
344
  Opened from a subagent view, the family is rooted at the topmost ancestor, so
305
345
  the whole family is shown, not just the child subtree.
306
346
 
307
- **Zero LLM calls.** Unlike `/context` and `/context-breakdown` (which run a real
308
- agent turn so the model renders the table), the panel only reads local TUI data
347
+ **Zero LLM calls.** Unlike the slash commands (which run a real agent turn so
348
+ the model renders the output), the panel only reads local TUI data
309
349
  and the state file; it never wakes the session and never grows the context.
310
350
 
311
351
  The **largest contributors (measured)** section char-counts the in-process
@@ -319,7 +359,7 @@ are shown. Empty state: `no messages yet`.
319
359
 
320
360
  Live only for a `file://` install from a path outside `node_modules`; the npm
321
361
  path auto-disables it (upstream `#33884`). The Desktop has no TUI, so it uses
322
- `/context` and `/context-breakdown` instead.
362
+ `/context`, `/context-breakdown` and `/context-diff` instead.
323
363
 
324
364
  #### Export (redacted)
325
365
 
@@ -345,7 +385,7 @@ Notes — these reflect what the plugin API actually exposes today:
345
385
  * **V2 has no toast channel on the server side.** The V2 plugin API
346
386
  (`ctx.*`) exposes no TUI/toast method (opencode issue `#49380`); the V2 path
347
387
  therefore delivers everything through the log file, the TUI sidebar, the
348
- `/cx` session panel and the `/context` / `/context-breakdown` slash commands. The sidebar is a TUI-process
388
+ `/cx` session panel and the `/context` / `/context-breakdown` / `/context-diff` slash commands. The sidebar is a TUI-process
349
389
  slot and does not exist in the Desktop app; the slash commands work in **both**.
350
390
  The TUI sidebar is **live only for a `file://` install** from a path outside
351
391
  `node_modules`; on the npm path it is auto-disabled (upstream `#33884`) — see
@@ -354,7 +394,7 @@ Notes — these reflect what the plugin API actually exposes today:
354
394
  `./tui` entry) is a V2 feature; OpenCode 1.x has no equivalent, so V1 is
355
395
  toast + log only.
356
396
  * The Desktop app is TUI-less; its V2 surfaces are the log file and the
357
- `/context` / `/context-breakdown` slash commands.
397
+ `/context` / `/context-breakdown` / `/context-diff` slash commands.
358
398
 
359
399
  ---
360
400
 
@@ -366,16 +406,24 @@ and `$TMPDIR` (usually `/tmp`) elsewhere:
366
406
  | File | Purpose |
367
407
  | --- | --- |
368
408
  | `context-breakdown.log` | Human-readable live snapshot + bounded final summaries |
369
- | `opencode-context-indicator-state.json` | Machine-readable snapshot consumed by the TUI sidebar and the `/cx` panel. Fields per entry: `sessionID`, `parentID`, `role`, `agent`, `model`, `providerID`, `ctx`, `input`, `usable` (compaction ceiling), `reserve` (tokens reserved for compaction), `limit` (context window), `reasoning`, `categories{user,assistant,reasoning,toolArgs,system,toolSchemas,other}`, `updatedAt`. |
409
+ | `opencode-context-indicator-state.json` | Machine-readable snapshot consumed by the TUI sidebar and the `/cx` panel. Fields per entry: `sessionID`, `parentID`, `role`, `agent`, `model`, `providerID`, `ctx`, `input`, `usable` (compaction ceiling), `reserve` (tokens reserved for compaction), `limit` (context window), `reasoning`, `categories{user,assistant,reasoning,toolArgs,toolOut,system,toolSchemas,other}`, `toolOutByTool` (per-tool repeated-output estimate), `reclaim` (duplicate tool output, bounded), `cacheRead`/`cacheWrite` (provider-reported prompt-cache tokens), `compactedAt`/`compactStale`, `top` (top-5 heaviest single items, path-free), `previous` (previous distinct measurement for `/context-diff`), `updatedAt`. |
370
410
  | `context-events.log` | Raw event tap — **only** when `DEBUG_EVENTS` is flipped to `true` in the source (off by default) |
371
411
 
372
- The live `context-breakdown.log` snapshot is **rewritten** (never grows); final
373
- summaries (on `session.idle`) and compaction notes are **appended** — log writes
374
- use plain `appendFileSync`, not atomic replacement. Only the machine-readable
375
- `opencode-context-indicator-state.json` is written **atomically** (temp file +
376
- rename), so concurrent plugin instances cannot corrupt it. Cross-instance
377
- duplicate final summaries are prevented separately by an atomic claim marker
378
- (see `lib/dedup.js`).
412
+ The whole `context-breakdown.log` is **rewritten atomically** (temp file +
413
+ rename, like the state file): the live snapshot is always regenerated and the
414
+ final summaries (on `session.idle`) plus compaction notes are re-rendered from
415
+ an in-memory buffer (bounded, last 50 per log), so the file never grows and
416
+ concurrent plugin instances cannot observe a half-written file. A summary-only
417
+ write (idle, compaction, command payload) re-renders the last live snapshot, so
418
+ adding a note never blanks that section. That snapshot may predate a compaction
419
+ or a deleted session - the leading `[<timestamp>]` line is the freshness cue.
420
+ Both parts are **process-local**: they
421
+ survive every rewrite and failed command delivery within the lifetime of the
422
+ running instance, but they are dropped when the plugin instance is re-created
423
+ (opencode restarts, or about 24 h of idling). A different instance that rewrites
424
+ the file keeps its own view of the summary buffer; cross-instance duplicate
425
+ summaries are prevented separately by an atomic claim marker (see
426
+ `lib/dedup.js`).
379
427
 
380
428
  ---
381
429
 
@@ -425,7 +473,7 @@ a slot mounted from an npm install renders once and then never live-updates.
425
473
  On the **npm** path the plugin detects that it lives under `node_modules` and
426
474
  **disables the sidebar and the panel gracefully** — it logs a single hint and
427
475
  registers no slot, so you never see a frozen "no data yet" panel. The `/context`
428
- / `/context-breakdown` slash commands and the breakdown log file keep working on
476
+ / `/context-breakdown` / `/context-diff` slash commands and the breakdown log file keep working on
429
477
  every install.
430
478
 
431
479
  #### Getting the live sidebar (`file://` install)
@@ -499,13 +547,13 @@ the sidebar instead.
499
547
  gracefully (the breakdown falls back to throttled `session.messages` fetches
500
548
  and toasts keep working).
501
549
  * **OpenCode ≥ 2.0.16** for the V2 path (built and verified against
502
- **2.1.2**).
550
+ **2.1.4**).
503
551
  * **TUI sidebar**: terminal TUI only, requires the OpenTUI rendering stack that
504
552
  ships with OpenCode. `@opentui/core` and `solid-js` are **optional** peer
505
553
  dependencies resolved by OpenCode at runtime; the main plugin installs and runs
506
554
  fine without them (the sidebar simply is not available). `@opentui/solid` is
507
- instead shipped as a **pinned direct dependency** (exact `0.6.6`, matching
508
- the OpenTUI stack OpenCode 2.1.2 ships): OpenCode's
555
+ instead shipped as a **pinned direct dependency** (exact `0.6.8`, matching
556
+ the OpenTUI stack OpenCode 2.1.4 ships): OpenCode's
509
557
  TUI loader does not expose a host instance of it, so for npm-installed plugins
510
558
  the JSX pragma would otherwise fail to resolve `@opentui/solid/jsx-runtime`
511
559
  (upstream: opencode issue #33884 — `node_modules` plugins are excluded from the
@@ -535,7 +583,7 @@ The package default export is a dual plugin:
535
583
  request), subscribes to the event stream (`session.created`,
536
584
  `session.step.ended`, `session.idle`, `session.compacted`, failure events),
537
585
  records limits from the model registry and registers the
538
- `/context` + `/context-breakdown` slash commands (via
586
+ `/context` + `/context-breakdown` + `/context-diff` slash commands (via
539
587
  `ctx.command.transform`). Read-only: the context is never mutated (the
540
588
  commands wake the session via `resume:true`, which is what renders the table).
541
589
  * `server({ client })` — V1 entry (OpenCode ≥ 1.18.29). Classic event handler +
@@ -578,7 +626,7 @@ caller.
578
626
  ### Running tests
579
627
 
580
628
  ```bash
581
- npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1-T25c unit checks
629
+ npm run test:prepublish # fast gate: tarball + tui.tsx + resolve + T1-T29 unit checks
582
630
  npm run test:e2e # full E2E: isolated plugin load + one "Say OK" round-trip
583
631
  npm run test:e2e:quick # E2E load-only, no LLM call
584
632
  npm run test:drift # advisory: warns on non-breaking host drift (also in CI)
@@ -597,12 +645,12 @@ STEP 80 s, ~3 min total.
597
645
 
598
646
  | Step | What | Why |
599
647
  |---|---|---|
600
- | (a) tarball composition | `package.json` `files[]` expands to the base files plus every `lib/*` module (count asserted dynamically) | no test/node_modules/tsconfig in the tarball |
648
+ | (a) tarball composition | `package.json` `files[]` expands to the base files plus every `lib/*` module (count asserted dynamically); `docs/` may contain images only | no test/node_modules/tsconfig in the tarball |
601
649
  | (b) pragma regression | esbuild `--jsx=automatic` → `@opentui/solid/jsx-runtime` present, `react/jsx-runtime` absent | a removed/broken pragma kills the sidebar on npm install |
602
650
  | (c) resolve chain | replica install: host-bridged + peer packages resolve; local tui.tsx relative imports resolve via node from the entry (resolve-bridge.cjs) | broken peer/optional dep or a bad local import silently breaks the sidebar |
603
- | (d) unit harness T1-T25c | limit/agent/state fix assertions plus measureContributors, redacted-export, shared category-helper, sanitizer/unicode, prompt-cache and waste-detector checks (62 total) | regressions in modelLimits, writeStateFile, hydrate, contributors, export |
604
- | (e) corporate-identifier scan | no forbidden identifiers in the published surface + test tree | compliance |
605
- | (f) non-English scan | no non-ASCII letters (`\p{L}` above U+007F) in any repo file | English-only source rule |
651
+ | (d) unit harness T1-T29 | limit/agent/state fix assertions plus measureContributors, redacted-export, shared category-helper, sanitizer/unicode, prompt-cache, waste-detector, compaction-reserve, top-consumer, diff and subagent-rollup checks (75 total) | regressions in modelLimits, writeStateFile, hydrate, contributors, export, subagent lifetime |
652
+ | (e) corporate-identifier scan | no forbidden identifiers in the published surface (`index.js`, `tui.tsx`, `lib/*`, `README.md`, `CHANGELOG.md`, `package.json`) or the test tree | compliance |
653
+ | (f) non-English scan | no non-ASCII letters (`\p{L}` above U+007F) in the same published surface and test tree (`docs/` is image-only and is not text-scanned) | English-only source rule |
606
654
 
607
655
  **Isolated E2E — `test/e2e-tui.mjs`** (requires the OpenCode CLI)
608
656
 
@@ -616,7 +664,10 @@ The E2E is fully **non-invasive** to your running OpenCode Desktop:
616
664
  - `--standalone` starts a *private* server instead of the background service Desktop uses;
617
665
  - `OPENCODE_DB` points at a throwaway DB in `%TEMP%` (your sessions are untouched);
618
666
  - `OPENCODE_CONTEXT_INDICATOR_STATE_FILE` points at a throwaway state file in `%TEMP%`
619
- — the live `opencode-context-indicator-state.json` is never read or written;
667
+ - the live `opencode-context-indicator-state.json` is never read or written;
668
+ - `OPENCODE_CONTEXT_INDICATOR_LOG_FILE` does the same for the human-readable
669
+ `context-breakdown.log` (used by the unit gate, which drives idle/compacted
670
+ handlers);
620
671
  - `OPENCODE_CONFIG_DIR` points at a throwaway global config written by the script
621
672
  (plugins: this checkout; `update: "disable"`) **for Check A only** — the load
622
673
  source is deterministic and fail-closed instead of following your live config;
Binary file
package/index.js CHANGED
@@ -67,7 +67,7 @@
67
67
  * event-driven and does not need the removed API.
68
68
  *
69
69
  * V2 slash commands (added 1.1.0, verified against the live Desktop 2.0.19
70
- * plugin runtime). Two commands:
70
+ * plugin runtime). Three commands:
71
71
  * - /context — a 3-line summary of THIS session only (model, tokens, %,
72
72
  * updated + category line). Main only: NO subagent traversal and NO live
73
73
  * RPC (cache + state.json only), so it is instant. Delivered with
@@ -78,7 +78,11 @@
78
78
  * descendants), collected in parallel under one budget. Delivered with
79
79
  * resume:true and a verbatim instruction prefix, so the agent echoes the
80
80
  * table into the transcript as a normal message (rendered in full by the
81
- * Desktop); resume:false remains a one-constant fallback.
81
+ * Desktop); resume:false remains a one-constant fallback. Includes the
82
+ * top-5 heaviest single context consumers (main).
83
+ * - /context-diff — the previous distinct measurement (ctx + per-category
84
+ * deltas) vs the current one, main only, cache + state.json only. Also
85
+ * delivered with resume:true under a <context-diff> tag.
82
86
  * - ctx.command.transform(editor => editor.add({ name, description,
83
87
  * execute })) — registers a slash command; `execute` receives the
84
88
  * invocation { sessionID, prompt:{text,files,agents,skills}, delivery }.
@@ -123,8 +127,8 @@
123
127
  *
124
128
  * - Metric: the native opencode overflow count — tokens.total when present,
125
129
  * else input + output + cache.read + cache.write — compared against the
126
- * model's compaction ceiling (opencode v2.0.23 calculateCeiling): window =
127
- * limit.input || limit.context, reserve = max(10% of window, 16k when
130
+ * model's compaction ceiling (opencode 2.1.4 calculateCeiling): window =
131
+ * limit.input || limit.context, reserve = max(15% of window, 16k when
128
132
  * window >= 32k), usable = window - reserve. Percentages are NOT
129
133
  * clamped at 100% - exceeding the ceiling must stay visible.
130
134
  * - Category estimates use a unicode-aware heuristic (see estimateTokens):
@@ -154,11 +158,13 @@ import { handleV2Event, onV2Context, onV2Compaction, estimateBreakdownFromContex
154
158
  import { breakdownCategories, computeReclaim, contentFingerprint, estimateTokens, extractToolTarget, isSafeSessionKey, safeCell, safeReclaim, safeSaneOrNull, safeToolOutByTool, sumCategories, toolOutEntries } from "./lib/estimate.js";
155
159
  import {
156
160
  commandContextBreakdown,
161
+ commandContextDiff,
157
162
  commandContextSummary,
158
163
  CONTEXT_BREAKDOWN_RESUME,
159
164
  collectSessionReport,
160
165
  renderSessionRow,
161
166
  renderBreakdownTable,
167
+ renderContextDiff,
162
168
  shortSummaryText,
163
169
  shortSummaryDescription,
164
170
  } from "./lib/commands.js";
@@ -197,6 +203,8 @@ export {
197
203
  collectSessionReport,
198
204
  renderSessionRow,
199
205
  renderBreakdownTable,
206
+ renderContextDiff,
207
+ commandContextDiff,
200
208
  shortSummaryText,
201
209
  shortSummaryDescription,
202
210
  ensureModelLimits,
@@ -276,8 +284,9 @@ export default {
276
284
  logErr("model limits prefetch failed", err),
277
285
  );
278
286
 
279
- // Two V2 slash commands (V2 only): /context (instant per-session summary)
280
- // and /context-breakdown (full table, main + all subagent descendants).
287
+ // Three V2 slash commands (V2 only): /context (instant per-session
288
+ // summary), /context-breakdown (full table, main + all subagent
289
+ // descendants), /context-diff (delta vs the previous measurement).
281
290
  // Guarded: older 2.0.x builds may lack ctx.command / ctx.session.synthetic —
282
291
  // registering a command whose handler cannot deliver would fail silently.
283
292
  try {
@@ -306,6 +315,13 @@ export default {
306
315
  name: "context-breakdown",
307
316
  }),
308
317
  });
318
+ editor.add({
319
+ name: "context-diff",
320
+ description:
321
+ "What changed in the context since the previous measurement (main session)",
322
+ execute: (invocation) =>
323
+ commandContextDiff(ctx, invocation, { name: "context-diff" }),
324
+ });
309
325
  } catch (err) {
310
326
  logErr("v2 command transform failed", err);
311
327
  }
package/lib/cache.js CHANGED
@@ -120,9 +120,15 @@ function lastKnownCacheWrite(sessionID) {
120
120
  // Clear per-session tracking. Full wipe on session.idle / session.deleted;
121
121
  // on session.compacted (keepToastDedup=true) the toast-dedup entries survive
122
122
  // the cleanup so consecutive compacted events still dedup.
123
+ //
124
+ // `keepSubagentRollup` preserves THIS session's parent attribution and token
125
+ // totals. A finished subagent goes idle while its parent is still running; the
126
+ // parent's live roll-up must keep counting that child, so the V2 idle handler
127
+ // passes `true` for children and lets the parent's own idle/deleted wipe them
128
+ // (see clearChildSubagentTracking).
123
129
  // ---------------------------------------------------------------------------
124
130
 
125
- function clearSessionTracking(sessionID, keepToastDedup = false) {
131
+ function clearSessionTracking(sessionID, keepToastDedup = false, keepSubagentRollup = false) {
126
132
  if (!sessionID) return;
127
133
  lastShown.delete(sessionID);
128
134
  breakdownCache.delete(sessionID);
@@ -134,7 +140,7 @@ function clearSessionTracking(sessionID, keepToastDedup = false) {
134
140
  lastCompactedToast.delete(sessionID);
135
141
  }
136
142
  // Attribution cache (re-resolved on the next toast if needed).
137
- sessionParentCache.delete(sessionID);
143
+ if (!keepSubagentRollup) sessionParentCache.delete(sessionID);
138
144
  // Idle-summary caches were previously left dirty — clean them too.
139
145
  lastKnownModelCache.delete(sessionID);
140
146
  lastKnownProviderCache.delete(sessionID);
@@ -145,7 +151,7 @@ function clearSessionTracking(sessionID, keepToastDedup = false) {
145
151
  lastKnownCacheWriteCache.delete(sessionID);
146
152
  lastBdWrite.delete(sessionID);
147
153
  // V2 subagent accumulation for this session.
148
- sessionTotals.delete(sessionID);
154
+ if (!keepSubagentRollup) sessionTotals.delete(sessionID);
149
155
  // V2 agent label + context-fallback throttle (previously left dirty).
150
156
  sessionAgentCache.delete(sessionID);
151
157
  v2ContextFetchTimes.delete(sessionID);
@@ -155,6 +161,33 @@ function clearSessionTracking(sessionID, keepToastDedup = false) {
155
161
  lastCompactionHookAt.delete(sessionID);
156
162
  }
157
163
 
164
+ // Drop the roll-up entries of every descendant of `rootID` (attribution plus
165
+ // accumulated totals). Called when a session itself goes idle or is deleted:
166
+ // until then, finished children stay in the cache so the live roll-up keeps
167
+ // reporting them instead of collapsing to "0 session(s)" the moment a child
168
+ // finishes. Breadth-first, because a sub-subagent hangs off a child and a
169
+ // single-level sweep would leave it as a permanent orphan. Deletion during Map
170
+ // iteration is well-defined in JS.
171
+ function clearChildSubagentTracking(rootID) {
172
+ if (!rootID) return;
173
+ const queue = [rootID];
174
+ const seen = new Set(queue);
175
+ while (queue.length > 0) {
176
+ const parent = queue.shift();
177
+ for (const [childID, parentID] of sessionParentCache) {
178
+ if (parentID !== parent) continue;
179
+ // Full per-session wipe, not just the two roll-up maps: the child's
180
+ // lastKnown*/breakdown/tool-schema caches would otherwise linger until
181
+ // FIFO eviction and never be updated again.
182
+ clearSessionTracking(childID);
183
+ if (!seen.has(childID)) {
184
+ seen.add(childID);
185
+ queue.push(childID);
186
+ }
187
+ }
188
+ }
189
+ }
190
+
158
191
  export {
159
192
  MAX_TRACKED_SESSIONS,
160
193
  BREAKDOWN_THROTTLE_MS,
@@ -190,4 +223,5 @@ export {
190
223
  lastKnownCacheRead,
191
224
  lastKnownCacheWrite,
192
225
  clearSessionTracking,
226
+ clearChildSubagentTracking,
193
227
  };