@cortexkit/opencode-magic-context 0.44.0 → 0.44.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 (237) hide show
  1. package/README.md +6 -0
  2. package/dist/agents/dreamer.d.ts +4 -6
  3. package/dist/agents/dreamer.d.ts.map +1 -1
  4. package/dist/agents/magic-context-prompt.d.ts +2 -0
  5. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  6. package/dist/{announcement-1a8bvswd.js → announcement-n8hs7kjw.js} +2 -2
  7. package/dist/config/live-child-output-cap.d.ts +18 -0
  8. package/dist/config/live-child-output-cap.d.ts.map +1 -0
  9. package/dist/config/schema/magic-context.d.ts +64 -11
  10. package/dist/config/schema/magic-context.d.ts.map +1 -1
  11. package/dist/{conflict-warning-hook-48f7np9d.js → conflict-warning-hook-1msyvb51.js} +6 -6
  12. package/dist/{derive-budgets-8kgevxmx.js → derive-budgets-gd2270s0.js} +3 -3
  13. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  14. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +11 -0
  15. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  16. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/docs-proposals.d.ts +2 -0
  18. package/dist/features/magic-context/dreamer/docs-proposals.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/manual-summary.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
  21. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/map-memories.d.ts +5 -0
  23. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  24. package/dist/features/magic-context/dreamer/open-opencode-db.d.ts +2 -0
  25. package/dist/features/magic-context/dreamer/open-opencode-db.d.ts.map +1 -1
  26. package/dist/features/magic-context/dreamer/primer-seed.d.ts +11 -5
  27. package/dist/features/magic-context/dreamer/primer-seed.d.ts.map +1 -1
  28. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +7 -2
  29. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  30. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +2 -2
  31. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
  32. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +8 -1
  33. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  34. package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
  35. package/dist/features/magic-context/dreamer/task-executor.d.ts +5 -0
  36. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  37. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  38. package/dist/features/magic-context/dreamer/task-prompts.d.ts +18 -2
  39. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  40. package/dist/features/magic-context/dreamer/task-registry.d.ts +12 -0
  41. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  42. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -1
  43. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  44. package/dist/features/magic-context/dreamer/token-budget.d.ts +33 -0
  45. package/dist/features/magic-context/dreamer/token-budget.d.ts.map +1 -0
  46. package/dist/features/magic-context/dreamer/verify-diff.d.ts +15 -0
  47. package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -0
  48. package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
  49. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
  50. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  51. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts +4 -0
  52. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts.map +1 -0
  53. package/dist/features/magic-context/dreamer/verify.d.ts +5 -0
  54. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  55. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  56. package/dist/features/magic-context/memory/project-identity-cache.d.ts +6 -0
  57. package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -0
  58. package/dist/features/magic-context/memory/project-identity.d.ts +15 -22
  59. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  60. package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -0
  61. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  62. package/dist/features/magic-context/memory/verification-paths.d.ts +1 -0
  63. package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
  64. package/dist/features/magic-context/mural/mural-font.generated.d.ts.map +1 -1
  65. package/dist/features/magic-context/project-embedding-registry.d.ts +5 -0
  66. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  67. package/dist/features/magic-context/search.d.ts.map +1 -1
  68. package/dist/features/magic-context/session-activity.d.ts +10 -0
  69. package/dist/features/magic-context/session-activity.d.ts.map +1 -0
  70. package/dist/features/magic-context/session-cache-ttl.d.ts +10 -0
  71. package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -0
  72. package/dist/features/magic-context/session-project-backfill.d.ts +4 -1
  73. package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
  74. package/dist/features/magic-context/smart-notes/compiler.d.ts +5 -0
  75. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  76. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +1 -0
  77. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
  78. package/dist/features/magic-context/smart-notes/storage.d.ts +1 -1
  79. package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
  80. package/dist/features/magic-context/smart-notes/types.d.ts +2 -0
  81. package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
  82. package/dist/features/magic-context/storage-db.d.ts +2 -1
  83. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  84. package/dist/features/magic-context/storage-identity-merge.d.ts +4 -1
  85. package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
  86. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  87. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
  88. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/compartment-runner.d.ts +1 -0
  93. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/embed-session-state.d.ts +3 -0
  96. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/event-handler.d.ts +1 -0
  98. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/event-payloads.d.ts +1 -0
  100. package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  104. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/lkg-replay.d.ts +6 -4
  107. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts +12 -0
  111. package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts.map +1 -0
  112. package/dist/hooks/magic-context/read-session-chunk.d.ts +16 -0
  113. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/read-session-db.d.ts +5 -1
  115. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  116. package/dist/hooks/magic-context/read-session-raw.d.ts +10 -0
  117. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  119. package/dist/hooks/magic-context/storage-busy-refusal.d.ts +7 -0
  120. package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -0
  121. package/dist/hooks/magic-context/strip-content.d.ts +1 -0
  122. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  123. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/transform.d.ts +1 -0
  125. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  126. package/dist/{index-5f4qsnaq.js → index-18tjha8g.js} +2 -2
  127. package/dist/{index-4b30nf6z.js → index-32b096cc.js} +1 -1
  128. package/dist/{index-tjq5jztd.js → index-3bygzk2d.js} +1 -1
  129. package/dist/{index-cj5qe2nw.js → index-3x9caerx.js} +8 -6
  130. package/dist/index-40s1xz22.js +125 -0
  131. package/dist/{index-pye48m79.js → index-egkfvhms.js} +130 -42
  132. package/dist/{index-2zf5fv89.js → index-esxfcz5z.js} +171 -12
  133. package/dist/index-gtzcmv90.js +7260 -0
  134. package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
  135. package/dist/{index-v6wsttqt.js → index-kr5qehbd.js} +1 -1
  136. package/dist/{index-dx0xnd4q.js → index-kwe03yde.js} +1 -1
  137. package/dist/{index-c906f3ff.js → index-r9vkp4n9.js} +4 -4
  138. package/dist/{index-6z9g64js.js → index-s70wtd8k.js} +4 -1
  139. package/dist/{index-01ft0h3p.js → index-stb5fg12.js} +2 -2
  140. package/dist/{index-rdey8znw.js → index-ttdn47aq.js} +35260 -32540
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +15123 -23504
  143. package/dist/{message-time-backfill-rm9wmcem.js → message-time-backfill-q374fxc5.js} +3 -3
  144. package/dist/plugin/dream-timer.d.ts.map +1 -1
  145. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  146. package/dist/plugin/messages-transform.d.ts +10 -9
  147. package/dist/plugin/messages-transform.d.ts.map +1 -1
  148. package/dist/plugin/rpc-handlers.d.ts +1 -0
  149. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  150. package/dist/{read-session-chunk-ktz5zt70.js → read-session-chunk-wmvgnd24.js} +6 -4
  151. package/dist/{recomp-orchestrator-ahekzt0s.js → recomp-orchestrator-rk40ff15.js} +10 -9
  152. package/dist/{safe-notification-target-6w0esgd6.js → safe-notification-target-50gm9c58.js} +3 -3
  153. package/dist/{send-session-notification-w7xbnrsm.js → send-session-notification-6xsrcxnp.js} +5 -5
  154. package/dist/shared/cache-ttl-display.d.ts +3 -1
  155. package/dist/shared/cache-ttl-display.d.ts.map +1 -1
  156. package/dist/shared/harness.d.ts +11 -0
  157. package/dist/shared/harness.d.ts.map +1 -1
  158. package/dist/shared/model-cache-ttl.d.ts +9 -0
  159. package/dist/shared/model-cache-ttl.d.ts.map +1 -0
  160. package/dist/shared/model-suggestion-retry.d.ts +1 -1
  161. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  162. package/dist/shared/prompt-async-transport.d.ts +7 -7
  163. package/dist/shared/prompt-async-transport.d.ts.map +1 -1
  164. package/dist/shared/rpc-server.d.ts.map +1 -1
  165. package/dist/shared/rpc-types.d.ts +1 -1
  166. package/dist/shared/rpc-types.d.ts.map +1 -1
  167. package/dist/shared/rpc-utils.d.ts +21 -0
  168. package/dist/shared/rpc-utils.d.ts.map +1 -1
  169. package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
  170. package/dist/shared/sqlite.d.ts +12 -0
  171. package/dist/shared/sqlite.d.ts.map +1 -1
  172. package/dist/shared/status-view.d.ts +1 -1
  173. package/dist/shared/status-view.d.ts.map +1 -1
  174. package/dist/shared/subagent-runner.d.ts +3 -1
  175. package/dist/shared/subagent-runner.d.ts.map +1 -1
  176. package/dist/shared/user-facing-codes.d.ts +5 -0
  177. package/dist/shared/user-facing-codes.d.ts.map +1 -1
  178. package/dist/{task-config-mgf5nsh3.js → task-config-2bm68628.js} +1 -1
  179. package/dist/tools/ctx-expand/render.d.ts.map +1 -1
  180. package/dist/transform-postprocess-phase-t7xgdhdf.js +36 -0
  181. package/dist/v2/hidden-completion.d.ts +2 -0
  182. package/dist/v2/hidden-completion.d.ts.map +1 -1
  183. package/dist/v2/hooks/context.d.ts.map +1 -1
  184. package/dist/v2/hooks/hidden-child.d.ts +1 -0
  185. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  186. package/dist/v2/hooks/lkg-system.d.ts +11 -0
  187. package/dist/v2/hooks/lkg-system.d.ts.map +1 -0
  188. package/dist/v2/hooks/session-project-backfill.d.ts +1 -1
  189. package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -1
  190. package/dist/v2/hooks/storage-gate.d.ts +11 -5
  191. package/dist/v2/hooks/storage-gate.d.ts.map +1 -1
  192. package/dist/v2/host-service.d.ts +2 -1
  193. package/dist/v2/host-service.d.ts.map +1 -1
  194. package/dist/v2/server.js +95347 -93236
  195. package/dist/v2/store-reader.d.ts +2 -0
  196. package/dist/v2/store-reader.d.ts.map +1 -1
  197. package/dist/wrapup-orchestrator-h8tr10c6.js +19 -0
  198. package/package.json +1 -1
  199. package/src/agents/dreamer.ts +4 -6
  200. package/src/agents/hidden-agent-registrations.ts +3 -3
  201. package/src/agents/magic-context-prompt.test.ts +125 -0
  202. package/src/agents/magic-context-prompt.ts +12 -7
  203. package/src/agents/permissions.test.ts +3 -4
  204. package/src/config/latch-permanence-guard.test.ts +9 -0
  205. package/src/config/live-child-output-cap.test.ts +101 -0
  206. package/src/config/live-child-output-cap.ts +74 -0
  207. package/src/config/live-run-config.test.ts +24 -11
  208. package/src/config/schema/magic-context.test.ts +16 -1
  209. package/src/config/schema/magic-context.ts +27 -10
  210. package/src/features/magic-context/dreamer/task-registry.ts +13 -0
  211. package/src/shared/cache-ttl-display.ts +14 -20
  212. package/src/shared/harness.ts +14 -0
  213. package/src/shared/logger.test.ts +1 -0
  214. package/src/shared/model-cache-ttl.test.ts +40 -0
  215. package/src/shared/model-cache-ttl.ts +58 -0
  216. package/src/shared/model-suggestion-retry.ts +9 -2
  217. package/src/shared/prompt-async-transport.test.ts +254 -1
  218. package/src/shared/prompt-async-transport.ts +153 -44
  219. package/src/shared/prompt-surface-a1-golden.md +13 -13
  220. package/src/shared/rpc-async-probes.test.ts +230 -0
  221. package/src/shared/rpc-server.ts +6 -4
  222. package/src/shared/rpc-types.ts +1 -1
  223. package/src/shared/rpc-utils.test.ts +27 -1
  224. package/src/shared/rpc-utils.ts +161 -9
  225. package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
  226. package/src/shared/sqlite-acquisition-routing.test.ts +94 -0
  227. package/src/shared/sqlite-pass-scope.test.ts +182 -0
  228. package/src/shared/sqlite-transaction-mode-guard.test.ts +42 -10
  229. package/src/shared/sqlite-write-locker-test-support.ts +1 -0
  230. package/src/shared/sqlite.ts +142 -56
  231. package/src/shared/status-view.ts +1 -1
  232. package/src/shared/storage-busy.test.ts +95 -0
  233. package/src/shared/subagent-runner.ts +4 -0
  234. package/src/shared/user-facing-codes.ts +6 -0
  235. package/src/shared/windows-hidden-spawns.test.ts +132 -0
  236. package/src/shared/write-transaction-attribution.test.ts +1 -0
  237. package/dist/wrapup-orchestrator-9srk0kjf.js +0 -18
@@ -0,0 +1,230 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import type { execFileSync } from "node:child_process";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ __resetSchemaFenceStateForTests,
8
+ getMigrationOnOpenRefusal,
9
+ openDatabaseAsync,
10
+ } from "../features/magic-context/storage-db";
11
+ import { createV2StorageGate, probeV2StorageAtBoot } from "../v2/hooks/storage-gate";
12
+ import {
13
+ __resetRpcIdentityTestHooks,
14
+ __setAsyncProcessProbeForTests,
15
+ __setRpcIdentityTestHooks,
16
+ inspectProcessesAsync,
17
+ } from "./rpc-utils";
18
+ import { Database } from "./sqlite";
19
+
20
+ afterEach(() => {
21
+ __resetSchemaFenceStateForTests();
22
+ __resetRpcIdentityTestHooks();
23
+ __setAsyncProcessProbeForTests();
24
+ });
25
+
26
+ test("async Windows inspection bypasses slow synchronous probes and shares a bounded snapshot", async () => {
27
+ let synchronousCalls = 0;
28
+ const slow = (() => {
29
+ synchronousCalls++;
30
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
31
+ return "";
32
+ }) as typeof execFileSync;
33
+ __setRpcIdentityTestHooks({
34
+ platform: "win32",
35
+ execFileSync: slow,
36
+ processListExecFileSync: slow,
37
+ });
38
+ let release!: (value: string) => void;
39
+ let calls = 0;
40
+ __setAsyncProcessProbeForTests(async (file, _args, timeout) => {
41
+ expect(file).toBe("powershell");
42
+ expect(timeout).toBe(5000);
43
+ calls++;
44
+ return new Promise<string>((resolve) => {
45
+ release = resolve;
46
+ });
47
+ });
48
+ const first = inspectProcessesAsync();
49
+ const second = inspectProcessesAsync();
50
+ expect(second).toBe(first);
51
+ const heartbeat = await Promise.race([
52
+ first.then(() => "probe"),
53
+ new Promise<string>((resolve) => setTimeout(() => resolve("heartbeat"), 10)),
54
+ ]);
55
+ expect(heartbeat).toBe("heartbeat");
56
+ release(
57
+ JSON.stringify([
58
+ {
59
+ ProcessId: 12345,
60
+ ParentProcessId: 1,
61
+ CommandLine: "opencode serve",
62
+ CreationDate: "2026-09-28T00:00:00Z",
63
+ },
64
+ ]),
65
+ );
66
+ const result = await first;
67
+ expect(result.liveness(12345)).toBe("alive");
68
+ expect(result.evidence(12345).commandLine).toBe("opencode serve");
69
+ expect(result.liveness(12346)).toBe("dead");
70
+ expect(await inspectProcessesAsync()).toBe(result);
71
+ expect(calls).toBe(1);
72
+ expect(synchronousCalls).toBe(0);
73
+ });
74
+
75
+ test("offline process inspection can bypass a cached snapshot", async () => {
76
+ __setRpcIdentityTestHooks({ platform: "win32" });
77
+ let calls = 0;
78
+ __setAsyncProcessProbeForTests(async () => {
79
+ calls++;
80
+ return JSON.stringify([
81
+ {
82
+ ProcessId: calls + 100,
83
+ ParentProcessId: 1,
84
+ Name: "opencode.exe",
85
+ CommandLine: null,
86
+ CreationDate: "2026-09-28T00:00:00Z",
87
+ },
88
+ ]);
89
+ });
90
+ const cached = await inspectProcessesAsync();
91
+ expect((await inspectProcessesAsync()).processSnapshot).toBe(cached.processSnapshot);
92
+ const fresh = await inspectProcessesAsync(true);
93
+ expect(fresh.processSnapshot?.source).toBe("cim");
94
+ expect(fresh.processSnapshot?.facts).toContainEqual(
95
+ expect.objectContaining({ pid: 102, imageName: "opencode.exe", commandLine: null }),
96
+ );
97
+ expect(calls).toBe(2);
98
+ });
99
+
100
+ test("async Windows inspection bounds CIM and tasklist fallback and caches failure", async () => {
101
+ __setRpcIdentityTestHooks({ platform: "win32" });
102
+ const calls: Array<[string, number]> = [];
103
+ __setAsyncProcessProbeForTests(async (file, _args, timeout) => {
104
+ calls.push([file, timeout]);
105
+ throw new Error("probe timed out");
106
+ });
107
+ const result = await inspectProcessesAsync();
108
+ expect(result.pi.state).toBe("unreadable");
109
+ expect(result.liveness(123)).toBe("inconclusive");
110
+ expect(result.evidence(123)).toEqual({ startTime: null, commandLine: null });
111
+ await inspectProcessesAsync();
112
+ expect(calls).toEqual([
113
+ ["powershell", 5000],
114
+ ["tasklist", 1000],
115
+ ]);
116
+ });
117
+
118
+ test("boot storage wait stays responsive with a locked v90 store and slow Windows probes", async () => {
119
+ const root = mkdtempSync(join(tmpdir(), "async-storage-guard-"));
120
+ const dbPath = join(root, "context.db");
121
+ const seeded = new Database(dbPath);
122
+ seeded.exec(
123
+ "CREATE TABLE schema_migrations(version INTEGER PRIMARY KEY); INSERT INTO schema_migrations VALUES(90)",
124
+ );
125
+ seeded.exec("BEGIN IMMEDIATE");
126
+ mkdirSync(join(root, "rpc", "blocker"), { recursive: true });
127
+ writeFileSync(
128
+ join(root, "rpc", "blocker", "port-12345.json"),
129
+ JSON.stringify({
130
+ pid: 12345,
131
+ port: 43123,
132
+ started_at: Date.now(),
133
+ kind: "OpenCode server",
134
+ }),
135
+ );
136
+ let syncCalls = 0;
137
+ const slow = (() => {
138
+ syncCalls++;
139
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
140
+ return "";
141
+ }) as typeof execFileSync;
142
+ __setRpcIdentityTestHooks({
143
+ platform: "win32",
144
+ execFileSync: slow,
145
+ processListExecFileSync: slow,
146
+ });
147
+ let release!: (value: string) => void;
148
+ __setAsyncProcessProbeForTests(
149
+ () =>
150
+ new Promise<string>((resolve) => {
151
+ release = resolve;
152
+ }),
153
+ );
154
+ try {
155
+ const gate = createV2StorageGate({
156
+ open: () => openDatabaseAsync({ dbPath, busyTimeoutMs: 0 }),
157
+ });
158
+ const started = performance.now();
159
+ const boot = probeV2StorageAtBoot(gate);
160
+ const opening = gate.probe();
161
+ expect(
162
+ await Promise.race([
163
+ opening.then(() => "opened"),
164
+ new Promise<string>((resolve) => setTimeout(() => resolve("responsive"), 10)),
165
+ ]),
166
+ ).toBe("responsive");
167
+ expect(performance.now() - started).toBeLessThan(300);
168
+ expect(gate.current()).toBeUndefined();
169
+ release(
170
+ JSON.stringify([
171
+ {
172
+ ProcessId: 12345,
173
+ ParentProcessId: 1,
174
+ CommandLine: "opencode serve",
175
+ CreationDate: "2026-01-01T00:00:00Z",
176
+ },
177
+ ]),
178
+ );
179
+ expect(await opening).toBeUndefined();
180
+ expect(await boot).toBeUndefined();
181
+ expect(gate.reason()?.kind).toBe("migration_guard");
182
+ expect(getMigrationOnOpenRefusal()?.serverPids).toEqual([12345]);
183
+ expect(syncCalls).toBe(0);
184
+ const checked = new Database(dbPath);
185
+ expect(
186
+ checked.prepare("SELECT MAX(version) AS version FROM schema_migrations").get(),
187
+ ).toEqual({ version: 90 });
188
+ checked.close();
189
+ } finally {
190
+ seeded.close();
191
+ rmSync(root, { recursive: true, force: true });
192
+ }
193
+ });
194
+
195
+ test("a timed-out refresh retains confirmed blockers until a successful process scan", async () => {
196
+ let now = 1000;
197
+ __setRpcIdentityTestHooks({ platform: "win32", nowMs: () => now });
198
+ let fail = false;
199
+ let calls = 0;
200
+ let pid = 12345;
201
+ __setAsyncProcessProbeForTests(async () => {
202
+ calls++;
203
+ if (fail) throw new Error("timeout");
204
+ return JSON.stringify([
205
+ {
206
+ ProcessId: pid,
207
+ ParentProcessId: 1,
208
+ CommandLine: "opencode serve",
209
+ CreationDate: "2026-01-01T00:00:00Z",
210
+ },
211
+ ]);
212
+ });
213
+ const first = await inspectProcessesAsync();
214
+ now += 1999;
215
+ expect(await inspectProcessesAsync()).toBe(first);
216
+ expect(calls).toBe(1);
217
+ now++;
218
+ fail = true;
219
+ const failed = await inspectProcessesAsync();
220
+ expect(calls).toBe(3);
221
+ expect(failed.liveness(12345)).toBe("alive");
222
+ expect(failed.liveness(54321)).toBe("inconclusive");
223
+ expect(failed.evidence(12345)).toEqual(first.evidence(12345));
224
+ now += 2000;
225
+ fail = false;
226
+ pid = 54321;
227
+ const refreshed = await inspectProcessesAsync();
228
+ expect(calls).toBe(4);
229
+ expect(refreshed.liveness(12345)).toBe("dead");
230
+ });
@@ -19,7 +19,7 @@ import {
19
19
  registerNotificationSink,
20
20
  } from "./rpc-notifications";
21
21
  import {
22
- isPidAlive,
22
+ inspectProcessesAsync,
23
23
  parseRpcPortFile,
24
24
  registerOwnRpcServerInstance,
25
25
  rpcPortDir,
@@ -176,7 +176,7 @@ export class MagicContextRpcServer {
176
176
  // supported: TUI discovery scans all live files and picks the most
177
177
  // recent instead of cross-wiring via one shared project file.
178
178
  try {
179
- this.warnIfOtherLiveInstance();
179
+ void this.warnIfOtherLiveInstance();
180
180
  const dir = dirname(this.portFilePath);
181
181
  // The port file carries the RPC bearer token. The normal policy keeps
182
182
  // it owner-only; a trusted-group deployment explicitly delegates every
@@ -263,12 +263,14 @@ export class MagicContextRpcServer {
263
263
  this.unregisterOwnInstance = null;
264
264
  }
265
265
 
266
- private warnIfOtherLiveInstance(): void {
266
+ private async warnIfOtherLiveInstance(): Promise<void> {
267
267
  try {
268
268
  for (const entry of readdirSync(this.portDir)) {
269
269
  if (!entry.startsWith("port-") || !entry.endsWith(".json")) continue;
270
270
  const record = parseRpcPortFile(readFileSync(`${this.portDir}/${entry}`, "utf-8"));
271
- if (!record || record.pid === process.pid || !isPidAlive(record.pid)) continue;
271
+ if (!record || record.pid === process.pid) continue;
272
+ const processes = await inspectProcessesAsync();
273
+ if (processes.liveness(record.pid) !== "alive") continue;
272
274
  log(
273
275
  `[rpc] another Magic Context RPC server is active for this project (pid ${record.pid}, port ${record.port}); starting separate instance on a new port`,
274
276
  );
@@ -280,7 +280,7 @@ export interface StatusDetail extends SidebarSnapshot {
280
280
  cacheExpired: boolean;
281
281
  /** Reports whether the displayed TTL came from config, persisted session metadata,
282
282
  * or the default; cache scheduling still uses the TTL stored in session metadata. */
283
- cacheTtlSource?: "config" | "session" | "default";
283
+ cacheTtlSource?: import("./cache-ttl-display").CacheTtlDisplaySource;
284
284
  cacheTtlModelKey?: string;
285
285
  configParseFailures?: ConfigParseFailure[];
286
286
  /** True when cacheTtl is "never" — the idle-TTL heuristic is disabled on
@@ -183,6 +183,32 @@ describe("discoverLivePiProcessIds", () => {
183
183
  });
184
184
 
185
185
  describe("#411 Windows OMP/Pi live-process scan", () => {
186
+ test("reuses a Windows snapshot briefly, then refreshes process discovery", () => {
187
+ let now = PLUGIN_BUILD_MS;
188
+ const calls: string[] = [];
189
+ __setRpcIdentityTestHooks({
190
+ platform: "win32",
191
+ nowMs: () => now,
192
+ processListExecFileSync: windowsProcessListExec((file) => {
193
+ calls.push(file);
194
+ return cimOutput([
195
+ {
196
+ ProcessId: WINDOWS_FOREIGN_PID,
197
+ ParentProcessId: 4,
198
+ CommandLine: OMP_PI_ARC_COMMAND,
199
+ CreationDate: wmiCreationDate(OLDER_THAN_PLUGIN_MS),
200
+ },
201
+ ]);
202
+ }),
203
+ });
204
+ expect(inspectLivePiProcesses().processIds).toEqual([WINDOWS_FOREIGN_PID]);
205
+ now += 1_999;
206
+ expect(inspectLivePiProcesses().processIds).toEqual([WINDOWS_FOREIGN_PID]);
207
+ expect(calls).toEqual(["powershell"]);
208
+ now += 1;
209
+ inspectLivePiProcesses();
210
+ expect(calls).toEqual(["powershell", "powershell"]);
211
+ });
186
212
  test("(a) tasklist-only fallback skips a parent omp.exe as an ancestor", () => {
187
213
  const calls: string[] = [];
188
214
  __setRpcIdentityTestHooks({
@@ -607,7 +633,7 @@ describe("isPidIdentityPlausible", () => {
607
633
  args: [
608
634
  "-NoProfile",
609
635
  "-Command",
610
- "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress",
636
+ "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine,CreationDate | ConvertTo-Json -Compress",
611
637
  ],
612
638
  },
613
639
  ]);
@@ -1,4 +1,4 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFile, execFileSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -118,12 +118,13 @@ const RPC_IDENTITY_SKEW_TOLERANCE_MS = 120_000;
118
118
  const LINUX_CLOCK_TICKS_PER_SECOND = 100;
119
119
  const PS_PROBE_TIMEOUT_MS = 1_000;
120
120
  const WINDOWS_CIM_PROBE_TIMEOUT_MS = 5_000;
121
+ const WINDOWS_PROCESS_SNAPSHOT_TTL_MS = 2_000;
121
122
  const MAX_ANCESTOR_WALK_DEPTH = 16;
122
123
  const OPEN_CODE_COMMAND_MARKERS = ["opencode", "node", "bun", "electron"];
123
124
  const TASKLIST_NO_TASKS_PATTERN =
124
125
  /^INFO:\s+No tasks are running which match the specified criteria\.?$/im;
125
126
  const WINDOWS_CIM_COMMAND =
126
- "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress";
127
+ "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine,CreationDate | ConvertTo-Json -Compress";
127
128
  const PI_HARNESS_ARC_MARKERS = [
128
129
  "pi-coding-agent",
129
130
  "oh-my-pi",
@@ -184,6 +185,7 @@ function readPsProcessStartTime(pid: number): number | null {
184
185
  encoding: "utf8",
185
186
  timeout: PS_PROBE_TIMEOUT_MS,
186
187
  stdio: ["ignore", "pipe", "pipe"],
188
+ windowsHide: true,
187
189
  });
188
190
  const processStartTime = Date.parse(String(output).trim());
189
191
  return Number.isFinite(processStartTime) ? processStartTime : null;
@@ -232,6 +234,7 @@ interface ProcessSnapshot {
232
234
  }
233
235
 
234
236
  let windowsProcessFactsCache: Map<number, ProcessFacts> | null = null;
237
+ let windowsProcessSnapshotCache: { snapshot: ProcessSnapshot; at: number } | null = null;
235
238
 
236
239
  function rememberWindowsProcessFacts(facts: ProcessFacts[]): void {
237
240
  windowsProcessFactsCache = new Map(facts.map((fact) => [fact.pid, fact]));
@@ -239,6 +242,7 @@ function rememberWindowsProcessFacts(facts: ProcessFacts[]): void {
239
242
 
240
243
  function clearWindowsProcessFactsCache(): void {
241
244
  windowsProcessFactsCache = null;
245
+ windowsProcessSnapshotCache = null;
242
246
  }
243
247
 
244
248
  function parseCsvLine(line: string): string[] | null {
@@ -292,6 +296,7 @@ function readWindowsProcess(pid: number): { state: PidLiveness; command?: string
292
296
  encoding: "utf8",
293
297
  timeout: PS_PROBE_TIMEOUT_MS,
294
298
  stdio: ["ignore", "pipe", "pipe"],
299
+ windowsHide: true,
295
300
  });
296
301
  const entries = parseTasklistOutput(String(output));
297
302
  if (entries === null) return { state: "inconclusive" };
@@ -325,6 +330,7 @@ function readPsProcessCommand(pid: number): string | null {
325
330
  encoding: "utf8",
326
331
  timeout: PS_PROBE_TIMEOUT_MS,
327
332
  stdio: ["ignore", "pipe", "pipe"],
333
+ windowsHide: true,
328
334
  });
329
335
  return String(output);
330
336
  } catch {
@@ -521,6 +527,7 @@ function execProcessList(
521
527
  encoding: "utf8",
522
528
  timeout,
523
529
  stdio: ["ignore", "pipe", "pipe"],
530
+ windowsHide: true,
524
531
  }),
525
532
  );
526
533
  }
@@ -588,7 +595,12 @@ function parseWindowsCimOutput(output: string): ProcessFacts[] | null {
588
595
  pid,
589
596
  parentPid: Number.isInteger(parentPid) && parentPid > 0 ? parentPid : null,
590
597
  commandLine,
591
- imageName: commandLine ? executableName(commandTokens(commandLine)[0]) : null,
598
+ imageName:
599
+ typeof record.Name === "string"
600
+ ? record.Name
601
+ : commandLine
602
+ ? executableName(commandTokens(commandLine)[0])
603
+ : null,
592
604
  startTime: parseWindowsCreationDate(record.CreationDate),
593
605
  });
594
606
  }
@@ -679,14 +691,18 @@ function readPosixParentPid(pid: number): number | null {
679
691
  * plugin runs. Bound the walk and stop on cycles so a broken parent map
680
692
  * cannot loop.
681
693
  */
682
- function collectAncestorPids(selfPid: number, parentByPid: Map<number, number>): Set<number> {
694
+ function collectAncestorPids(
695
+ selfPid: number,
696
+ parentByPid: Map<number, number>,
697
+ probeParents = true,
698
+ ): Set<number> {
683
699
  const ancestors = new Set<number>();
684
700
  let current = selfPid;
685
701
  for (let depth = 0; depth < MAX_ANCESTOR_WALK_DEPTH; depth += 1) {
686
702
  let ppid: number | null = null;
687
703
  if (parentByPid.has(current)) {
688
704
  ppid = parentByPid.get(current) ?? null;
689
- } else if (rpcIdentityPlatform !== "win32") {
705
+ } else if (rpcIdentityPlatform !== "win32" && probeParents) {
690
706
  ppid = readPosixParentPid(current);
691
707
  if (ppid == null && current === process.pid && process.ppid > 0) {
692
708
  ppid = process.ppid;
@@ -703,8 +719,11 @@ function collectAncestorPids(selfPid: number, parentByPid: Map<number, number>):
703
719
  return ancestors;
704
720
  }
705
721
 
706
- function classifyLivePiSnapshot(snapshot: ProcessSnapshot): PiProcessDiscovery {
707
- const ancestors = collectAncestorPids(process.pid, snapshot.parentByPid);
722
+ function classifyLivePiSnapshot(
723
+ snapshot: ProcessSnapshot,
724
+ probeParents = true,
725
+ ): PiProcessDiscovery {
726
+ const ancestors = collectAncestorPids(process.pid, snapshot.parentByPid, probeParents);
708
727
  const processIds = new Set<number>();
709
728
  const inconclusivePids = new Set<number>();
710
729
  const skippedAncestorPids: number[] = [];
@@ -773,8 +792,14 @@ export function inspectLivePiProcesses(): PiProcessDiscovery {
773
792
  // Prefer one CIM query (pid, parent, command line, start time).
774
793
  // tasklist is image-name-only, so it is a fallback and never a
775
794
  // verified live-harness source.
776
- const cim = tryReadWindowsCimSnapshot(rpcProcessListExecFileSync);
777
- const snapshot = cim ?? tryReadWindowsTasklistSnapshot();
795
+ const now = rpcIdentityNowMs();
796
+ const cached = windowsProcessSnapshotCache;
797
+ // A brief reuse avoids spawning PowerShell on every blocker scan.
798
+ const snapshot =
799
+ cached && now >= cached.at && now - cached.at < WINDOWS_PROCESS_SNAPSHOT_TTL_MS
800
+ ? cached.snapshot
801
+ : (tryReadWindowsCimSnapshot(rpcProcessListExecFileSync) ??
802
+ tryReadWindowsTasklistSnapshot());
778
803
  if (!snapshot) {
779
804
  return {
780
805
  state: "unreadable",
@@ -782,6 +807,7 @@ export function inspectLivePiProcesses(): PiProcessDiscovery {
782
807
  error: "process list unavailable",
783
808
  };
784
809
  }
810
+ if (snapshot !== cached?.snapshot) windowsProcessSnapshotCache = { snapshot, at: now };
785
811
  rememberWindowsProcessFacts(snapshot.facts);
786
812
  return classifyLivePiSnapshot(snapshot);
787
813
  }
@@ -834,3 +860,129 @@ export function parseRpcPortFile(content: string, fallbackPid = 0): RpcPortFileR
834
860
  function isValidPort(port: number): boolean {
835
861
  return Number.isInteger(port) && port > 0 && port <= 65535;
836
862
  }
863
+
864
+ /** One bounded, shared process scan for asynchronous storage opens. */
865
+ export interface AsyncProcessInspection {
866
+ pi: PiProcessDiscovery;
867
+ /** Includes process names and commands so offline maintenance can rule out live store users. */
868
+ processSnapshot?: {
869
+ source: ProcessSnapshotSource;
870
+ facts: Array<{ pid: number; imageName: string | null; commandLine: string | null }>;
871
+ };
872
+ evidence(pid: number): ProcessProbeEvidence;
873
+ liveness(pid: number): PidLiveness;
874
+ }
875
+
876
+ function execProcessListAsync(file: string, args: string[], timeout: number): Promise<string> {
877
+ return new Promise((resolve, reject) => {
878
+ execFile(
879
+ file,
880
+ args,
881
+ { encoding: "utf8", timeout, windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
882
+ (error, stdout) => {
883
+ if (error) reject(error);
884
+ else resolve(stdout);
885
+ },
886
+ );
887
+ });
888
+ }
889
+
890
+ let asyncProcessExec = execProcessListAsync;
891
+ let asyncInspection: Promise<AsyncProcessInspection> | undefined;
892
+ let asyncInspectionExpires = 0;
893
+ let lastAsyncSnapshot: ProcessSnapshot | null = null;
894
+
895
+ /** Inject only the asynchronous transport; legacy synchronous probes stay isolated. */
896
+ export function __setAsyncProcessProbeForTests(probe?: typeof execProcessListAsync): void {
897
+ asyncProcessExec = probe ?? execProcessListAsync;
898
+ asyncInspection = undefined;
899
+ asyncInspectionExpires = 0;
900
+ lastAsyncSnapshot = null;
901
+ }
902
+
903
+ export function inspectProcessesAsync(forceFresh = false): Promise<AsyncProcessInspection> {
904
+ if (!forceFresh && asyncInspection && rpcIdentityNowMs() < asyncInspectionExpires)
905
+ return asyncInspection;
906
+ asyncInspectionExpires = Number.POSITIVE_INFINITY;
907
+ asyncInspection = (async () => {
908
+ let snapshot: ProcessSnapshot | null = null;
909
+ try {
910
+ if (rpcIdentityPlatform === "win32") {
911
+ try {
912
+ const output = await asyncProcessExec(
913
+ "powershell",
914
+ ["-NoProfile", "-Command", WINDOWS_CIM_COMMAND],
915
+ WINDOWS_CIM_PROBE_TIMEOUT_MS,
916
+ );
917
+ const facts = parseWindowsCimOutput(output);
918
+ if (facts) snapshot = snapshotFromFacts(facts, "cim");
919
+ } catch {
920
+ /* tasklist is the bounded fallback when CIM is unavailable. */
921
+ }
922
+ if (!snapshot) {
923
+ const entries = parseTasklistOutput(
924
+ await asyncProcessExec("tasklist", ["/FO", "CSV"], PS_PROBE_TIMEOUT_MS),
925
+ );
926
+ if (entries)
927
+ snapshot = snapshotFromFacts(
928
+ entries.map(({ pid, command }) => ({
929
+ pid,
930
+ parentPid: null,
931
+ commandLine: null,
932
+ imageName: command,
933
+ startTime: null,
934
+ })),
935
+ "tasklist",
936
+ );
937
+ }
938
+ } else {
939
+ const output = await asyncProcessExec(
940
+ "ps",
941
+ ["-axo", "pid=,ppid=,lstart=,command="],
942
+ PS_PROBE_TIMEOUT_MS,
943
+ );
944
+ const facts: ProcessFacts[] = [];
945
+ for (const line of output.split(/\r?\n/)) {
946
+ const match =
947
+ /^\s*(\d+)\s+(\d+)\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(.+)$/.exec(
948
+ line,
949
+ );
950
+ if (!match) continue;
951
+ const startTime = Date.parse(match[3]);
952
+ facts.push({
953
+ pid: Number(match[1]),
954
+ parentPid: Number(match[2]),
955
+ commandLine: match[4],
956
+ imageName: null,
957
+ startTime: Number.isFinite(startTime) ? startTime : null,
958
+ });
959
+ }
960
+ snapshot = facts.length > 0 ? snapshotFromFacts(facts, "ps") : null;
961
+ }
962
+ } catch {
963
+ /* Missing process evidence is inconclusive, never proof of death. */
964
+ }
965
+ // A timed-out refresh cannot prove that a process blocking migration has
966
+ // exited. Keep its evidence until a successful scan can establish death.
967
+ const fresh = snapshot !== null;
968
+ if (snapshot) lastAsyncSnapshot = snapshot;
969
+ else snapshot = lastAsyncSnapshot;
970
+ const byPid = new Map(snapshot?.facts.map((fact) => [fact.pid, fact]));
971
+ return {
972
+ pi: snapshot
973
+ ? classifyLivePiSnapshot(snapshot, false)
974
+ : { state: "unreadable", processIds: [] },
975
+ ...(fresh && snapshot
976
+ ? { processSnapshot: { source: snapshot.source, facts: snapshot.facts } }
977
+ : {}),
978
+ evidence: (pid: number) => ({
979
+ startTime: byPid.get(pid)?.startTime ?? null,
980
+ commandLine: byPid.get(pid)?.commandLine ?? byPid.get(pid)?.imageName ?? null,
981
+ }),
982
+ liveness: (pid: number) => (byPid.has(pid) ? "alive" : fresh ? "dead" : "inconclusive"),
983
+ } satisfies AsyncProcessInspection;
984
+ })().finally(() => {
985
+ asyncInspectionExpires = rpcIdentityNowMs() + WINDOWS_PROCESS_SNAPSHOT_TTL_MS;
986
+ });
987
+ return asyncInspection;
988
+ }