@cortexkit/opencode-magic-context 0.32.0 → 0.32.2

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.
Files changed (191) hide show
  1. package/README.md +3 -3
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/config/project-security.d.ts +3 -0
  4. package/dist/config/project-security.d.ts.map +1 -1
  5. package/dist/config/schema/magic-context.d.ts +82 -2
  6. package/dist/config/schema/magic-context.d.ts.map +1 -1
  7. package/dist/config/transform-mode.d.ts +15 -0
  8. package/dist/config/transform-mode.d.ts.map +1 -0
  9. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -1
  10. package/dist/features/magic-context/dreamer/cron.d.ts +2 -2
  11. package/dist/features/magic-context/dreamer/cron.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  13. package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
  14. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  15. package/dist/features/magic-context/memory/embedding-provider.d.ts +6 -0
  16. package/dist/features/magic-context/memory/embedding-provider.d.ts.map +1 -1
  17. package/dist/features/magic-context/memory/embedding-synapse.d.ts +98 -0
  18. package/dist/features/magic-context/memory/embedding-synapse.d.ts.map +1 -0
  19. package/dist/features/magic-context/memory/embedding.d.ts +1 -1
  20. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  21. package/dist/features/magic-context/message-index-async.d.ts +4 -1
  22. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  23. package/dist/features/magic-context/message-index.d.ts +7 -4
  24. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  25. package/dist/features/magic-context/migrations.d.ts +21 -16
  26. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  27. package/dist/features/magic-context/plugin-messages.d.ts.map +1 -1
  28. package/dist/features/magic-context/project-embedding-registry.d.ts +27 -0
  29. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  30. package/dist/features/magic-context/search-measurement.d.ts +14 -0
  31. package/dist/features/magic-context/search-measurement.d.ts.map +1 -0
  32. package/dist/features/magic-context/search.d.ts +11 -1
  33. package/dist/features/magic-context/search.d.ts.map +1 -1
  34. package/dist/features/magic-context/session-project-backfill.d.ts +6 -3
  35. package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
  36. package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
  37. package/dist/features/magic-context/smart-notes/capabilities.d.ts.map +1 -1
  38. package/dist/features/magic-context/smart-notes/compiler.d.ts +3 -1
  39. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  40. package/dist/features/magic-context/smart-notes/runner.d.ts +1 -0
  41. package/dist/features/magic-context/smart-notes/runner.d.ts.map +1 -1
  42. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +8 -1
  43. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
  44. package/dist/features/magic-context/smart-notes/ssrf-guard.d.ts +10 -0
  45. package/dist/features/magic-context/smart-notes/ssrf-guard.d.ts.map +1 -1
  46. package/dist/features/magic-context/smart-notes/storage.d.ts +16 -1
  47. package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
  48. package/dist/features/magic-context/storage-clone.d.ts +4 -0
  49. package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
  50. package/dist/features/magic-context/storage-db.d.ts +7 -1
  51. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  52. package/dist/features/magic-context/storage-embedding-measurements.d.ts +60 -0
  53. package/dist/features/magic-context/storage-embedding-measurements.d.ts.map +1 -0
  54. package/dist/features/magic-context/storage-meta-persisted.d.ts +10 -5
  55. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  56. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  57. package/dist/features/magic-context/storage-schema-helpers.d.ts +3 -4
  58. package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
  59. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  60. package/dist/features/magic-context/storage.d.ts +1 -0
  61. package/dist/features/magic-context/storage.d.ts.map +1 -1
  62. package/dist/features/magic-context/tool-owner-backfill.d.ts +2 -0
  63. package/dist/features/magic-context/tool-owner-backfill.d.ts.map +1 -1
  64. package/dist/features/magic-context/transform-decision-log.d.ts +1 -1
  65. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  66. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/auto-search-runner.d.ts +7 -1
  68. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/channel2-delivery.d.ts +2 -0
  70. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/command-handler.d.ts +5 -0
  72. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +4 -0
  74. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/compartment-render-epoch.d.ts +12 -0
  76. package/dist/hooks/magic-context/compartment-render-epoch.d.ts.map +1 -0
  77. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/decay-render.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/emergency-drop.d.ts +1 -0
  80. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/emergency-fail-closed.d.ts +7 -0
  82. package/dist/hooks/magic-context/emergency-fail-closed.d.ts.map +1 -0
  83. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +29 -0
  85. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -0
  86. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -0
  87. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +1 -1
  89. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/hook-handlers.d.ts +6 -0
  91. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/hook.d.ts +19 -3
  93. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/index.d.ts +1 -1
  95. package/dist/hooks/magic-context/index.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/inject-compartments.d.ts +1 -0
  97. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/lkg-replay.d.ts +51 -0
  99. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -0
  100. package/dist/hooks/magic-context/lkg-slot.d.ts +25 -0
  101. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -0
  102. package/dist/hooks/magic-context/module-state-sync.d.ts +169 -0
  103. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -0
  104. package/dist/hooks/magic-context/module-wire.d.ts +73 -0
  105. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -0
  106. package/dist/hooks/magic-context/pass-outcome.d.ts +15 -0
  107. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -0
  108. package/dist/hooks/magic-context/read-session-chunk.d.ts +17 -1
  109. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/read-session-raw.d.ts +30 -2
  111. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/rust-mode-transform.d.ts +64 -0
  114. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -0
  115. package/dist/hooks/magic-context/shadow-sender.d.ts +84 -49
  116. package/dist/hooks/magic-context/shadow-sender.d.ts.map +1 -1
  117. package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
  118. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  119. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +56 -0
  120. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  121. package/dist/hooks/magic-context/transform.d.ts +9 -0
  122. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +17355 -10510
  125. package/dist/plugin/boot-quiet.d.ts +12 -0
  126. package/dist/plugin/boot-quiet.d.ts.map +1 -0
  127. package/dist/plugin/dream-timer.d.ts +6 -2
  128. package/dist/plugin/dream-timer.d.ts.map +1 -1
  129. package/dist/plugin/embedding-bootstrap-helpers.d.ts.map +1 -1
  130. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  131. package/dist/plugin/embedding-routing.d.ts +32 -0
  132. package/dist/plugin/embedding-routing.d.ts.map +1 -0
  133. package/dist/plugin/hooks/create-session-hooks.d.ts +54 -2
  134. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  135. package/dist/plugin/messages-transform.d.ts +4 -3
  136. package/dist/plugin/messages-transform.d.ts.map +1 -1
  137. package/dist/plugin/rpc-handlers.d.ts +19 -2
  138. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  139. package/dist/plugin/rust-tool-backends.d.ts +10 -0
  140. package/dist/plugin/rust-tool-backends.d.ts.map +1 -0
  141. package/dist/plugin/tool-registry.d.ts +2 -0
  142. package/dist/plugin/tool-registry.d.ts.map +1 -1
  143. package/dist/shared/models-dev-cache.d.ts +5 -0
  144. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  145. package/dist/shared/rpc-client.d.ts +3 -1
  146. package/dist/shared/rpc-client.d.ts.map +1 -1
  147. package/dist/shared/rpc-notifications.d.ts +11 -6
  148. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  149. package/dist/shared/rpc-server.d.ts +2 -2
  150. package/dist/shared/rpc-server.d.ts.map +1 -1
  151. package/dist/shared/rpc-types.d.ts +3 -0
  152. package/dist/shared/rpc-types.d.ts.map +1 -1
  153. package/dist/shared/sqlite.d.ts.map +1 -1
  154. package/dist/shared/tui-config.d.ts +5 -3
  155. package/dist/shared/tui-config.d.ts.map +1 -1
  156. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  157. package/dist/tools/ctx-memory/types.d.ts +2 -0
  158. package/dist/tools/ctx-memory/types.d.ts.map +1 -1
  159. package/dist/tools/ctx-reduce/tools.d.ts +2 -0
  160. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  161. package/dist/tui/data/context-db.d.ts +9 -2
  162. package/dist/tui/data/context-db.d.ts.map +1 -1
  163. package/dist/tui/data/notification-socket.d.ts +4 -5
  164. package/dist/tui/data/notification-socket.d.ts.map +1 -1
  165. package/dist/tui-compiled/data/context-db.d.ts +9 -2
  166. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  167. package/dist/tui-compiled/data/notification-socket.d.ts +4 -5
  168. package/dist/tui-compiled/data/notification-socket.d.ts.map +1 -1
  169. package/package.json +2 -1
  170. package/src/shared/models-dev-cache.test.ts +5 -0
  171. package/src/shared/models-dev-cache.ts +44 -6
  172. package/src/shared/opencode-compaction-detector.test.ts +11 -2
  173. package/src/shared/rpc-client.test.ts +213 -9
  174. package/src/shared/rpc-client.ts +48 -39
  175. package/src/shared/rpc-notifications.test.ts +26 -5
  176. package/src/shared/rpc-notifications.ts +44 -35
  177. package/src/shared/rpc-server.ts +69 -33
  178. package/src/shared/rpc-types.ts +3 -0
  179. package/src/shared/sqlite.ts +22 -3
  180. package/src/shared/tui-config.test.ts +4 -12
  181. package/src/shared/tui-config.ts +6 -6
  182. package/src/tui/data/context-db.test.ts +113 -0
  183. package/src/tui/data/context-db.ts +26 -19
  184. package/src/tui/data/notification-socket.test.ts +277 -11
  185. package/src/tui/data/notification-socket.ts +257 -86
  186. package/src/tui/index.tsx +42 -25
  187. package/src/tui/slots/sidebar-content.tsx +62 -24
  188. package/src/tui-compiled/data/context-db.ts +26 -19
  189. package/src/tui-compiled/data/notification-socket.ts +257 -86
  190. package/src/tui-compiled/index.tsx +83 -30
  191. package/src/tui-compiled/slots/sidebar-content.tsx +45 -23
@@ -33,6 +33,7 @@ export interface SidebarController {
33
33
  prefs: () => MagicContextTuiPrefs
34
34
  collapsed: () => boolean
35
35
  toggleCollapsed: () => void
36
+ dispose: () => void
36
37
  }
37
38
 
38
39
  // The TUI may unmount and remount sidebar_content when the user switches views
@@ -52,11 +53,10 @@ function createSidebarController(initialPrefs: MagicContextTuiPrefs): SidebarCon
52
53
  let lastPersistedCollapsed: boolean | null = initialPrefs.collapsed
53
54
  let lastApplied = JSON.stringify(initialPrefs)
54
55
 
55
- // Watcher lives for the process lifetime — intentionally never disposed.
56
56
  // Collapse echo guard: lastPersistedCollapsed advances only once our own
57
57
  // write lands, so a watcher echo of the value we just wrote is rejected by
58
58
  // the `!==` check and cannot revert a user click.
59
- watchTuiPreferences(() => {
59
+ const stopWatchingPreferences = watchTuiPreferences(() => {
60
60
  void (async () => {
61
61
  const next = resolveMagicContextPrefs(await readTuiPreferencesFile())
62
62
  const serialized = JSON.stringify(next)
@@ -84,7 +84,12 @@ function createSidebarController(initialPrefs: MagicContextTuiPrefs): SidebarCon
84
84
  }
85
85
  }
86
86
 
87
- return { prefs, collapsed, toggleCollapsed }
87
+ return {
88
+ prefs,
89
+ collapsed,
90
+ toggleCollapsed,
91
+ dispose: stopWatchingPreferences,
92
+ }
88
93
  }
89
94
 
90
95
  function compactTokens(value: number): string {
@@ -490,6 +495,8 @@ const SidebarContent = (props: {
490
495
  let recompSawPhase = false
491
496
  let recompPollCount = 0
492
497
  let recompConsecutiveAbsent = 0
498
+ let recompSessionId: string | null = null
499
+ let snapshotRequestSequence = 0
493
500
  const RECOMP_PROBE_MAX = 12 // ~15s for the server's "Starting…" to land
494
501
  // After we've SEEN an active phase, a momentarily absent snapshot is almost
495
502
  // always transient — the server's sticky cache serves a pre-recomp snapshot
@@ -505,13 +512,14 @@ const SidebarContent = (props: {
505
512
  const refresh = () => {
506
513
  const sid = props.sessionID()
507
514
  if (!sid) return
515
+ const sequence = ++snapshotRequestSequence
508
516
  const directory = props.api.state.path.directory ?? ""
509
517
  void loadSidebarSnapshot(sid, directory)
510
518
  .then((data) => {
511
519
  // Guard against a session switch while this load was in flight:
512
520
  // painting session A's snapshot into the now-active session B shows
513
521
  // the wrong session's numbers until B's own refresh resolves.
514
- if (props.sessionID() !== sid) return
522
+ if (props.sessionID() !== sid || sequence !== snapshotRequestSequence) return
515
523
  setSnapshot(data)
516
524
  try {
517
525
  props.api.renderer.requestRender()
@@ -524,11 +532,16 @@ const SidebarContent = (props: {
524
532
  const phase = data?.recompProgress?.phase
525
533
  if ((phase === "recomp" || phase === "migration") && !recompActive) {
526
534
  kickRecompPoll()
535
+ } else if (recompActive && recompSessionId === sid) {
536
+ scheduleRecompTick()
527
537
  }
528
538
  })
529
539
  .catch(() => {
530
- // one-shot refresh failure is non-fatal; the recomp loop (if any)
531
- // has its own resilient retry.
540
+ // A one-shot refresh failure is non-fatal. If it superseded a fast
541
+ // poll request, keep that poll moving for the captured session.
542
+ if (recompActive && recompSessionId === sid && props.sessionID() === sid) {
543
+ scheduleRecompTick()
544
+ }
532
545
  })
533
546
  }
534
547
 
@@ -540,6 +553,14 @@ const SidebarContent = (props: {
540
553
  }, REFRESH_DEBOUNCE_MS)
541
554
  }
542
555
 
556
+ const stopRecompPoll = () => {
557
+ recompActive = false
558
+ recompSessionId = null
559
+ snapshotRequestSequence += 1
560
+ if (recompPollTimer) clearTimeout(recompPollTimer)
561
+ recompPollTimer = undefined
562
+ }
563
+
543
564
  const scheduleRecompTick = () => {
544
565
  if (!recompActive) return
545
566
  if (recompPollTimer) clearTimeout(recompPollTimer)
@@ -547,20 +568,26 @@ const SidebarContent = (props: {
547
568
  }
548
569
 
549
570
  function recompTick(): void {
550
- if (!recompActive) return
551
- recompPollCount += 1
552
- if (recompPollCount > RECOMP_MAX_POLLS) {
553
- recompActive = false
571
+ const sid = recompSessionId
572
+ if (!recompActive || !sid || props.sessionID() !== sid) {
573
+ stopRecompPoll()
554
574
  return
555
575
  }
556
- const sid = props.sessionID()
557
- if (!sid) {
558
- recompActive = false
576
+ recompPollCount += 1
577
+ if (recompPollCount > RECOMP_MAX_POLLS) {
578
+ stopRecompPoll()
559
579
  return
560
580
  }
581
+ const sequence = ++snapshotRequestSequence
561
582
  const directory = props.api.state.path.directory ?? ""
562
583
  void loadSidebarSnapshot(sid, directory)
563
584
  .then((data) => {
585
+ if (
586
+ !recompActive ||
587
+ recompSessionId !== sid ||
588
+ props.sessionID() !== sid ||
589
+ sequence !== snapshotRequestSequence
590
+ ) return
564
591
  const phase = data?.recompProgress?.phase
565
592
  // While a recomp is known-active, a transient snapshot that lost
566
593
  // recompProgress (sticky cache / busy-DB empty) must NOT wipe the
@@ -585,7 +612,7 @@ const SidebarContent = (props: {
585
612
  // Terminal state rendered — stop. The server keeps "done"/
586
613
  // "skipped" for a grace window and "failed" until the next run,
587
614
  // so the outcome stays visible without further polling.
588
- recompActive = false
615
+ stopRecompPoll()
589
616
  } else {
590
617
  // Phase absent this poll.
591
618
  recompConsecutiveAbsent += 1
@@ -593,7 +620,7 @@ const SidebarContent = (props: {
593
620
  // Still waiting for the server's first "Starting…".
594
621
  if (recompPollCount < RECOMP_PROBE_MAX) scheduleRecompTick()
595
622
  else {
596
- recompActive = false
623
+ stopRecompPoll()
597
624
  }
598
625
  } else if (recompConsecutiveAbsent < RECOMP_ABSENT_GIVEUP) {
599
626
  // Seen it active — absent is almost certainly the sticky
@@ -603,14 +630,19 @@ const SidebarContent = (props: {
603
630
  scheduleRecompTick()
604
631
  } else {
605
632
  // Long continuous absence — the entry is genuinely gone.
606
- recompActive = false
633
+ stopRecompPoll()
607
634
  }
608
635
  }
609
636
  })
610
- .catch((err) => {
611
- // CRITICAL: a failed/slow fetch must NOT kill the loop — keep
612
- // polling while active so we still catch the terminal state.
613
- scheduleRecompTick()
637
+ .catch(() => {
638
+ // A failed fetch must not kill the loop, but a response from a
639
+ // superseded session or sequence must not restart it either.
640
+ if (
641
+ recompActive &&
642
+ recompSessionId === sid &&
643
+ props.sessionID() === sid &&
644
+ sequence === snapshotRequestSequence
645
+ ) scheduleRecompTick()
614
646
  })
615
647
  }
616
648
 
@@ -618,8 +650,12 @@ const SidebarContent = (props: {
618
650
  // first detects an active recomp). The server emits an immediate "Starting…"
619
651
  // entry; the probe window covers the brief RPC race before it lands.
620
652
  function kickRecompPoll(): void {
621
- if (recompActive) return // already running
653
+ const sid = props.sessionID()
654
+ if (!sid) return
655
+ if (recompActive && recompSessionId === sid) return
656
+ stopRecompPoll()
622
657
  recompActive = true
658
+ recompSessionId = sid
623
659
  recompSawPhase = false
624
660
  recompPollCount = 0
625
661
  recompConsecutiveAbsent = 0
@@ -630,14 +666,15 @@ const SidebarContent = (props: {
630
666
 
631
667
  onCleanup(() => {
632
668
  if (refreshTimer) clearTimeout(refreshTimer)
633
- if (recompPollTimer) clearTimeout(recompPollTimer)
634
- recompActive = false
669
+ stopRecompPoll()
635
670
  if (activeRecompPollKick === kickRecompPoll) activeRecompPollKick = null
636
671
  })
637
672
 
638
673
  // Refresh on session change
639
674
  createEffect(
640
675
  on(props.sessionID, () => {
676
+ stopRecompPoll()
677
+ setSnapshot(null)
641
678
  refresh()
642
679
  }),
643
680
  )
@@ -893,7 +930,7 @@ const SidebarContent = (props: {
893
930
  )
894
931
  }
895
932
 
896
- export function createSidebarContentSlot(api: TuiPluginApi): TuiSlotPlugin {
933
+ export function createSidebarContentSlot(api: TuiPluginApi): TuiSlotPlugin & { dispose: () => void } {
897
934
  // Seed synchronously at slot construction so the sidebar renders at its
898
935
  // final collapse state + order on the first paint (no async flicker). The
899
936
  // controller lives here in the factory closure for the plugin lifetime, so
@@ -903,6 +940,7 @@ export function createSidebarContentSlot(api: TuiPluginApi): TuiSlotPlugin {
903
940
  const effectiveOrder = computeEffectiveOrder(seedRoot, PLUGIN_KEY, DEFAULT_SLOT_ORDER)
904
941
  return {
905
942
  order: effectiveOrder,
943
+ dispose: controller.dispose,
906
944
  slots: {
907
945
  sidebar_content: (ctx, value) => {
908
946
  const theme = createMemo(() => ctx.theme.current)
@@ -102,7 +102,13 @@ const STICKY_MAX_ENTRIES = 100;
102
102
  const stickySidebarCache = new Map<string, CachedSnapshot>();
103
103
 
104
104
  function rememberSidebarSnapshot(snapshot: SidebarSnapshot): void {
105
- if (!snapshot.sessionId || snapshot.inputTokens <= 0) return;
105
+ if (!snapshot.sessionId) return;
106
+ if (snapshot.inputTokens <= 0) {
107
+ // A successful zero is authoritative (new/deleted/reverted session) and
108
+ // must prevent a later transport failure from resurrecting old values.
109
+ stickySidebarCache.delete(snapshot.sessionId);
110
+ return;
111
+ }
106
112
  // LRU-style bound: drop the oldest entry once we hit the cap. With a
107
113
  // 5-min TTL most stale entries time out naturally; this just prevents
108
114
  // unbounded growth across many session switches in a long TUI session.
@@ -126,19 +132,9 @@ function recallSidebarSnapshot(sessionId: string, fallback: SidebarSnapshot): Si
126
132
  stickySidebarCache.delete(sessionId);
127
133
  return fallback;
128
134
  }
129
- if (!hasInFlightEvidence(fallback)) {
130
- stickySidebarCache.delete(sessionId);
131
- return fallback;
132
- }
133
135
  return cached.snapshot;
134
136
  }
135
137
 
136
- function hasInFlightEvidence(snapshot: SidebarSnapshot): boolean {
137
- return (
138
- snapshot.compartmentInProgress || snapshot.historianRunning || snapshot.pendingOpsCount > 0
139
- );
140
- }
141
-
142
138
  /** Fetch sidebar snapshot from the server via RPC. */
143
139
  export async function loadSidebarSnapshot(
144
140
  sessionId: string,
@@ -152,9 +148,11 @@ export async function loadSidebarSnapshot(
152
148
  directory,
153
149
  });
154
150
  if ((result as unknown as Record<string, unknown>).error) {
151
+ // Snapshot-build errors are explicit failure envelopes, equivalent to
152
+ // a transport failure: retain the last known-good client snapshot.
155
153
  return recallSidebarSnapshot(sessionId, empty);
156
154
  }
157
- // Trust successful server responses. The server has its own sticky
155
+ // Trust successful server responses, including authoritative zeroes. The server has its own sticky
158
156
  // sidebar cache (`sidebar-snapshot-cache.ts`) that handles transient
159
157
  // zero-token windows by hybriding cached breakdown values into a
160
158
  // fresh snapshot, AND clears that cache on `session.deleted`. If the
@@ -249,14 +247,23 @@ export async function loadEmbedDetail(sessionId: string, directory: string): Pro
249
247
  }
250
248
  }
251
249
 
252
- /** Get compartment count via RPC. */
253
- export async function getCompartmentCount(sessionId: string): Promise<number> {
254
- if (!rpcClient) return 0;
250
+ export type CompartmentCountResult = { ok: true; count: number } | { ok: false; error: string };
251
+
252
+ /** Get compartment count without making transport failure look like a real zero. */
253
+ export async function getCompartmentCount(sessionId: string): Promise<CompartmentCountResult> {
254
+ if (!rpcClient) return { ok: false, error: "RPC client is not initialized" };
255
255
  try {
256
- const result = await rpcClient.call<{ count: number }>("compartment-count", { sessionId });
257
- return result.count ?? 0;
258
- } catch {
259
- return 0;
256
+ const result = await rpcClient.call<{ count?: number; error?: string }>(
257
+ "compartment-count",
258
+ { sessionId },
259
+ );
260
+ if (typeof result.error === "string") return { ok: false, error: result.error };
261
+ if (typeof result.count !== "number" || !Number.isFinite(result.count)) {
262
+ return { ok: false, error: "Invalid compartment count response" };
263
+ }
264
+ return { ok: true, count: result.count };
265
+ } catch (error) {
266
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
260
267
  }
261
268
  }
262
269