@cortexkit/opencode-magic-context 0.44.0 → 0.44.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/agents/dreamer.d.ts +4 -6
- package/dist/agents/dreamer.d.ts.map +1 -1
- package/dist/agents/magic-context-prompt.d.ts +2 -0
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/{announcement-1a8bvswd.js → announcement-n8hs7kjw.js} +2 -2
- package/dist/config/live-child-output-cap.d.ts +18 -0
- package/dist/config/live-child-output-cap.d.ts.map +1 -0
- package/dist/config/schema/magic-context.d.ts +64 -11
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-48f7np9d.js → conflict-warning-hook-1msyvb51.js} +6 -6
- package/dist/{derive-budgets-8kgevxmx.js → derive-budgets-gd2270s0.js} +3 -3
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +11 -0
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/docs-proposals.d.ts +2 -0
- package/dist/features/magic-context/dreamer/docs-proposals.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/manual-summary.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +5 -0
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/open-opencode-db.d.ts +2 -0
- package/dist/features/magic-context/dreamer/open-opencode-db.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/primer-seed.d.ts +11 -5
- package/dist/features/magic-context/dreamer/primer-seed.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +7 -2
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +2 -2
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +8 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts +5 -0
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +18 -2
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts +12 -0
- package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/token-budget.d.ts +33 -0
- package/dist/features/magic-context/dreamer/token-budget.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify-diff.d.ts +15 -0
- package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-read-cap.d.ts +4 -0
- package/dist/features/magic-context/dreamer/verify-read-cap.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify.d.ts +5 -0
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity-cache.d.ts +6 -0
- package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -0
- package/dist/features/magic-context/memory/project-identity.d.ts +15 -22
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -0
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/verification-paths.d.ts +1 -0
- package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
- package/dist/features/magic-context/mural/mural-font.generated.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +5 -0
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/session-activity.d.ts +10 -0
- package/dist/features/magic-context/session-activity.d.ts.map +1 -0
- package/dist/features/magic-context/session-cache-ttl.d.ts +10 -0
- package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -0
- package/dist/features/magic-context/session-project-backfill.d.ts +4 -1
- package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts +5 -0
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +1 -0
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts +1 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/types.d.ts +2 -0
- package/dist/features/magic-context/smart-notes/types.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-identity-merge.d.ts +4 -1
- package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
- package/dist/hooks/magic-context/embed-session-state.d.ts +3 -0
- package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-payloads.d.ts +1 -0
- package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts +6 -4
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
- package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts +12 -0
- package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +16 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +5 -1
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +10 -0
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts +7 -0
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +1 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-5f4qsnaq.js → index-18tjha8g.js} +2 -2
- package/dist/{index-4b30nf6z.js → index-32b096cc.js} +1 -1
- package/dist/{index-tjq5jztd.js → index-3bygzk2d.js} +1 -1
- package/dist/{index-cj5qe2nw.js → index-3x9caerx.js} +8 -6
- package/dist/index-40s1xz22.js +125 -0
- package/dist/{index-pye48m79.js → index-egkfvhms.js} +130 -42
- package/dist/{index-2zf5fv89.js → index-esxfcz5z.js} +171 -12
- package/dist/index-gtzcmv90.js +7260 -0
- package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
- package/dist/{index-v6wsttqt.js → index-kr5qehbd.js} +1 -1
- package/dist/{index-dx0xnd4q.js → index-kwe03yde.js} +1 -1
- package/dist/{index-c906f3ff.js → index-r9vkp4n9.js} +4 -4
- package/dist/{index-6z9g64js.js → index-s70wtd8k.js} +4 -1
- package/dist/{index-01ft0h3p.js → index-stb5fg12.js} +2 -2
- package/dist/{index-rdey8znw.js → index-ttdn47aq.js} +35260 -32540
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15123 -23504
- package/dist/{message-time-backfill-rm9wmcem.js → message-time-backfill-q374fxc5.js} +3 -3
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +10 -9
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +1 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-ktz5zt70.js → read-session-chunk-wmvgnd24.js} +6 -4
- package/dist/{recomp-orchestrator-ahekzt0s.js → recomp-orchestrator-rk40ff15.js} +10 -9
- package/dist/{safe-notification-target-6w0esgd6.js → safe-notification-target-50gm9c58.js} +3 -3
- package/dist/{send-session-notification-w7xbnrsm.js → send-session-notification-6xsrcxnp.js} +5 -5
- package/dist/shared/cache-ttl-display.d.ts +3 -1
- package/dist/shared/cache-ttl-display.d.ts.map +1 -1
- package/dist/shared/harness.d.ts +11 -0
- package/dist/shared/harness.d.ts.map +1 -1
- package/dist/shared/model-cache-ttl.d.ts +9 -0
- package/dist/shared/model-cache-ttl.d.ts.map +1 -0
- package/dist/shared/model-suggestion-retry.d.ts +1 -1
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-async-transport.d.ts +7 -7
- package/dist/shared/prompt-async-transport.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +1 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +21 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +12 -0
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/status-view.d.ts +1 -1
- package/dist/shared/status-view.d.ts.map +1 -1
- package/dist/shared/subagent-runner.d.ts +3 -1
- package/dist/shared/subagent-runner.d.ts.map +1 -1
- package/dist/shared/user-facing-codes.d.ts +5 -0
- package/dist/shared/user-facing-codes.d.ts.map +1 -1
- package/dist/{task-config-mgf5nsh3.js → task-config-2bm68628.js} +1 -1
- package/dist/tools/ctx-expand/render.d.ts.map +1 -1
- package/dist/transform-postprocess-phase-t7xgdhdf.js +36 -0
- package/dist/v2/hidden-completion.d.ts +2 -0
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +1 -0
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/lkg-system.d.ts +11 -0
- package/dist/v2/hooks/lkg-system.d.ts.map +1 -0
- package/dist/v2/hooks/session-project-backfill.d.ts +1 -1
- package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -1
- package/dist/v2/hooks/storage-gate.d.ts +11 -5
- package/dist/v2/hooks/storage-gate.d.ts.map +1 -1
- package/dist/v2/host-service.d.ts +2 -1
- package/dist/v2/host-service.d.ts.map +1 -1
- package/dist/v2/server.js +95347 -93236
- package/dist/v2/store-reader.d.ts +2 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/wrapup-orchestrator-h8tr10c6.js +19 -0
- package/package.json +1 -1
- package/src/agents/dreamer.ts +4 -6
- package/src/agents/hidden-agent-registrations.ts +3 -3
- package/src/agents/magic-context-prompt.test.ts +125 -0
- package/src/agents/magic-context-prompt.ts +12 -7
- package/src/agents/permissions.test.ts +3 -4
- package/src/config/latch-permanence-guard.test.ts +9 -0
- package/src/config/live-child-output-cap.test.ts +101 -0
- package/src/config/live-child-output-cap.ts +74 -0
- package/src/config/live-run-config.test.ts +24 -11
- package/src/config/schema/magic-context.test.ts +16 -1
- package/src/config/schema/magic-context.ts +27 -10
- package/src/features/magic-context/dreamer/task-registry.ts +13 -0
- package/src/shared/cache-ttl-display.ts +14 -20
- package/src/shared/harness.ts +14 -0
- package/src/shared/logger.test.ts +1 -0
- package/src/shared/model-cache-ttl.test.ts +40 -0
- package/src/shared/model-cache-ttl.ts +58 -0
- package/src/shared/model-suggestion-retry.ts +9 -2
- package/src/shared/prompt-async-transport.test.ts +254 -1
- package/src/shared/prompt-async-transport.ts +153 -44
- package/src/shared/prompt-surface-a1-golden.md +13 -13
- package/src/shared/rpc-async-probes.test.ts +230 -0
- package/src/shared/rpc-server.ts +6 -4
- package/src/shared/rpc-types.ts +1 -1
- package/src/shared/rpc-utils.test.ts +27 -1
- package/src/shared/rpc-utils.ts +161 -9
- package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +94 -0
- package/src/shared/sqlite-pass-scope.test.ts +182 -0
- package/src/shared/sqlite-transaction-mode-guard.test.ts +42 -10
- package/src/shared/sqlite-write-locker-test-support.ts +1 -0
- package/src/shared/sqlite.ts +142 -56
- package/src/shared/status-view.ts +1 -1
- package/src/shared/storage-busy.test.ts +95 -0
- package/src/shared/subagent-runner.ts +4 -0
- package/src/shared/user-facing-codes.ts +6 -0
- package/src/shared/windows-hidden-spawns.test.ts +132 -0
- package/src/shared/write-transaction-attribution.test.ts +1 -0
- package/dist/wrapup-orchestrator-9srk0kjf.js +0 -18
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import type { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
__resetSchemaFenceStateForTests,
|
|
8
|
+
getMigrationOnOpenRefusal,
|
|
9
|
+
openDatabaseAsync,
|
|
10
|
+
} from "../features/magic-context/storage-db";
|
|
11
|
+
import { createV2StorageGate, probeV2StorageAtBoot } from "../v2/hooks/storage-gate";
|
|
12
|
+
import {
|
|
13
|
+
__resetRpcIdentityTestHooks,
|
|
14
|
+
__setAsyncProcessProbeForTests,
|
|
15
|
+
__setRpcIdentityTestHooks,
|
|
16
|
+
inspectProcessesAsync,
|
|
17
|
+
} from "./rpc-utils";
|
|
18
|
+
import { Database } from "./sqlite";
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
__resetSchemaFenceStateForTests();
|
|
22
|
+
__resetRpcIdentityTestHooks();
|
|
23
|
+
__setAsyncProcessProbeForTests();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("async Windows inspection bypasses slow synchronous probes and shares a bounded snapshot", async () => {
|
|
27
|
+
let synchronousCalls = 0;
|
|
28
|
+
const slow = (() => {
|
|
29
|
+
synchronousCalls++;
|
|
30
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
|
|
31
|
+
return "";
|
|
32
|
+
}) as typeof execFileSync;
|
|
33
|
+
__setRpcIdentityTestHooks({
|
|
34
|
+
platform: "win32",
|
|
35
|
+
execFileSync: slow,
|
|
36
|
+
processListExecFileSync: slow,
|
|
37
|
+
});
|
|
38
|
+
let release!: (value: string) => void;
|
|
39
|
+
let calls = 0;
|
|
40
|
+
__setAsyncProcessProbeForTests(async (file, _args, timeout) => {
|
|
41
|
+
expect(file).toBe("powershell");
|
|
42
|
+
expect(timeout).toBe(5000);
|
|
43
|
+
calls++;
|
|
44
|
+
return new Promise<string>((resolve) => {
|
|
45
|
+
release = resolve;
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
const first = inspectProcessesAsync();
|
|
49
|
+
const second = inspectProcessesAsync();
|
|
50
|
+
expect(second).toBe(first);
|
|
51
|
+
const heartbeat = await Promise.race([
|
|
52
|
+
first.then(() => "probe"),
|
|
53
|
+
new Promise<string>((resolve) => setTimeout(() => resolve("heartbeat"), 10)),
|
|
54
|
+
]);
|
|
55
|
+
expect(heartbeat).toBe("heartbeat");
|
|
56
|
+
release(
|
|
57
|
+
JSON.stringify([
|
|
58
|
+
{
|
|
59
|
+
ProcessId: 12345,
|
|
60
|
+
ParentProcessId: 1,
|
|
61
|
+
CommandLine: "opencode serve",
|
|
62
|
+
CreationDate: "2026-09-28T00:00:00Z",
|
|
63
|
+
},
|
|
64
|
+
]),
|
|
65
|
+
);
|
|
66
|
+
const result = await first;
|
|
67
|
+
expect(result.liveness(12345)).toBe("alive");
|
|
68
|
+
expect(result.evidence(12345).commandLine).toBe("opencode serve");
|
|
69
|
+
expect(result.liveness(12346)).toBe("dead");
|
|
70
|
+
expect(await inspectProcessesAsync()).toBe(result);
|
|
71
|
+
expect(calls).toBe(1);
|
|
72
|
+
expect(synchronousCalls).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("offline process inspection can bypass a cached snapshot", async () => {
|
|
76
|
+
__setRpcIdentityTestHooks({ platform: "win32" });
|
|
77
|
+
let calls = 0;
|
|
78
|
+
__setAsyncProcessProbeForTests(async () => {
|
|
79
|
+
calls++;
|
|
80
|
+
return JSON.stringify([
|
|
81
|
+
{
|
|
82
|
+
ProcessId: calls + 100,
|
|
83
|
+
ParentProcessId: 1,
|
|
84
|
+
Name: "opencode.exe",
|
|
85
|
+
CommandLine: null,
|
|
86
|
+
CreationDate: "2026-09-28T00:00:00Z",
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
const cached = await inspectProcessesAsync();
|
|
91
|
+
expect((await inspectProcessesAsync()).processSnapshot).toBe(cached.processSnapshot);
|
|
92
|
+
const fresh = await inspectProcessesAsync(true);
|
|
93
|
+
expect(fresh.processSnapshot?.source).toBe("cim");
|
|
94
|
+
expect(fresh.processSnapshot?.facts).toContainEqual(
|
|
95
|
+
expect.objectContaining({ pid: 102, imageName: "opencode.exe", commandLine: null }),
|
|
96
|
+
);
|
|
97
|
+
expect(calls).toBe(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("async Windows inspection bounds CIM and tasklist fallback and caches failure", async () => {
|
|
101
|
+
__setRpcIdentityTestHooks({ platform: "win32" });
|
|
102
|
+
const calls: Array<[string, number]> = [];
|
|
103
|
+
__setAsyncProcessProbeForTests(async (file, _args, timeout) => {
|
|
104
|
+
calls.push([file, timeout]);
|
|
105
|
+
throw new Error("probe timed out");
|
|
106
|
+
});
|
|
107
|
+
const result = await inspectProcessesAsync();
|
|
108
|
+
expect(result.pi.state).toBe("unreadable");
|
|
109
|
+
expect(result.liveness(123)).toBe("inconclusive");
|
|
110
|
+
expect(result.evidence(123)).toEqual({ startTime: null, commandLine: null });
|
|
111
|
+
await inspectProcessesAsync();
|
|
112
|
+
expect(calls).toEqual([
|
|
113
|
+
["powershell", 5000],
|
|
114
|
+
["tasklist", 1000],
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("boot storage wait stays responsive with a locked v90 store and slow Windows probes", async () => {
|
|
119
|
+
const root = mkdtempSync(join(tmpdir(), "async-storage-guard-"));
|
|
120
|
+
const dbPath = join(root, "context.db");
|
|
121
|
+
const seeded = new Database(dbPath);
|
|
122
|
+
seeded.exec(
|
|
123
|
+
"CREATE TABLE schema_migrations(version INTEGER PRIMARY KEY); INSERT INTO schema_migrations VALUES(90)",
|
|
124
|
+
);
|
|
125
|
+
seeded.exec("BEGIN IMMEDIATE");
|
|
126
|
+
mkdirSync(join(root, "rpc", "blocker"), { recursive: true });
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(root, "rpc", "blocker", "port-12345.json"),
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
pid: 12345,
|
|
131
|
+
port: 43123,
|
|
132
|
+
started_at: Date.now(),
|
|
133
|
+
kind: "OpenCode server",
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
let syncCalls = 0;
|
|
137
|
+
const slow = (() => {
|
|
138
|
+
syncCalls++;
|
|
139
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
|
|
140
|
+
return "";
|
|
141
|
+
}) as typeof execFileSync;
|
|
142
|
+
__setRpcIdentityTestHooks({
|
|
143
|
+
platform: "win32",
|
|
144
|
+
execFileSync: slow,
|
|
145
|
+
processListExecFileSync: slow,
|
|
146
|
+
});
|
|
147
|
+
let release!: (value: string) => void;
|
|
148
|
+
__setAsyncProcessProbeForTests(
|
|
149
|
+
() =>
|
|
150
|
+
new Promise<string>((resolve) => {
|
|
151
|
+
release = resolve;
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
try {
|
|
155
|
+
const gate = createV2StorageGate({
|
|
156
|
+
open: () => openDatabaseAsync({ dbPath, busyTimeoutMs: 0 }),
|
|
157
|
+
});
|
|
158
|
+
const started = performance.now();
|
|
159
|
+
const boot = probeV2StorageAtBoot(gate);
|
|
160
|
+
const opening = gate.probe();
|
|
161
|
+
expect(
|
|
162
|
+
await Promise.race([
|
|
163
|
+
opening.then(() => "opened"),
|
|
164
|
+
new Promise<string>((resolve) => setTimeout(() => resolve("responsive"), 10)),
|
|
165
|
+
]),
|
|
166
|
+
).toBe("responsive");
|
|
167
|
+
expect(performance.now() - started).toBeLessThan(300);
|
|
168
|
+
expect(gate.current()).toBeUndefined();
|
|
169
|
+
release(
|
|
170
|
+
JSON.stringify([
|
|
171
|
+
{
|
|
172
|
+
ProcessId: 12345,
|
|
173
|
+
ParentProcessId: 1,
|
|
174
|
+
CommandLine: "opencode serve",
|
|
175
|
+
CreationDate: "2026-01-01T00:00:00Z",
|
|
176
|
+
},
|
|
177
|
+
]),
|
|
178
|
+
);
|
|
179
|
+
expect(await opening).toBeUndefined();
|
|
180
|
+
expect(await boot).toBeUndefined();
|
|
181
|
+
expect(gate.reason()?.kind).toBe("migration_guard");
|
|
182
|
+
expect(getMigrationOnOpenRefusal()?.serverPids).toEqual([12345]);
|
|
183
|
+
expect(syncCalls).toBe(0);
|
|
184
|
+
const checked = new Database(dbPath);
|
|
185
|
+
expect(
|
|
186
|
+
checked.prepare("SELECT MAX(version) AS version FROM schema_migrations").get(),
|
|
187
|
+
).toEqual({ version: 90 });
|
|
188
|
+
checked.close();
|
|
189
|
+
} finally {
|
|
190
|
+
seeded.close();
|
|
191
|
+
rmSync(root, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("a timed-out refresh retains confirmed blockers until a successful process scan", async () => {
|
|
196
|
+
let now = 1000;
|
|
197
|
+
__setRpcIdentityTestHooks({ platform: "win32", nowMs: () => now });
|
|
198
|
+
let fail = false;
|
|
199
|
+
let calls = 0;
|
|
200
|
+
let pid = 12345;
|
|
201
|
+
__setAsyncProcessProbeForTests(async () => {
|
|
202
|
+
calls++;
|
|
203
|
+
if (fail) throw new Error("timeout");
|
|
204
|
+
return JSON.stringify([
|
|
205
|
+
{
|
|
206
|
+
ProcessId: pid,
|
|
207
|
+
ParentProcessId: 1,
|
|
208
|
+
CommandLine: "opencode serve",
|
|
209
|
+
CreationDate: "2026-01-01T00:00:00Z",
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
const first = await inspectProcessesAsync();
|
|
214
|
+
now += 1999;
|
|
215
|
+
expect(await inspectProcessesAsync()).toBe(first);
|
|
216
|
+
expect(calls).toBe(1);
|
|
217
|
+
now++;
|
|
218
|
+
fail = true;
|
|
219
|
+
const failed = await inspectProcessesAsync();
|
|
220
|
+
expect(calls).toBe(3);
|
|
221
|
+
expect(failed.liveness(12345)).toBe("alive");
|
|
222
|
+
expect(failed.liveness(54321)).toBe("inconclusive");
|
|
223
|
+
expect(failed.evidence(12345)).toEqual(first.evidence(12345));
|
|
224
|
+
now += 2000;
|
|
225
|
+
fail = false;
|
|
226
|
+
pid = 54321;
|
|
227
|
+
const refreshed = await inspectProcessesAsync();
|
|
228
|
+
expect(calls).toBe(4);
|
|
229
|
+
expect(refreshed.liveness(12345)).toBe("dead");
|
|
230
|
+
});
|
package/src/shared/rpc-server.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
registerNotificationSink,
|
|
20
20
|
} from "./rpc-notifications";
|
|
21
21
|
import {
|
|
22
|
-
|
|
22
|
+
inspectProcessesAsync,
|
|
23
23
|
parseRpcPortFile,
|
|
24
24
|
registerOwnRpcServerInstance,
|
|
25
25
|
rpcPortDir,
|
|
@@ -176,7 +176,7 @@ export class MagicContextRpcServer {
|
|
|
176
176
|
// supported: TUI discovery scans all live files and picks the most
|
|
177
177
|
// recent instead of cross-wiring via one shared project file.
|
|
178
178
|
try {
|
|
179
|
-
this.warnIfOtherLiveInstance();
|
|
179
|
+
void this.warnIfOtherLiveInstance();
|
|
180
180
|
const dir = dirname(this.portFilePath);
|
|
181
181
|
// The port file carries the RPC bearer token. The normal policy keeps
|
|
182
182
|
// it owner-only; a trusted-group deployment explicitly delegates every
|
|
@@ -263,12 +263,14 @@ export class MagicContextRpcServer {
|
|
|
263
263
|
this.unregisterOwnInstance = null;
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
private warnIfOtherLiveInstance(): void {
|
|
266
|
+
private async warnIfOtherLiveInstance(): Promise<void> {
|
|
267
267
|
try {
|
|
268
268
|
for (const entry of readdirSync(this.portDir)) {
|
|
269
269
|
if (!entry.startsWith("port-") || !entry.endsWith(".json")) continue;
|
|
270
270
|
const record = parseRpcPortFile(readFileSync(`${this.portDir}/${entry}`, "utf-8"));
|
|
271
|
-
if (!record || record.pid === process.pid
|
|
271
|
+
if (!record || record.pid === process.pid) continue;
|
|
272
|
+
const processes = await inspectProcessesAsync();
|
|
273
|
+
if (processes.liveness(record.pid) !== "alive") continue;
|
|
272
274
|
log(
|
|
273
275
|
`[rpc] another Magic Context RPC server is active for this project (pid ${record.pid}, port ${record.port}); starting separate instance on a new port`,
|
|
274
276
|
);
|
package/src/shared/rpc-types.ts
CHANGED
|
@@ -280,7 +280,7 @@ export interface StatusDetail extends SidebarSnapshot {
|
|
|
280
280
|
cacheExpired: boolean;
|
|
281
281
|
/** Reports whether the displayed TTL came from config, persisted session metadata,
|
|
282
282
|
* or the default; cache scheduling still uses the TTL stored in session metadata. */
|
|
283
|
-
cacheTtlSource?: "
|
|
283
|
+
cacheTtlSource?: import("./cache-ttl-display").CacheTtlDisplaySource;
|
|
284
284
|
cacheTtlModelKey?: string;
|
|
285
285
|
configParseFailures?: ConfigParseFailure[];
|
|
286
286
|
/** True when cacheTtl is "never" — the idle-TTL heuristic is disabled on
|
|
@@ -183,6 +183,32 @@ describe("discoverLivePiProcessIds", () => {
|
|
|
183
183
|
});
|
|
184
184
|
|
|
185
185
|
describe("#411 Windows OMP/Pi live-process scan", () => {
|
|
186
|
+
test("reuses a Windows snapshot briefly, then refreshes process discovery", () => {
|
|
187
|
+
let now = PLUGIN_BUILD_MS;
|
|
188
|
+
const calls: string[] = [];
|
|
189
|
+
__setRpcIdentityTestHooks({
|
|
190
|
+
platform: "win32",
|
|
191
|
+
nowMs: () => now,
|
|
192
|
+
processListExecFileSync: windowsProcessListExec((file) => {
|
|
193
|
+
calls.push(file);
|
|
194
|
+
return cimOutput([
|
|
195
|
+
{
|
|
196
|
+
ProcessId: WINDOWS_FOREIGN_PID,
|
|
197
|
+
ParentProcessId: 4,
|
|
198
|
+
CommandLine: OMP_PI_ARC_COMMAND,
|
|
199
|
+
CreationDate: wmiCreationDate(OLDER_THAN_PLUGIN_MS),
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
expect(inspectLivePiProcesses().processIds).toEqual([WINDOWS_FOREIGN_PID]);
|
|
205
|
+
now += 1_999;
|
|
206
|
+
expect(inspectLivePiProcesses().processIds).toEqual([WINDOWS_FOREIGN_PID]);
|
|
207
|
+
expect(calls).toEqual(["powershell"]);
|
|
208
|
+
now += 1;
|
|
209
|
+
inspectLivePiProcesses();
|
|
210
|
+
expect(calls).toEqual(["powershell", "powershell"]);
|
|
211
|
+
});
|
|
186
212
|
test("(a) tasklist-only fallback skips a parent omp.exe as an ancestor", () => {
|
|
187
213
|
const calls: string[] = [];
|
|
188
214
|
__setRpcIdentityTestHooks({
|
|
@@ -607,7 +633,7 @@ describe("isPidIdentityPlausible", () => {
|
|
|
607
633
|
args: [
|
|
608
634
|
"-NoProfile",
|
|
609
635
|
"-Command",
|
|
610
|
-
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress",
|
|
636
|
+
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine,CreationDate | ConvertTo-Json -Compress",
|
|
611
637
|
],
|
|
612
638
|
},
|
|
613
639
|
]);
|
package/src/shared/rpc-utils.ts
CHANGED
|
@@ -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:
|
|
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(
|
|
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(
|
|
707
|
-
|
|
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
|
|
777
|
-
const
|
|
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
|
+
}
|