opencode-context-indicator 1.8.2 → 1.8.3
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 +27 -0
- package/README.md +12 -8
- package/dist/gui-experimental.cjs +1 -1
- package/index.js +2 -0
- package/lib/state.js +196 -16
- package/lib/v1.js +5 -0
- package/lib/v2.js +6 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ 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.8.3 — 2026-11-03
|
|
8
|
+
|
|
9
|
+
- **A deleted session disappears immediately.** `session.deleted` now removes the
|
|
10
|
+
session's `state.json` entry - and every entry under it in the `parentID` chain -
|
|
11
|
+
instead of leaving it to the 7-day tombstone. The row used to stay in
|
|
12
|
+
`/context-breakdown`, the Desktop tab and the `/cx` picker for up to a week, and
|
|
13
|
+
a phantom row has no step event left for the phantom filter to catch. Removed
|
|
14
|
+
IDs are remembered for the lifetime of the instance (the host never reuses an
|
|
15
|
+
ID), so a late write cannot recreate the row: without that barrier a child that
|
|
16
|
+
was still running when its parent was deleted came back as a phantom ROOT,
|
|
17
|
+
because deleting the parent had already wiped its attribution cache. The same
|
|
18
|
+
barrier covers a child that has no persisted entry yet. A state lock that is
|
|
19
|
+
busy at delete time queues the removal for the next write instead of dropping
|
|
20
|
+
it. The write path follows the same lock/atomic-rename contract as the state
|
|
21
|
+
writer; a missing entry is a no-op and an unsafe key is rejected.
|
|
22
|
+
- **The Desktop subagent list is heaviest-first.** A family can hold a dozen
|
|
23
|
+
children and the RPC order is the `parentID` walk, which buried the session
|
|
24
|
+
actually eating the window; the list is now sorted by `ctx` (the polled array is
|
|
25
|
+
copied first, never reordered in place).
|
|
26
|
+
- **`.gitattributes` normalises line endings.** Text files are stored with LF and
|
|
27
|
+
the image assets are marked binary, so a checkout on Windows and on the Linux CI
|
|
28
|
+
produces the same bytes instead of a CRLF/LF diff on every edit.
|
|
29
|
+
- Unit harness extended to T43 (deleted-session purge: subtree removal, unrelated
|
|
30
|
+
entries kept, missing/unsafe keys are no-ops, late writes for a pruned or
|
|
31
|
+
not-yet-persisted child are dropped, a busy lock queues the whole subtree for
|
|
32
|
+
the next write), 116 checks in total.
|
|
33
|
+
|
|
7
34
|
## 1.8.2 — 2026-11-03
|
|
8
35
|
|
|
9
36
|
- **Model-rendered commands no longer echo the instruction.** `/context`,
|
package/README.md
CHANGED
|
@@ -358,12 +358,16 @@ notice if needed.
|
|
|
358
358
|
unambiguous payload boundary instead of a leading "reproduce exactly"
|
|
359
359
|
phrasing. Residual risk: a model may still add light commentary or include the
|
|
360
360
|
tags themselves.
|
|
361
|
-
* **Deleted sessions
|
|
362
|
-
the
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
361
|
+
* **Deleted sessions are dropped immediately.** A `session.deleted` event removes
|
|
362
|
+
the session's `state.json` entry - and every entry under it in the `parentID`
|
|
363
|
+
chain - instead of waiting for the 7-day tombstone prune, so the row leaves
|
|
364
|
+
`/context-breakdown`, the `/cx` picker and the Desktop tab at once. The removed
|
|
365
|
+
IDs are remembered for the lifetime of the instance (the host never reuses an
|
|
366
|
+
ID), so a late idle/step write, or a child that was still running when its
|
|
367
|
+
parent was deleted, cannot recreate the row - without that barrier it came back
|
|
368
|
+
as a phantom ROOT session. A state lock that is busy at delete time queues the
|
|
369
|
+
removal for the next write instead of losing it. The 7-day tombstone remains as
|
|
370
|
+
the backstop for sessions that vanish without an event.
|
|
367
371
|
|
|
368
372
|
### TUI session panel (V2, terminal only)
|
|
369
373
|
|
|
@@ -766,7 +770,7 @@ caller.
|
|
|
766
770
|
|
|
767
771
|
```bash
|
|
768
772
|
npm run build:gui # build dist/gui-experimental.cjs (required by the gate)
|
|
769
|
-
npm run test:prepublish # fast gate: tarball + JSX-free TUI + resolve + T1-
|
|
773
|
+
npm run test:prepublish # fast gate: tarball + JSX-free TUI + resolve + T1-T43 unit checks + GUI bundle + GUI runtime smoke
|
|
770
774
|
npm run test:types # tsc for the TUI entry AND the Desktop GUI part
|
|
771
775
|
npm run test:e2e # full E2E: plugin load + one "Say OK" round-trip + npm-layout liveness
|
|
772
776
|
npm run test:e2e:quick # E2E load-only, no LLM call
|
|
@@ -790,7 +794,7 @@ STEP 80 s + PROBE 45 s, ~4 min total.
|
|
|
790
794
|
| (a) tarball composition | `package.json` `files[]` expands to the base files plus every `lib/*` module (count asserted dynamically); `docs/` may contain images only; `dist/` may contain only the built GUI bundle | no test/node_modules/tsconfig in the tarball, and no published export pointing at nothing |
|
|
791
795
|
| (b) JSX-free TUI | no `.tsx`/`.jsx` in the TUI surface (root + `lib/`), every TUI source parses with esbuild `loader:"ts"` (JSX syntax fails), no `@jsx*` pragma, imports stay inside the host-bridgeable allowlist, relative imports carry a file extension | OpenCode skips its Solid transform under `node_modules` (#33884): one JSX file freezes the npm-installed sidebar |
|
|
792
796
|
| (c) resolve chain | walks the real TUI import graph (`resolve-bridge.cjs`): every bare specifier is host-bridged, every relative one resolves to a shipped file, and **no host-bridged module is a production dependency** (a second Solid/OpenTUI instance freezes the surfaces) | an accidental runtime dependency, or a bad local import, silently breaks the sidebar |
|
|
793
|
-
| (d) unit harness T1-
|
|
797
|
+
| (d) unit harness T1-T43 | limit/agent/state fix assertions plus measureContributors, redacted-export, shared category-helper, sanitizer/unicode, prompt-cache, waste-detector, compaction-reserve, top-consumer, diff, subagent-rollup and Desktop-RPC checks (116 total; T30 schema-vs-builder drift, T31 registration + handler, T32-T36 fallback-vs-hook category priority across the report, the shared state file and the live log (T36: a counter-less hook entry borrows the snapshot or prints `?`), T37 the opencode 2.2.5 `min(input, context)` window, T38 the configured `compaction.buffer` (JSONC strip, project/global precedence, percent/token conversion, env/argument priority, per-project isolation), T39 the TUI composer capsule thresholds, T40 the data-first command payload + locale-free clock + labelled contributors, T41 model-scoped breakdowns after an in-session model switch, T42 the panel row width budget (the column sum never exceeds the panel; the stale-marker columns come out of the row, not on top of it), T43 deleted-session purge of `state.json` (subtree removed, peers kept, unsafe keys rejected, late writes for a pruned or not-yet-persisted child dropped), T25c/T25d compaction-hook arming, T34 cross-instance summary merge) | regressions in modelLimits, writeStateFile, hydrate, contributors, export, subagent lifetime, and a GUI/server wire mismatch |
|
|
794
798
|
| (e) corporate-identifier scan | no forbidden identifiers in the published surface (`index.js`, `tui.ts`, `lib/*`, `gui/*`, `README.md`, `CHANGELOG.md`, `package.json`, the GUI build config) or the test tree | compliance |
|
|
795
799
|
| (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 |
|
|
796
800
|
| (g) Desktop GUI bundle | `dist/gui-experimental.cjs` exists and is a non-trivial single-file CJS bundle: no ESM `import`, no dynamic `import()`, no Node built-in, and every app-provided module (`solid-js`, `solid-js/web`, `@opencode/plugin/gui`, `@opencode/plugin/rpc`) still a `require()` - no bundled second copy, no React/OpenTUI/bare-`opencode` import; the artifact must also be newer than every source file it inlines | a stale or hand-edited bundle, or a build that inlines the app instance, breaks the Desktop panel at load time |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});let e=require("solid-js/web"),t=require("@opencode/plugin/gui"),n=require("solid-js"),r=require("@opencode/plugin/rpc");var i={sessionID:{type:`string`},parentID:{type:`string`},agent:{type:`string`},modelID:{type:`string`},providerID:{type:`string`},ctx:{type:`number`},input:{type:`number`},usable:{type:`number`},reserve:{type:`number`},user:{type:`number`},assistant:{type:`number`},reasoning:{type:`number`},toolArgs:{type:`number`},toolOut:{type:`number`},system:{type:`number`},toolSchemas:{type:`number`},other:{type:`number`},source:{type:`string`},updatedAt:{type:`string`},stale:{type:`boolean`}},a=Object.keys(i),o=r.Rpc.define({id:`context-indicator`,methods:{family:{input:{type:`object`,properties:{sessionID:{type:`string`}},required:[`sessionID`],additionalProperties:!1},output:{type:`object`,properties:{rows:{type:`array`,items:{type:`object`,properties:i,required:a,additionalProperties:!1}}},required:[`rows`],additionalProperties:!1}}},events:{}});function s(e){return Number.isFinite(e)?e>=1e3?`${(e/1e3).toFixed(1)}k`:String(Math.round(e)):`0`}var c=(0,e.template)(`<div>Server part unavailable: <!> (retrying)`),l=(0,e.template)(`<div>`),u=(0,e.template)(`<div>Loading context...`),d=(0,e.template)(`<div>No measurement yet. Send a prompt in this session and the numbers appear here.`),f=(0,e.template)(`<div><span></span><span>`),p=(0,e.template)(`<span>stale after compaction`),m=(0,e.template)(`<span>reserve `),h=(0,e.template)(`<div>Subagents (<!>)`),g=(0,e.template)(`<div><span></span><span></span><span><span style=display:block></span></span><span>`),_=1500,v=[[`system`,`System`],[`toolSchemas`,`Tool schemas`],[`user`,`User`],[`assistant`,`Assistant`],[`reasoning`,`Reasoning`],[`toolArgs`,`Tool args`],[`toolOut`,`Tool output`],[`other`,`Other`]];function y(e){if(!e)return`?`;try{let t=new Date(e);if(Number.isNaN(t.getTime()))return`?`;let n=e=>String(e).padStart(2,`0`);return`${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`}catch{return`?`}}function b(e,t){return!t||t<=0?``:`${(e/t*100).toFixed(0)}%`}var x={panel:{display:`flex`,"flex-direction":`column`,gap:`10px`,padding:`12px`,"font-size":`12px`,"line-height":`1.45`},hint:{opacity:`0.65`,"font-size":`12px`},header:{display:`flex`,"align-items":`baseline`,"justify-content":`space-between`,gap:`8px`},model:{"font-weight":`600`,overflow:`hidden`,"text-overflow":`ellipsis`,"white-space":`nowrap`},total:{"font-variant-numeric":`tabular-nums`,"white-space":`nowrap`},section:{display:`flex`,"flex-direction":`column`,gap:`6px`},row:{display:`grid`,"grid-template-columns":`1fr auto 64px auto`,"align-items":`center`,gap:`6px`},label:{opacity:`0.8`,overflow:`hidden`,"text-overflow":`ellipsis`,"white-space":`nowrap`},value:{"font-variant-numeric":`tabular-nums`},track:{height:`4px`,"border-radius":`2px`,background:`currentColor`,opacity:`0.15`,overflow:`hidden`},fill:{height:`4px`,"border-radius":`2px`,background:`currentColor`,opacity:`0.9`},share:{opacity:`0.65`,"text-align":`right`,"font-variant-numeric":`tabular-nums`},meta:{display:`flex`,gap:`8px`,opacity:`0.6`,"font-size":`11px`},sub:{display:`flex`,"justify-content":`space-between`,gap:`8px`,"font-size":`11px`},subName:{overflow:`hidden`,"text-overflow":`ellipsis`,"white-space":`nowrap`,opacity:`0.85`},title:{"font-weight":`600`,opacity:`0.85`}};function S(r){let i=(0,t.usePlugin)(),[a,S]=(0,n.createSignal)([]),[C,w]=(0,n.createSignal)(``),[T,E]=(0,n.createSignal)(!1);(0,n.createEffect)(()=>{let e=r.session.id,t=!1,a=!1;E(!1),S([]),w(``);let s=async()=>{if(!(a||t)){t=!0;try{let t=await i.client.rpc(o).family({sessionID:e});if(a)return;S(Array.isArray(t?.rows)?t.rows:[]),w(``)}catch(e){if(a)return;w(e instanceof Error?e.message:String(e))}finally{t=!1,a||E(!0)}}};s();let c=setInterval(()=>void s(),_);(0,n.onCleanup)(()=>{a=!0,clearInterval(c)})});let D=()=>a()[0],O=()=>a().slice(1),k=e=>{let t=D()?.ctx||0;return t<=0||e<=0?`0%`:`${Math.min(100,Math.max(2,Math.round(e/t*100)))}%`};return(()=>{var t=l();return(0,e.insert)(t,(0,e.createComponent)(n.Show,{get when(){return T()},get fallback(){return(()=>{var t=u();return(0,e.effect)(n=>(0,e.style)(t,x.hint,n)),t})()},get children(){return[(0,e.createComponent)(n.Show,{get when(){return C()},get children(){var t=c(),n=t.firstChild.nextSibling;return n.nextSibling,(0,e.insert)(t,C,n),(0,e.effect)(n=>(0,e.style)(t,x.hint,n)),t}}),(0,e.createComponent)(n.Show,{get when(){return D()},get fallback(){return(0,e.createComponent)(n.Show,{get when(){return!C()},get children(){var t=d();return(0,e.effect)(n=>(0,e.style)(t,x.hint,n)),t}})},children:t=>[(()=>{var n=f(),r=n.firstChild,i=r.nextSibling;return(0,e.insert)(r,()=>t().modelID||`unknown model`),(0,e.insert)(i,()=>s(t().ctx),null),(0,e.insert)(i,(()=>{var n=(0,e.memo)(()=>t().usable>0);return()=>n()?` / ${s(t().usable)} (${b(t().ctx,t().usable)})`:``})(),null),(0,e.effect)(a=>{var o=x.header,s=x.model,c=t().modelID,l=x.total;return a.e=(0,e.style)(n,o,a.e),a.t=(0,e.style)(r,s,a.t),c!==a.a&&(0,e.setAttribute)(r,`title`,a.a=c),a.o=(0,e.style)(i,l,a.o),a},{e:void 0,t:void 0,a:void 0,o:void 0}),n})(),(()=>{var r=l();return(0,e.insert)(r,(0,e.createComponent)(n.For,{each:v,children:n=>(()=>{var r=g(),i=r.firstChild,a=i.nextSibling,o=a.nextSibling,c=o.firstChild,l=o.nextSibling;return(0,e.insert)(i,()=>n[1]),(0,e.insert)(a,()=>s(t()[n[0]])),(0,e.insert)(l,()=>b(t()[n[0]],t().ctx)),(0,e.effect)(s=>{var u=x.row,d=x.label,f=x.value,p=x.track,m={...x.fill,width:k(t()[n[0]])},h=x.share;return s.e=(0,e.style)(r,u,s.e),s.t=(0,e.style)(i,d,s.t),s.a=(0,e.style)(a,f,s.a),s.o=(0,e.style)(o,p,s.o),s.i=(0,e.style)(c,m,s.i),s.n=(0,e.style)(l,h,s.n),s},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0}),r})()})),(0,e.effect)(t=>(0,e.style)(r,x.section,t)),r})(),(()=>{var r=f(),i=r.firstChild,a=i.nextSibling;return(0,e.insert)(i,()=>y(t().updatedAt)),(0,e.insert)(a,()=>t().source||`none`),(0,e.insert)(r,(0,e.createComponent)(n.Show,{get when(){return t().stale},get children(){return p()}}),null),(0,e.insert)(r,(0,e.createComponent)(n.Show,{get when(){return t().reserve>0},get children(){var n=m();return n.firstChild,(0,e.insert)(n,()=>s(t().reserve),null),n}}),null),(0,e.effect)(t=>(0,e.style)(r,x.meta,t)),r})(),(0,e.createComponent)(n.Show,{get when(){return O().length>0},get children(){return[(()=>{var t=h(),n=t.firstChild.nextSibling;return n.nextSibling,(0,e.insert)(t,()=>O().length,n),(0,e.effect)(n=>(0,e.style)(t,x.title,n)),t})(),(()=>{var t=l();return(0,e.insert)(t,(0,e.createComponent)(n.For,{get each(){return O()},children:t=>(()=>{var n=f(),r=n.firstChild,i=r.nextSibling;return(0,e.insert)(r,()=>t.agent||`sub`),(0,e.insert)(i,()=>s(t.ctx),null),(0,e.insert)(i,(()=>{var n=(0,e.memo)(()=>t.usable>0);return()=>n()?` (${b(t.ctx,t.usable)})`:``})(),null),(0,e.effect)(a=>{var o=x.sub,s=x.subName,c=t.modelID,l=x.value;return a.e=(0,e.style)(n,o,a.e),a.t=(0,e.style)(r,s,a.t),c!==a.a&&(0,e.setAttribute)(r,`title`,a.a=c),a.o=(0,e.style)(i,l,a.o),a},{e:void 0,t:void 0,a:void 0,o:void 0}),n})()})),(0,e.effect)(n=>(0,e.style)(t,x.section,n)),t})()]}})]})]}})),(0,e.effect)(n=>(0,e.style)(t,x.panel,n)),t})()}var C=`context`,w=`window-analytics`,T=`Context`,E=5e3,D=65,O=85,k={id:C,title:T},A=t.Plugin.define({setup(r){let i=`${r.id}:${C}`,a=e=>{let t=e??r.sessions.current();t&&r.layout.open(i,t,{tab:`select`})};r.add(t.Panel,()=>({id:C,region:`side`,wide:!0,list:e=>e.open.includes(C)?[k]:[],render:t=>(0,e.createComponent)(S,{get session(){return t.session}}),mobile:{title:T,order:1,kind:`tab`,icon:w}})),r.add(t.MenuItem,()=>({id:C,menu:`session.panel`,title:T,icon:w,description:`Context-window usage`,run:()=>a()})),r.add(t.Command,()=>({id:`open`,title:`Open context panel`,description:`Context-window usage for this session`,keywords:`context tokens window panel`,section:`session`,slash:{name:`cx`},run:()=>a()}));let[s,c]=(0,n.createSignal)(void 0),l=async()=>{let e=r.sessions.current();if(!e)c(void 0);else try{let t=(await r.client.rpc(o).family({sessionID:e.id}))?.rows?.[0];if(!t||t.usable<=0){c(void 0);return}let n=t.ctx/t.usable*100;c({sessionID:e.id,label:`${n.toFixed(0)}% ctx`,tone:n>=O?`danger`:n>=D?`warning`:void 0})}catch{c(void 0)}};l();let u=setInterval(()=>void l(),E);(0,n.onCleanup)(()=>clearInterval(u)),r.add(t.ComposerPill,()=>({id:`usage`,show:({session:e})=>{let t=s();if(t&&t.sessionID===e.id)return{label:t.label,icon:w,tone:t.tone,title:`Context window usage`,run:()=>a(e)}}}))}});exports.default=A;
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});let e=require("solid-js/web"),t=require("@opencode/plugin/gui"),n=require("solid-js"),r=require("@opencode/plugin/rpc");var i={sessionID:{type:`string`},parentID:{type:`string`},agent:{type:`string`},modelID:{type:`string`},providerID:{type:`string`},ctx:{type:`number`},input:{type:`number`},usable:{type:`number`},reserve:{type:`number`},user:{type:`number`},assistant:{type:`number`},reasoning:{type:`number`},toolArgs:{type:`number`},toolOut:{type:`number`},system:{type:`number`},toolSchemas:{type:`number`},other:{type:`number`},source:{type:`string`},updatedAt:{type:`string`},stale:{type:`boolean`}},a=Object.keys(i),o=r.Rpc.define({id:`context-indicator`,methods:{family:{input:{type:`object`,properties:{sessionID:{type:`string`}},required:[`sessionID`],additionalProperties:!1},output:{type:`object`,properties:{rows:{type:`array`,items:{type:`object`,properties:i,required:a,additionalProperties:!1}}},required:[`rows`],additionalProperties:!1}}},events:{}});function s(e){return Number.isFinite(e)?e>=1e3?`${(e/1e3).toFixed(1)}k`:String(Math.round(e)):`0`}var c=(0,e.template)(`<div>Server part unavailable: <!> (retrying)`),l=(0,e.template)(`<div>`),u=(0,e.template)(`<div>Loading context...`),d=(0,e.template)(`<div>No measurement yet. Send a prompt in this session and the numbers appear here.`),f=(0,e.template)(`<div><span></span><span>`),p=(0,e.template)(`<span>stale after compaction`),m=(0,e.template)(`<span>reserve `),h=(0,e.template)(`<div>Subagents (<!>)`),g=(0,e.template)(`<div><span></span><span></span><span><span style=display:block></span></span><span>`),_=1500,v=[[`system`,`System`],[`toolSchemas`,`Tool schemas`],[`user`,`User`],[`assistant`,`Assistant`],[`reasoning`,`Reasoning`],[`toolArgs`,`Tool args`],[`toolOut`,`Tool output`],[`other`,`Other`]];function y(e){if(!e)return`?`;try{let t=new Date(e);if(Number.isNaN(t.getTime()))return`?`;let n=e=>String(e).padStart(2,`0`);return`${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`}catch{return`?`}}function b(e,t){return!t||t<=0?``:`${(e/t*100).toFixed(0)}%`}var x={panel:{display:`flex`,"flex-direction":`column`,gap:`10px`,padding:`12px`,"font-size":`12px`,"line-height":`1.45`},hint:{opacity:`0.65`,"font-size":`12px`},header:{display:`flex`,"align-items":`baseline`,"justify-content":`space-between`,gap:`8px`},model:{"font-weight":`600`,overflow:`hidden`,"text-overflow":`ellipsis`,"white-space":`nowrap`},total:{"font-variant-numeric":`tabular-nums`,"white-space":`nowrap`},section:{display:`flex`,"flex-direction":`column`,gap:`6px`},row:{display:`grid`,"grid-template-columns":`1fr auto 64px auto`,"align-items":`center`,gap:`6px`},label:{opacity:`0.8`,overflow:`hidden`,"text-overflow":`ellipsis`,"white-space":`nowrap`},value:{"font-variant-numeric":`tabular-nums`},track:{height:`4px`,"border-radius":`2px`,background:`currentColor`,opacity:`0.15`,overflow:`hidden`},fill:{height:`4px`,"border-radius":`2px`,background:`currentColor`,opacity:`0.9`},share:{opacity:`0.65`,"text-align":`right`,"font-variant-numeric":`tabular-nums`},meta:{display:`flex`,gap:`8px`,opacity:`0.6`,"font-size":`11px`},sub:{display:`flex`,"justify-content":`space-between`,gap:`8px`,"font-size":`11px`},subName:{overflow:`hidden`,"text-overflow":`ellipsis`,"white-space":`nowrap`,opacity:`0.85`},title:{"font-weight":`600`,opacity:`0.85`}};function S(r){let i=(0,t.usePlugin)(),[a,S]=(0,n.createSignal)([]),[C,w]=(0,n.createSignal)(``),[T,E]=(0,n.createSignal)(!1);(0,n.createEffect)(()=>{let e=r.session.id,t=!1,a=!1;E(!1),S([]),w(``);let s=async()=>{if(!(a||t)){t=!0;try{let t=await i.client.rpc(o).family({sessionID:e});if(a)return;S(Array.isArray(t?.rows)?t.rows:[]),w(``)}catch(e){if(a)return;w(e instanceof Error?e.message:String(e))}finally{t=!1,a||E(!0)}}};s();let c=setInterval(()=>void s(),_);(0,n.onCleanup)(()=>{a=!0,clearInterval(c)})});let D=()=>a()[0],O=()=>a().slice(1).sort((e,t)=>(t.ctx||0)-(e.ctx||0)),k=e=>{let t=D()?.ctx||0;return t<=0||e<=0?`0%`:`${Math.min(100,Math.max(2,Math.round(e/t*100)))}%`};return(()=>{var t=l();return(0,e.insert)(t,(0,e.createComponent)(n.Show,{get when(){return T()},get fallback(){return(()=>{var t=u();return(0,e.effect)(n=>(0,e.style)(t,x.hint,n)),t})()},get children(){return[(0,e.createComponent)(n.Show,{get when(){return C()},get children(){var t=c(),n=t.firstChild.nextSibling;return n.nextSibling,(0,e.insert)(t,C,n),(0,e.effect)(n=>(0,e.style)(t,x.hint,n)),t}}),(0,e.createComponent)(n.Show,{get when(){return D()},get fallback(){return(0,e.createComponent)(n.Show,{get when(){return!C()},get children(){var t=d();return(0,e.effect)(n=>(0,e.style)(t,x.hint,n)),t}})},children:t=>[(()=>{var n=f(),r=n.firstChild,i=r.nextSibling;return(0,e.insert)(r,()=>t().modelID||`unknown model`),(0,e.insert)(i,()=>s(t().ctx),null),(0,e.insert)(i,(()=>{var n=(0,e.memo)(()=>t().usable>0);return()=>n()?` / ${s(t().usable)} (${b(t().ctx,t().usable)})`:``})(),null),(0,e.effect)(a=>{var o=x.header,s=x.model,c=t().modelID,l=x.total;return a.e=(0,e.style)(n,o,a.e),a.t=(0,e.style)(r,s,a.t),c!==a.a&&(0,e.setAttribute)(r,`title`,a.a=c),a.o=(0,e.style)(i,l,a.o),a},{e:void 0,t:void 0,a:void 0,o:void 0}),n})(),(()=>{var r=l();return(0,e.insert)(r,(0,e.createComponent)(n.For,{each:v,children:n=>(()=>{var r=g(),i=r.firstChild,a=i.nextSibling,o=a.nextSibling,c=o.firstChild,l=o.nextSibling;return(0,e.insert)(i,()=>n[1]),(0,e.insert)(a,()=>s(t()[n[0]])),(0,e.insert)(l,()=>b(t()[n[0]],t().ctx)),(0,e.effect)(s=>{var u=x.row,d=x.label,f=x.value,p=x.track,m={...x.fill,width:k(t()[n[0]])},h=x.share;return s.e=(0,e.style)(r,u,s.e),s.t=(0,e.style)(i,d,s.t),s.a=(0,e.style)(a,f,s.a),s.o=(0,e.style)(o,p,s.o),s.i=(0,e.style)(c,m,s.i),s.n=(0,e.style)(l,h,s.n),s},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0}),r})()})),(0,e.effect)(t=>(0,e.style)(r,x.section,t)),r})(),(()=>{var r=f(),i=r.firstChild,a=i.nextSibling;return(0,e.insert)(i,()=>y(t().updatedAt)),(0,e.insert)(a,()=>t().source||`none`),(0,e.insert)(r,(0,e.createComponent)(n.Show,{get when(){return t().stale},get children(){return p()}}),null),(0,e.insert)(r,(0,e.createComponent)(n.Show,{get when(){return t().reserve>0},get children(){var n=m();return n.firstChild,(0,e.insert)(n,()=>s(t().reserve),null),n}}),null),(0,e.effect)(t=>(0,e.style)(r,x.meta,t)),r})(),(0,e.createComponent)(n.Show,{get when(){return O().length>0},get children(){return[(()=>{var t=h(),n=t.firstChild.nextSibling;return n.nextSibling,(0,e.insert)(t,()=>O().length,n),(0,e.effect)(n=>(0,e.style)(t,x.title,n)),t})(),(()=>{var t=l();return(0,e.insert)(t,(0,e.createComponent)(n.For,{get each(){return O()},children:t=>(()=>{var n=f(),r=n.firstChild,i=r.nextSibling;return(0,e.insert)(r,()=>t.agent||`sub`),(0,e.insert)(i,()=>s(t.ctx),null),(0,e.insert)(i,(()=>{var n=(0,e.memo)(()=>t.usable>0);return()=>n()?` (${b(t.ctx,t.usable)})`:``})(),null),(0,e.effect)(a=>{var o=x.sub,s=x.subName,c=t.modelID,l=x.value;return a.e=(0,e.style)(n,o,a.e),a.t=(0,e.style)(r,s,a.t),c!==a.a&&(0,e.setAttribute)(r,`title`,a.a=c),a.o=(0,e.style)(i,l,a.o),a},{e:void 0,t:void 0,a:void 0,o:void 0}),n})()})),(0,e.effect)(n=>(0,e.style)(t,x.section,n)),t})()]}})]})]}})),(0,e.effect)(n=>(0,e.style)(t,x.panel,n)),t})()}var C=`context`,w=`window-analytics`,T=`Context`,E=5e3,D=65,O=85,k={id:C,title:T},A=t.Plugin.define({setup(r){let i=`${r.id}:${C}`,a=e=>{let t=e??r.sessions.current();t&&r.layout.open(i,t,{tab:`select`})};r.add(t.Panel,()=>({id:C,region:`side`,wide:!0,list:e=>e.open.includes(C)?[k]:[],render:t=>(0,e.createComponent)(S,{get session(){return t.session}}),mobile:{title:T,order:1,kind:`tab`,icon:w}})),r.add(t.MenuItem,()=>({id:C,menu:`session.panel`,title:T,icon:w,description:`Context-window usage`,run:()=>a()})),r.add(t.Command,()=>({id:`open`,title:`Open context panel`,description:`Context-window usage for this session`,keywords:`context tokens window panel`,section:`session`,slash:{name:`cx`},run:()=>a()}));let[s,c]=(0,n.createSignal)(void 0),l=async()=>{let e=r.sessions.current();if(!e)c(void 0);else try{let t=(await r.client.rpc(o).family({sessionID:e.id}))?.rows?.[0];if(!t||t.usable<=0){c(void 0);return}let n=t.ctx/t.usable*100;c({sessionID:e.id,label:`${n.toFixed(0)}% ctx`,tone:n>=O?`danger`:n>=D?`warning`:void 0})}catch{c(void 0)}};l();let u=setInterval(()=>void l(),E);(0,n.onCleanup)(()=>clearInterval(u)),r.add(t.ComposerPill,()=>({id:`usage`,show:({session:e})=>{let t=s();if(t&&t.sessionID===e.id)return{label:t.label,icon:w,tone:t.tone,title:`Context window usage`,run:()=>a(e)}}}))}});exports.default=A;
|
package/index.js
CHANGED
|
@@ -184,6 +184,7 @@ import { breakdownCache, breakdownForModel, lastKnownCacheRead, lastKnownCacheWr
|
|
|
184
184
|
import { registerDesktopRpc, toDesktopRow } from "./lib/desktop.js";
|
|
185
185
|
import {
|
|
186
186
|
logErr,
|
|
187
|
+
pruneSessionState,
|
|
187
188
|
readStateSnapshot,
|
|
188
189
|
writeStateFile,
|
|
189
190
|
} from "./lib/state.js";
|
|
@@ -201,6 +202,7 @@ export {
|
|
|
201
202
|
sessionAgentCache,
|
|
202
203
|
sessionParentCache,
|
|
203
204
|
writeStateFile,
|
|
205
|
+
pruneSessionState,
|
|
204
206
|
hydrateModelLimitsFromState,
|
|
205
207
|
getUsableContext,
|
|
206
208
|
getModelLimit,
|
package/lib/state.js
CHANGED
|
@@ -429,6 +429,103 @@ function renameWithRetry(from, to, attempts = 3, delayMs = 50) {
|
|
|
429
429
|
// 7 days: a tombstone only matters while a sibling/parent poll can still see it.
|
|
430
430
|
const PRUNE_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
|
|
431
431
|
|
|
432
|
+
// Session IDs whose persisted entry was removed by `session.deleted` (or is
|
|
433
|
+
// queued for removal because the state lock was busy). The host never reuses an
|
|
434
|
+
// ID, so once a session is gone every later write for it must be DROPPED - a
|
|
435
|
+
// write would recreate the entry, and after `clearChildSubagentTracking()` there
|
|
436
|
+
// is no parent cache left either, so the child came back as a phantom ROOT row
|
|
437
|
+
// that lived up to the 7-day tombstone (worse than the old phantom child).
|
|
438
|
+
// Bounded FIFO like the other per-instance maps.
|
|
439
|
+
const PRUNED_MAX = 512;
|
|
440
|
+
const prunedSessions = new Set();
|
|
441
|
+
// DELETE REQUESTS waiting for a state-file write: the lock was busy when the
|
|
442
|
+
// event arrived. Stored as the deleted ROOT id, not as a flat list of entries -
|
|
443
|
+
// the subtree is unknown without the file, and the request is replayed under the
|
|
444
|
+
// next write's lock (where the snapshot is available) so a busy lock cannot leave
|
|
445
|
+
// descendants alive. Bounded FIFO as well.
|
|
446
|
+
const pendingPruneRoots = new Set();
|
|
447
|
+
// Diagnostics: one log line per session that a late write was blocked for. A
|
|
448
|
+
// silent drop is impossible to debug in the field, but a line per idle/step
|
|
449
|
+
// would flood the log.
|
|
450
|
+
const barrierLogged = new Set();
|
|
451
|
+
|
|
452
|
+
function rememberPruned(id) {
|
|
453
|
+
if (typeof id !== "string" || !id) return;
|
|
454
|
+
prunedSessions.add(id);
|
|
455
|
+
if (prunedSessions.size > PRUNED_MAX) {
|
|
456
|
+
const oldest = prunedSessions.values().next().value;
|
|
457
|
+
if (oldest !== undefined) prunedSessions.delete(oldest);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function queuePrune(rootID) {
|
|
462
|
+
if (typeof rootID !== "string" || !rootID) return;
|
|
463
|
+
pendingPruneRoots.add(rootID);
|
|
464
|
+
if (pendingPruneRoots.size > PRUNED_MAX) {
|
|
465
|
+
const oldest = pendingPruneRoots.values().next().value;
|
|
466
|
+
if (oldest !== undefined) pendingPruneRoots.delete(oldest);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function logBarrierOnce(id) {
|
|
471
|
+
if (barrierLogged.has(id)) return;
|
|
472
|
+
barrierLogged.add(id);
|
|
473
|
+
if (barrierLogged.size > PRUNED_MAX) {
|
|
474
|
+
const oldest = barrierLogged.values().next().value;
|
|
475
|
+
if (oldest !== undefined) barrierLogged.delete(oldest);
|
|
476
|
+
}
|
|
477
|
+
logErr(`writeStateFile dropped for deleted session=${oneLine(id)}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// `rootID` plus every persisted entry below it in the parentID chain. The index
|
|
481
|
+
// is built once (O(n)) instead of rescanning the whole snapshot per visited node:
|
|
482
|
+
// the file is user-writable and unbounded on read, and this runs on the event
|
|
483
|
+
// loop. `doomed` doubles as the cycle guard.
|
|
484
|
+
function subtreeIDs(all, rootID) {
|
|
485
|
+
const childrenByParent = new Map();
|
|
486
|
+
for (const [id, entry] of Object.entries(all)) {
|
|
487
|
+
const parent = entry?.parentID;
|
|
488
|
+
if (typeof parent !== "string") continue;
|
|
489
|
+
const kids = childrenByParent.get(parent);
|
|
490
|
+
if (kids) kids.push(id);
|
|
491
|
+
else childrenByParent.set(parent, [id]);
|
|
492
|
+
}
|
|
493
|
+
const doomed = new Set([rootID]);
|
|
494
|
+
const stack = [rootID];
|
|
495
|
+
while (stack.length > 0) {
|
|
496
|
+
const current = stack.pop();
|
|
497
|
+
for (const child of childrenByParent.get(current) || []) {
|
|
498
|
+
if (doomed.has(child)) continue;
|
|
499
|
+
doomed.add(child);
|
|
500
|
+
stack.push(child);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return doomed;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Atomic commit of a state snapshot: random temp suffix + exclusive create
|
|
507
|
+
// (CWE-377), rename over a regular file only, temp cleanup on failure. Shared by
|
|
508
|
+
// the writer and the deleter so the two can never drift apart.
|
|
509
|
+
function commitState(all) {
|
|
510
|
+
const tmp = `${STATE_FILE}.${randomBytes(6).toString("hex")}.tmp`;
|
|
511
|
+
writeFileSync(tmp, JSON.stringify(all), { encoding: "utf8", flag: "wx" });
|
|
512
|
+
let renamed = false;
|
|
513
|
+
try {
|
|
514
|
+
ensureRegularFile(STATE_FILE); // never rename over a planted symlink
|
|
515
|
+
renamed = renameWithRetry(tmp, STATE_FILE);
|
|
516
|
+
} finally {
|
|
517
|
+
// A rename that exhausted its retries leaves the temp file behind; drop it
|
|
518
|
+
// so repeated failures cannot litter %TEMP% (best-effort: on a SUCCESSFUL
|
|
519
|
+
// rename the temp is already gone, existsSync filters that).
|
|
520
|
+
try {
|
|
521
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
522
|
+
} catch {
|
|
523
|
+
/* best-effort cleanup */
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return renamed;
|
|
527
|
+
}
|
|
528
|
+
|
|
432
529
|
function writeStateFile(
|
|
433
530
|
sessionID,
|
|
434
531
|
modelID,
|
|
@@ -447,6 +544,16 @@ function writeStateFile(
|
|
|
447
544
|
logErr(`writeStateFile skipped unsafe session key ${oneLine(sessionID)}`);
|
|
448
545
|
return false;
|
|
449
546
|
}
|
|
547
|
+
// The session was deleted: never recreate its entry. Without this barrier a
|
|
548
|
+
// late idle/step write (or a live child whose parent was just deleted)
|
|
549
|
+
// re-adds the row, and with the parent cache already cleared it lands with
|
|
550
|
+
// `parentID: null` - a phantom ROOT session in the table, the picker and the
|
|
551
|
+
// Desktop tab until the tombstone. Logged once per ID: the check runs on
|
|
552
|
+
// every write for that session and a silent drop would be undebuggable.
|
|
553
|
+
if (prunedSessions.has(sessionID)) {
|
|
554
|
+
logBarrierOnce(sessionID);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
450
557
|
const lock = acquireLock(STATE_LOCK_FILE);
|
|
451
558
|
// {ok:false} => busy: skip (value stays in memory, lands next cycle), as
|
|
452
559
|
// before. null => unexpected error: fail-open, write WITHOUT the lock and do
|
|
@@ -817,24 +924,23 @@ function writeStateFile(
|
|
|
817
924
|
.slice(MAX_TRACKED_SESSIONS)
|
|
818
925
|
.forEach((k) => delete all[k]);
|
|
819
926
|
}
|
|
820
|
-
//
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (existsSync(tmp)) unlinkSync(tmp);
|
|
833
|
-
} catch {
|
|
834
|
-
/* best-effort cleanup */
|
|
927
|
+
// Replay queued deletes inside THIS write's lock: the snapshot is only
|
|
928
|
+
// readable under it, so a busy lock at delete time cannot leave descendants
|
|
929
|
+
// alive. The requests are kept until the commit SUCCEEDS - clearing the
|
|
930
|
+
// queue before a rename that never happened would drop the delete silently
|
|
931
|
+
// and leave the row to the 7-day tombstone.
|
|
932
|
+
const replaying = pendingPruneRoots.size > 0;
|
|
933
|
+
if (replaying) {
|
|
934
|
+
for (const root of pendingPruneRoots) {
|
|
935
|
+
for (const id of subtreeIDs(all, root)) {
|
|
936
|
+
delete all[id];
|
|
937
|
+
rememberPruned(id);
|
|
938
|
+
}
|
|
835
939
|
}
|
|
836
940
|
}
|
|
837
|
-
|
|
941
|
+
const committed = commitState(all);
|
|
942
|
+
if (committed && replaying) pendingPruneRoots.clear();
|
|
943
|
+
return committed;
|
|
838
944
|
} finally {
|
|
839
945
|
// Release ONLY a lock we actually own (lock === null => fail-open, skip).
|
|
840
946
|
if (lock && lock.ok) releaseLock(STATE_LOCK_FILE, lock.token);
|
|
@@ -931,10 +1037,84 @@ function pushFinalSummary(claimKey, summaryLines) {
|
|
|
931
1037
|
}
|
|
932
1038
|
}
|
|
933
1039
|
|
|
1040
|
+
// Delete one session's entry AND its whole subtree from the shared state file.
|
|
1041
|
+
//
|
|
1042
|
+
// `session.deleted` is authoritative, so waiting for the tombstone (7 days) or
|
|
1043
|
+
// the LRU cap keeps a dead session visible in `/context-breakdown`, the Desktop
|
|
1044
|
+
// tab and the `/cx` picker - and a phantom row has no step event left to be
|
|
1045
|
+
// filtered out by. The walk follows the persisted parentID chain (the same
|
|
1046
|
+
// source discovery uses), so a deleted parent takes its descendants with it and
|
|
1047
|
+
// a deleted child takes its own sub-subagents.
|
|
1048
|
+
//
|
|
1049
|
+
// Every removed ID also enters `prunedSessions`: the host never reuses an ID, so
|
|
1050
|
+
// a later write for it is dropped instead of recreating the row (a live child
|
|
1051
|
+
// whose parent was deleted would otherwise come back as a phantom ROOT once
|
|
1052
|
+
// clearChildSubagentTracking() had dropped its parent cache).
|
|
1053
|
+
//
|
|
1054
|
+
// Same locking/atomic-write contract as writeStateFile, except busy: the delete
|
|
1055
|
+
// is QUEUED for the next write (which already holds the lock) rather than lost -
|
|
1056
|
+
// the tombstone would not remove it for 7 days.
|
|
1057
|
+
function pruneSessionState(sessionID) {
|
|
1058
|
+
try {
|
|
1059
|
+
if (!sessionID || !isSafeSessionKey(sessionID)) return false;
|
|
1060
|
+
// In-memory descendants first: a child that has not written its first entry
|
|
1061
|
+
// yet is invisible to the disk walk below, but it WILL write later - and by
|
|
1062
|
+
// then `session.deleted` has already wiped its parent attribution, so it
|
|
1063
|
+
// would land as a phantom root. Remember them regardless of the lock.
|
|
1064
|
+
const kidsByParent = new Map();
|
|
1065
|
+
for (const [childID, parentID] of sessionParentCache) {
|
|
1066
|
+
if (typeof parentID !== "string") continue;
|
|
1067
|
+
const kids = kidsByParent.get(parentID);
|
|
1068
|
+
if (kids) kids.push(childID);
|
|
1069
|
+
else kidsByParent.set(parentID, [childID]);
|
|
1070
|
+
}
|
|
1071
|
+
const inMemory = new Set([sessionID]);
|
|
1072
|
+
const pending = [sessionID];
|
|
1073
|
+
while (pending.length > 0) {
|
|
1074
|
+
const current = pending.pop();
|
|
1075
|
+
for (const child of kidsByParent.get(current) || []) {
|
|
1076
|
+
if (inMemory.has(child)) continue;
|
|
1077
|
+
inMemory.add(child);
|
|
1078
|
+
pending.push(child);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
for (const id of inMemory) rememberPruned(id);
|
|
1082
|
+
const lock = acquireLock(STATE_LOCK_FILE);
|
|
1083
|
+
if (lock && !lock.ok) {
|
|
1084
|
+
logErr(`pruneSessionState queued (state lock busy) session=${oneLine(sessionID)}`);
|
|
1085
|
+
queuePrune(sessionID); // replayed under the next write's lock, with the subtree
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
const all = readStateSnapshot();
|
|
1090
|
+
const doomed = subtreeIDs(all, sessionID);
|
|
1091
|
+
let removed = 0;
|
|
1092
|
+
for (const id of doomed) {
|
|
1093
|
+
// Remembered even when nothing was persisted: the write barrier has to
|
|
1094
|
+
// cover a session whose entry was already gone (another instance pruned
|
|
1095
|
+
// it) and a child that has not written its first entry yet.
|
|
1096
|
+
rememberPruned(id);
|
|
1097
|
+
if (Object.prototype.hasOwnProperty.call(all, id)) {
|
|
1098
|
+
delete all[id];
|
|
1099
|
+
removed++;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (removed === 0) return false; // nothing persisted (already gone)
|
|
1103
|
+
return commitState(all);
|
|
1104
|
+
} finally {
|
|
1105
|
+
if (lock && lock.ok) releaseLock(STATE_LOCK_FILE, lock.token);
|
|
1106
|
+
}
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
logErr("pruneSessionState failed", err);
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
934
1113
|
export {
|
|
935
1114
|
logErr,
|
|
936
1115
|
writeLog,
|
|
937
1116
|
writeStateFile,
|
|
1117
|
+
pruneSessionState,
|
|
938
1118
|
debugEvent,
|
|
939
1119
|
readStateSnapshot,
|
|
940
1120
|
pushFinalSummary,
|
package/lib/v1.js
CHANGED
|
@@ -671,6 +671,11 @@ function makeV1(client) {
|
|
|
671
671
|
) {
|
|
672
672
|
// session.idle / session.compacted -> properties.sessionID;
|
|
673
673
|
// session.deleted -> properties.info.id
|
|
674
|
+
//
|
|
675
|
+
// V1 is feature-frozen: unlike the V2 handler, a V1 delete event does NOT
|
|
676
|
+
// purge the state.json entry (it ages out via the 7-day tombstone), so a
|
|
677
|
+
// V1 host can still show a phantom row. Kept intentionally small; V2 is
|
|
678
|
+
// the supported path.
|
|
674
679
|
const sid =
|
|
675
680
|
event.properties?.sessionID ?? event.properties?.info?.id;
|
|
676
681
|
if (!sid) return;
|
package/lib/v2.js
CHANGED
|
@@ -66,6 +66,7 @@ import { locationDirectory } from "./config.js";
|
|
|
66
66
|
import {
|
|
67
67
|
debugEvent,
|
|
68
68
|
logErr,
|
|
69
|
+
pruneSessionState,
|
|
69
70
|
pushFinalSummary,
|
|
70
71
|
readStateSnapshot,
|
|
71
72
|
writeLog,
|
|
@@ -995,10 +996,11 @@ async function handleV2Event(ctx, event) {
|
|
|
995
996
|
if (event.type === "session.deleted") {
|
|
996
997
|
const sid = event.data?.sessionID;
|
|
997
998
|
if (!sid) return;
|
|
998
|
-
// Discovery reads the persisted parentID chain, so
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
999
|
+
// Discovery reads the persisted parentID chain, so the entry has to go too:
|
|
1000
|
+
// clearing the in-memory caches alone leaves the row in state.json until the
|
|
1001
|
+
// tombstone (7 days) or the LRU cap drops it, and there is no step event left
|
|
1002
|
+
// to filter it out as a phantom.
|
|
1003
|
+
pruneSessionState(sid);
|
|
1002
1004
|
clearSessionTracking(sid);
|
|
1003
1005
|
// Whatever this session was, its own descendants must go too: a deleted
|
|
1004
1006
|
// parent takes the whole subtree, and a deleted child takes its own
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-context-indicator",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.3",
|
|
4
4
|
"description": "Real-time context-window usage for OpenCode: per-category token breakdown, /context-diff, a zero-LLM /cx panel with per-session cost and a searchable family-session picker, a TUI sidebar, and an experimental Desktop panel. Dual V1 + V2 plugin entry.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|