@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.
- package/dist/config/schema/magic-context.d.ts +15 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-xj1r88a7.js → conflict-warning-hook-rf17b6et.js} +3 -3
- package/dist/{derive-budgets-3xfyphvk.js → derive-budgets-9q2d3r93.js} +2 -2
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +16 -17
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/context-authority.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/curate-memory-safety.d.ts +21 -0
- package/dist/features/magic-context/dreamer/curate-memory-safety.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -6
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-failure.d.ts +10 -2
- package/dist/features/magic-context/memory/embedding-failure.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts +19 -2
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-synapse.d.ts +10 -1
- package/dist/features/magic-context/memory/embedding-synapse.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/onnxruntime-node-wasm-shim.d.ts +3 -0
- package/dist/features/magic-context/memory/onnxruntime-node-wasm-shim.d.ts.map +1 -0
- package/dist/features/magic-context/memory/project-identity.d.ts +4 -0
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/sharp-wasm-shim.d.ts +2 -0
- package/dist/features/magic-context/memory/sharp-wasm-shim.d.ts.map +1 -0
- package/dist/features/magic-context/memory/transformers-node-wasm-entry.d.ts +3 -0
- package/dist/features/magic-context/memory/transformers-node-wasm-entry.d.ts.map +1 -0
- package/dist/features/magic-context/memory/transformers-web-entry.d.ts +2 -0
- package/dist/features/magic-context/memory/transformers-web-entry.d.ts.map +1 -0
- package/dist/features/magic-context/message-fts-rowid-map.d.ts +28 -0
- package/dist/features/magic-context/message-fts-rowid-map.d.ts.map +1 -0
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +15 -2
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +18 -0
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +19 -2
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts +3 -2
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +2 -2
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-ops.d.ts +2 -0
- package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
- package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +9 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +13 -0
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +2 -2
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts +3 -0
- package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts +4 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/format-embed-failure.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/live-session-state.d.ts +3 -1
- package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +9 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +13 -37
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +12 -5
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-id-fallback.d.ts +3 -1
- package/dist/hooks/magic-context/tag-id-fallback.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +28 -2
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-b4gv9f7g.js → index-4x691bz5.js} +87 -2
- package/dist/{index-knna83k0.js → index-9j3tx8hy.js} +1 -1
- package/dist/{index-jmwq2wxr.js → index-b9j85d29.js} +1 -1
- package/dist/{index-2bykz3bx.js → index-cvda5jb3.js} +7 -2
- package/dist/{index-bf799gbq.js → index-d1mxrd3p.js} +0 -1
- package/dist/{index-dmgbar7r.js → index-eq9wr75c.js} +2 -2
- package/dist/{index-w8rkbhn6.js → index-j8r0ejb3.js} +1232 -261
- package/dist/{index-26ds03jd.js → index-vnz6jvdc.js} +1 -1
- package/dist/{index-x9dmzyp5.js → index-y1yr57fw.js} +81 -17
- package/dist/{index-88za3sz8.js → index-zwjhba2r.js} +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1771 -1109
- package/dist/{model-resolution-jgrkt0z2.js → model-resolution-6w6wzw18.js} +1 -1
- package/dist/plugin/dream-timer.d.ts +4 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/embedding-routing.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +2 -2
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-1w4xj6jt.js → recomp-orchestrator-m37p3fq6.js} +6 -6
- package/dist/{safe-notification-target-p0c0csxw.js → safe-notification-target-2a8py95v.js} +2 -2
- package/dist/{send-session-notification-kn34g7mx.js → send-session-notification-bw7eex6w.js} +3 -3
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/model-resolution.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +3 -1
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/summarize-child-stderr.d.ts +2 -0
- package/dist/shared/summarize-child-stderr.d.ts.map +1 -0
- package/dist/shared/test-temp-dir.d.ts +40 -0
- package/dist/shared/test-temp-dir.d.ts.map +1 -0
- package/dist/shared/window-geometry.d.ts +2 -0
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/{storage-meta-session-snbzd26z.js → storage-meta-session-dse3qv6s.js} +5 -3
- package/dist/{task-config-ty08ptc2.js → task-config-gv6s04fp.js} +2 -2
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/types.d.ts +2 -0
- package/dist/tools/ctx-memory/types.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/dist/transformers-node-wasm.js +25676 -0
- package/dist/transformers-web.js +24570 -0
- package/dist/transformers.node-b1wa0hgx.js +28808 -0
- package/package.json +8 -3
- package/src/config/index.test.ts +50 -4
- package/src/config/latch-permanence-guard.test.ts +5 -0
- package/src/config/schema/magic-context.test.ts +20 -0
- package/src/config/schema/magic-context.ts +8 -0
- package/src/shared/conflict-detector.test.ts +6 -19
- package/src/shared/conflict-fixer.test.ts +5 -8
- package/src/shared/index.ts +1 -0
- package/src/shared/logger.test.ts +135 -40
- package/src/shared/logger.ts +115 -2
- package/src/shared/model-resolution.test.ts +21 -19
- package/src/shared/model-resolution.ts +0 -1
- package/src/shared/rpc-utils.test.ts +246 -5
- package/src/shared/rpc-utils.ts +340 -49
- package/src/shared/summarize-child-stderr.test.ts +55 -0
- package/src/shared/summarize-child-stderr.ts +33 -0
- package/src/shared/test-temp-dir.test.ts +57 -0
- package/src/shared/test-temp-dir.ts +165 -0
- package/src/shared/window-geometry.ts +9 -0
- /package/dist/{index-rpgg35ms.js → index-8sseka65.js} +0 -0
package/src/shared/rpc-utils.ts
CHANGED
|
@@ -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
|
-
?
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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: "
|
|
755
|
+
error: "process list unavailable",
|
|
455
756
|
};
|
|
456
757
|
}
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
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
|
+
});
|