@cortexkit/opencode-magic-context 0.21.8 → 0.22.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.
Files changed (207) hide show
  1. package/README.md +116 -325
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/agents/permissions.d.ts +29 -14
  4. package/dist/agents/permissions.d.ts.map +1 -1
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/migrate-experimental.d.ts +29 -0
  7. package/dist/config/migrate-experimental.d.ts.map +1 -0
  8. package/dist/config/schema/magic-context.d.ts +80 -104
  9. package/dist/config/schema/magic-context.d.ts.map +1 -1
  10. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  11. package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
  12. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
  13. package/dist/features/magic-context/compartment-events.d.ts +50 -0
  14. package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
  15. package/dist/features/magic-context/compartment-storage.d.ts +22 -0
  16. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
  19. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
  21. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
  23. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  24. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  25. package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
  26. package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
  27. package/dist/features/magic-context/key-files/project-key-files.d.ts.map +1 -1
  28. package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
  29. package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/constants.d.ts +4 -0
  31. package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
  32. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  33. package/dist/features/magic-context/memory/index.d.ts +1 -1
  34. package/dist/features/magic-context/memory/index.d.ts.map +1 -1
  35. package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
  36. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
  37. package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
  38. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  40. package/dist/features/magic-context/memory/storage-memory.d.ts +15 -1
  41. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  42. package/dist/features/magic-context/memory/types.d.ts +3 -1
  43. package/dist/features/magic-context/memory/types.d.ts.map +1 -1
  44. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  45. package/dist/features/magic-context/migrations.d.ts +7 -0
  46. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  47. package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
  48. package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
  49. package/dist/features/magic-context/project-identity.d.ts +2 -0
  50. package/dist/features/magic-context/project-identity.d.ts.map +1 -0
  51. package/dist/features/magic-context/storage-db.d.ts +51 -7
  52. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  53. package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
  54. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
  55. package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
  56. package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
  57. package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
  58. package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
  59. package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
  60. package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
  61. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  62. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  63. package/dist/features/magic-context/storage-meta-shared.d.ts +44 -0
  64. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  65. package/dist/features/magic-context/storage-meta.d.ts +1 -0
  66. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  67. package/dist/features/magic-context/storage-project-state.d.ts +19 -0
  68. package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
  69. package/dist/features/magic-context/storage-subagent-invocations.d.ts +9 -0
  70. package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
  71. package/dist/features/magic-context/storage-tags.d.ts +21 -1
  72. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  73. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
  74. package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
  75. package/dist/features/magic-context/storage.d.ts +12 -3
  76. package/dist/features/magic-context/storage.d.ts.map +1 -1
  77. package/dist/features/magic-context/subagent-token-capture.d.ts +1 -1
  78. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
  79. package/dist/features/magic-context/tagger.d.ts +15 -1
  80. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  81. package/dist/features/magic-context/types.d.ts +21 -0
  82. package/dist/features/magic-context/types.d.ts.map +1 -1
  83. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  84. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  85. package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
  86. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
  87. package/dist/features/magic-context/work-metrics.d.ts +66 -0
  88. package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
  89. package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
  90. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
  92. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/command-handler.d.ts +13 -1
  94. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
  96. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
  98. package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +6 -2
  101. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
  104. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/compartment-runner-types.d.ts +67 -4
  106. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
  110. package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
  111. package/dist/hooks/magic-context/decay-render.d.ts +67 -0
  112. package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
  113. package/dist/hooks/magic-context/event-handler.d.ts +1 -1
  114. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  115. package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
  116. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  117. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
  119. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
  120. package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
  121. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/hook.d.ts +9 -21
  123. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
  125. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  126. package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
  127. package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
  128. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  129. package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
  130. package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
  131. package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
  132. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  133. package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
  134. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  135. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
  136. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
  137. package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
  138. package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
  139. package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
  140. package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
  141. package/dist/hooks/magic-context/send-session-notification.d.ts +1 -1
  142. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  143. package/dist/hooks/magic-context/system-prompt-hash.d.ts +5 -6
  144. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  145. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  146. package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
  147. package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
  148. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
  149. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  150. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
  151. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  152. package/dist/hooks/magic-context/transform.d.ts +9 -7
  153. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  154. package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
  155. package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +9258 -3915
  158. package/dist/plugin/conflict-warning-hook.d.ts +13 -0
  159. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  160. package/dist/plugin/dream-timer.d.ts.map +1 -1
  161. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  162. package/dist/plugin/messages-transform.d.ts.map +1 -1
  163. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  164. package/dist/plugin/tool-registry.d.ts.map +1 -1
  165. package/dist/shared/announcement.d.ts +1 -1
  166. package/dist/shared/announcement.d.ts.map +1 -1
  167. package/dist/shared/rpc-client.d.ts +1 -0
  168. package/dist/shared/rpc-client.d.ts.map +1 -1
  169. package/dist/shared/rpc-notifications.d.ts +27 -5
  170. package/dist/shared/rpc-notifications.d.ts.map +1 -1
  171. package/dist/shared/rpc-server.d.ts +1 -0
  172. package/dist/shared/rpc-server.d.ts.map +1 -1
  173. package/dist/shared/rpc-types.d.ts +30 -2
  174. package/dist/shared/rpc-types.d.ts.map +1 -1
  175. package/dist/shared/rpc-utils.d.ts +9 -0
  176. package/dist/shared/rpc-utils.d.ts.map +1 -1
  177. package/dist/shared/sqlite-helpers.d.ts +7 -7
  178. package/dist/shared/sqlite.d.ts +23 -14
  179. package/dist/shared/sqlite.d.ts.map +1 -1
  180. package/dist/shared/tag-transcript.d.ts +10 -1
  181. package/dist/shared/tag-transcript.d.ts.map +1 -1
  182. package/dist/tools/ctx-expand/tools.d.ts +5 -1
  183. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  184. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  185. package/dist/tui/data/context-db.d.ts +16 -1
  186. package/dist/tui/data/context-db.d.ts.map +1 -1
  187. package/package.json +2 -4
  188. package/src/shared/announcement.ts +6 -7
  189. package/src/shared/rpc-client.test.ts +49 -2
  190. package/src/shared/rpc-client.ts +19 -9
  191. package/src/shared/rpc-notifications.test.ts +54 -1
  192. package/src/shared/rpc-notifications.ts +82 -13
  193. package/src/shared/rpc-server.ts +33 -4
  194. package/src/shared/rpc-types.ts +30 -2
  195. package/src/shared/rpc-utils.ts +10 -0
  196. package/src/shared/sqlite-helpers.ts +9 -9
  197. package/src/shared/sqlite.ts +99 -80
  198. package/src/shared/tag-transcript.test.ts +280 -0
  199. package/src/shared/tag-transcript.ts +162 -33
  200. package/src/tui/data/context-db.ts +75 -11
  201. package/src/tui/index.tsx +223 -32
  202. package/src/tui/slots/sidebar-content.tsx +366 -34
  203. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
  204. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
  205. package/dist/shared/native-binding.d.ts +0 -87
  206. package/dist/shared/native-binding.d.ts.map +0 -1
  207. package/src/shared/native-binding.ts +0 -311
package/src/tui/index.tsx CHANGED
@@ -4,9 +4,9 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs"
4
4
  import { dirname, join } from "node:path"
5
5
  import { createMemo } from "solid-js"
6
6
  import type { TuiPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
7
- import { createSidebarContentSlot } from "./slots/sidebar-content"
7
+ import { createSidebarContentSlot, kickRecompProgressRefresh } from "./slots/sidebar-content"
8
8
  import packageJson from "../../package.json"
9
- import { closeRpc, consumeTuiMessages, getAnnouncement, getCompartmentCount, initRpcClient, loadStatusDetail, markAnnounced, requestRecomp, type StatusDetail } from "./data/context-db"
9
+ import { closeRpc, consumeTuiMessages, dismissUpgradeReminder, getAnnouncement, getCompartmentCount, getRpcGeneration, initRpcClient, loadStatusDetail, markAnnounced, markTuiMessagesHandled, requestRecomp, requestUpgrade, type TuiMessage, type StatusDetail } from "./data/context-db"
10
10
  import { formatThresholdPercent } from "../shared/format-threshold"
11
11
  import { detectConflicts } from "../shared/conflict-detector"
12
12
  import { fixConflicts } from "../shared/conflict-fixer"
@@ -213,9 +213,11 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
213
213
  const COLORS = {
214
214
  // Cool / structured — injected by the plugin into message[0]
215
215
  system: "#c084fc",
216
+ docs: "#22d3ee",
216
217
  compartments: "#60a5fa",
217
218
  facts: "#fbbf24",
218
219
  memories: "#34d399",
220
+ profile: "#a3e635",
219
221
  // Warm / user-facing — chat and tool traffic
220
222
  conversation: "#f87171",
221
223
  toolCalls: "#fb923c",
@@ -229,6 +231,8 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
229
231
 
230
232
  if (d.systemPromptTokens > 0)
231
233
  segs.push({ label: "System", tokens: d.systemPromptTokens, color: COLORS.system })
234
+ if (d.docsTokens > 0)
235
+ segs.push({ label: "Docs", tokens: d.docsTokens, color: COLORS.docs })
232
236
  if (d.compartmentTokens > 0)
233
237
  segs.push({
234
238
  label: "Compartments",
@@ -250,6 +254,8 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
250
254
  color: COLORS.memories,
251
255
  detail: `(${d.memoryBlockCount})`,
252
256
  })
257
+ if (d.profileTokens > 0)
258
+ segs.push({ label: "User Profile", tokens: d.profileTokens, color: COLORS.profile })
253
259
 
254
260
  if (d.conversationTokens > 0)
255
261
  segs.push({ label: "Conversation", tokens: d.conversationTokens, color: COLORS.conversation })
@@ -316,6 +322,35 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
316
322
  })}
317
323
  </box>
318
324
 
325
+ {/* Recomp / session-upgrade live progress (full width, only while
326
+ running or just finished — dogfood 2026-05-30). */}
327
+ {s().recompProgress && (
328
+ <box marginTop={1} width="100%" flexDirection="column">
329
+ <text fg={t().text}><b>Recomp / Upgrade</b></text>
330
+ {(() => {
331
+ const p = s().recompProgress!
332
+ if (p.phase === "recomp") {
333
+ const frac = p.totalMessages > 0 ? p.processedMessages / p.totalMessages : 0
334
+ const width = 24
335
+ const filled = Math.round(Math.max(0, Math.min(1, frac)) * width)
336
+ const bar = p.totalMessages > 0
337
+ ? `[${"█".repeat(filled)}${"░".repeat(width - filled)}]`
338
+ : "(starting…)"
339
+ return (
340
+ <>
341
+ <R t={t()} l="upgrading" v={p.totalMessages > 0 ? `${bar} ${Math.round(frac * 100)}%` : bar} fg={t().warning} />
342
+ {p.note ? <R t={t()} l="Status" v={p.note} fg={t().textMuted} /> : null}
343
+ <R t={t()} l="Compartments" v={`${p.compartmentsCreated} (${p.passCount} pass${p.passCount === 1 ? "" : "es"})`} fg={t().textMuted} />
344
+ </>
345
+ )
346
+ }
347
+ if (p.phase === "migration") return <R t={t()} l="Status" v={p.note ?? "Migrating memories ⟳"} fg={t().warning} />
348
+ if (p.phase === "done") return <R t={t()} l="Status" v="✓ Upgrade complete" fg={t().accent} />
349
+ return <R t={t()} l="Status" v={`✗ Failed${p.message ? `: ${p.message}` : ""}`} fg={t().error} />
350
+ })()}
351
+ </box>
352
+ )}
353
+
319
354
  {/* 2-column layout */}
320
355
  <box flexDirection="row" width="100%" marginTop={1} gap={4}>
321
356
  {/* Left column */}
@@ -406,49 +441,139 @@ function getModelKeyFromMessages(api: TuiPluginApi, sessionId: string): string |
406
441
  return undefined
407
442
  }
408
443
 
409
- function showRecompDialog(api: TuiPluginApi) {
410
- const sessionId = getSessionId(api)
444
+ async function showRecompDialog(api: TuiPluginApi, targetSessionId = getSessionId(api)): Promise<boolean> {
445
+ const sessionId = targetSessionId
411
446
  if (!sessionId) {
412
447
  api.ui.toast({ message: "No active session", variant: "warning" })
413
- return
448
+ return false
414
449
  }
415
450
 
416
- void getCompartmentCount(sessionId).then((count) => {
417
- api.ui.dialog.replace(() => (
451
+ const count = await getCompartmentCount(sessionId)
452
+ // Ack only after the dialog is actually shown for the same active session;
453
+ // route switches while the RPC detail load is in flight must leave it pending.
454
+ if (getSessionId(api) !== sessionId) return false
455
+
456
+ api.ui.dialog.replace(() => (
457
+ <api.ui.DialogConfirm
458
+ title="⚠️ Recomp Confirmation"
459
+ message={[
460
+ `You have ${count} compartments.`,
461
+ "",
462
+ "Recomp will regenerate all compartments and facts from raw history.",
463
+ "This may take a long time and consume significant tokens.",
464
+ "",
465
+ "Proceed?",
466
+ ].join("\n")}
467
+ onConfirm={() => {
468
+ void requestRecomp(sessionId)
469
+ kickRecompProgressRefresh()
470
+ api.ui.toast({ message: "Recomp requested — historian will start shortly", variant: "info", duration: 5000 })
471
+ }}
472
+ onCancel={() => {
473
+ api.ui.toast({ message: "Recomp cancelled", variant: "info", duration: 3000 })
474
+ }}
475
+ />
476
+ ))
477
+ return true
478
+ }
479
+
480
+ function showUpgradeDialog(
481
+ api: TuiPluginApi,
482
+ resume?: { stagedCount: number; stagedThrough: number },
483
+ targetSessionId = getSessionId(api),
484
+ ): boolean {
485
+ const sessionId = targetSessionId
486
+ if (!sessionId) {
487
+ // No active session — nothing to upgrade. Silently skip (the server only
488
+ // enqueues this for sessions with legacy compartments, but the TUI may
489
+ // have switched sessions before the poller fired).
490
+ return false
491
+ }
492
+
493
+ if (getSessionId(api) !== sessionId) return false
494
+
495
+ const title = resume ? "🎆 Resume the interrupted upgrade?" : "🎆 Historian V2 is released!"
496
+ const message = resume
497
+ ? [
498
+ `An earlier upgrade to the new historian format was interrupted. ${resume.stagedCount} compartment${resume.stagedCount === 1 ? " was" : "s were"} already rebuilt (through message ${resume.stagedThrough}). Resuming continues from where it left off — nothing already rebuilt is reprocessed.`,
499
+ "",
500
+ "Resuming will:",
501
+ "• Rebuild the remaining compartments into the new layered format",
502
+ "• Re-organize this project's memories into the new taxonomy (once per project)",
503
+ "",
504
+ "The historian runs in the background and you can keep working. You can also resume via /ctx-session-upgrade later.",
505
+ "",
506
+ "Resume the upgrade now?",
507
+ ].join("\n")
508
+ : [
509
+ "This session's compartments are written by the old historian. The session is still usable with its old compartments, however it's strongly advised to upgrade them to the new format. This means every compartment needs to be reprocessed by the new historian, which might take a while depending on how big your session is.",
510
+ "",
511
+ "Running the upgrade will:",
512
+ "• Rebuild this session's compartments into the new layered format",
513
+ "• Re-organize this project's memories into the new taxonomy (once per project)",
514
+ "",
515
+ "The historian runs in the background and you can keep working while older compartments are reprocessed. You can also upgrade via /ctx-session-upgrade later.",
516
+ "",
517
+ "Run the upgrade now?",
518
+ ].join("\n")
519
+
520
+ api.ui.dialog.replace(
521
+ () => (
418
522
  <api.ui.DialogConfirm
419
- title="⚠️ Recomp Confirmation"
420
- message={[
421
- `You have ${count} compartments.`,
422
- "",
423
- "Recomp will regenerate all compartments and facts from raw history.",
424
- "This may take a long time and consume significant tokens.",
425
- "",
426
- "Proceed?",
427
- ].join("\n")}
523
+ title={title}
524
+ message={message}
428
525
  onConfirm={() => {
429
- void requestRecomp(sessionId)
430
- api.ui.toast({ message: "Recomp requested historian will start shortly", variant: "info", duration: 5000 })
526
+ // Explicit choice → dismiss the fresh reminder durably so it
527
+ // won't re-show. (Resume prompts are staging-driven and still
528
+ // fire if this run is later interrupted.)
529
+ void dismissUpgradeReminder(sessionId)
530
+ void requestUpgrade(sessionId)
531
+ // Start the sidebar's recomp self-poll immediately — the RPC
532
+ // call fires no message event, so without this the progress
533
+ // bar wouldn't appear until the upgrade finished.
534
+ kickRecompProgressRefresh()
535
+ api.ui.toast({
536
+ message: resume
537
+ ? "Resuming session upgrade — running in the background"
538
+ : "Session upgrade started — running in the background",
539
+ variant: "info",
540
+ duration: 5000,
541
+ })
431
542
  }}
432
543
  onCancel={() => {
433
- api.ui.toast({ message: "Recomp cancelled", variant: "info", duration: 3000 })
544
+ // Explicit decline set the durable stamp so we don't re-prompt
545
+ // on every restart. The fix for stamp-on-display trapping a
546
+ // never-upgraded session (dogfood 2026-05-30) relies on THIS
547
+ // being the only place the TUI path stamps.
548
+ void dismissUpgradeReminder(sessionId)
549
+ api.ui.toast({
550
+ message: "Upgrade skipped — run /ctx-session-upgrade anytime",
551
+ variant: "info",
552
+ duration: 4000,
553
+ })
434
554
  }}
435
555
  />
436
- ))
437
- })
556
+ ),
557
+ )
558
+ return true
438
559
  }
439
560
 
440
- function showStatusDialog(api: TuiPluginApi) {
441
- const sessionId = getSessionId(api)
561
+ async function showStatusDialog(api: TuiPluginApi, targetSessionId = getSessionId(api)): Promise<boolean> {
562
+ const sessionId = targetSessionId
442
563
  if (!sessionId) {
443
564
  api.ui.toast({ message: "No active session", variant: "warning" })
444
- return
565
+ return false
445
566
  }
446
567
 
447
568
  const directory = api.state.path.directory ?? ""
448
569
  const modelKey = getModelKeyFromMessages(api, sessionId)
449
- void loadStatusDetail(sessionId, directory, modelKey).then((detail) => {
450
- api.ui.dialog.replace(() => <StatusDialog api={api} s={detail} />)
451
- })
570
+ const detail = await loadStatusDetail(sessionId, directory, modelKey)
571
+ // Ack only after the dialog is actually shown for the same active session;
572
+ // route switches while the RPC detail load is in flight must leave it pending.
573
+ if (getSessionId(api) !== sessionId) return false
574
+
575
+ api.ui.dialog.replace(() => <StatusDialog api={api} s={detail} />)
576
+ return true
452
577
  }
453
578
 
454
579
  /**
@@ -636,10 +761,46 @@ const tui: TuiPlugin = async (api, _options, meta) => {
636
761
  registerCommandPaletteEntries(api)
637
762
 
638
763
  // Poll for server→TUI messages: toasts and dialog requests.
639
- // Single poller because consumeTuiMessages() is destructive (deletes consumed rows).
764
+ // The poller owns cursor advancement so notifications are acked only after
765
+ // they are accepted for the still-active session and delivered to the UI.
766
+ let pollInFlight = false
640
767
  const messagePoller = setInterval(() => {
641
- void consumeTuiMessages().then((messages) => {
642
- for (const msg of messages) {
768
+ // Scope the drain to the TUI's active session so notifications tagged for
769
+ // a different session (served by the same RPC process) are not consumed
770
+ // here. Do not poll on non-session routes: a session-scoped action fetched
771
+ // while sessionless could otherwise be acked without being shown.
772
+ // Avoid overlapping read-only drains: the server re-delivers until acked,
773
+ // so a second in-flight poll can fetch and dispatch the same batch twice.
774
+ if (pollInFlight) return
775
+
776
+ const requestedSessionId = getSessionId(api)
777
+ if (!requestedSessionId) return
778
+
779
+ pollInFlight = true
780
+ const pollGeneration = getRpcGeneration()
781
+ void consumeTuiMessages(requestedSessionId).then(async (messages) => {
782
+ // The dialog handlers read the current session when they run. If the
783
+ // user switched routes while the RPC was in flight, drop this whole
784
+ // batch without advancing the cursor; the next poll for the new
785
+ // session will fetch the right notifications.
786
+ // Ignore late responses from an older RPC client generation; close/init
787
+ // clears cursors and stale callbacks must not recreate them.
788
+ if (getRpcGeneration() !== pollGeneration) return
789
+
790
+ if (getSessionId(api) !== requestedSessionId) return
791
+
792
+ const orderedMessages = [...messages].sort((a, b) => a.id - b.id)
793
+ const handledMessageIds = new Set<number>()
794
+ for (const msg of orderedMessages) {
795
+ // Drop any action/dialog whose sessionId doesn't match this TUI's
796
+ // active session (session-less/global notifications still apply).
797
+ if (
798
+ msg.type === "action" &&
799
+ msg.sessionId &&
800
+ msg.sessionId !== requestedSessionId
801
+ ) {
802
+ continue
803
+ }
643
804
  if (msg.type === "toast") {
644
805
  const p = msg.payload
645
806
  api.ui.toast({
@@ -647,17 +808,47 @@ const tui: TuiPlugin = async (api, _options, meta) => {
647
808
  variant: (p.variant as "info" | "warning" | "error" | "success") ?? "info",
648
809
  duration: typeof p.duration === "number" ? p.duration : 5000,
649
810
  })
811
+ handledMessageIds.add(msg.id)
650
812
  } else if (msg.type === "action") {
651
813
  const action = msg.payload?.action
652
814
  if (action === "show-status-dialog") {
653
- showStatusDialog(api)
815
+ if (await showStatusDialog(api, requestedSessionId)) {
816
+ handledMessageIds.add(msg.id)
817
+ }
654
818
  } else if (action === "show-recomp-dialog") {
655
- showRecompDialog(api)
819
+ if (await showRecompDialog(api, requestedSessionId)) {
820
+ handledMessageIds.add(msg.id)
821
+ }
822
+ } else if (action === "show-upgrade-dialog") {
823
+ const resume =
824
+ msg.payload?.resume === true
825
+ ? {
826
+ stagedCount: Number(msg.payload?.stagedCount ?? 0),
827
+ stagedThrough: Number(msg.payload?.stagedThrough ?? 0),
828
+ }
829
+ : undefined
830
+ if (showUpgradeDialog(api, resume, requestedSessionId)) {
831
+ handledMessageIds.add(msg.id)
832
+ }
656
833
  }
657
834
  }
658
835
  }
836
+ const handledPrefixMessages: TuiMessage[] = []
837
+ for (const msg of orderedMessages) {
838
+ if (!handledMessageIds.has(msg.id)) break
839
+ handledPrefixMessages.push(msg)
840
+ }
841
+ // A dialog helper may have awaited more RPC work; re-check before
842
+ // acking so a dispose/reinit or route switch during that await cannot
843
+ // advance a stale cursor.
844
+ if (getRpcGeneration() !== pollGeneration) return
845
+ if (getSessionId(api) !== requestedSessionId) return
846
+
847
+ markTuiMessagesHandled(requestedSessionId, handledPrefixMessages)
659
848
  }).catch(() => {
660
849
  // Intentional: message polling should never crash the TUI
850
+ }).finally(() => {
851
+ pollInFlight = false
661
852
  })
662
853
  }, 500)
663
854