@cortexkit/opencode-magic-context 0.40.1 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +1 -1
  2. package/dist/{announcement-5eg4t64s.js → announcement-3g9xg433.js} +2 -2
  3. package/dist/config/schema/magic-context.d.ts +15 -0
  4. package/dist/config/schema/magic-context.d.ts.map +1 -1
  5. package/dist/{conflict-warning-hook-f406ggbw.js → conflict-warning-hook-rf17b6et.js} +4 -4
  6. package/dist/{derive-budgets-xk8cy5e6.js → derive-budgets-bb7mrpzs.js} +3 -3
  7. package/dist/features/builtin-commands/commands.d.ts +35 -2
  8. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  9. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +12 -16
  10. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  11. package/dist/features/magic-context/context-authority.d.ts +10 -0
  12. package/dist/features/magic-context/context-authority.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/classify.d.ts +1 -0
  14. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/lease.d.ts +12 -27
  16. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
  18. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/map-memories.d.ts +2 -0
  20. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/memory-claim-safety.d.ts +9 -0
  22. package/dist/features/magic-context/dreamer/memory-claim-safety.d.ts.map +1 -0
  23. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  24. package/dist/features/magic-context/dreamer/task-registry.d.ts +2 -0
  25. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  26. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +7 -2
  27. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  28. package/dist/features/magic-context/dreamer/verify.d.ts +13 -10
  29. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/embedding-failure.d.ts +1 -1
  31. package/dist/features/magic-context/memory/embedding-failure.d.ts.map +1 -1
  32. package/dist/features/magic-context/memory/embedding-local.d.ts +12 -2
  33. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  34. package/dist/features/magic-context/memory/embedding-synapse.d.ts +10 -1
  35. package/dist/features/magic-context/memory/embedding-synapse.d.ts.map +1 -1
  36. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  37. package/dist/features/magic-context/memory/project-identity.d.ts +4 -0
  38. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/storage-memory-verifications.d.ts +5 -1
  40. package/dist/features/magic-context/memory/storage-memory-verifications.d.ts.map +1 -1
  41. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  42. package/dist/features/magic-context/memory/transformers-web-entry.d.ts +2 -0
  43. package/dist/features/magic-context/memory/transformers-web-entry.d.ts.map +1 -0
  44. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  45. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  46. package/dist/features/magic-context/project-embedding-registry.d.ts +3 -0
  47. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  48. package/dist/features/magic-context/search.d.ts +18 -0
  49. package/dist/features/magic-context/search.d.ts.map +1 -1
  50. package/dist/features/magic-context/storage-db.d.ts +1 -1
  51. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  52. package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
  53. package/dist/features/magic-context/storage-meta-persisted.d.ts +29 -3
  54. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  55. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  56. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  57. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  58. package/dist/features/magic-context/storage-ops.d.ts +2 -0
  59. package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
  60. package/dist/features/magic-context/storage-tags.d.ts +7 -0
  61. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  62. package/dist/features/magic-context/storage.d.ts +2 -2
  63. package/dist/features/magic-context/storage.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/boundary-execution.d.ts +3 -0
  65. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/channel2-delivery.d.ts +2 -4
  67. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/command-handler.d.ts +7 -0
  69. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  72. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -0
  74. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +15 -14
  76. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/emergency-drop.d.ts +6 -8
  78. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/event-resolvers.d.ts +2 -2
  80. package/dist/hooks/magic-context/execute-status.d.ts +4 -1
  81. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/format-embed-failure.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +6 -0
  84. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/hook-handlers.d.ts +21 -8
  86. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/hook.d.ts +13 -0
  88. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/inject-compartments.d.ts +6 -16
  90. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/maintenance-authority.d.ts +3 -0
  92. package/dist/hooks/magic-context/maintenance-authority.d.ts.map +1 -0
  93. package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +1 -0
  97. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/rust-mode-transform.d.ts +9 -0
  99. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/strip-content.d.ts +8 -0
  101. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/stripped-command.d.ts +19 -0
  103. package/dist/hooks/magic-context/stripped-command.d.ts.map +1 -0
  104. package/dist/hooks/magic-context/supersession-reclaim.d.ts +11 -0
  105. package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/tag-messages.d.ts +6 -0
  107. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +11 -0
  109. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/transform-operations.d.ts +1 -1
  111. package/dist/hooks/magic-context/transform-operations.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +8 -1
  113. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  115. package/dist/{index-mg1yx1ph.js → index-2d4kf2ve.js} +2 -2
  116. package/dist/{index-mmrhhk9a.js → index-37yzyeca.js} +36 -8
  117. package/dist/index-4x691bz5.js +369 -0
  118. package/dist/{index-3dkzzh0t.js → index-9j3tx8hy.js} +1 -1
  119. package/dist/{index-sw8gnq34.js → index-b9j85d29.js} +2 -2
  120. package/dist/{index-ehpzdy0v.js → index-d1mxrd3p.js} +7 -1
  121. package/dist/{index-x64cmpej.js → index-dh0vfghk.js} +817 -438
  122. package/dist/{index-qf20mv7e.js → index-dmvy29z0.js} +1 -1
  123. package/dist/{index-1h9t9xb5.js → index-eq9wr75c.js} +2 -2
  124. package/dist/{index-pck9vadr.js → index-vnz6jvdc.js} +1 -1
  125. package/dist/{index-rw0qrkvq.js → index-z893kjyt.js} +17 -4
  126. package/dist/{index-btj2qhmc.js → index-zwjhba2r.js} +3 -3
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +3230 -2422
  129. package/dist/{model-resolution-r9jbcz86.js → model-resolution-6w6wzw18.js} +3 -1
  130. package/dist/plugin/dream-timer.d.ts.map +1 -1
  131. package/dist/plugin/embedding-routing.d.ts.map +1 -1
  132. package/dist/plugin/hooks/create-session-hooks.d.ts +14 -0
  133. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  134. package/dist/plugin/rpc-handlers.d.ts +10 -0
  135. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  136. package/dist/{recomp-orchestrator-dtgww0f0.js → recomp-orchestrator-w7xg48e3.js} +7 -7
  137. package/dist/{safe-notification-target-tepzc2xa.js → safe-notification-target-2a8py95v.js} +3 -3
  138. package/dist/{send-session-notification-6v5b72b4.js → send-session-notification-bw7eex6w.js} +4 -4
  139. package/dist/shared/data-path.d.ts +12 -3
  140. package/dist/shared/data-path.d.ts.map +1 -1
  141. package/dist/shared/logger.d.ts.map +1 -1
  142. package/dist/shared/model-resolution.d.ts +1 -0
  143. package/dist/shared/model-resolution.d.ts.map +1 -1
  144. package/dist/shared/rpc-types.d.ts +5 -0
  145. package/dist/shared/rpc-types.d.ts.map +1 -1
  146. package/dist/shared/rust-mode-status.d.ts +3 -0
  147. package/dist/shared/rust-mode-status.d.ts.map +1 -0
  148. package/dist/shared/status-detail-text.d.ts.map +1 -1
  149. package/dist/shared/subagent-runner.d.ts +4 -0
  150. package/dist/shared/subagent-runner.d.ts.map +1 -1
  151. package/dist/shared/test-temp-dir.d.ts +40 -0
  152. package/dist/shared/test-temp-dir.d.ts.map +1 -0
  153. package/dist/shared/write-transaction-timing.d.ts +11 -0
  154. package/dist/shared/write-transaction-timing.d.ts.map +1 -0
  155. package/dist/{storage-meta-session-yvtbaw7c.js → storage-meta-session-gr4zn7sq.js} +4 -4
  156. package/dist/{task-config-a1qmj9bx.js → task-config-gv6s04fp.js} +2 -2
  157. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  158. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  159. package/dist/tools/ctx-search/tools.d.ts.map +1 -1
  160. package/dist/transformers-web.js +24570 -0
  161. package/dist/transformers.node-b1wa0hgx.js +28808 -0
  162. package/dist/tui/data/context-db.d.ts +10 -3
  163. package/dist/tui/data/context-db.d.ts.map +1 -1
  164. package/dist/tui-compiled/data/context-db.d.ts +10 -3
  165. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  166. package/package.json +7 -3
  167. package/src/config/index.test.ts +47 -4
  168. package/src/config/schema/magic-context.test.ts +20 -0
  169. package/src/config/schema/magic-context.ts +8 -0
  170. package/src/features/magic-context/dreamer/task-registry.ts +2 -0
  171. package/src/shared/conflict-detector.test.ts +6 -19
  172. package/src/shared/conflict-fixer.test.ts +5 -8
  173. package/src/shared/data-path.test.ts +44 -4
  174. package/src/shared/data-path.ts +54 -11
  175. package/src/shared/logger.test.ts +145 -35
  176. package/src/shared/logger.ts +124 -6
  177. package/src/shared/model-resolution.test.ts +37 -0
  178. package/src/shared/model-resolution.ts +7 -0
  179. package/src/shared/redaction.ts +2 -2
  180. package/src/shared/rpc-types.ts +5 -0
  181. package/src/shared/rust-mode-status.ts +3 -0
  182. package/src/shared/status-detail-text.test.ts +13 -0
  183. package/src/shared/status-detail-text.ts +2 -0
  184. package/src/shared/subagent-runner.ts +4 -0
  185. package/src/shared/test-temp-dir.test.ts +57 -0
  186. package/src/shared/test-temp-dir.ts +165 -0
  187. package/src/shared/write-transaction-timing.ts +25 -0
  188. package/src/tui/data/context-db.test.ts +31 -1
  189. package/src/tui/data/context-db.ts +17 -61
  190. package/src/tui/index.tsx +36 -5
  191. package/src/tui-compiled/data/context-db.ts +17 -61
  192. package/src/tui-compiled/index.tsx +229 -191
  193. package/dist/index-8pqfcghm.js +0 -81
  194. /package/dist/{index-rpgg35ms.js → index-8sseka65.js} +0 -0
@@ -0,0 +1,165 @@
1
+ import { mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+
5
+ const STALE_TEMP_DIR_AGE_MS = 60 * 60 * 1000;
6
+
7
+ // Keep this list narrow: startup cleanup must never turn into a shared /tmp purge.
8
+ export const TEST_TEMP_DIR_PREFIXES = [
9
+ "mc-plugin-test-xdg-pid-",
10
+ "mc-pi-test-xdg-",
11
+ "mc-conflict-",
12
+ "mc-conflict-fixer-",
13
+ "mc-config-secret-",
14
+ "pi-reasoning-replay-",
15
+ "pi-session-api-test-",
16
+ "pi-symlink-test-",
17
+ "pi-running-vs-stale-",
18
+ "pi-dist-metadata-",
19
+ "pi-traversal-",
20
+ "pi-source-mode-",
21
+ "pi-source-missing-",
22
+ "pi-nonprefixed-",
23
+ "pi-default-cond-",
24
+ "pi-array-exports-",
25
+ "pi-extensionless-",
26
+ "pi-tsx-source-",
27
+ "pi-guidance-",
28
+ "pi-system-v2-",
29
+ "pi-no-guidance-",
30
+ "pi-guidance-dedup-",
31
+ "pi-noreduce-",
32
+ "pi-language-baseline-",
33
+ "pi-language-unset-",
34
+ "pi-language-set-",
35
+ "pi-a1-guidance-",
36
+ "pi-prompt-epoch-",
37
+ "pi-embedding-bootstrap-",
38
+ "pi-embedding-home-",
39
+ "magic-context-pi-latch-test-",
40
+ "magic-context-pi-index-test-",
41
+ "mc-test-temp-dir-helper-",
42
+ ] as const;
43
+
44
+ export type TestTempDirPrefix = (typeof TEST_TEMP_DIR_PREFIXES)[number];
45
+
46
+ type AfterAll = (callback: () => void) => void;
47
+ type SweepOptions = {
48
+ tempDir?: string;
49
+ nowMs?: number;
50
+ staleAgeMs?: number;
51
+ };
52
+
53
+ const registeredTempDirs = new Set<string>();
54
+ let lifecycleCleanupInstalled = false;
55
+
56
+ function hasRecognizedPrefix(name: string): boolean {
57
+ return TEST_TEMP_DIR_PREFIXES.some((prefix) => name.startsWith(prefix));
58
+ }
59
+
60
+ function isRegisteredTempDir(directory: string): boolean {
61
+ const resolvedDirectory = resolve(directory);
62
+ return (
63
+ registeredTempDirs.has(resolvedDirectory) &&
64
+ dirname(resolvedDirectory) === resolve(tmpdir()) &&
65
+ hasRecognizedPrefix(basename(resolvedDirectory))
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Creates a test-owned root under the system temp directory. The returned cleanup
71
+ * only removes roots made by this module, so an accidental path cannot delete an
72
+ * unrelated temp directory.
73
+ */
74
+ export function createTestTempDir(
75
+ prefix: TestTempDirPrefix,
76
+ nameSuffix = "",
77
+ ): { dir: string; cleanup: () => void } {
78
+ const dir = resolve(mkdtempSync(join(tmpdir(), `${prefix}${nameSuffix}`)));
79
+ registeredTempDirs.add(dir);
80
+ return { dir, cleanup: () => cleanupTestTempDir(dir) };
81
+ }
82
+
83
+ /** Removes one registered test root and forgets it even when best-effort removal fails. */
84
+ export function cleanupTestTempDir(directory: string): void {
85
+ const resolvedDirectory = resolve(directory);
86
+ if (!isRegisteredTempDir(resolvedDirectory)) return;
87
+
88
+ registeredTempDirs.delete(resolvedDirectory);
89
+ try {
90
+ rmSync(resolvedDirectory, {
91
+ recursive: true,
92
+ force: true,
93
+ maxRetries: 10,
94
+ retryDelay: 100,
95
+ });
96
+ } catch {
97
+ // A process can still hold a file open on Windows. The next startup sweep retries it.
98
+ }
99
+ }
100
+
101
+ /** Removes every root registered by this process. Safe to call more than once. */
102
+ export function cleanupRegisteredTestTempDirs(): void {
103
+ for (const directory of [...registeredTempDirs]) cleanupTestTempDir(directory);
104
+ }
105
+
106
+ /**
107
+ * Runs a callback with a fresh root and removes that root even when the callback throws.
108
+ * This keeps small fixture builders from forgetting a failure-path cleanup.
109
+ */
110
+ export function withTestTempDir<T>(
111
+ prefix: TestTempDirPrefix,
112
+ callback: (directory: string) => T,
113
+ ): T {
114
+ const { dir, cleanup } = createTestTempDir(prefix);
115
+ try {
116
+ return callback(dir);
117
+ } finally {
118
+ cleanup();
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Installs both Bun's normal suite cleanup and an exit fallback for aborted runs.
124
+ * The callback is passed in so this utility remains usable from the test preload
125
+ * without making production code import bun:test.
126
+ */
127
+ export function installTestTempDirCleanup(afterAll: AfterAll): void {
128
+ if (lifecycleCleanupInstalled) return;
129
+ lifecycleCleanupInstalled = true;
130
+ afterAll(cleanupRegisteredTestTempDirs);
131
+ process.once("exit", cleanupRegisteredTestTempDirs);
132
+ }
133
+
134
+ /**
135
+ * Removes only old, recognized test roots from one temp directory. The scan does
136
+ * one directory read, ignores non-directories, and leaves fresh roots and every
137
+ * unrecognized name alone so concurrently running tools stay isolated.
138
+ */
139
+ export function sweepStaleTestTempDirs({
140
+ tempDir = tmpdir(),
141
+ nowMs = Date.now(),
142
+ staleAgeMs = STALE_TEMP_DIR_AGE_MS,
143
+ }: SweepOptions = {}): string[] {
144
+ let entries: Array<{ isDirectory(): boolean; name: string }>;
145
+ try {
146
+ entries = readdirSync(tempDir, { withFileTypes: true });
147
+ } catch {
148
+ return [];
149
+ }
150
+
151
+ const removed: string[] = [];
152
+ for (const entry of entries) {
153
+ if (!entry.isDirectory() || !hasRecognizedPrefix(entry.name)) continue;
154
+
155
+ const directory = join(tempDir, entry.name);
156
+ try {
157
+ if (nowMs - statSync(directory).mtimeMs <= staleAgeMs) continue;
158
+ rmSync(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
159
+ removed.push(directory);
160
+ } catch {
161
+ // Temp cleanup is best-effort; a later test run retries busy directories.
162
+ }
163
+ }
164
+ return removed;
165
+ }
@@ -0,0 +1,25 @@
1
+ import { log } from "./logger";
2
+
3
+ /**
4
+ * A one-second threshold catches transactions that can noticeably hold back
5
+ * sibling writers without turning normal SQLite work into log noise.
6
+ */
7
+ export const SLOW_WRITE_TRANSACTION_THRESHOLD_MS = 1_000;
8
+
9
+ /**
10
+ * Log a slow write only after its COMMIT has completed. The timing diagnostic is
11
+ * deliberately best-effort so it can never change the transaction's behavior.
12
+ */
13
+ export function logSlowWriteTransaction(
14
+ site: string,
15
+ startedAt: number,
16
+ thresholdMs = SLOW_WRITE_TRANSACTION_THRESHOLD_MS,
17
+ ): void {
18
+ try {
19
+ const durationMs = performance.now() - startedAt;
20
+ if (durationMs < thresholdMs) return;
21
+ log(`[magic-context] slow write transaction: site=${site} held=${durationMs.toFixed(1)}ms`);
22
+ } catch {
23
+ // Timing diagnostics must never make a committed write appear to fail.
24
+ }
25
+ }
@@ -4,9 +4,17 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { MagicContextRpcServer } from "../../shared/rpc-server";
6
6
  import type { SidebarSnapshot } from "../../shared/rpc-types";
7
- import { closeRpc, getCompartmentCount, initRpcClient, loadSidebarSnapshot } from "./context-db";
7
+ import {
8
+ closeRpc,
9
+ getCompartmentCount,
10
+ initRpcClient,
11
+ loadSidebarSnapshot,
12
+ loadStatusDetail,
13
+ } from "./context-db";
8
14
 
9
15
  const originalXdgDataHome = process.env.XDG_DATA_HOME;
16
+ const originalTestDataDir = process.env.MAGIC_CONTEXT_TEST_DATA_DIR;
17
+ const originalStorageDir = process.env.MAGIC_CONTEXT_STORAGE_DIR;
10
18
  const tempDirs: string[] = [];
11
19
  const servers: MagicContextRpcServer[] = [];
12
20
 
@@ -18,12 +26,19 @@ afterEach(() => {
18
26
  }
19
27
  if (originalXdgDataHome === undefined) delete process.env.XDG_DATA_HOME;
20
28
  else process.env.XDG_DATA_HOME = originalXdgDataHome;
29
+ if (originalTestDataDir === undefined) delete process.env.MAGIC_CONTEXT_TEST_DATA_DIR;
30
+ else process.env.MAGIC_CONTEXT_TEST_DATA_DIR = originalTestDataDir;
31
+ if (originalStorageDir === undefined) delete process.env.MAGIC_CONTEXT_STORAGE_DIR;
32
+ else process.env.MAGIC_CONTEXT_STORAGE_DIR = originalStorageDir;
21
33
  });
22
34
 
23
35
  function makeDataHome(): string {
24
36
  const dir = mkdtempSync(join(tmpdir(), "mc-context-db-"));
25
37
  tempDirs.push(dir);
26
38
  process.env.XDG_DATA_HOME = dir;
39
+ // The shared resolver gives test isolation priority. Keep both values
40
+ // aligned so the RPC client and fixture server exercise one directory.
41
+ process.env.MAGIC_CONTEXT_TEST_DATA_DIR = dir;
27
42
  return dir;
28
43
  }
29
44
 
@@ -96,6 +111,21 @@ describe("TUI context RPC data", () => {
96
111
  expect((await loadSidebarSnapshot(sessionId, directory)).inputTokens).toBe(0);
97
112
  });
98
113
 
114
+ test("does not turn a status authority failure into an empty session", async () => {
115
+ const dataHome = makeDataHome();
116
+ const directory = "/repo-status-error";
117
+ const server = await startServer(dataHome, directory, () => ({}));
118
+ server.handle("status-detail", async () => ({
119
+ error: "Rust module status unavailable; canonical session state was not read",
120
+ }));
121
+ initRpcClient(directory);
122
+
123
+ expect(await loadStatusDetail("ses_rust", directory)).toEqual({
124
+ ok: false,
125
+ error: "Rust module status unavailable; canonical session state was not read",
126
+ });
127
+ });
128
+
99
129
  test("distinguishes a real zero compartment count from an RPC failure", async () => {
100
130
  const dataHome = makeDataHome();
101
131
  const directory = "/repo-count";
@@ -2,8 +2,7 @@
2
2
  * TUI data layer — pure RPC client, no direct SQLite access.
3
3
  * All data is fetched from the server plugin via HTTP RPC.
4
4
  */
5
- import os from "node:os";
6
- import path from "node:path";
5
+ import { getMagicContextStorageDir } from "../../shared/data-path";
7
6
  import { MagicContextRpcClient } from "../../shared/rpc-client";
8
7
  import type { EmbedDetail, SidebarSnapshot, StatusDetail } from "../../shared/rpc-types";
9
8
 
@@ -12,18 +11,9 @@ export type { EmbedDetail, SidebarSnapshot, StatusDetail };
12
11
  let rpcClient: MagicContextRpcClient | null = null;
13
12
  let rpcGeneration = 0;
14
13
 
15
- function getStorageDir(): string {
16
- // Plugin v0.16+ uses the shared cortexkit/magic-context path so OpenCode
17
- // and Pi can share state. The TUI just needs to point its RPC client at
18
- // the same storage directory the server plugin uses for the lock-file
19
- // discovery convention.
20
- const dataDir = process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
21
- return path.join(dataDir, "cortexkit", "magic-context");
22
- }
23
-
24
14
  /** Initialize the RPC client. Call once on TUI startup. */
25
15
  export function initRpcClient(directory: string): void {
26
- const storageDir = getStorageDir();
16
+ const storageDir = getMagicContextStorageDir();
27
17
  // Bump the generation before replacing the client so late notification
28
18
  // responses from a disposed client are ignored (the WS socket observes the
29
19
  // new generation and abandons its in-flight connect).
@@ -172,63 +162,26 @@ export async function loadSidebarSnapshot(
172
162
  }
173
163
  }
174
164
 
175
- /** Fetch full status detail from the server via RPC. */
165
+ export type StatusDetailResult = { ok: true; detail: StatusDetail } | { ok: false; error: string };
166
+
167
+ /** Fetch full status detail without presenting transport failure as an empty session. */
176
168
  export async function loadStatusDetail(
177
169
  sessionId: string,
178
170
  directory: string,
179
171
  modelKey?: string,
180
- ): Promise<StatusDetail> {
181
- const emptyDetail: StatusDetail = {
182
- ...EMPTY_SNAPSHOT,
183
- sessionId,
184
- activeProfile: null,
185
- tagCounter: 0,
186
- activeTags: 0,
187
- droppedTags: 0,
188
- totalTags: 0,
189
- activeBytes: 0,
190
- lastResponseTime: 0,
191
- lastNudgeTokens: 0,
192
- lastTransformError: null,
193
- isSubagent: false,
194
- pendingOps: [],
195
- contextLimit: 0,
196
- cacheTtlMs: 0,
197
- cacheRemainingMs: 0,
198
- cacheExpired: false,
199
- cacheNeverExpires: false,
200
- executeThreshold: 65,
201
- executeThresholdMode: "percentage",
202
- protectedTagCount: 20,
203
- historyBudgetPercentage: 0.15,
204
- historyBlockTokens: 0,
205
- compressionBudget: null,
206
- compressionUsage: null,
207
- toastDurationMs: 5000,
208
- loggerDiagnostics: {
209
- swallowedWriteCount: 0,
210
- lastErrorMessage: null,
211
- lastErrorTime: null,
212
- },
213
- storage_versions: {
214
- context_db_schema_version: null,
215
- plugin_supported_version: 0,
216
- },
217
- };
218
-
219
- if (!rpcClient) return emptyDetail;
172
+ ): Promise<StatusDetailResult> {
173
+ if (!rpcClient) return { ok: false, error: "RPC client is not initialized" };
220
174
  try {
221
175
  const result = await rpcClient.call<StatusDetail>("status-detail", {
222
176
  sessionId,
223
177
  directory,
224
178
  modelKey,
225
179
  });
226
- if ((result as unknown as Record<string, unknown>).error) {
227
- return emptyDetail;
228
- }
229
- return result;
230
- } catch {
231
- return emptyDetail;
180
+ const error = (result as unknown as Record<string, unknown>).error;
181
+ if (typeof error === "string") return { ok: false, error };
182
+ return { ok: true, detail: result };
183
+ } catch (error) {
184
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
232
185
  }
233
186
  }
234
187
 
@@ -262,12 +215,15 @@ export async function loadEmbedDetail(sessionId: string, directory: string): Pro
262
215
  export type CompartmentCountResult = { ok: true; count: number } | { ok: false; error: string };
263
216
 
264
217
  /** Get compartment count without making transport failure look like a real zero. */
265
- export async function getCompartmentCount(sessionId: string): Promise<CompartmentCountResult> {
218
+ export async function getCompartmentCount(
219
+ sessionId: string,
220
+ directory?: string,
221
+ ): Promise<CompartmentCountResult> {
266
222
  if (!rpcClient) return { ok: false, error: "RPC client is not initialized" };
267
223
  try {
268
224
  const result = await rpcClient.call<{ count?: number; error?: string }>(
269
225
  "compartment-count",
270
- { sessionId },
226
+ { sessionId, directory },
271
227
  );
272
228
  if (typeof result.error === "string") return { ok: false, error: result.error };
273
229
  if (typeof result.count !== "number" || !Number.isFinite(result.count)) {
package/src/tui/index.tsx CHANGED
@@ -12,6 +12,7 @@ import { closeRpc, dismissUpgradeReminder, getAnnouncement, getCompartmentCount,
12
12
  import { startNotificationSocket, stopNotificationSocket, type SocketNotification } from "./data/notification-socket"
13
13
  import { formatThresholdPercent } from "../shared/format-threshold"
14
14
  import { formatTailHygiene } from "../shared/tail-hygiene-status"
15
+ import { RUST_MODE_HOST_PATHS_LINE } from "../shared/rust-mode-status"
15
16
  import { formatWindowDerivationLine } from "../shared/window-geometry"
16
17
  import { compactionOffSidebarRows, nativeCompactionContextLabel } from "./compaction-off"
17
18
  import { isCompactionEnabled } from "../config/agent-disable"
@@ -321,6 +322,13 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
321
322
  )
322
323
  })()}
323
324
 
325
+ {s().hostBackendsModuleSide && (
326
+ <box marginTop={1} width="100%" flexDirection="column">
327
+ <text fg={t().text}><b>Rust Mode</b></text>
328
+ <text fg={t().textMuted}>{RUST_MODE_HOST_PATHS_LINE}</text>
329
+ </box>
330
+ )}
331
+
324
332
  <box flexDirection="row" width="100%" marginTop={1} gap={4}>
325
333
  {compactionOff() ? (
326
334
  <box flexDirection="column" flexGrow={1} flexBasis={0}>
@@ -344,8 +352,24 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
344
352
  <>
345
353
  <box flexDirection="column" flexGrow={1} flexBasis={0}>
346
354
  <text fg={t().text}><b>Tags</b></text>
347
- <R t={t()} l="Active" v={`${s().activeTags} (~${fmtBytes(s().activeBytes)})`} />
348
- <R t={t()} l="Dropped" v={String(s().droppedTags)} />
355
+ <R
356
+ t={t()}
357
+ l="Active"
358
+ v={
359
+ s().tagCountsAuthoritative === false
360
+ ? "n/a (module total only)"
361
+ : `${s().activeTags} (~${fmtBytes(s().activeBytes)})`
362
+ }
363
+ />
364
+ <R
365
+ t={t()}
366
+ l="Dropped"
367
+ v={
368
+ s().tagCountsAuthoritative === false
369
+ ? "n/a (module total only)"
370
+ : String(s().droppedTags)
371
+ }
372
+ />
349
373
  <R t={t()} l="Total" v={String(s().totalTags)} fg={t().textMuted} />
350
374
  <box marginTop={1}>
351
375
  <text fg={t().text}><b>Pending Queue</b></text>
@@ -459,7 +483,7 @@ async function showRecompDialog(api: TuiPluginApi, targetSessionId = getSessionI
459
483
  return false
460
484
  }
461
485
 
462
- const countResult = await getCompartmentCount(sessionId)
486
+ const countResult = await getCompartmentCount(sessionId, api.state.path.directory ?? "")
463
487
  // Ack only after the dialog is actually shown for the same active session;
464
488
  // route switches while the RPC detail load is in flight must leave it pending.
465
489
  if (getSessionId(api) !== sessionId) return false
@@ -588,10 +612,17 @@ async function showStatusDialog(api: TuiPluginApi, targetSessionId = getSessionI
588
612
 
589
613
  const directory = api.state.path.directory ?? ""
590
614
  const modelKey = getModelKeyFromMessages(api, sessionId)
591
- const detail = await loadStatusDetail(sessionId, directory, modelKey)
615
+ const result = await loadStatusDetail(sessionId, directory, modelKey)
592
616
  if (getSessionId(api) !== sessionId) return false
617
+ if (!result.ok) {
618
+ showToast(api, {
619
+ message: `Status unavailable: ${result.error}`,
620
+ variant: "warning",
621
+ })
622
+ return false
623
+ }
593
624
 
594
- api.ui.dialog.replace(() => <StatusDialog api={api} s={detail} />)
625
+ api.ui.dialog.replace(() => <StatusDialog api={api} s={result.detail} />)
595
626
  return true
596
627
  }
597
628
 
@@ -2,8 +2,7 @@
2
2
  * TUI data layer — pure RPC client, no direct SQLite access.
3
3
  * All data is fetched from the server plugin via HTTP RPC.
4
4
  */
5
- import os from "node:os";
6
- import path from "node:path";
5
+ import { getMagicContextStorageDir } from "../../shared/data-path";
7
6
  import { MagicContextRpcClient } from "../../shared/rpc-client";
8
7
  import type { EmbedDetail, SidebarSnapshot, StatusDetail } from "../../shared/rpc-types";
9
8
 
@@ -12,18 +11,9 @@ export type { EmbedDetail, SidebarSnapshot, StatusDetail };
12
11
  let rpcClient: MagicContextRpcClient | null = null;
13
12
  let rpcGeneration = 0;
14
13
 
15
- function getStorageDir(): string {
16
- // Plugin v0.16+ uses the shared cortexkit/magic-context path so OpenCode
17
- // and Pi can share state. The TUI just needs to point its RPC client at
18
- // the same storage directory the server plugin uses for the lock-file
19
- // discovery convention.
20
- const dataDir = process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
21
- return path.join(dataDir, "cortexkit", "magic-context");
22
- }
23
-
24
14
  /** Initialize the RPC client. Call once on TUI startup. */
25
15
  export function initRpcClient(directory: string): void {
26
- const storageDir = getStorageDir();
16
+ const storageDir = getMagicContextStorageDir();
27
17
  // Bump the generation before replacing the client so late notification
28
18
  // responses from a disposed client are ignored (the WS socket observes the
29
19
  // new generation and abandons its in-flight connect).
@@ -172,63 +162,26 @@ export async function loadSidebarSnapshot(
172
162
  }
173
163
  }
174
164
 
175
- /** Fetch full status detail from the server via RPC. */
165
+ export type StatusDetailResult = { ok: true; detail: StatusDetail } | { ok: false; error: string };
166
+
167
+ /** Fetch full status detail without presenting transport failure as an empty session. */
176
168
  export async function loadStatusDetail(
177
169
  sessionId: string,
178
170
  directory: string,
179
171
  modelKey?: string,
180
- ): Promise<StatusDetail> {
181
- const emptyDetail: StatusDetail = {
182
- ...EMPTY_SNAPSHOT,
183
- sessionId,
184
- activeProfile: null,
185
- tagCounter: 0,
186
- activeTags: 0,
187
- droppedTags: 0,
188
- totalTags: 0,
189
- activeBytes: 0,
190
- lastResponseTime: 0,
191
- lastNudgeTokens: 0,
192
- lastTransformError: null,
193
- isSubagent: false,
194
- pendingOps: [],
195
- contextLimit: 0,
196
- cacheTtlMs: 0,
197
- cacheRemainingMs: 0,
198
- cacheExpired: false,
199
- cacheNeverExpires: false,
200
- executeThreshold: 65,
201
- executeThresholdMode: "percentage",
202
- protectedTagCount: 20,
203
- historyBudgetPercentage: 0.15,
204
- historyBlockTokens: 0,
205
- compressionBudget: null,
206
- compressionUsage: null,
207
- toastDurationMs: 5000,
208
- loggerDiagnostics: {
209
- swallowedWriteCount: 0,
210
- lastErrorMessage: null,
211
- lastErrorTime: null,
212
- },
213
- storage_versions: {
214
- context_db_schema_version: null,
215
- plugin_supported_version: 0,
216
- },
217
- };
218
-
219
- if (!rpcClient) return emptyDetail;
172
+ ): Promise<StatusDetailResult> {
173
+ if (!rpcClient) return { ok: false, error: "RPC client is not initialized" };
220
174
  try {
221
175
  const result = await rpcClient.call<StatusDetail>("status-detail", {
222
176
  sessionId,
223
177
  directory,
224
178
  modelKey,
225
179
  });
226
- if ((result as unknown as Record<string, unknown>).error) {
227
- return emptyDetail;
228
- }
229
- return result;
230
- } catch {
231
- return emptyDetail;
180
+ const error = (result as unknown as Record<string, unknown>).error;
181
+ if (typeof error === "string") return { ok: false, error };
182
+ return { ok: true, detail: result };
183
+ } catch (error) {
184
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
232
185
  }
233
186
  }
234
187
 
@@ -262,12 +215,15 @@ export async function loadEmbedDetail(sessionId: string, directory: string): Pro
262
215
  export type CompartmentCountResult = { ok: true; count: number } | { ok: false; error: string };
263
216
 
264
217
  /** Get compartment count without making transport failure look like a real zero. */
265
- export async function getCompartmentCount(sessionId: string): Promise<CompartmentCountResult> {
218
+ export async function getCompartmentCount(
219
+ sessionId: string,
220
+ directory?: string,
221
+ ): Promise<CompartmentCountResult> {
266
222
  if (!rpcClient) return { ok: false, error: "RPC client is not initialized" };
267
223
  try {
268
224
  const result = await rpcClient.call<{ count?: number; error?: string }>(
269
225
  "compartment-count",
270
- { sessionId },
226
+ { sessionId, directory },
271
227
  );
272
228
  if (typeof result.error === "string") return { ok: false, error: result.error };
273
229
  if (typeof result.count !== "number" || !Number.isFinite(result.count)) {