@cortexkit/opencode-magic-context 0.44.1 → 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 (180) 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/config/live-child-output-cap.d.ts.map +1 -1
  5. package/dist/config/schema/magic-context.d.ts +63 -10
  6. package/dist/config/schema/magic-context.d.ts.map +1 -1
  7. package/dist/{conflict-warning-hook-d14ah73v.js → conflict-warning-hook-1msyvb51.js} +3 -3
  8. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  9. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +11 -0
  10. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  11. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/docs-proposals.d.ts +2 -0
  13. package/dist/features/magic-context/dreamer/docs-proposals.d.ts.map +1 -1
  14. package/dist/features/magic-context/dreamer/manual-summary.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
  16. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/map-memories.d.ts +5 -0
  18. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +5 -0
  20. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +2 -2
  22. package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +8 -1
  24. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
  26. package/dist/features/magic-context/dreamer/task-executor.d.ts +5 -0
  27. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  28. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  29. package/dist/features/magic-context/dreamer/task-prompts.d.ts +18 -2
  30. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  31. package/dist/features/magic-context/dreamer/task-registry.d.ts +12 -0
  32. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  33. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -1
  34. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  35. package/dist/features/magic-context/dreamer/token-budget.d.ts +33 -0
  36. package/dist/features/magic-context/dreamer/token-budget.d.ts.map +1 -0
  37. package/dist/features/magic-context/dreamer/verify-diff.d.ts +15 -0
  38. package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -0
  39. package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
  40. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
  41. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  42. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts +4 -0
  43. package/dist/features/magic-context/dreamer/verify-read-cap.d.ts.map +1 -0
  44. package/dist/features/magic-context/dreamer/verify.d.ts +5 -0
  45. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  46. package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
  47. package/dist/features/magic-context/memory/project-identity-cache.d.ts +6 -0
  48. package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -0
  49. package/dist/features/magic-context/memory/project-identity.d.ts +8 -22
  50. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  51. package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -0
  52. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  53. package/dist/features/magic-context/memory/verification-paths.d.ts +1 -0
  54. package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
  55. package/dist/features/magic-context/project-embedding-registry.d.ts +5 -0
  56. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  57. package/dist/features/magic-context/search.d.ts.map +1 -1
  58. package/dist/features/magic-context/session-activity.d.ts +10 -0
  59. package/dist/features/magic-context/session-activity.d.ts.map +1 -0
  60. package/dist/features/magic-context/session-project-backfill.d.ts +4 -1
  61. package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
  62. package/dist/features/magic-context/smart-notes/compiler.d.ts +5 -0
  63. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  64. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +1 -0
  65. package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
  66. package/dist/features/magic-context/smart-notes/storage.d.ts +1 -1
  67. package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
  68. package/dist/features/magic-context/smart-notes/types.d.ts +2 -0
  69. package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
  70. package/dist/features/magic-context/storage-db.d.ts +2 -1
  71. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  72. package/dist/features/magic-context/storage-identity-merge.d.ts +4 -1
  73. package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
  74. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  75. package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
  76. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/compartment-runner.d.ts +1 -0
  80. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/embed-session-state.d.ts +3 -0
  83. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/event-handler.d.ts +1 -0
  85. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/event-payloads.d.ts +1 -0
  87. package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/lkg-replay.d.ts +6 -4
  91. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/storage-busy-refusal.d.ts +7 -0
  96. package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -0
  97. package/dist/hooks/magic-context/strip-content.d.ts +1 -0
  98. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  101. package/dist/{index-d107az47.js → index-18tjha8g.js} +1 -1
  102. package/dist/{index-0rp6pj54.js → index-3x9caerx.js} +7 -5
  103. package/dist/index-40s1xz22.js +125 -0
  104. package/dist/{index-cz88kw0n.js → index-egkfvhms.js} +117 -39
  105. package/dist/{index-s6b7h12g.js → index-esxfcz5z.js} +73 -2
  106. package/dist/index-gtzcmv90.js +7260 -0
  107. package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
  108. package/dist/{index-a9vbtdzv.js → index-r9vkp4n9.js} +1 -1
  109. package/dist/{index-jtdrvmcn.js → index-ttdn47aq.js} +37431 -37044
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +8195 -14449
  112. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  113. package/dist/plugin/messages-transform.d.ts +10 -9
  114. package/dist/plugin/messages-transform.d.ts.map +1 -1
  115. package/dist/plugin/rpc-handlers.d.ts +1 -0
  116. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  117. package/dist/{read-session-chunk-fkdv2g5b.js → read-session-chunk-wmvgnd24.js} +2 -2
  118. package/dist/{recomp-orchestrator-k4h5n0p3.js → recomp-orchestrator-rk40ff15.js} +5 -4
  119. package/dist/{send-session-notification-g2k03zyn.js → send-session-notification-6xsrcxnp.js} +2 -2
  120. package/dist/shared/model-suggestion-retry.d.ts +1 -1
  121. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  122. package/dist/shared/prompt-async-transport.d.ts +7 -7
  123. package/dist/shared/prompt-async-transport.d.ts.map +1 -1
  124. package/dist/shared/rpc-server.d.ts.map +1 -1
  125. package/dist/shared/rpc-utils.d.ts +21 -0
  126. package/dist/shared/rpc-utils.d.ts.map +1 -1
  127. package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
  128. package/dist/shared/sqlite.d.ts +12 -0
  129. package/dist/shared/sqlite.d.ts.map +1 -1
  130. package/dist/shared/subagent-runner.d.ts +3 -1
  131. package/dist/shared/subagent-runner.d.ts.map +1 -1
  132. package/dist/shared/user-facing-codes.d.ts +5 -0
  133. package/dist/shared/user-facing-codes.d.ts.map +1 -1
  134. package/dist/{task-config-mgf5nsh3.js → task-config-2bm68628.js} +1 -1
  135. package/dist/transform-postprocess-phase-t7xgdhdf.js +36 -0
  136. package/dist/v2/hidden-completion.d.ts +2 -0
  137. package/dist/v2/hidden-completion.d.ts.map +1 -1
  138. package/dist/v2/hooks/context.d.ts.map +1 -1
  139. package/dist/v2/hooks/hidden-child.d.ts +1 -0
  140. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  141. package/dist/v2/hooks/lkg-system.d.ts +11 -0
  142. package/dist/v2/hooks/lkg-system.d.ts.map +1 -0
  143. package/dist/v2/hooks/session-project-backfill.d.ts +1 -1
  144. package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -1
  145. package/dist/v2/hooks/storage-gate.d.ts +11 -5
  146. package/dist/v2/hooks/storage-gate.d.ts.map +1 -1
  147. package/dist/v2/host-service.d.ts +2 -1
  148. package/dist/v2/host-service.d.ts.map +1 -1
  149. package/dist/v2/server.js +24481 -22623
  150. package/dist/v2/store-reader.d.ts +2 -0
  151. package/dist/v2/store-reader.d.ts.map +1 -1
  152. package/dist/{wrapup-orchestrator-35y56636.js → wrapup-orchestrator-h8tr10c6.js} +6 -5
  153. package/package.json +1 -1
  154. package/src/agents/dreamer.ts +4 -6
  155. package/src/agents/hidden-agent-registrations.ts +3 -3
  156. package/src/agents/permissions.test.ts +3 -4
  157. package/src/config/latch-permanence-guard.test.ts +9 -0
  158. package/src/config/live-child-output-cap.test.ts +26 -0
  159. package/src/config/live-child-output-cap.ts +6 -2
  160. package/src/config/schema/magic-context.ts +21 -6
  161. package/src/features/magic-context/dreamer/task-registry.ts +13 -0
  162. package/src/shared/logger.test.ts +1 -0
  163. package/src/shared/model-suggestion-retry.ts +9 -2
  164. package/src/shared/prompt-async-transport.test.ts +254 -1
  165. package/src/shared/prompt-async-transport.ts +153 -44
  166. package/src/shared/rpc-async-probes.test.ts +230 -0
  167. package/src/shared/rpc-server.ts +6 -4
  168. package/src/shared/rpc-utils.test.ts +27 -1
  169. package/src/shared/rpc-utils.ts +161 -9
  170. package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
  171. package/src/shared/sqlite-acquisition-routing.test.ts +94 -0
  172. package/src/shared/sqlite-pass-scope.test.ts +182 -0
  173. package/src/shared/sqlite-transaction-mode-guard.test.ts +42 -10
  174. package/src/shared/sqlite-write-locker-test-support.ts +1 -0
  175. package/src/shared/sqlite.ts +142 -56
  176. package/src/shared/storage-busy.test.ts +95 -0
  177. package/src/shared/subagent-runner.ts +4 -0
  178. package/src/shared/user-facing-codes.ts +6 -0
  179. package/src/shared/windows-hidden-spawns.test.ts +132 -0
  180. package/src/shared/write-transaction-attribution.test.ts +1 -0
@@ -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
+ }
@@ -0,0 +1,132 @@
1
+ import { expect, test } from "bun:test";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { join, relative, resolve } from "node:path";
4
+ import ts from "typescript";
5
+
6
+ const root = resolve(import.meta.dir, "../../../..");
7
+ const shared = "packages/plugin/src/shared/sqlite.ts";
8
+ function files(dir: string): string[] {
9
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
10
+ const path = join(dir, entry.name);
11
+ if (entry.isDirectory()) return files(path);
12
+ return /\.tsx?$/.test(path) && !/\.test\.|test-support|\.d\.ts$/.test(path) ? [path] : [];
13
+ });
14
+ }
15
+
16
+ function scan(
17
+ path: string,
18
+ text: string,
19
+ ): { violations: string[]; transactions: number; literals: number } {
20
+ const violations: string[] = [];
21
+ let transactions = 0;
22
+ let literals = 0;
23
+ const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
24
+ const visit = (node: ts.Node) => {
25
+ if (
26
+ ts.isCallExpression(node) &&
27
+ ts.isPropertyAccessExpression(node.expression) &&
28
+ node.expression.name.text === "transaction"
29
+ )
30
+ transactions++;
31
+ if (
32
+ path !== shared &&
33
+ ts.isImportDeclaration(node) &&
34
+ ts.isStringLiteral(node.moduleSpecifier) &&
35
+ !node.importClause?.isTypeOnly &&
36
+ /^(bun:sqlite|node:sqlite|better-sqlite3)$/.test(node.moduleSpecifier.text)
37
+ ) {
38
+ violations.push(`${path}: native SQLite import bypasses shared acquisition`);
39
+ }
40
+ if (
41
+ path !== shared &&
42
+ ts.isExportDeclaration(node) &&
43
+ !node.isTypeOnly &&
44
+ node.moduleSpecifier &&
45
+ ts.isStringLiteral(node.moduleSpecifier) &&
46
+ /^(bun:sqlite|node:sqlite|better-sqlite3)$/.test(node.moduleSpecifier.text)
47
+ ) {
48
+ violations.push(`${path}: native SQLite re-export bypasses shared acquisition`);
49
+ }
50
+ if (
51
+ path !== shared &&
52
+ ts.isCallExpression(node) &&
53
+ (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
54
+ (ts.isIdentifier(node.expression) && node.expression.text === "require")) &&
55
+ /(?:bun:sqlite|node:sqlite|better-sqlite3)/.test(
56
+ node.arguments.map((arg) => arg.getText(source)).join(""),
57
+ )
58
+ ) {
59
+ violations.push(`${path}: dynamic native SQLite import bypasses shared acquisition`);
60
+ }
61
+ const sqlText = ts.isStringLiteralLike(node)
62
+ ? node.text
63
+ : ts.isTemplateExpression(node)
64
+ ? node.head.text + node.templateSpans.map((span) => span.literal.text).join("")
65
+ : undefined;
66
+ if (
67
+ sqlText !== undefined &&
68
+ /\bBEGIN\s+(?:IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*(?:;|$)/i.test(sqlText)
69
+ ) {
70
+ literals++;
71
+ if (path !== shared) {
72
+ const call = node.parent;
73
+ const routed =
74
+ !ts.isTemplateExpression(node) &&
75
+ ts.isCallExpression(call) &&
76
+ ts.isPropertyAccessExpression(call.expression) &&
77
+ call.expression.name.text === "exec" &&
78
+ /^\s*BEGIN\s+(IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*;?\s*$/i.test(sqlText);
79
+ if (!routed)
80
+ violations.push(
81
+ `${path}:${source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1}: BEGIN acquisition must be a standalone shared Database.exec call`,
82
+ );
83
+ }
84
+ }
85
+ ts.forEachChild(node, visit);
86
+ };
87
+ visit(source);
88
+ return { violations, transactions, literals };
89
+ }
90
+
91
+ test("all production transaction acquisition routes through the shared SQLite runtime", () => {
92
+ const scans = ["packages/plugin/src", "packages/pi-plugin/src", "packages/cli/src"]
93
+ .flatMap((directory) => files(join(root, directory)))
94
+ .map((path) => scan(relative(root, path), readFileSync(path, "utf8")));
95
+ expect(scans.reduce((sum, scan) => sum + scan.transactions, 0)).toBeGreaterThan(50);
96
+ expect(scans.reduce((sum, scan) => sum + scan.literals, 0)).toBeGreaterThan(20);
97
+ expect(scans.flatMap((scan) => scan.violations)).toEqual([]);
98
+ });
99
+
100
+ test("acquisition fence rejects raw native transactions and unrouted BEGIN shapes", () => {
101
+ expect(
102
+ scan(
103
+ "new-writer.ts",
104
+ 'import { Database } from "bun:sqlite"; new Database(":memory:").transaction(() => write())();',
105
+ ).violations,
106
+ ).toHaveLength(1);
107
+ expect(
108
+ scan(
109
+ "new-writer.ts",
110
+ 'const { DatabaseSync } = require("node:sqlite"); new DatabaseSync(":memory:").transaction(() => write())();',
111
+ ).violations,
112
+ ).toHaveLength(1);
113
+ expect(scan("new-writer.ts", 'export { Database } from "bun:sqlite";').violations).toHaveLength(
114
+ 1,
115
+ );
116
+ expect(scan("new-writer.ts", 'db.prepare("BEGIN IMMEDIATE").run();').violations).toHaveLength(
117
+ 1,
118
+ );
119
+ expect(
120
+ scan("new-writer.ts", 'db.exec("BEGIN IMMEDIATE; INSERT INTO result VALUES (1)");')
121
+ .violations,
122
+ ).toHaveLength(1);
123
+ expect(scan("new-writer.ts", "db.exec(`BEGIN IMMEDIATE; ${extra}`);").violations).toHaveLength(
124
+ 1,
125
+ );
126
+ expect(
127
+ scan(
128
+ "new-writer.ts",
129
+ 'import { Database } from "./shared/sqlite"; db.exec("BEGIN IMMEDIATE"); db.transaction(() => write()).immediate();',
130
+ ).violations,
131
+ ).toEqual([]);
132
+ });
@@ -0,0 +1,94 @@
1
+ import { expect, spyOn, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Database, withPrivilegedWriter, withSqliteTransformPass } from "./sqlite";
6
+ import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
7
+
8
+ for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
9
+ test(`shared SQLite retries ${mode} acquisition before running any writes`, async () => {
10
+ const dir = mkdtempSync(join(tmpdir(), "mc-routed-acquisition-"));
11
+ const path = join(dir, "context.db");
12
+ const db = new Database(path);
13
+ db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=50; CREATE TABLE result(value TEXT)");
14
+ const locker = await startSqliteWriteLocker(path, 650);
15
+ let calls = 0;
16
+ const write = () => {
17
+ calls++;
18
+ db.prepare("INSERT INTO result VALUES (?)").run("once");
19
+ return "managed";
20
+ };
21
+ try {
22
+ withSqliteTransformPass(() => {
23
+ if (mode === "literal") {
24
+ db.exec("BEGIN IMMEDIATE");
25
+ write();
26
+ db.exec("COMMIT");
27
+ } else {
28
+ const transaction = db.transaction(write);
29
+ expect(mode === "default" ? transaction() : transaction[mode]()).toBe(
30
+ "managed",
31
+ );
32
+ }
33
+ });
34
+ expect(calls).toBe(1);
35
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([{ value: "once" }]);
36
+ } finally {
37
+ await locker.exited;
38
+ db.close();
39
+ rmSync(dir, { recursive: true, force: true });
40
+ }
41
+ }, 30000);
42
+ }
43
+
44
+ test("routed transactions preserve nesting, receiver, arguments and rollback without callback retries", () => {
45
+ const db = new Database(":memory:");
46
+ db.exec("CREATE TABLE result(value TEXT)");
47
+ let calls = 0;
48
+ const transaction = db.transaction(function (this: { prefix: string }, value: string) {
49
+ calls++;
50
+ db.prepare("INSERT INTO result VALUES (?)").run(this.prefix + value);
51
+ db.transaction(() => {
52
+ db.exec("INSERT INTO result VALUES ('inner')");
53
+ }).immediate();
54
+ throw Object.assign(new Error("busy after writes"), { code: "SQLITE_BUSY" });
55
+ });
56
+ try {
57
+ expect(() =>
58
+ withSqliteTransformPass(() => transaction.call({ prefix: "outer-" }, "value")),
59
+ ).toThrow("busy after writes");
60
+ expect(calls).toBe(1);
61
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
62
+ } finally {
63
+ db.close();
64
+ }
65
+ });
66
+
67
+ test("exhausted routed acquisition never enters the callback or multiplies privileged retries", () => {
68
+ const dir = mkdtempSync(join(tmpdir(), "mc-routed-exhaustion-"));
69
+ const path = join(dir, "context.db");
70
+ const blocker = new Database(path);
71
+ const db = new Database(path);
72
+ blocker.exec("PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT)");
73
+ db.exec("PRAGMA busy_timeout=0");
74
+ blocker.exec("BEGIN IMMEDIATE");
75
+ const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
76
+ let callbacks = 0;
77
+ try {
78
+ for (const run of [
79
+ () => db.transaction(() => callbacks++)(),
80
+ () => withPrivilegedWriter(db, () => callbacks++),
81
+ ]) {
82
+ wait.mockClear();
83
+ expect(() => withSqliteTransformPass(run)).toThrow("after 3 attempts");
84
+ expect(wait).toHaveBeenCalledTimes(2);
85
+ expect(callbacks).toBe(0);
86
+ }
87
+ } finally {
88
+ wait.mockRestore();
89
+ blocker.exec("ROLLBACK");
90
+ blocker.close();
91
+ db.close();
92
+ rmSync(dir, { recursive: true, force: true });
93
+ }
94
+ });
@@ -0,0 +1,182 @@
1
+ import { expect, spyOn, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ Database,
7
+ withoutSqliteTransformPass,
8
+ withPrivilegedWriter,
9
+ withSqliteTransformPass,
10
+ } from "./sqlite";
11
+
12
+ test("background privileged acquisition attempts once without backoff", () => {
13
+ const db = new Database(":memory:");
14
+ const error = Object.assign(new Error("original busy"), { code: "SQLITE_BUSY" });
15
+ const exec = spyOn(db, "exec").mockImplementation(() => {
16
+ throw error;
17
+ });
18
+ const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
19
+ let callbacks = 0;
20
+ try {
21
+ expect(() => withPrivilegedWriter(db, () => callbacks++)).toThrow("original busy");
22
+ expect(exec).toHaveBeenCalledTimes(1);
23
+ expect(wait).not.toHaveBeenCalled();
24
+ expect(callbacks).toBe(0);
25
+ } finally {
26
+ exec.mockRestore();
27
+ wait.mockRestore();
28
+ db.close();
29
+ }
30
+ });
31
+
32
+ test("background transaction exhausts one five-second busy timeout", () => {
33
+ const dir = mkdtempSync(join(tmpdir(), "mc-background-busy-"));
34
+ const path = join(dir, "context.db");
35
+ const blocker = new Database(path);
36
+ const writer = new Database(path);
37
+ blocker.exec("PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT)");
38
+ writer.exec("PRAGMA busy_timeout=5000");
39
+ blocker.exec("BEGIN IMMEDIATE");
40
+ let callbacks = 0;
41
+ try {
42
+ const start = performance.now();
43
+ expect(() => writer.transaction(() => callbacks++)()).toThrow();
44
+ const elapsed = performance.now() - start;
45
+ expect(callbacks).toBe(0);
46
+ expect(elapsed).toBeGreaterThanOrEqual(4500);
47
+ expect(elapsed).toBeLessThan(7500);
48
+ } finally {
49
+ blocker.exec("ROLLBACK");
50
+ blocker.close();
51
+ writer.close();
52
+ rmSync(dir, { recursive: true, force: true });
53
+ }
54
+ }, 30000);
55
+
56
+ test("foreground retry scope crosses awaits, nests and excludes detached maintenance", async () => {
57
+ const db = new Database(":memory:");
58
+ const error = Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
59
+ const exec = spyOn(db, "exec").mockImplementation(() => {
60
+ throw error;
61
+ });
62
+ const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
63
+ const acquire = () =>
64
+ withPrivilegedWriter(db, () => {
65
+ throw new Error("callback must not run");
66
+ });
67
+ let late: Promise<void> | undefined;
68
+ let release!: () => void;
69
+ const gate = new Promise<void>((resolve) => {
70
+ release = resolve;
71
+ });
72
+ try {
73
+ await withSqliteTransformPass(async () => {
74
+ await Promise.resolve();
75
+ expect(() => withSqliteTransformPass(acquire)).toThrow("after 3 attempts");
76
+ expect(exec).toHaveBeenCalledTimes(3);
77
+ exec.mockClear();
78
+ wait.mockClear();
79
+ withoutSqliteTransformPass(() => {
80
+ expect(acquire).toThrow("busy");
81
+ expect(exec).toHaveBeenCalledTimes(1);
82
+ expect(wait).not.toHaveBeenCalled();
83
+ });
84
+ exec.mockClear();
85
+ expect(acquire).toThrow("after 3 attempts");
86
+ expect(exec).toHaveBeenCalledTimes(3);
87
+ late = (async () => {
88
+ await gate;
89
+ exec.mockClear();
90
+ wait.mockClear();
91
+ expect(acquire).toThrow("busy");
92
+ expect(exec).toHaveBeenCalledTimes(1);
93
+ expect(wait).not.toHaveBeenCalled();
94
+ })();
95
+ });
96
+ release();
97
+ await late;
98
+ exec.mockClear();
99
+ expect(acquire).toThrow("busy");
100
+ expect(exec).toHaveBeenCalledTimes(1);
101
+ } finally {
102
+ release();
103
+ exec.mockRestore();
104
+ wait.mockRestore();
105
+ db.close();
106
+ }
107
+ });
108
+
109
+ test("throwing foreground passes cannot leak retry policy to later work", async () => {
110
+ const db = new Database(":memory:");
111
+ const exec = spyOn(db, "exec").mockImplementation(() => {
112
+ throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
113
+ });
114
+ const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
115
+ try {
116
+ await expect(
117
+ withSqliteTransformPass(async () => {
118
+ throw new Error("pass failed");
119
+ }),
120
+ ).rejects.toThrow("pass failed");
121
+ expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("busy");
122
+ expect(exec).toHaveBeenCalledTimes(1);
123
+ expect(wait).not.toHaveBeenCalled();
124
+ } finally {
125
+ exec.mockRestore();
126
+ wait.mockRestore();
127
+ db.close();
128
+ }
129
+ });
130
+
131
+ test("explicit deferred reads on a writable handle do not contend with the writer", () => {
132
+ const dir = mkdtempSync(join(tmpdir(), "mc-deferred-read-"));
133
+ const path = join(dir, "context.db");
134
+ const writer = new Database(path);
135
+ const reader = new Database(path);
136
+ writer.exec(
137
+ "PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT); INSERT INTO result VALUES ('committed')",
138
+ );
139
+ reader.exec("PRAGMA busy_timeout=0");
140
+ writer.exec("BEGIN IMMEDIATE");
141
+ writer.exec("INSERT INTO result VALUES ('uncommitted')");
142
+ try {
143
+ const rows = withSqliteTransformPass(() =>
144
+ reader.transaction(() => reader.prepare("SELECT value FROM result").all()).deferred(),
145
+ );
146
+ expect(rows).toEqual([{ value: "committed" }]);
147
+ } finally {
148
+ writer.exec("ROLLBACK");
149
+ writer.close();
150
+ reader.close();
151
+ rmSync(dir, { recursive: true, force: true });
152
+ }
153
+ });
154
+
155
+ test("an explicitly started foreground pass owns its lifetime independently", async () => {
156
+ const db = new Database(":memory:");
157
+ const exec = spyOn(db, "exec").mockImplementation(() => {
158
+ throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
159
+ });
160
+ const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
161
+ let release!: () => void;
162
+ const gate = new Promise<void>((resolve) => {
163
+ release = resolve;
164
+ });
165
+ let child!: Promise<void>;
166
+ try {
167
+ await withSqliteTransformPass(async () => {
168
+ child = withSqliteTransformPass(async () => {
169
+ await gate;
170
+ expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("after 3 attempts");
171
+ expect(exec).toHaveBeenCalledTimes(3);
172
+ });
173
+ });
174
+ release();
175
+ await child;
176
+ } finally {
177
+ release();
178
+ exec.mockRestore();
179
+ wait.mockRestore();
180
+ db.close();
181
+ }
182
+ });