opencode-context-indicator 1.4.0 → 1.5.0
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 +55 -0
- package/README.md +37 -5
- package/index.js +18 -1
- package/lib/cache.js +22 -0
- package/lib/commands.js +121 -31
- package/lib/contributors.js +10 -41
- package/lib/dedup.js +4 -4
- package/lib/estimate.js +313 -4
- package/lib/limits.js +0 -1
- package/lib/state.js +79 -29
- package/lib/tui-data.d.ts +24 -0
- package/lib/tui-data.js +13 -26
- package/lib/tui-panel.tsx +53 -7
- package/lib/tui-sidebar.tsx +3 -0
- package/lib/v2.js +169 -21
- package/package.json +2 -2
package/lib/tui-panel.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { mkdirSync, writeFileSync } from "node:fs"
|
|
|
11
11
|
import { tmpdir } from "node:os"
|
|
12
12
|
import { basename, join } from "node:path"
|
|
13
13
|
import type { StateEntry } from "./tui-data.js"
|
|
14
|
-
import { collectFamily, fit, fmt, num, updatedAt } from "./tui-data.js"
|
|
14
|
+
import { collectFamily, fit, fmt, num, toolOutEntries, updatedAt } from "./tui-data.js"
|
|
15
15
|
import { token } from "./tui-theme.js"
|
|
16
16
|
import {
|
|
17
17
|
buildRedactedPayload,
|
|
@@ -475,11 +475,13 @@ export function BreakdownPanel(props: {
|
|
|
475
475
|
const model = safeText(r.model && r.model !== "unknown" ? r.model : "model?")
|
|
476
476
|
const ctx = fmt(num(r.ctx))
|
|
477
477
|
const d = panelDenom(r)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
478
|
+
const base =
|
|
479
|
+
d > 0
|
|
480
|
+
? `Context breakdown \u2014 ${model} ${ctx}/${fmt(d)} (${Math.round(
|
|
481
|
+
(num(r.ctx) / d) * 100,
|
|
482
|
+
)}%)`
|
|
483
|
+
: `Context breakdown \u2014 ${model} ${ctx}`
|
|
484
|
+
return r.compactStale === true ? `${base} (stale — compacted)` : base
|
|
483
485
|
}
|
|
484
486
|
|
|
485
487
|
const layout = (): {
|
|
@@ -523,10 +525,27 @@ export function BreakdownPanel(props: {
|
|
|
523
525
|
L.gutter +
|
|
524
526
|
fit(money(costOf(e.sessionID)), L.costW) +
|
|
525
527
|
L.gutter +
|
|
526
|
-
updatedAt(e.updatedAt)
|
|
528
|
+
updatedAt(e.updatedAt) +
|
|
529
|
+
(e.compactStale === true ? " (stale)" : "")
|
|
527
530
|
)
|
|
528
531
|
}
|
|
529
532
|
|
|
533
|
+
// One compact line: the main session's tool results split by tool name
|
|
534
|
+
// (server-side aggregate; the per-call "largest contributors" block above is
|
|
535
|
+
// the measured, client-side view).
|
|
536
|
+
const byToolLine = (): string => {
|
|
537
|
+
const all = data().root?.toolOutByTool
|
|
538
|
+
const entries = toolOutEntries(all, 5)
|
|
539
|
+
if (entries.length === 0) return ""
|
|
540
|
+
const total =
|
|
541
|
+
all && typeof all === "object"
|
|
542
|
+
? Object.values(all).filter((v) => num(v) > 0).length
|
|
543
|
+
: 0
|
|
544
|
+
const parts = entries.map(([name, val]) => `${safeText(name)} ${fmt(val)}`)
|
|
545
|
+
if (total > entries.length) parts.push(`+${total - entries.length} more`)
|
|
546
|
+
return parts.join(" \u00b7 ")
|
|
547
|
+
}
|
|
548
|
+
|
|
530
549
|
return (
|
|
531
550
|
<box flexDirection="column">
|
|
532
551
|
<text fg={base()}>{header()}</text>
|
|
@@ -551,6 +570,29 @@ export function BreakdownPanel(props: {
|
|
|
551
570
|
: "")}
|
|
552
571
|
</text>
|
|
553
572
|
</Show>
|
|
573
|
+
<Show
|
|
574
|
+
when={
|
|
575
|
+
num(data().root?.cacheRead) > 0 ||
|
|
576
|
+
num(data().root?.cacheWrite) > 0
|
|
577
|
+
}
|
|
578
|
+
>
|
|
579
|
+
<text fg={muted()}>
|
|
580
|
+
{" prompt cache r " +
|
|
581
|
+
fmt(num(data().root?.cacheRead)) +
|
|
582
|
+
" / w " +
|
|
583
|
+
fmt(num(data().root?.cacheWrite)) +
|
|
584
|
+
" (provider-reported)"}
|
|
585
|
+
</text>
|
|
586
|
+
</Show>
|
|
587
|
+
<Show when={num(data().root?.reclaim?.tokens) > 0}>
|
|
588
|
+
<text fg={muted()}>
|
|
589
|
+
{" reclaim ~" +
|
|
590
|
+
fmt(num(data().root?.reclaim?.tokens)) +
|
|
591
|
+
" redundant tool output (" +
|
|
592
|
+
num(data().root?.reclaim?.count) +
|
|
593
|
+
" dup)"}
|
|
594
|
+
</text>
|
|
595
|
+
</Show>
|
|
554
596
|
<Show when={messages().available}>
|
|
555
597
|
<text fg={base()}>{"largest contributors (measured)"}</text>
|
|
556
598
|
<Show
|
|
@@ -569,6 +611,10 @@ export function BreakdownPanel(props: {
|
|
|
569
611
|
}
|
|
570
612
|
</text>
|
|
571
613
|
</Show>
|
|
614
|
+
<Show when={byToolLine().length > 0}>
|
|
615
|
+
<text fg={base()}>{"tool results by tool"}</text>
|
|
616
|
+
<text fg={muted()}>{byToolLine()}</text>
|
|
617
|
+
</Show>
|
|
572
618
|
</Show>
|
|
573
619
|
</box>
|
|
574
620
|
)
|
package/lib/tui-sidebar.tsx
CHANGED
|
@@ -66,6 +66,9 @@ export function Sidebar(props: { entry: () => StateEntry | null }) {
|
|
|
66
66
|
: line("tool schemas", e()?.categories?.toolSchemas)}
|
|
67
67
|
</text>
|
|
68
68
|
<text fg={muted()}>{line("other", e()?.categories?.other)}</text>
|
|
69
|
+
<Show when={e()?.compactStale === true}>
|
|
70
|
+
<text fg={muted()}>{" stale — compacted"}</text>
|
|
71
|
+
</Show>
|
|
69
72
|
<text fg={muted()}>{`updated ${updatedAt(e()?.updatedAt)}`}</text>
|
|
70
73
|
</Show>
|
|
71
74
|
</box>
|
package/lib/v2.js
CHANGED
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
// from index.js.
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
+
breakdownCategories,
|
|
11
|
+
computeReclaim,
|
|
12
|
+
contentFingerprint,
|
|
10
13
|
estJson,
|
|
11
14
|
estimateTokens,
|
|
15
|
+
extractToolTarget,
|
|
12
16
|
fmt,
|
|
17
|
+
isSafeSessionKey,
|
|
13
18
|
oneLine,
|
|
19
|
+
safeSaneOrNull,
|
|
14
20
|
tokenCountOf,
|
|
21
|
+
TOOL_OUT_MAX_KEYS,
|
|
15
22
|
withTimeout,
|
|
16
23
|
} from "./estimate.js";
|
|
17
24
|
import {
|
|
@@ -22,6 +29,10 @@ import {
|
|
|
22
29
|
lastBdWrite,
|
|
23
30
|
lastKnownCtx,
|
|
24
31
|
lastKnownCtxCache,
|
|
32
|
+
lastKnownCacheRead,
|
|
33
|
+
lastKnownCacheReadCache,
|
|
34
|
+
lastKnownCacheWrite,
|
|
35
|
+
lastKnownCacheWriteCache,
|
|
25
36
|
lastKnownInput,
|
|
26
37
|
lastKnownInputCache,
|
|
27
38
|
lastKnownModel,
|
|
@@ -75,10 +86,11 @@ const V2_FALLBACK_TIMEOUT_MS = 3000;
|
|
|
75
86
|
// every schema does not run on every single model request.
|
|
76
87
|
// Cheap structural fingerprint of the tool schema map: count + total description
|
|
77
88
|
// length + number of schema property keys + two O(n) charCode checksums (mod
|
|
78
|
-
// 0xFFFFFF) over every description and over the
|
|
79
|
-
// checksums widen the fingerprint so two different
|
|
80
|
-
// the same length/key counts do not collide into a
|
|
81
|
-
// paying for full JSON.stringify (the whole point of
|
|
89
|
+
// 0xFFFFFF) over every description and over the PARAMETER names
|
|
90
|
+
// (input.properties keys). The checksums widen the fingerprint so two different
|
|
91
|
+
// schemas that happen to share the same length/key counts do not collide into a
|
|
92
|
+
// wrong cache hit — without paying for full JSON.stringify (the whole point of
|
|
93
|
+
// the cache).
|
|
82
94
|
function toolsFingerprint(tools) {
|
|
83
95
|
if (!tools || typeof tools !== "object") return "none";
|
|
84
96
|
let n = 0;
|
|
@@ -98,14 +110,16 @@ function toolsFingerprint(tools) {
|
|
|
98
110
|
const input = t?.input;
|
|
99
111
|
if (input && typeof input === "object") {
|
|
100
112
|
const props = input.properties;
|
|
101
|
-
|
|
113
|
+
// Hash the PARAMETER names (input.properties keys) when present — a
|
|
114
|
+
// property rename that keeps the same count/description must change the
|
|
115
|
+
// fingerprint, or the TTL cache would serve a stale schema estimate.
|
|
116
|
+
// Fall back to the top-level schema keys for schemas without `properties`.
|
|
117
|
+
const nameSource =
|
|
102
118
|
props && typeof props === "object"
|
|
103
|
-
? Object.keys(props)
|
|
104
|
-
: Object.keys(input)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const topKeys = Object.keys(input);
|
|
108
|
-
for (const k of topKeys) {
|
|
119
|
+
? Object.keys(props)
|
|
120
|
+
: Object.keys(input);
|
|
121
|
+
propKeys += nameSource.length;
|
|
122
|
+
for (const k of nameSource) {
|
|
109
123
|
for (let i = 0; i < k.length; i++) {
|
|
110
124
|
keyHash = (keyHash + k.charCodeAt(i)) & 0xffffff;
|
|
111
125
|
}
|
|
@@ -138,14 +152,69 @@ function toolResultChars(p) {
|
|
|
138
152
|
return estJson(r.value); // json | error | unexpected shapes
|
|
139
153
|
}
|
|
140
154
|
|
|
155
|
+
// Concatenated text of a ToolResultPart payload, for content FINGERPRINTING
|
|
156
|
+
// (bounded — the first ~4KB is enough to spot byte-identical outputs). Mirrors
|
|
157
|
+
// the shapes handled by toolResultChars; file items are URI references, skipped.
|
|
158
|
+
function toolResultText(p) {
|
|
159
|
+
const r = p?.result;
|
|
160
|
+
if (!r || typeof r !== "object") return "";
|
|
161
|
+
if (r.type === "content" && Array.isArray(r.value)) {
|
|
162
|
+
let out = "";
|
|
163
|
+
for (const item of r.value) {
|
|
164
|
+
if (item?.type === "text" && typeof item.text === "string") {
|
|
165
|
+
out += item.text;
|
|
166
|
+
if (out.length > 4096) break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
if (r.type === "text" && typeof r.value === "string") return r.value;
|
|
172
|
+
try {
|
|
173
|
+
return JSON.stringify(r.value);
|
|
174
|
+
} catch {
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Bound on tool-call instances tracked per request for waste detection (the
|
|
180
|
+
// message history is untrusted and a runaway session could be huge).
|
|
181
|
+
const MAX_WASTE_INSTANCES = 256;
|
|
182
|
+
|
|
183
|
+
// Accumulate the per-tool tool-result estimate. Tool names are untrusted
|
|
184
|
+
// (host tool ids, plugin-defined tools): unsafe names and any name beyond the
|
|
185
|
+
// cap fold into "other tools" so the bucket never becomes a prototype vector
|
|
186
|
+
// and the map stays bounded. The sum of the map mirrors the toolOut category.
|
|
187
|
+
// The cap is the SHARED TOOL_OUT_MAX_KEYS (lib/estimate.js) so the accumulation
|
|
188
|
+
// bound and the persisted/displayed bound can never drift apart.
|
|
189
|
+
function addToolOut(map, name, chars) {
|
|
190
|
+
if (!(chars > 0)) return;
|
|
191
|
+
const safe =
|
|
192
|
+
isSafeSessionKey(name) && name.length <= 64 ? name : "other tools";
|
|
193
|
+
if (
|
|
194
|
+
safe !== "other tools" &&
|
|
195
|
+
!Object.prototype.hasOwnProperty.call(map, safe) &&
|
|
196
|
+
Object.keys(map).length >= TOOL_OUT_MAX_KEYS
|
|
197
|
+
) {
|
|
198
|
+
map["other tools"] = (map["other tools"] || 0) + chars;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
map[safe] = (map[safe] || 0) + chars;
|
|
202
|
+
}
|
|
203
|
+
|
|
141
204
|
function estimateBreakdownFromContext(context, sessionID) {
|
|
142
205
|
let userChars = 0;
|
|
143
206
|
let assistantChars = 0;
|
|
144
207
|
let reasoningChars = 0;
|
|
145
208
|
let toolArgsChars = 0;
|
|
146
209
|
let toolOutChars = 0;
|
|
210
|
+
const toolOutByTool = {};
|
|
147
211
|
|
|
148
212
|
const messages = Array.isArray(context?.messages) ? context.messages : [];
|
|
213
|
+
// Waste detection: correlate tool-call (input -> target) with its tool-result
|
|
214
|
+
// (-> tokens + content fingerprint) by call id, over the calls still present
|
|
215
|
+
// in the assembled context.
|
|
216
|
+
const callById = new Map();
|
|
217
|
+
const instances = [];
|
|
149
218
|
for (const msg of messages) {
|
|
150
219
|
const parts = Array.isArray(msg?.content) ? msg.content : [];
|
|
151
220
|
for (const p of parts) {
|
|
@@ -153,7 +222,19 @@ function estimateBreakdownFromContext(context, sessionID) {
|
|
|
153
222
|
// carrying them varies by host version (user, assistant, or the dedicated
|
|
154
223
|
// "tool" role) — count them role-independently.
|
|
155
224
|
if (p?.type === "tool-result") {
|
|
156
|
-
|
|
225
|
+
const chars = toolResultChars(p);
|
|
226
|
+
toolOutChars += chars;
|
|
227
|
+
addToolOut(toolOutByTool, p?.name, chars);
|
|
228
|
+
if (chars > 0 && instances.length < MAX_WASTE_INSTANCES) {
|
|
229
|
+
const call = p?.id != null ? callById.get(p.id) : null;
|
|
230
|
+
const name = p?.name || call?.name || null;
|
|
231
|
+
instances.push({
|
|
232
|
+
name,
|
|
233
|
+
target: extractToolTarget(name, call?.input),
|
|
234
|
+
tokens: chars,
|
|
235
|
+
fp: contentFingerprint(toolResultText(p)),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
157
238
|
continue;
|
|
158
239
|
}
|
|
159
240
|
if (msg?.role === "user") {
|
|
@@ -165,10 +246,15 @@ function estimateBreakdownFromContext(context, sessionID) {
|
|
|
165
246
|
reasoningChars += estimateTokens(p.text);
|
|
166
247
|
} else if (p?.type === "tool-call") {
|
|
167
248
|
toolArgsChars += estJson(p.input);
|
|
249
|
+
if (p?.id != null && callById.size < MAX_WASTE_INSTANCES && !callById.has(p.id)) {
|
|
250
|
+
callById.set(p.id, { name: p.name, input: p.input });
|
|
251
|
+
}
|
|
168
252
|
}
|
|
169
253
|
}
|
|
170
254
|
}
|
|
171
255
|
}
|
|
256
|
+
const reclaimRaw = instances.length > 0 ? computeReclaim(instances) : null;
|
|
257
|
+
const reclaim = reclaimRaw && reclaimRaw.tokens > 0 ? reclaimRaw : null;
|
|
172
258
|
|
|
173
259
|
let systemChars = 0;
|
|
174
260
|
const system = Array.isArray(context?.system) ? context.system : [];
|
|
@@ -206,8 +292,10 @@ function estimateBreakdownFromContext(context, sessionID) {
|
|
|
206
292
|
reasoningChars,
|
|
207
293
|
toolArgsChars,
|
|
208
294
|
toolOutChars,
|
|
295
|
+
toolOutByTool,
|
|
209
296
|
systemChars,
|
|
210
297
|
toolSchemasChars,
|
|
298
|
+
reclaim,
|
|
211
299
|
};
|
|
212
300
|
}
|
|
213
301
|
|
|
@@ -259,6 +347,8 @@ function estimateBreakdownFromClientMessages(messages) {
|
|
|
259
347
|
let reasoningChars = 0;
|
|
260
348
|
let toolArgsChars = 0;
|
|
261
349
|
let toolOutChars = 0;
|
|
350
|
+
const toolOutByTool = {};
|
|
351
|
+
const instances = [];
|
|
262
352
|
for (const m of messages || []) {
|
|
263
353
|
if (!m || typeof m !== "object") continue;
|
|
264
354
|
if (m.type === "user") {
|
|
@@ -282,24 +372,43 @@ function estimateBreakdownFromClientMessages(messages) {
|
|
|
282
372
|
// policy as the hook path).
|
|
283
373
|
const content = p?.state?.content;
|
|
284
374
|
if (Array.isArray(content)) {
|
|
375
|
+
let outChars = 0;
|
|
376
|
+
let text = "";
|
|
285
377
|
for (const item of content) {
|
|
286
378
|
if (item?.type === "text" && typeof item.text === "string") {
|
|
287
|
-
|
|
379
|
+
outChars += estimateTokens(item.text);
|
|
380
|
+
if (text.length <= 4096) text += item.text;
|
|
288
381
|
}
|
|
289
382
|
}
|
|
383
|
+
toolOutChars += outChars;
|
|
384
|
+
// The persisted tool part carries its name on `p.name`
|
|
385
|
+
// (SessionMessageAssistantTool.name) — same field as the hook path.
|
|
386
|
+
addToolOut(toolOutByTool, p?.name, outChars);
|
|
387
|
+
if (outChars > 0 && instances.length < MAX_WASTE_INSTANCES) {
|
|
388
|
+
instances.push({
|
|
389
|
+
name: p?.name || null,
|
|
390
|
+
target: extractToolTarget(p?.name, input),
|
|
391
|
+
tokens: outChars,
|
|
392
|
+
fp: contentFingerprint(text),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
290
395
|
}
|
|
291
396
|
}
|
|
292
397
|
}
|
|
293
398
|
}
|
|
294
399
|
}
|
|
400
|
+
const reclaimRaw = instances.length > 0 ? computeReclaim(instances) : null;
|
|
401
|
+
const reclaim = reclaimRaw && reclaimRaw.tokens > 0 ? reclaimRaw : null;
|
|
295
402
|
return {
|
|
296
403
|
userChars,
|
|
297
404
|
assistantChars,
|
|
298
405
|
reasoningChars,
|
|
299
406
|
toolArgsChars,
|
|
300
407
|
toolOutChars,
|
|
408
|
+
toolOutByTool,
|
|
301
409
|
systemChars: null,
|
|
302
410
|
toolSchemasChars: null,
|
|
411
|
+
reclaim,
|
|
303
412
|
};
|
|
304
413
|
}
|
|
305
414
|
|
|
@@ -423,14 +532,9 @@ function buildLiveLinesV2(sessionID, modelID, providerID, inputTokens, ctxTokens
|
|
|
423
532
|
const source = bd?.source || "none";
|
|
424
533
|
const rt = typeof reasoningTokens === "number" ? reasoningTokens : 0;
|
|
425
534
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
(bd?.toolArgsChars || 0) +
|
|
430
|
-
(bd?.toolOutChars || 0) +
|
|
431
|
-
(bd?.systemChars || 0) +
|
|
432
|
-
(bd?.toolSchemasChars || 0);
|
|
433
|
-
const other = Math.max(0, inputTokens - sumEstimates);
|
|
535
|
+
// Residual = input minus every measured category (shared normaliser; `bd`
|
|
536
|
+
// may be absent -> everything 0 and the residual is the whole input).
|
|
537
|
+
const other = breakdownCategories(bd, inputTokens).other;
|
|
434
538
|
|
|
435
539
|
if (bd) {
|
|
436
540
|
lines.push(` user ${fmt(bd.userChars || 0)} (est tokens)`);
|
|
@@ -463,6 +567,25 @@ function buildLiveLinesV2(sessionID, modelID, providerID, inputTokens, ctxTokens
|
|
|
463
567
|
lines.push(` other ${fmt(other)} (= input - sum of estimates)`);
|
|
464
568
|
lines.push(` source ${source}`);
|
|
465
569
|
|
|
570
|
+
// Provider-reported prompt cache (EXACT, not an estimate; a subset of input —
|
|
571
|
+
// never folded into the category sum). Absent when the provider stays silent.
|
|
572
|
+
const cr = lastKnownCacheRead(sessionID);
|
|
573
|
+
const cw = lastKnownCacheWrite(sessionID);
|
|
574
|
+
lines.push(
|
|
575
|
+
cr == null && cw == null
|
|
576
|
+
? ` prompt cache n/a (provider reported no cache tokens)`
|
|
577
|
+
: ` prompt cache read=${fmt(cr || 0)} write=${fmt(cw || 0)} (provider-reported; subset of input)`,
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
// Redundant tool output already in context (repeated target / identical
|
|
581
|
+
// output). An estimate of what trimming/compaction could reclaim.
|
|
582
|
+
const rcl = bd?.reclaim || null;
|
|
583
|
+
if (rcl && rcl.tokens > 0) {
|
|
584
|
+
lines.push(
|
|
585
|
+
` reclaim ~${fmt(rcl.tokens)} redundant tool output (${rcl.count} duplicate(s) — estimate)`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
466
589
|
try {
|
|
467
590
|
const sub = aggregateSubagentsV2(sessionID);
|
|
468
591
|
if (sub.count > 0) {
|
|
@@ -569,6 +692,15 @@ async function handleV2Event(ctx, event) {
|
|
|
569
692
|
lastKnownInput(sid),
|
|
570
693
|
lastKnownCtx(sid),
|
|
571
694
|
lastKnownReasoning(sid),
|
|
695
|
+
// This write re-persists the PRE-compaction snapshot (the compaction
|
|
696
|
+
// flow fires no context hook — see README): mark the entry stale so the
|
|
697
|
+
// UI shows the numbers as pre-compaction until the next model request
|
|
698
|
+
// refreshes them.
|
|
699
|
+
{
|
|
700
|
+
compacted: true,
|
|
701
|
+
cacheRead: lastKnownCacheRead(sid),
|
|
702
|
+
cacheWrite: lastKnownCacheWrite(sid),
|
|
703
|
+
},
|
|
572
704
|
);
|
|
573
705
|
pushFinalSummary(
|
|
574
706
|
`compacted:${event.id ?? sid}`,
|
|
@@ -608,6 +740,10 @@ async function handleV2Event(ctx, event) {
|
|
|
608
740
|
lastKnownInput(sid),
|
|
609
741
|
lastKnownCtx(sid),
|
|
610
742
|
lastKnownReasoning(sid),
|
|
743
|
+
{
|
|
744
|
+
cacheRead: lastKnownCacheRead(sid),
|
|
745
|
+
cacheWrite: lastKnownCacheWrite(sid),
|
|
746
|
+
},
|
|
611
747
|
);
|
|
612
748
|
pushFinalSummary(`final:${event.id ?? sid}`, [
|
|
613
749
|
`[${new Date().toISOString()}] FINAL session=${sid}`,
|
|
@@ -630,10 +766,16 @@ async function handleV2Event(ctx, event) {
|
|
|
630
766
|
const output = typeof t.output === "number" ? t.output : 0;
|
|
631
767
|
const reasoning = typeof t.reasoning === "number" ? t.reasoning : 0;
|
|
632
768
|
const contextTokens = tokenCountOf(t);
|
|
769
|
+
// Provider-reported prompt-cache figures (exact): a subset of `input`.
|
|
770
|
+
// Sanitised — a NaN/negative/absurd provider value must never be persisted.
|
|
771
|
+
const cacheRead = safeSaneOrNull(t.cache?.read ?? t.cacheRead);
|
|
772
|
+
const cacheWrite = safeSaneOrNull(t.cache?.write ?? t.cacheWrite);
|
|
633
773
|
|
|
634
774
|
lastKnownInputCache.set(sid, input);
|
|
635
775
|
lastKnownCtxCache.set(sid, contextTokens);
|
|
636
776
|
lastKnownReasoningCache.set(sid, reasoning);
|
|
777
|
+
if (cacheRead != null) lastKnownCacheReadCache.set(sid, cacheRead);
|
|
778
|
+
if (cacheWrite != null) lastKnownCacheWriteCache.set(sid, cacheWrite);
|
|
637
779
|
if (!lastKnownModelCache.has(sid) && data.model) {
|
|
638
780
|
// Some builds put the model on the step payload; harmless when absent.
|
|
639
781
|
if (typeof data.model.id === "string") lastKnownModelCache.set(sid, data.model.id);
|
|
@@ -644,6 +786,8 @@ async function handleV2Event(ctx, event) {
|
|
|
644
786
|
pruneMap(lastKnownInputCache, MAX_TRACKED_SESSIONS);
|
|
645
787
|
pruneMap(lastKnownCtxCache, MAX_TRACKED_SESSIONS);
|
|
646
788
|
pruneMap(lastKnownReasoningCache, MAX_TRACKED_SESSIONS);
|
|
789
|
+
pruneMap(lastKnownCacheReadCache, MAX_TRACKED_SESSIONS);
|
|
790
|
+
pruneMap(lastKnownCacheWriteCache, MAX_TRACKED_SESSIONS);
|
|
647
791
|
pruneMap(lastKnownModelCache, MAX_TRACKED_SESSIONS);
|
|
648
792
|
pruneMap(lastKnownProviderCache, MAX_TRACKED_SESSIONS);
|
|
649
793
|
|
|
@@ -702,6 +846,10 @@ async function handleV2Event(ctx, event) {
|
|
|
702
846
|
input,
|
|
703
847
|
contextTokens,
|
|
704
848
|
reasoning,
|
|
849
|
+
{
|
|
850
|
+
cacheRead: lastKnownCacheRead(sid),
|
|
851
|
+
cacheWrite: lastKnownCacheWrite(sid),
|
|
852
|
+
},
|
|
705
853
|
);
|
|
706
854
|
writeLog(lines);
|
|
707
855
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-context-indicator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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.30",
|
|
68
68
|
"@opentui/core": "0.5.17",
|
|
69
69
|
"@types/node": "26.6.3",
|
|
70
70
|
"esbuild": "^0.28.2",
|