@cortexkit/opencode-magic-context 0.41.0 → 0.41.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 (143) hide show
  1. package/dist/config/schema/magic-context.d.ts +15 -0
  2. package/dist/config/schema/magic-context.d.ts.map +1 -1
  3. package/dist/{conflict-warning-hook-xj1r88a7.js → conflict-warning-hook-rf17b6et.js} +3 -3
  4. package/dist/{derive-budgets-3xfyphvk.js → derive-budgets-9q2d3r93.js} +2 -2
  5. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +16 -17
  6. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  7. package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -1
  8. package/dist/features/magic-context/context-authority.d.ts.map +1 -1
  9. package/dist/features/magic-context/dreamer/curate-memory-safety.d.ts +21 -0
  10. package/dist/features/magic-context/dreamer/curate-memory-safety.d.ts.map +1 -0
  11. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -6
  13. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  14. package/dist/features/magic-context/memory/embedding-failure.d.ts +10 -2
  15. package/dist/features/magic-context/memory/embedding-failure.d.ts.map +1 -1
  16. package/dist/features/magic-context/memory/embedding-local.d.ts +19 -2
  17. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  18. package/dist/features/magic-context/memory/embedding-synapse.d.ts +10 -1
  19. package/dist/features/magic-context/memory/embedding-synapse.d.ts.map +1 -1
  20. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  21. package/dist/features/magic-context/memory/onnxruntime-node-wasm-shim.d.ts +3 -0
  22. package/dist/features/magic-context/memory/onnxruntime-node-wasm-shim.d.ts.map +1 -0
  23. package/dist/features/magic-context/memory/project-identity.d.ts +4 -0
  24. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  25. package/dist/features/magic-context/memory/sharp-wasm-shim.d.ts +2 -0
  26. package/dist/features/magic-context/memory/sharp-wasm-shim.d.ts.map +1 -0
  27. package/dist/features/magic-context/memory/transformers-node-wasm-entry.d.ts +3 -0
  28. package/dist/features/magic-context/memory/transformers-node-wasm-entry.d.ts.map +1 -0
  29. package/dist/features/magic-context/memory/transformers-web-entry.d.ts +2 -0
  30. package/dist/features/magic-context/memory/transformers-web-entry.d.ts.map +1 -0
  31. package/dist/features/magic-context/message-fts-rowid-map.d.ts +28 -0
  32. package/dist/features/magic-context/message-fts-rowid-map.d.ts.map +1 -0
  33. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  34. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  35. package/dist/features/magic-context/overflow-detection.d.ts +15 -2
  36. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  37. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  38. package/dist/features/magic-context/search.d.ts +18 -0
  39. package/dist/features/magic-context/search.d.ts.map +1 -1
  40. package/dist/features/magic-context/storage-db.d.ts +2 -1
  41. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  42. package/dist/features/magic-context/storage-meta-persisted.d.ts +19 -2
  43. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  44. package/dist/features/magic-context/storage-meta-session.d.ts +3 -2
  45. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  46. package/dist/features/magic-context/storage-meta.d.ts +2 -2
  47. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  48. package/dist/features/magic-context/storage-ops.d.ts +2 -0
  49. package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
  50. package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
  51. package/dist/features/magic-context/storage-session-tables.d.ts +9 -1
  52. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  53. package/dist/features/magic-context/storage-tags.d.ts +13 -0
  54. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  55. package/dist/features/magic-context/storage.d.ts +2 -2
  56. package/dist/features/magic-context/storage.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/boundary-execution.d.ts +3 -0
  58. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/event-handler.d.ts +4 -1
  60. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/format-embed-failure.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
  63. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/hook.d.ts +1 -1
  65. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/live-session-state.d.ts +3 -1
  67. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/rust-mode-transform.d.ts +9 -1
  69. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/sentinel.d.ts +13 -37
  71. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  72. package/dist/hooks/magic-context/strip-content.d.ts +12 -5
  73. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  74. package/dist/hooks/magic-context/tag-id-fallback.d.ts +3 -1
  75. package/dist/hooks/magic-context/tag-id-fallback.d.ts.map +1 -1
  76. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +28 -2
  78. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/transform.d.ts +1 -1
  80. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  81. package/dist/{index-b4gv9f7g.js → index-4x691bz5.js} +87 -2
  82. package/dist/{index-knna83k0.js → index-9j3tx8hy.js} +1 -1
  83. package/dist/{index-jmwq2wxr.js → index-b9j85d29.js} +1 -1
  84. package/dist/{index-2bykz3bx.js → index-cvda5jb3.js} +7 -2
  85. package/dist/{index-bf799gbq.js → index-d1mxrd3p.js} +0 -1
  86. package/dist/{index-dmgbar7r.js → index-eq9wr75c.js} +2 -2
  87. package/dist/{index-w8rkbhn6.js → index-j8r0ejb3.js} +1232 -261
  88. package/dist/{index-26ds03jd.js → index-vnz6jvdc.js} +1 -1
  89. package/dist/{index-x9dmzyp5.js → index-y1yr57fw.js} +81 -17
  90. package/dist/{index-88za3sz8.js → index-zwjhba2r.js} +3 -3
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +1771 -1109
  93. package/dist/{model-resolution-jgrkt0z2.js → model-resolution-6w6wzw18.js} +1 -1
  94. package/dist/plugin/dream-timer.d.ts +4 -0
  95. package/dist/plugin/dream-timer.d.ts.map +1 -1
  96. package/dist/plugin/embedding-routing.d.ts.map +1 -1
  97. package/dist/plugin/hooks/create-session-hooks.d.ts +2 -2
  98. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  99. package/dist/{recomp-orchestrator-1w4xj6jt.js → recomp-orchestrator-m37p3fq6.js} +6 -6
  100. package/dist/{safe-notification-target-p0c0csxw.js → safe-notification-target-2a8py95v.js} +2 -2
  101. package/dist/{send-session-notification-kn34g7mx.js → send-session-notification-bw7eex6w.js} +3 -3
  102. package/dist/shared/index.d.ts +1 -0
  103. package/dist/shared/index.d.ts.map +1 -1
  104. package/dist/shared/logger.d.ts.map +1 -1
  105. package/dist/shared/model-resolution.d.ts.map +1 -1
  106. package/dist/shared/rpc-utils.d.ts +3 -1
  107. package/dist/shared/rpc-utils.d.ts.map +1 -1
  108. package/dist/shared/summarize-child-stderr.d.ts +2 -0
  109. package/dist/shared/summarize-child-stderr.d.ts.map +1 -0
  110. package/dist/shared/test-temp-dir.d.ts +40 -0
  111. package/dist/shared/test-temp-dir.d.ts.map +1 -0
  112. package/dist/shared/window-geometry.d.ts +2 -0
  113. package/dist/shared/window-geometry.d.ts.map +1 -1
  114. package/dist/{storage-meta-session-snbzd26z.js → storage-meta-session-dse3qv6s.js} +5 -3
  115. package/dist/{task-config-ty08ptc2.js → task-config-gv6s04fp.js} +2 -2
  116. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  117. package/dist/tools/ctx-memory/types.d.ts +2 -0
  118. package/dist/tools/ctx-memory/types.d.ts.map +1 -1
  119. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  120. package/dist/tools/ctx-search/tools.d.ts.map +1 -1
  121. package/dist/transformers-node-wasm.js +25676 -0
  122. package/dist/transformers-web.js +24570 -0
  123. package/dist/transformers.node-b1wa0hgx.js +28808 -0
  124. package/package.json +8 -3
  125. package/src/config/index.test.ts +50 -4
  126. package/src/config/latch-permanence-guard.test.ts +5 -0
  127. package/src/config/schema/magic-context.test.ts +20 -0
  128. package/src/config/schema/magic-context.ts +8 -0
  129. package/src/shared/conflict-detector.test.ts +6 -19
  130. package/src/shared/conflict-fixer.test.ts +5 -8
  131. package/src/shared/index.ts +1 -0
  132. package/src/shared/logger.test.ts +135 -40
  133. package/src/shared/logger.ts +115 -2
  134. package/src/shared/model-resolution.test.ts +21 -19
  135. package/src/shared/model-resolution.ts +0 -1
  136. package/src/shared/rpc-utils.test.ts +246 -5
  137. package/src/shared/rpc-utils.ts +340 -49
  138. package/src/shared/summarize-child-stderr.test.ts +55 -0
  139. package/src/shared/summarize-child-stderr.ts +33 -0
  140. package/src/shared/test-temp-dir.test.ts +57 -0
  141. package/src/shared/test-temp-dir.ts +165 -0
  142. package/src/shared/window-geometry.ts +9 -0
  143. /package/dist/{index-rpgg35ms.js → index-8sseka65.js} +0 -0
@@ -3,6 +3,8 @@ import { createHash } from "node:crypto";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
 
6
+ import { log } from "./logger";
7
+
6
8
  export type ProcessKind = "OpenCode server" | "OpenCode instance (TUI/CLI)" | "Pi" | "process";
7
9
 
8
10
  /** Best-effort process details captured while validating a migration blocker. */
@@ -86,9 +88,21 @@ export function isPidAlive(pid: number): PidLiveness {
86
88
  const RPC_IDENTITY_SKEW_TOLERANCE_MS = 120_000;
87
89
  const LINUX_CLOCK_TICKS_PER_SECOND = 100;
88
90
  const PS_PROBE_TIMEOUT_MS = 1_000;
91
+ const WINDOWS_CIM_PROBE_TIMEOUT_MS = 5_000;
92
+ const MAX_ANCESTOR_WALK_DEPTH = 16;
89
93
  const OPEN_CODE_COMMAND_MARKERS = ["opencode", "node", "bun", "electron"];
90
94
  const TASKLIST_NO_TASKS_PATTERN =
91
95
  /^INFO:\s+No tasks are running which match the specified criteria\.?$/im;
96
+ const WINDOWS_CIM_COMMAND =
97
+ "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress";
98
+ const PI_HARNESS_ARC_MARKERS = [
99
+ "pi-coding-agent",
100
+ "oh-my-pi",
101
+ "@oh-my-pi",
102
+ "cljs/dist",
103
+ "dist/bundle/cli",
104
+ ];
105
+ const PI_IMAGE_NAMES = new Set(["pi", "pi.cmd", "omp", "oh-my-pi"]);
92
106
 
93
107
  let rpcIdentityReadFileSync: typeof readFileSync = readFileSync;
94
108
  let rpcIdentityExecFileSync: typeof execFileSync = execFileSync;
@@ -157,7 +171,7 @@ export function readProcessStartTime(pid: number): number | null {
157
171
  return rpcIdentityPlatform === "linux"
158
172
  ? readLinuxProcessStartTime(pid)
159
173
  : rpcIdentityPlatform === "win32"
160
- ? null
174
+ ? readWindowsProcessStartTime(pid)
161
175
  : readPsProcessStartTime(pid);
162
176
  }
163
177
 
@@ -174,6 +188,32 @@ interface ProcessListEntry {
174
188
  command: string;
175
189
  }
176
190
 
191
+ interface ProcessFacts {
192
+ pid: number;
193
+ parentPid: number | null;
194
+ commandLine: string | null;
195
+ imageName: string | null;
196
+ startTime: number | null;
197
+ }
198
+
199
+ type ProcessSnapshotSource = "cim" | "tasklist" | "ps";
200
+
201
+ interface ProcessSnapshot {
202
+ facts: ProcessFacts[];
203
+ parentByPid: Map<number, number>;
204
+ source: ProcessSnapshotSource;
205
+ }
206
+
207
+ let windowsProcessFactsCache: Map<number, ProcessFacts> | null = null;
208
+
209
+ function rememberWindowsProcessFacts(facts: ProcessFacts[]): void {
210
+ windowsProcessFactsCache = new Map(facts.map((fact) => [fact.pid, fact]));
211
+ }
212
+
213
+ function clearWindowsProcessFactsCache(): void {
214
+ windowsProcessFactsCache = null;
215
+ }
216
+
177
217
  function parseCsvLine(line: string): string[] | null {
178
218
  const fields: string[] = [];
179
219
  let field = "";
@@ -235,6 +275,15 @@ function readWindowsProcess(pid: number): { state: PidLiveness; command?: string
235
275
  }
236
276
  }
237
277
 
278
+ function readWindowsProcessStartTime(pid: number): number | null {
279
+ const cached = windowsProcessFactsCache?.get(pid);
280
+ if (cached) return cached.startTime;
281
+ const snapshot = tryReadWindowsCimSnapshot(rpcIdentityExecFileSync);
282
+ if (!snapshot) return null;
283
+ rememberWindowsProcessFacts(snapshot.facts);
284
+ return windowsProcessFactsCache?.get(pid)?.startTime ?? null;
285
+ }
286
+
238
287
  function readLinuxProcessCommand(pid: number): string | null {
239
288
  try {
240
289
  return String(rpcIdentityReadFileSync(`/proc/${pid}/cmdline`, "utf8"));
@@ -259,11 +308,13 @@ function readPsProcessCommand(pid: number): string | null {
259
308
  /** Reuse the platform-gated command probes used by PID identity checks. */
260
309
  export function readProcessCommand(pid: number): string | null {
261
310
  if (!Number.isInteger(pid) || pid <= 0) return null;
262
- return rpcIdentityPlatform === "linux"
263
- ? readLinuxProcessCommand(pid)
264
- : rpcIdentityPlatform === "win32"
265
- ? (readWindowsProcess(pid).command ?? null)
266
- : readPsProcessCommand(pid);
311
+ if (rpcIdentityPlatform === "linux") return readLinuxProcessCommand(pid);
312
+ if (rpcIdentityPlatform === "win32") {
313
+ const cached = windowsProcessFactsCache?.get(pid);
314
+ if (cached?.commandLine) return cached.commandLine;
315
+ return readWindowsProcess(pid).command ?? null;
316
+ }
317
+ return readPsProcessCommand(pid);
267
318
  }
268
319
 
269
320
  function executableName(token: string | undefined): string {
@@ -378,6 +429,7 @@ export function __setRpcIdentityTestHooks(hooks: {
378
429
  platform?: NodeJS.Platform;
379
430
  nowMs?: () => number;
380
431
  }): void {
432
+ clearWindowsProcessFactsCache();
381
433
  rpcIdentityReadFileSync = hooks.readFileSync ?? readFileSync;
382
434
  rpcIdentityExecFileSync = hooks.execFileSync ?? execFileSync;
383
435
  rpcIdentityProcessKill = hooks.processKill ?? process.kill;
@@ -388,6 +440,7 @@ export function __setRpcIdentityTestHooks(hooks: {
388
440
  }
389
441
 
390
442
  export function __resetRpcIdentityTestHooks(): void {
443
+ clearWindowsProcessFactsCache();
391
444
  rpcIdentityReadFileSync = readFileSync;
392
445
  rpcIdentityExecFileSync = execFileSync;
393
446
  rpcIdentityProcessKill = process.kill;
@@ -397,27 +450,284 @@ export function __resetRpcIdentityTestHooks(): void {
397
450
  rpcIdentityNowMs = () => Date.now();
398
451
  }
399
452
 
400
- function commandLooksLikePi(command: string): boolean {
401
- const normalized = command.trim().toLowerCase().replaceAll("\\", "/");
402
- const tokens = normalized.split(/\s+/).filter(Boolean);
403
- const executableName = (token: string | undefined): string =>
404
- (token ?? "").split("/").at(-1) ?? "";
405
- const first = executableName(tokens[0]).replace(/\.exe$/, "");
406
- if (["pi", "pi.cmd", "omp", "oh-my-pi"].includes(first)) return true;
453
+ function commandLooksLikePiImage(command: string): boolean {
454
+ const tokens = commandTokens(command);
455
+ const first = executableName(tokens[0]).replace(/\.(?:exe|cmd)$/, "");
456
+ return PI_IMAGE_NAMES.has(first);
457
+ }
458
+
459
+ /**
460
+ * A process is a verified Pi/OMP harness only when its command line names a
461
+ * known package entry (pi-coding-agent, oh-my-pi, the bun/node cli shim).
462
+ * Bare `omp.exe` / `pi.exe` image names are not enough: Windows tasklist
463
+ * reports only the image, and that matches the session's own launcher shim.
464
+ */
465
+ function commandHasPiHarnessArc(command: string): boolean {
466
+ const normalized = command.trim().toLowerCase().replaceAll("\\", "/").replaceAll("\u0000", " ");
467
+ if (!normalized) return false;
468
+ const tokens = commandTokens(command);
469
+ if (tokens.length === 0) return false;
470
+ const hasArc = PI_HARNESS_ARC_MARKERS.some((marker) => normalized.includes(marker));
471
+ const first = executableName(tokens[0]).replace(/\.(?:exe|cmd)$/, "");
472
+ if (hasArc && ["pi", "omp", "oh-my-pi", "node", "bun", "deno", "cmd"].includes(first)) {
473
+ return true;
474
+ }
475
+ if (hasArc && PI_HARNESS_ARC_MARKERS.some((marker) => tokens[0].includes(marker))) {
476
+ return true;
477
+ }
407
478
  if (["node", "bun", "deno"].includes(first)) {
408
- const script = executableName(tokens[1]);
409
- return (
410
- ["pi", "pi.js", "pi.mjs", "pi.cjs"].includes(script) ||
411
- normalized.includes("pi-coding-agent")
412
- );
479
+ const script = executableName(tokens[1]).replace(/\.(?:exe|cmd)$/, "");
480
+ if (["pi", "pi.js", "pi.mjs", "pi.cjs"].includes(script)) return true;
481
+ if (hasArc) return true;
413
482
  }
414
483
  return false;
415
484
  }
416
485
 
486
+ function execProcessList(
487
+ exec: typeof execFileSync,
488
+ file: string,
489
+ args: readonly string[],
490
+ timeout = PS_PROBE_TIMEOUT_MS,
491
+ ): string {
492
+ return String(
493
+ exec(file, [...args], {
494
+ encoding: "utf8",
495
+ timeout,
496
+ stdio: ["ignore", "pipe", "pipe"],
497
+ }),
498
+ );
499
+ }
500
+
501
+ function parseWindowsCreationDate(value: unknown): number | null {
502
+ if (typeof value === "number" && Number.isFinite(value)) {
503
+ return value > 1e12 ? value : value * 1_000;
504
+ }
505
+ if (typeof value !== "string") return null;
506
+ const trimmed = value.trim();
507
+ if (!trimmed) return null;
508
+ const wmi = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{6})([+-])(\d{3})$/.exec(trimmed);
509
+ if (wmi) {
510
+ const utcMs = Date.UTC(
511
+ Number(wmi[1]),
512
+ Number(wmi[2]) - 1,
513
+ Number(wmi[3]),
514
+ Number(wmi[4]),
515
+ Number(wmi[5]),
516
+ Number(wmi[6]),
517
+ Number(wmi[7]) / 1_000,
518
+ );
519
+ if (!Number.isFinite(utcMs)) return null;
520
+ const offsetMinutes = Number(wmi[9]);
521
+ const sign = wmi[8] === "+" ? 1 : -1;
522
+ return utcMs - sign * offsetMinutes * 60_000;
523
+ }
524
+ const dotNet = /^\/Date\((-?\d+)\)\/$/.exec(trimmed);
525
+ if (dotNet) {
526
+ const milliseconds = Number(dotNet[1]);
527
+ return Number.isFinite(milliseconds) ? milliseconds : null;
528
+ }
529
+ const parsed = Date.parse(trimmed);
530
+ return Number.isFinite(parsed) ? parsed : null;
531
+ }
532
+
533
+ function parseWindowsCimOutput(output: string): ProcessFacts[] | null {
534
+ const trimmed = output.trim();
535
+ if (!trimmed) return null;
536
+ const bracket = trimmed.indexOf("[");
537
+ const brace = trimmed.indexOf("{");
538
+ const start = Math.min(
539
+ bracket === -1 ? Number.POSITIVE_INFINITY : bracket,
540
+ brace === -1 ? Number.POSITIVE_INFINITY : brace,
541
+ );
542
+ if (!Number.isFinite(start)) return null;
543
+ let parsed: unknown;
544
+ try {
545
+ parsed = JSON.parse(trimmed.slice(start));
546
+ } catch {
547
+ return null;
548
+ }
549
+ if (parsed == null) return null;
550
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
551
+ const facts: ProcessFacts[] = [];
552
+ for (const row of rows) {
553
+ if (!row || typeof row !== "object") continue;
554
+ const record = row as Record<string, unknown>;
555
+ const pid = Number(record.ProcessId);
556
+ if (!Number.isInteger(pid) || pid <= 0) continue;
557
+ const parentRaw = record.ParentProcessId;
558
+ const parentPid = parentRaw == null || parentRaw === "" ? Number.NaN : Number(parentRaw);
559
+ const commandLine = typeof record.CommandLine === "string" ? record.CommandLine : null;
560
+ facts.push({
561
+ pid,
562
+ parentPid: Number.isInteger(parentPid) && parentPid > 0 ? parentPid : null,
563
+ commandLine,
564
+ imageName: commandLine ? executableName(commandTokens(commandLine)[0]) : null,
565
+ startTime: parseWindowsCreationDate(record.CreationDate),
566
+ });
567
+ }
568
+ return facts.length > 0 ? facts : null;
569
+ }
570
+
571
+ function snapshotFromFacts(facts: ProcessFacts[], source: ProcessSnapshotSource): ProcessSnapshot {
572
+ const parentByPid = new Map<number, number>();
573
+ for (const fact of facts) {
574
+ if (fact.parentPid != null) parentByPid.set(fact.pid, fact.parentPid);
575
+ }
576
+ return { facts, parentByPid, source };
577
+ }
578
+
579
+ function tryReadWindowsCimSnapshot(exec: typeof execFileSync): ProcessSnapshot | null {
580
+ try {
581
+ const output = execProcessList(
582
+ exec,
583
+ "powershell",
584
+ ["-NoProfile", "-Command", WINDOWS_CIM_COMMAND],
585
+ WINDOWS_CIM_PROBE_TIMEOUT_MS,
586
+ );
587
+ const facts = parseWindowsCimOutput(output);
588
+ return facts ? snapshotFromFacts(facts, "cim") : null;
589
+ } catch {
590
+ return null;
591
+ }
592
+ }
593
+
594
+ function tryReadWindowsTasklistSnapshot(): ProcessSnapshot | null {
595
+ try {
596
+ const output = execProcessList(rpcProcessListExecFileSync, "tasklist", ["/FO", "CSV"]);
597
+ const entries = parseTasklistOutput(output);
598
+ if (entries === null) return null;
599
+ const facts = entries.map((entry) => ({
600
+ pid: entry.pid,
601
+ parentPid: null,
602
+ commandLine: null,
603
+ imageName: entry.command,
604
+ startTime: null,
605
+ }));
606
+ return snapshotFromFacts(facts, "tasklist");
607
+ } catch {
608
+ return null;
609
+ }
610
+ }
611
+
612
+ function readPosixProcessSnapshot(): ProcessSnapshot {
613
+ const output = execProcessList(rpcProcessListExecFileSync, "ps", ["-axo", "pid=,command="]);
614
+ const facts: ProcessFacts[] = [];
615
+ for (const line of output.split(/\r?\n/)) {
616
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
617
+ if (!match) continue;
618
+ const pid = Number(match[1]);
619
+ if (!Number.isInteger(pid) || pid <= 0) continue;
620
+ facts.push({
621
+ pid,
622
+ parentPid: null,
623
+ commandLine: match[2],
624
+ imageName: executableName(commandTokens(match[2])[0]),
625
+ startTime: null,
626
+ });
627
+ }
628
+ return snapshotFromFacts(facts, "ps");
629
+ }
630
+
631
+ function readPosixParentPid(pid: number): number | null {
632
+ try {
633
+ const output = execProcessList(rpcProcessListExecFileSync, "ps", [
634
+ "-o",
635
+ "ppid=",
636
+ "-p",
637
+ String(pid),
638
+ ]);
639
+ const match = /^\s*(\d+)\s*$/.exec(output);
640
+ if (!match) return null;
641
+ const ppid = Number(match[1]);
642
+ return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
643
+ } catch {
644
+ return null;
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Walk toward init from `selfPid` so the session's own launcher cannot be
650
+ * treated as a foreign blocker. On Windows, OMP/Pi is two processes: a
651
+ * long-lived `omp.exe`/`pi.exe` shim and the bun/node child where this
652
+ * plugin runs. Bound the walk and stop on cycles so a broken parent map
653
+ * cannot loop.
654
+ */
655
+ function collectAncestorPids(selfPid: number, parentByPid: Map<number, number>): Set<number> {
656
+ const ancestors = new Set<number>();
657
+ let current = selfPid;
658
+ for (let depth = 0; depth < MAX_ANCESTOR_WALK_DEPTH; depth += 1) {
659
+ let ppid: number | null = null;
660
+ if (parentByPid.has(current)) {
661
+ ppid = parentByPid.get(current) ?? null;
662
+ } else if (rpcIdentityPlatform !== "win32") {
663
+ ppid = readPosixParentPid(current);
664
+ if (ppid == null && current === process.pid && process.ppid > 0) {
665
+ ppid = process.ppid;
666
+ }
667
+ } else if (current === process.pid && process.ppid > 0) {
668
+ ppid = process.ppid;
669
+ } else {
670
+ break;
671
+ }
672
+ if (ppid == null || ppid <= 0 || ppid === current || ancestors.has(ppid)) break;
673
+ ancestors.add(ppid);
674
+ current = ppid;
675
+ }
676
+ return ancestors;
677
+ }
678
+
679
+ function classifyLivePiSnapshot(snapshot: ProcessSnapshot): PiProcessDiscovery {
680
+ const ancestors = collectAncestorPids(process.pid, snapshot.parentByPid);
681
+ const processIds = new Set<number>();
682
+ const inconclusivePids = new Set<number>();
683
+ const skippedAncestorPids: number[] = [];
684
+ for (const fact of snapshot.facts) {
685
+ if (fact.pid === process.pid) continue;
686
+ const command = fact.commandLine ?? fact.imageName ?? "";
687
+ const looksLikeHarness =
688
+ commandHasPiHarnessArc(command) || commandLooksLikePiImage(command);
689
+ if (!looksLikeHarness) continue;
690
+ if (ancestors.has(fact.pid)) {
691
+ skippedAncestorPids.push(fact.pid);
692
+ log(
693
+ `[magic-context] Pi process scan: skipping ancestor PID ${fact.pid} (session launcher shim)`,
694
+ );
695
+ continue;
696
+ }
697
+ if (commandHasPiHarnessArc(command)) {
698
+ processIds.add(fact.pid);
699
+ continue;
700
+ }
701
+ inconclusivePids.add(fact.pid);
702
+ log(
703
+ `[magic-context] Pi process scan: PID ${fact.pid} command line is ambiguous (image-name or missing Pi/OMP arc); treating as inconclusive`,
704
+ );
705
+ }
706
+ skippedAncestorPids.sort((left, right) => left - right);
707
+ const verified = [...processIds].sort((left, right) => left - right);
708
+ const inconclusive = [...inconclusivePids].sort((left, right) => left - right);
709
+ if (verified.length === 0 && inconclusive.length > 0) {
710
+ return {
711
+ state: "inconclusive",
712
+ processIds: [],
713
+ inconclusivePids: inconclusive,
714
+ ...(skippedAncestorPids.length > 0 ? { skippedAncestorPids } : {}),
715
+ };
716
+ }
717
+ return {
718
+ state: "known",
719
+ processIds: verified,
720
+ ...(inconclusive.length > 0 ? { inconclusivePids: inconclusive } : {}),
721
+ ...(skippedAncestorPids.length > 0 ? { skippedAncestorPids } : {}),
722
+ };
723
+ }
724
+
417
725
  /** Result of checking whether Pi/OMP processes may currently hold the shared database. */
418
726
  export interface PiProcessDiscovery {
419
- state: "known" | "unreadable";
727
+ state: "known" | "unreadable" | "inconclusive";
420
728
  processIds: number[];
729
+ inconclusivePids?: number[];
730
+ skippedAncestorPids?: number[];
421
731
  error?: string;
422
732
  }
423
733
 
@@ -432,42 +742,23 @@ export function inspectLivePiProcesses(): PiProcessDiscovery {
432
742
  return { state: "known", processIds: [] };
433
743
  }
434
744
  try {
435
- const isWindows = rpcIdentityPlatform === "win32";
436
- const output = String(
437
- rpcProcessListExecFileSync(
438
- isWindows ? "tasklist" : "ps",
439
- isWindows ? ["/FO", "CSV"] : ["-axo", "pid=,command="],
440
- {
441
- encoding: "utf8",
442
- timeout: PS_PROBE_TIMEOUT_MS,
443
- stdio: ["ignore", "pipe", "pipe"],
444
- },
445
- ),
446
- );
447
- const pids = new Set<number>();
448
- if (isWindows) {
449
- const entries = parseTasklistOutput(output);
450
- if (entries === null) {
745
+ if (rpcIdentityPlatform === "win32") {
746
+ // Prefer one CIM query (pid, parent, command line, start time).
747
+ // tasklist is image-name-only, so it is a fallback and never a
748
+ // verified live-harness source.
749
+ const cim = tryReadWindowsCimSnapshot(rpcProcessListExecFileSync);
750
+ const snapshot = cim ?? tryReadWindowsTasklistSnapshot();
751
+ if (!snapshot) {
451
752
  return {
452
753
  state: "unreadable",
453
754
  processIds: [],
454
- error: "tasklist output unavailable",
755
+ error: "process list unavailable",
455
756
  };
456
757
  }
457
- for (const entry of entries) {
458
- if (entry.pid === process.pid) continue;
459
- if (commandLooksLikePi(entry.command)) pids.add(entry.pid);
460
- }
461
- } else {
462
- for (const line of output.split(/\r?\n/)) {
463
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
464
- if (!match) continue;
465
- const pid = Number(match[1]);
466
- if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) continue;
467
- if (commandLooksLikePi(match[2])) pids.add(pid);
468
- }
758
+ rememberWindowsProcessFacts(snapshot.facts);
759
+ return classifyLivePiSnapshot(snapshot);
469
760
  }
470
- return { state: "known", processIds: [...pids].sort((left, right) => left - right) };
761
+ return classifyLivePiSnapshot(readPosixProcessSnapshot());
471
762
  } catch (error) {
472
763
  return {
473
764
  state: "unreadable",
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { summarizeChildStderr } from "./summarize-child-stderr";
3
+
4
+ function bundledCrashStderr(): { sourceLine: string; stderr: string } {
5
+ const sourceLine = `263 | ${"FROM cacheInterceptorV${VERSION2} ".repeat(100)}`.slice(0, 3_000);
6
+ return {
7
+ sourceLine,
8
+ stderr: [
9
+ sourceLine,
10
+ "^",
11
+ "SqliteError: database is locked",
12
+ " at openDatabase (bundle.js:100:20)",
13
+ " at runHistorian (runner.js:12:4)",
14
+ " at main (cli.js:2:1)",
15
+ ].join("\n"),
16
+ };
17
+ }
18
+
19
+ describe("summarizeChildStderr", () => {
20
+ it("keeps an error line and the first two stack frames after bundled source context", () => {
21
+ const { sourceLine, stderr } = bundledCrashStderr();
22
+
23
+ const excerpt = summarizeChildStderr(stderr);
24
+
25
+ expect(excerpt).toContain("SqliteError: database is locked");
26
+ expect(excerpt).toContain("at openDatabase (bundle.js:100:20)");
27
+ expect(excerpt).toContain("at runHistorian (runner.js:12:4)");
28
+ expect(excerpt).not.toContain("at main (cli.js:2:1)");
29
+ expect(excerpt.startsWith(sourceLine)).toBe(false);
30
+ });
31
+
32
+ it("passes through short stderr without an error-shaped line unchanged", () => {
33
+ const stderr = "auth failure: missing API key";
34
+
35
+ expect(summarizeChildStderr(stderr)).toBe(stderr);
36
+ });
37
+
38
+ it("keeps the tail of long chatty stderr when no error line is present", () => {
39
+ const stderr = `${"head-only\n".repeat(2_000)}${"tail-only\n".repeat(2_000)}`;
40
+ const excerpt = summarizeChildStderr(stderr);
41
+
42
+ expect(stderr.length).toBeGreaterThan(16_000);
43
+ expect(excerpt).toBe(stderr.slice(-500));
44
+ expect(excerpt).toContain("tail-only");
45
+ expect(excerpt).not.toContain("head-only");
46
+ });
47
+
48
+ it("recognizes Node's bracketed error code form", () => {
49
+ expect(
50
+ summarizeChildStderr(
51
+ "Error [ERR_MODULE_NOT_FOUND]: Cannot find package\n at loader (node:internal/modules:1:1)",
52
+ ),
53
+ ).toContain("Error [ERR_MODULE_NOT_FOUND]: Cannot find package");
54
+ });
55
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Return the useful part of a child-process diagnostic without allowing a
3
+ * bundled source line to hide the error that follows it.
4
+ *
5
+ * Node prints uncaught exceptions as source context, a caret, and then the
6
+ * error line and stack. Prefer that error line plus the first two stack frames;
7
+ * when no recognizable error line exists, the end of stderr is most likely to
8
+ * contain the actionable failure detail.
9
+ */
10
+ const MAX_CHILD_STDERR_SUMMARY_LENGTH = 500;
11
+ const CHILD_ERROR_LINE_PATTERN = /^(?:[A-Za-z]*Error|SqliteError|Error)\b[^\n]*/;
12
+ const STACK_FRAME_PATTERN = /^\s*at\b/;
13
+
14
+ export function summarizeChildStderr(stderr: string): string {
15
+ const lines = stderr.split(/\r?\n/);
16
+ const errorLineIndex = lines.findIndex((line) => CHILD_ERROR_LINE_PATTERN.test(line));
17
+
18
+ if (errorLineIndex < 0) {
19
+ return stderr.length > MAX_CHILD_STDERR_SUMMARY_LENGTH
20
+ ? stderr.slice(-MAX_CHILD_STDERR_SUMMARY_LENGTH)
21
+ : stderr;
22
+ }
23
+
24
+ const excerptLines = [lines[errorLineIndex]];
25
+ let stackFrames = 0;
26
+ for (let index = errorLineIndex + 1; index < lines.length && stackFrames < 2; index += 1) {
27
+ if (!STACK_FRAME_PATTERN.test(lines[index])) continue;
28
+ excerptLines.push(lines[index]);
29
+ stackFrames += 1;
30
+ }
31
+
32
+ return excerptLines.join("\n").slice(0, MAX_CHILD_STDERR_SUMMARY_LENGTH);
33
+ }
@@ -0,0 +1,57 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, utimesSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ cleanupTestTempDir,
6
+ createTestTempDir,
7
+ sweepStaleTestTempDirs,
8
+ withTestTempDir,
9
+ } from "./test-temp-dir";
10
+
11
+ const tempDirs: string[] = [];
12
+
13
+ afterEach(() => {
14
+ for (const directory of tempDirs.splice(0)) cleanupTestTempDir(directory);
15
+ });
16
+
17
+ function createFixtureRoot(): string {
18
+ const { dir } = createTestTempDir("mc-test-temp-dir-helper-");
19
+ tempDirs.push(dir);
20
+ return dir;
21
+ }
22
+
23
+ describe("test temp directories", () => {
24
+ it("removes a root when its fixture callback throws", () => {
25
+ let directory = "";
26
+
27
+ expect(() =>
28
+ withTestTempDir("mc-test-temp-dir-helper-", (tempDir) => {
29
+ directory = tempDir;
30
+ throw new Error("fixture setup failed");
31
+ }),
32
+ ).toThrow("fixture setup failed");
33
+
34
+ expect(existsSync(directory)).toBe(false);
35
+ });
36
+
37
+ it("sweeps only aged directories with recognized prefixes", () => {
38
+ const root = createFixtureRoot();
39
+ const staleRecognized = join(root, "mc-config-secret-stale");
40
+ const freshRecognized = join(root, "mc-config-secret-fresh");
41
+ const staleBystander = join(root, "unrelated-tool-stale");
42
+ const nowMs = Date.now();
43
+
44
+ for (const directory of [staleRecognized, freshRecognized, staleBystander]) {
45
+ mkdirSync(directory);
46
+ }
47
+ utimesSync(staleRecognized, new Date(nowMs - 7_200_000), new Date(nowMs - 7_200_000));
48
+ utimesSync(staleBystander, new Date(nowMs - 7_200_000), new Date(nowMs - 7_200_000));
49
+
50
+ const removed = sweepStaleTestTempDirs({ tempDir: root, nowMs });
51
+
52
+ expect(removed).toEqual([staleRecognized]);
53
+ expect(existsSync(staleRecognized)).toBe(false);
54
+ expect(existsSync(freshRecognized)).toBe(true);
55
+ expect(existsSync(staleBystander)).toBe(true);
56
+ });
57
+ });