opencode-context-indicator 1.5.0 → 1.5.1
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 +14 -0
- package/README.md +13 -9
- package/index.js +20 -2
- package/lib/cache.js +10 -0
- package/lib/v2.js +118 -17
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.1 — 2026-10-19
|
|
8
|
+
|
|
9
|
+
- **Compaction now handled by the real `compaction` hook.** opencode dispatches
|
|
10
|
+
a `compaction` hook at the start of a compaction request with the same
|
|
11
|
+
pre-compaction payload as the `context` hook. The plugin registers it and
|
|
12
|
+
persists the *true* pre-compaction breakdown at that moment — previously the
|
|
13
|
+
trailing `session.compacted` event re-persisted whatever the last model
|
|
14
|
+
request had left behind. The event is kept as a fallback (hosts/paths where
|
|
15
|
+
the hook does not fire), and a no-op compaction ("Nothing to compact yet")
|
|
16
|
+
writes no stale entry. Same `(stale — compacted)` UX, more accurate numbers.
|
|
17
|
+
- Dev: pinned the `@opencode/plugin` devDependency to `2.0.32`.
|
|
18
|
+
- Tests: T25 / T25a / T25b / T25c (compaction-hook snapshot, no-op guard,
|
|
19
|
+
event fallback, cold-cache clobber guard) — 62 unit checks total.
|
|
20
|
+
|
|
7
21
|
## 1.5.0 — 2026-10-16
|
|
8
22
|
|
|
9
23
|
Highlights: two new context-health signals — the **provider-reported prompt
|
package/README.md
CHANGED
|
@@ -529,9 +529,10 @@ V2 path). It does not register configurable keys of its own.
|
|
|
529
529
|
|
|
530
530
|
The package default export is a dual plugin:
|
|
531
531
|
|
|
532
|
-
* `setup(ctx)` — V2 entry (OpenCode ≥ 2.0.16). Registers
|
|
533
|
-
to capture the final messages / system
|
|
534
|
-
|
|
532
|
+
* `setup(ctx)` — V2 entry (OpenCode ≥ 2.0.16). Registers `session.hook("context", …)`
|
|
533
|
+
and `session.hook("compaction", …)` to capture the final messages / system
|
|
534
|
+
prompt / tool schemas right before each model request (and each compaction
|
|
535
|
+
request), subscribes to the event stream (`session.created`,
|
|
535
536
|
`session.step.ended`, `session.idle`, `session.compacted`, failure events),
|
|
536
537
|
records limits from the model registry and registers the
|
|
537
538
|
`/context` + `/context-breakdown` slash commands (via
|
|
@@ -549,12 +550,15 @@ duplicate final summaries are suppressed with an atomic claim marker
|
|
|
549
550
|
(`lib/dedup.js`). Every file / estimate / subagent path is fault-tolerant: a
|
|
550
551
|
failure is logged and never breaks the main event or toast path.
|
|
551
552
|
|
|
552
|
-
**Compaction staleness.** OpenCode
|
|
553
|
-
|
|
554
|
-
the
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
553
|
+
**Compaction staleness.** OpenCode dispatches a dedicated `compaction` hook at
|
|
554
|
+
the start of a compaction request, carrying the same pre-compaction payload as
|
|
555
|
+
the `context` hook. The plugin registers it and persists the *true*
|
|
556
|
+
pre-compaction breakdown at that moment, flagging the entry stale
|
|
557
|
+
(`compactStale` in `state.json`) so every surface shows `(stale — compacted)`
|
|
558
|
+
until the next model request refreshes it. The trailing `session.compacted`
|
|
559
|
+
event is kept as a fallback (for hosts or paths where the hook does not fire);
|
|
560
|
+
a no-op compaction ("Nothing to compact yet") writes no stale entry. The data
|
|
561
|
+
is always exactly as fresh as the last model request in the session.
|
|
558
562
|
|
|
559
563
|
This plugin does not touch `opencode-token-monitor`
|
|
560
564
|
(`token_stats` / `token_history` / `token_export` keep working unchanged).
|
package/index.js
CHANGED
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
*/
|
|
151
151
|
|
|
152
152
|
import { makeV1 } from "./lib/v1.js";
|
|
153
|
-
import { handleV2Event, onV2Context, estimateBreakdownFromContext, estimateBreakdownFromClientMessages } from "./lib/v2.js";
|
|
153
|
+
import { handleV2Event, onV2Context, onV2Compaction, estimateBreakdownFromContext, estimateBreakdownFromClientMessages } from "./lib/v2.js";
|
|
154
154
|
import { breakdownCategories, computeReclaim, contentFingerprint, estimateTokens, extractToolTarget, isSafeSessionKey, safeCell, safeReclaim, safeSaneOrNull, safeToolOutByTool, sumCategories, toolOutEntries } from "./lib/estimate.js";
|
|
155
155
|
import {
|
|
156
156
|
commandContextBreakdown,
|
|
@@ -170,7 +170,7 @@ import {
|
|
|
170
170
|
modelLimits,
|
|
171
171
|
reserveFor,
|
|
172
172
|
} from "./lib/limits.js";
|
|
173
|
-
import { lastKnownCacheRead, lastKnownCacheWrite, sessionAgentCache, sessionParentCache } from "./lib/cache.js";
|
|
173
|
+
import { lastKnownCacheRead, lastKnownCacheWrite, lastKnownCtxCache, lastKnownInputCache, lastKnownReasoningCache, sessionAgentCache, sessionParentCache } from "./lib/cache.js";
|
|
174
174
|
import {
|
|
175
175
|
logErr,
|
|
176
176
|
readStateSnapshot,
|
|
@@ -204,6 +204,7 @@ export {
|
|
|
204
204
|
estimateBreakdownFromContext,
|
|
205
205
|
estimateBreakdownFromClientMessages,
|
|
206
206
|
onV2Context,
|
|
207
|
+
onV2Compaction,
|
|
207
208
|
safeToolOutByTool,
|
|
208
209
|
toolOutEntries,
|
|
209
210
|
sumCategories,
|
|
@@ -218,6 +219,10 @@ export {
|
|
|
218
219
|
safeSaneOrNull,
|
|
219
220
|
lastKnownCacheRead,
|
|
220
221
|
lastKnownCacheWrite,
|
|
222
|
+
lastKnownCtxCache,
|
|
223
|
+
lastKnownInputCache,
|
|
224
|
+
lastKnownReasoningCache,
|
|
225
|
+
handleV2Event,
|
|
221
226
|
};
|
|
222
227
|
|
|
223
228
|
// ---------------------------------------------------------------------------
|
|
@@ -238,6 +243,19 @@ export default {
|
|
|
238
243
|
logErr("v2 context hook registration failed", err);
|
|
239
244
|
}
|
|
240
245
|
|
|
246
|
+
// The compaction request carries the same pre-compaction context payload as
|
|
247
|
+
// the context hook. Registering it lets us persist the TRUE pre-compaction
|
|
248
|
+
// breakdown (see onV2Compaction); the `session.compacted` event then only
|
|
249
|
+
// acts as a fallback. A registration failure is non-fatal — the event path
|
|
250
|
+
// still marks the entry stale.
|
|
251
|
+
try {
|
|
252
|
+
registrations.push(
|
|
253
|
+
await ctx.session.hook("compaction", (context) => onV2Compaction(context)),
|
|
254
|
+
);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
logErr("v2 compaction hook registration failed", err);
|
|
257
|
+
}
|
|
258
|
+
|
|
241
259
|
// The model registry is captured asynchronously through ensureModelLimits
|
|
242
260
|
// (ctx.model.list(), rank 3) — the authoritative source that carries the
|
|
243
261
|
// user's merged config overrides. ctx.model.transform fires synchronously
|
package/lib/cache.js
CHANGED
|
@@ -69,6 +69,13 @@ const lastErrorToast = new Map();
|
|
|
69
69
|
// sessionID -> timestamp — dedup for session.compacted toasts
|
|
70
70
|
const lastCompactedToast = new Map();
|
|
71
71
|
|
|
72
|
+
// sessionID -> timestamp — when the `compaction` HOOK last fired for a session.
|
|
73
|
+
// The hook is dispatched at the START of a compaction request (before the model
|
|
74
|
+
// summarises), carrying the exact pre-compaction context, so it lets us persist
|
|
75
|
+
// the TRUE pre-compaction breakdown. It also tells the later `session.compacted`
|
|
76
|
+
// EVENT that the hook already marked the entry (the event stays a fallback).
|
|
77
|
+
const lastCompactionHookAt = new Map();
|
|
78
|
+
|
|
72
79
|
// sessionID -> parentID (null = main session) — [main]/[sub] attribution cache
|
|
73
80
|
const sessionParentCache = new Map();
|
|
74
81
|
|
|
@@ -144,6 +151,8 @@ function clearSessionTracking(sessionID, keepToastDedup = false) {
|
|
|
144
151
|
v2ContextFetchTimes.delete(sessionID);
|
|
145
152
|
// Per-session tool-schema TTL cache (see estimateBreakdownFromContext).
|
|
146
153
|
toolSchemasBySession.delete(sessionID);
|
|
154
|
+
// Compaction-hook marker (re-armed by the next compaction on this session).
|
|
155
|
+
lastCompactionHookAt.delete(sessionID);
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
export {
|
|
@@ -168,6 +177,7 @@ export {
|
|
|
168
177
|
lastBdWrite,
|
|
169
178
|
lastErrorToast,
|
|
170
179
|
lastCompactedToast,
|
|
180
|
+
lastCompactionHookAt,
|
|
171
181
|
sessionParentCache,
|
|
172
182
|
sessionTotals,
|
|
173
183
|
sessionAgentCache,
|
package/lib/v2.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
lastBdWrite,
|
|
30
30
|
lastKnownCtx,
|
|
31
31
|
lastKnownCtxCache,
|
|
32
|
+
lastCompactionHookAt,
|
|
32
33
|
lastKnownCacheRead,
|
|
33
34
|
lastKnownCacheReadCache,
|
|
34
35
|
lastKnownCacheWrite,
|
|
@@ -68,6 +69,11 @@ const V2_CONTEXT_FETCH_THROTTLE_MS = 5000; // min gap between fallback fetches
|
|
|
68
69
|
// event handlers): cap each at 3s so a slow/hung client cannot pin resources.
|
|
69
70
|
const V2_FALLBACK_TIMEOUT_MS = 3000;
|
|
70
71
|
|
|
72
|
+
// Window within which a `session.compacted` EVENT is considered already handled
|
|
73
|
+
// by the preceding `compaction` HOOK: if the hook marked the entry recently, the
|
|
74
|
+
// event only logs + clears tracking instead of re-persisting the snapshot.
|
|
75
|
+
const COMPACTION_HOOK_FALLBACK_MS = 60000;
|
|
76
|
+
|
|
71
77
|
// Estimate the per-category breakdown directly from a V2 SessionContext.
|
|
72
78
|
// `context.messages` are @opencode/ai Message objects:
|
|
73
79
|
// { role, content: [ {type:"text",text} | {type:"media"} |
|
|
@@ -329,6 +335,93 @@ function onV2Context(context) {
|
|
|
329
335
|
}
|
|
330
336
|
}
|
|
331
337
|
|
|
338
|
+
// V2 compaction hook handler: opencode dispatches `compaction` at the START of a
|
|
339
|
+
// compaction request with the SAME pre-compaction payload as the context hook
|
|
340
|
+
// (system / messages / tools / model). We use it to persist the TRUE
|
|
341
|
+
// pre-compaction breakdown instead of relying on the `session.compacted` event
|
|
342
|
+
// to re-persist whatever a previous request left behind.
|
|
343
|
+
//
|
|
344
|
+
// The entry is marked stale BY CONSTRUCTION (the context is about to be
|
|
345
|
+
// compacted, so the numbers change once the summary lands and the next context
|
|
346
|
+
// hook fires). Guard: the hook also fires for a NO-OP compaction ("Nothing to
|
|
347
|
+
// compact yet") on a session with no completed request — there is no measured
|
|
348
|
+
// context to mark, so we only cache and skip the state write (no fake stale
|
|
349
|
+
// badge).
|
|
350
|
+
function onV2Compaction(context) {
|
|
351
|
+
try {
|
|
352
|
+
if (!context || typeof context !== "object") return;
|
|
353
|
+
const sid = context.sessionID;
|
|
354
|
+
if (!sid) return;
|
|
355
|
+
|
|
356
|
+
const parts = estimateBreakdownFromContext(context, sid);
|
|
357
|
+
breakdownCache.set(sid, {
|
|
358
|
+
...parts,
|
|
359
|
+
capturedAt: Date.now(),
|
|
360
|
+
source: "compaction-hook",
|
|
361
|
+
});
|
|
362
|
+
pruneMap(breakdownCache, MAX_TRACKED_SESSIONS);
|
|
363
|
+
|
|
364
|
+
const model = context.model;
|
|
365
|
+
if (model && typeof model === "object") {
|
|
366
|
+
if (typeof model.id === "string") lastKnownModelCache.set(sid, model.id);
|
|
367
|
+
if (typeof model.providerID === "string") {
|
|
368
|
+
lastKnownProviderCache.set(sid, model.providerID);
|
|
369
|
+
}
|
|
370
|
+
pruneMap(lastKnownModelCache, MAX_TRACKED_SESSIONS);
|
|
371
|
+
pruneMap(lastKnownProviderCache, MAX_TRACKED_SESSIONS);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Arm the fallback guard for the trailing `session.compacted` event.
|
|
375
|
+
lastCompactionHookAt.set(sid, Date.now());
|
|
376
|
+
pruneMap(lastCompactionHookAt, MAX_TRACKED_SESSIONS);
|
|
377
|
+
|
|
378
|
+
const ctxTokens = lastKnownCtx(sid);
|
|
379
|
+
if (ctxTokens > 0) {
|
|
380
|
+
const lines = buildLiveLinesV2(
|
|
381
|
+
sid,
|
|
382
|
+
lastKnownModel(sid),
|
|
383
|
+
lastKnownProvider(sid),
|
|
384
|
+
lastKnownInput(sid),
|
|
385
|
+
ctxTokens,
|
|
386
|
+
lastKnownReasoning(sid),
|
|
387
|
+
);
|
|
388
|
+
writeStateFile(
|
|
389
|
+
sid,
|
|
390
|
+
lastKnownModel(sid),
|
|
391
|
+
lastKnownProvider(sid),
|
|
392
|
+
lastKnownInput(sid),
|
|
393
|
+
ctxTokens,
|
|
394
|
+
lastKnownReasoning(sid),
|
|
395
|
+
{
|
|
396
|
+
compacted: true,
|
|
397
|
+
cacheRead: lastKnownCacheRead(sid),
|
|
398
|
+
cacheWrite: lastKnownCacheWrite(sid),
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
// Claim key derived from the PAYLOAD shape (message count + total
|
|
402
|
+
// estimated chars): identical across plugin instances for the SAME
|
|
403
|
+
// compaction (cross-instance dedup preserved) yet distinct per compaction
|
|
404
|
+
// — a session-only key would log just the first compaction on a session.
|
|
405
|
+
const messages = Array.isArray(context.messages) ? context.messages : [];
|
|
406
|
+
const sig =
|
|
407
|
+
`${messages.length}:` +
|
|
408
|
+
(parts.userChars +
|
|
409
|
+
parts.assistantChars +
|
|
410
|
+
parts.reasoningChars +
|
|
411
|
+
parts.toolArgsChars +
|
|
412
|
+
parts.toolOutChars +
|
|
413
|
+
(parts.systemChars || 0) +
|
|
414
|
+
(parts.toolSchemasChars || 0));
|
|
415
|
+
pushFinalSummary(`compaction-hook:${sid}:${sig}`, [
|
|
416
|
+
`[${new Date().toISOString()}] COMPACTION (hook) session=${sid} — pre-compaction snapshot captured`,
|
|
417
|
+
...lines.slice(1),
|
|
418
|
+
]);
|
|
419
|
+
}
|
|
420
|
+
} catch (err) {
|
|
421
|
+
logErr("v2 compaction hook failed", err);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
332
425
|
// ---------------------------------------------------------------------------
|
|
333
426
|
// V2 fallback: derive the message-based categories from the session's persisted
|
|
334
427
|
// message list when the context hook did NOT populate the cache for this
|
|
@@ -685,23 +778,30 @@ async function handleV2Event(ctx, event) {
|
|
|
685
778
|
lastKnownCtx(sid),
|
|
686
779
|
lastKnownReasoning(sid),
|
|
687
780
|
);
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
781
|
+
// The `compaction` HOOK already captured + persisted the exact
|
|
782
|
+
// pre-compaction snapshot and marked the entry stale. This event is now a
|
|
783
|
+
// FALLBACK: it re-persists only when the hook did NOT fire recently (host
|
|
784
|
+
// gap, mirror instance, or a compaction path that bypassed the hook).
|
|
785
|
+
// Either way the write re-persists the PRE-compaction numbers and marks
|
|
786
|
+
// the entry stale, so the UI shows them as pre-compaction until the next
|
|
787
|
+
// model request refreshes them.
|
|
788
|
+
const hookAt = lastCompactionHookAt.get(sid) || 0;
|
|
789
|
+
const handledByHook = Date.now() - hookAt < COMPACTION_HOOK_FALLBACK_MS;
|
|
790
|
+
if (!handledByHook) {
|
|
791
|
+
writeStateFile(
|
|
792
|
+
sid,
|
|
793
|
+
lastKnownModel(sid),
|
|
794
|
+
lastKnownProvider(sid),
|
|
795
|
+
lastKnownInput(sid),
|
|
796
|
+
lastKnownCtx(sid),
|
|
797
|
+
lastKnownReasoning(sid),
|
|
798
|
+
{
|
|
799
|
+
compacted: true,
|
|
800
|
+
cacheRead: lastKnownCacheRead(sid),
|
|
801
|
+
cacheWrite: lastKnownCacheWrite(sid),
|
|
802
|
+
},
|
|
803
|
+
);
|
|
804
|
+
}
|
|
705
805
|
pushFinalSummary(
|
|
706
806
|
`compacted:${event.id ?? sid}`,
|
|
707
807
|
[
|
|
@@ -886,6 +986,7 @@ async function handleV2Event(ctx, event) {
|
|
|
886
986
|
|
|
887
987
|
export {
|
|
888
988
|
onV2Context,
|
|
989
|
+
onV2Compaction,
|
|
889
990
|
handleV2Event,
|
|
890
991
|
estimateBreakdownFromContext,
|
|
891
992
|
estimateBreakdownFromClientMessages,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-context-indicator",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Real-time context-window usage for OpenCode: per-category token breakdown, a zero-LLM /cx panel with per-session cost and a searchable family-session picker, and a TUI sidebar. Dual V1 + V2 plugin entry.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@opentui/solid": "0.5.17"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@opencode/plugin": "2.0.
|
|
67
|
+
"@opencode/plugin": "2.0.32",
|
|
68
68
|
"@opentui/core": "0.5.17",
|
|
69
69
|
"@types/node": "26.6.3",
|
|
70
70
|
"esbuild": "^0.28.2",
|