@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/logger.ts
CHANGED
|
@@ -9,6 +9,12 @@ let buffer: string[] = [];
|
|
|
9
9
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
10
10
|
const FLUSH_INTERVAL_MS = 500;
|
|
11
11
|
const BUFFER_SIZE_LIMIT = 50;
|
|
12
|
+
const MAX_LOG_FILE_BYTES = 32 * 1024 * 1024;
|
|
13
|
+
const SIZE_CHECK_INTERVAL_FLUSHES = 64;
|
|
14
|
+
|
|
15
|
+
let activeLogFile: string | null = null;
|
|
16
|
+
let activeLogSize: number | null = null;
|
|
17
|
+
let flushesSinceSizeCheck = 0;
|
|
12
18
|
|
|
13
19
|
export interface LoggerDiagnostics {
|
|
14
20
|
swallowedWriteCount: number;
|
|
@@ -36,19 +42,126 @@ function ensureDir(filePath: string): void {
|
|
|
36
42
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
function isMissingFile(error: unknown): boolean {
|
|
46
|
+
return (
|
|
47
|
+
typeof error === "object" &&
|
|
48
|
+
error !== null &&
|
|
49
|
+
"code" in error &&
|
|
50
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getCurrentLogSize(logFile: string): number {
|
|
55
|
+
if (
|
|
56
|
+
activeLogFile === logFile &&
|
|
57
|
+
activeLogSize !== null &&
|
|
58
|
+
flushesSinceSizeCheck < SIZE_CHECK_INTERVAL_FLUSHES
|
|
59
|
+
) {
|
|
60
|
+
return activeLogSize;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const stat = fs.statSync(logFile);
|
|
65
|
+
if (!stat.isFile()) {
|
|
66
|
+
throw new Error(`Magic Context log path is not a regular file: ${logFile}`);
|
|
67
|
+
}
|
|
68
|
+
// Logs can contain diagnostic context, so tighten files created by older
|
|
69
|
+
// versions before copying or appending any new content.
|
|
70
|
+
fs.chmodSync(logFile, 0o600);
|
|
71
|
+
activeLogFile = logFile;
|
|
72
|
+
activeLogSize = stat.size;
|
|
73
|
+
flushesSinceSizeCheck = 0;
|
|
74
|
+
return stat.size;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!isMissingFile(error)) throw error;
|
|
77
|
+
activeLogFile = logFile;
|
|
78
|
+
activeLogSize = 0;
|
|
79
|
+
flushesSinceSizeCheck = 0;
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function capLogData(data: string): string {
|
|
85
|
+
if (Buffer.byteLength(data) <= MAX_LOG_FILE_BYTES) return data;
|
|
86
|
+
|
|
87
|
+
// A single diagnostic payload must not defeat the on-disk bound. Cutting a
|
|
88
|
+
// Buffer can split UTF-8, so trim any replacement character expansion too.
|
|
89
|
+
let bounded = Buffer.from(data).subarray(0, MAX_LOG_FILE_BYTES).toString("utf8");
|
|
90
|
+
while (Buffer.byteLength(bounded) > MAX_LOG_FILE_BYTES) {
|
|
91
|
+
bounded = bounded.slice(0, -1);
|
|
92
|
+
}
|
|
93
|
+
return bounded;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeBoundedPredecessor(logFile: string, predecessorPath: string, size: number): void {
|
|
97
|
+
const predecessorFd = fs.openSync(predecessorPath, "w", 0o600);
|
|
98
|
+
try {
|
|
99
|
+
// `openSync(..., "w")` preserves an existing file's mode, so set it
|
|
100
|
+
// before any copied diagnostic text reaches the predecessor.
|
|
101
|
+
fs.fchmodSync(predecessorFd, 0o600);
|
|
102
|
+
const bytesToCopy = Math.min(size, MAX_LOG_FILE_BYTES);
|
|
103
|
+
const sourceFd = fs.openSync(logFile, "r");
|
|
104
|
+
try {
|
|
105
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, bytesToCopy));
|
|
106
|
+
let remaining = bytesToCopy;
|
|
107
|
+
let position = Math.max(0, size - bytesToCopy);
|
|
108
|
+
while (remaining > 0) {
|
|
109
|
+
const bytesRead = fs.readSync(
|
|
110
|
+
sourceFd,
|
|
111
|
+
chunk,
|
|
112
|
+
0,
|
|
113
|
+
Math.min(chunk.length, remaining),
|
|
114
|
+
position,
|
|
115
|
+
);
|
|
116
|
+
if (bytesRead === 0) break;
|
|
117
|
+
fs.writeSync(predecessorFd, chunk, 0, bytesRead);
|
|
118
|
+
remaining -= bytesRead;
|
|
119
|
+
position += bytesRead;
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
fs.closeSync(sourceFd);
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
fs.closeSync(predecessorFd);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function rotateLogFile(logFile: string, size: number): void {
|
|
130
|
+
const predecessorPath = `${logFile}.1`;
|
|
131
|
+
writeBoundedPredecessor(logFile, predecessorPath, size);
|
|
132
|
+
// Keep the current inode in place so diagnostic readers can observe a
|
|
133
|
+
// shorter log during rotation instead of seeing the path disappear.
|
|
134
|
+
fs.truncateSync(logFile, 0);
|
|
135
|
+
activeLogSize = 0;
|
|
136
|
+
flushesSinceSizeCheck = 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
39
139
|
function flush(): void {
|
|
40
140
|
if (flushTimer) {
|
|
41
141
|
clearTimeout(flushTimer);
|
|
42
142
|
flushTimer = null;
|
|
43
143
|
}
|
|
44
144
|
if (buffer.length === 0) return;
|
|
45
|
-
const
|
|
145
|
+
const bufferedData = buffer.join("");
|
|
46
146
|
buffer = [];
|
|
47
147
|
try {
|
|
148
|
+
const data = capLogData(bufferedData);
|
|
48
149
|
const logFile = getMagicContextLogPath();
|
|
49
150
|
ensureDir(logFile);
|
|
50
|
-
|
|
151
|
+
let currentSize = getCurrentLogSize(logFile);
|
|
152
|
+
const dataSize = Buffer.byteLength(data);
|
|
153
|
+
if (currentSize > 0 && currentSize + dataSize > MAX_LOG_FILE_BYTES) {
|
|
154
|
+
rotateLogFile(logFile, currentSize);
|
|
155
|
+
currentSize = 0;
|
|
156
|
+
}
|
|
157
|
+
fs.appendFileSync(logFile, data, { encoding: "utf8", mode: 0o600 });
|
|
158
|
+
activeLogFile = logFile;
|
|
159
|
+
activeLogSize = currentSize + dataSize;
|
|
160
|
+
flushesSinceSizeCheck++;
|
|
51
161
|
} catch (error) {
|
|
162
|
+
activeLogFile = null;
|
|
163
|
+
activeLogSize = null;
|
|
164
|
+
flushesSinceSizeCheck = 0;
|
|
52
165
|
recordSwallowedWrite(error);
|
|
53
166
|
}
|
|
54
167
|
}
|
|
@@ -9,23 +9,26 @@ import {
|
|
|
9
9
|
} from "./model-resolution";
|
|
10
10
|
|
|
11
11
|
describe("model-resolution", () => {
|
|
12
|
-
test("
|
|
13
|
-
|
|
14
|
-
temperature:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
test("keeps historian temperature opt-in while preserving explicit overrides", () => {
|
|
13
|
+
const fixtures = [
|
|
14
|
+
{ temperature: undefined, expected: undefined },
|
|
15
|
+
{ temperature: 0.1, expected: 0.1 },
|
|
16
|
+
{ temperature: 0, expected: 0 },
|
|
17
|
+
] as const;
|
|
18
|
+
for (const fixture of fixtures) {
|
|
19
|
+
const resolved = resolveHistorianAgentOverrides({
|
|
20
|
+
...(fixture.temperature !== undefined ? { temperature: fixture.temperature } : {}),
|
|
20
21
|
maxTokens: 16_000,
|
|
21
22
|
opencode: { model: "google/flash", variant: "low" },
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
pi: { model: "pi/must-not-leak", thinking_level: "high" },
|
|
24
|
+
});
|
|
25
|
+
expect(resolved).toEqual({
|
|
26
|
+
maxTokens: 16_000,
|
|
27
|
+
...(fixture.expected !== undefined ? { temperature: fixture.expected } : {}),
|
|
28
|
+
model: "google/flash",
|
|
29
|
+
variant: "low",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
test("normalizes string and object entries to the same model identity", () => {
|
|
@@ -56,7 +59,7 @@ describe("model-resolution", () => {
|
|
|
56
59
|
]);
|
|
57
60
|
});
|
|
58
61
|
|
|
59
|
-
test("
|
|
62
|
+
test("keeps the flash calibration available only as an explicit override", () => {
|
|
60
63
|
expect(
|
|
61
64
|
resolveHistorianAgentOverrides({
|
|
62
65
|
opencode: { model: { model: "google/flash", variant: "fast" } },
|
|
@@ -64,15 +67,14 @@ describe("model-resolution", () => {
|
|
|
64
67
|
two_pass: true,
|
|
65
68
|
}),
|
|
66
69
|
).toEqual({
|
|
67
|
-
temperature: 0.1,
|
|
68
70
|
maxTokens: 32_000,
|
|
69
71
|
two_pass: true,
|
|
70
72
|
model: "google/flash",
|
|
71
73
|
variant: "fast",
|
|
72
74
|
});
|
|
73
75
|
expect(
|
|
74
|
-
resolveHistorianAgentOverrides({ temperature: 0.
|
|
75
|
-
).toMatchObject({ temperature: 0.
|
|
76
|
+
resolveHistorianAgentOverrides({ temperature: 0.1, maxTokens: 12_000 }),
|
|
77
|
+
).toMatchObject({ temperature: 0.1, maxTokens: 12_000 });
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
test("does not inherit a block qualifier into unqualified fallbacks", () => {
|
|
@@ -228,7 +228,6 @@ export function resolveOpenCodeAgentOverrides(agent: unknown): Record<string, un
|
|
|
228
228
|
|
|
229
229
|
export function resolveHistorianAgentOverrides(agent: unknown): Record<string, unknown> {
|
|
230
230
|
return {
|
|
231
|
-
temperature: 0.1,
|
|
232
231
|
maxTokens: 32_000,
|
|
233
232
|
...resolveOpenCodeAgentOverrides(agent),
|
|
234
233
|
};
|
|
@@ -53,6 +53,45 @@ function tasklistOutput(entries: Array<[number, string]>): string {
|
|
|
53
53
|
].join("\r\n");
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const WINDOWS_LAUNCHER_PID = 42424;
|
|
57
|
+
const WINDOWS_FOREIGN_PID = 41001;
|
|
58
|
+
const PLUGIN_BUILD_MS = 1_700_000_000_000;
|
|
59
|
+
const OLDER_THAN_PLUGIN_MS = PLUGIN_BUILD_MS - 60_000;
|
|
60
|
+
const OMP_PI_ARC_COMMAND =
|
|
61
|
+
"C:\\Users\\qiiks\\.bun\\bin\\omp.exe C:\\Users\\qiiks\\AppData\\Roaming\\npm\\node_modules\\@oh-my-pi\\pi-coding-agent\\dist\\cli.js";
|
|
62
|
+
const BUN_PI_CHILD_COMMAND =
|
|
63
|
+
"C:\\Users\\qiiks\\.bun\\bin\\bun.exe C:\\Users\\qiiks\\AppData\\Roaming\\npm\\node_modules\\@oh-my-pi\\pi-coding-agent\\dist\\cli.js";
|
|
64
|
+
const BARE_OMP_COMMAND = "C:\\Users\\qiiks\\.bun\\bin\\omp.exe";
|
|
65
|
+
|
|
66
|
+
function padWmi(value: number, width = 2): string {
|
|
67
|
+
return String(value).padStart(width, "0");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Format an epoch millisecond timestamp as a WMI CreationDate string. */
|
|
71
|
+
function wmiCreationDate(ms: number, offsetMinutes = 0): string {
|
|
72
|
+
const shifted = new Date(ms + offsetMinutes * 60_000);
|
|
73
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
74
|
+
return `${shifted.getUTCFullYear()}${padWmi(shifted.getUTCMonth() + 1)}${padWmi(shifted.getUTCDate())}${padWmi(shifted.getUTCHours())}${padWmi(shifted.getUTCMinutes())}${padWmi(shifted.getUTCSeconds())}.000000${sign}${padWmi(Math.abs(offsetMinutes), 3)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cimOutput(
|
|
78
|
+
rows: Array<{
|
|
79
|
+
ProcessId: number;
|
|
80
|
+
ParentProcessId: number;
|
|
81
|
+
CommandLine: string | null;
|
|
82
|
+
CreationDate: string;
|
|
83
|
+
}>,
|
|
84
|
+
): string {
|
|
85
|
+
return JSON.stringify(rows.length === 1 ? rows[0] : rows);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function windowsProcessListExec(
|
|
89
|
+
handler: (file: string, args: readonly string[]) => string,
|
|
90
|
+
): typeof execFileSync {
|
|
91
|
+
return ((file: string | URL, args: readonly string[] = []) =>
|
|
92
|
+
handler(String(file), args)) as typeof execFileSync;
|
|
93
|
+
}
|
|
94
|
+
|
|
56
95
|
afterEach(() => {
|
|
57
96
|
__resetRpcIdentityTestHooks();
|
|
58
97
|
});
|
|
@@ -97,7 +136,12 @@ describe("discoverLivePiProcessIds", () => {
|
|
|
97
136
|
].join("\n")) as typeof execFileSync,
|
|
98
137
|
});
|
|
99
138
|
|
|
100
|
-
expect(
|
|
139
|
+
expect(inspectLivePiProcesses()).toEqual({
|
|
140
|
+
state: "known",
|
|
141
|
+
processIds: [41002, 41003],
|
|
142
|
+
inconclusivePids: [41001, 41007],
|
|
143
|
+
});
|
|
144
|
+
expect(discoverLivePiProcessIds()).toEqual([41002, 41003]);
|
|
101
145
|
});
|
|
102
146
|
|
|
103
147
|
test("reports uncertainty instead of treating an unavailable process list as empty", () => {
|
|
@@ -114,12 +158,13 @@ describe("discoverLivePiProcessIds", () => {
|
|
|
114
158
|
});
|
|
115
159
|
});
|
|
116
160
|
|
|
117
|
-
test("
|
|
161
|
+
test("tries PowerShell CIM before tasklist on Windows", () => {
|
|
118
162
|
const calls: string[] = [];
|
|
119
163
|
__setRpcIdentityTestHooks({
|
|
120
164
|
platform: "win32",
|
|
121
165
|
processListExecFileSync: ((file: string | URL) => {
|
|
122
166
|
calls.push(String(file));
|
|
167
|
+
if (String(file) === "powershell") throw new Error("powershell unavailable");
|
|
123
168
|
return tasklistOutput([
|
|
124
169
|
[process.pid, "pi.exe"],
|
|
125
170
|
[41001, "pi.exe"],
|
|
@@ -129,10 +174,197 @@ describe("discoverLivePiProcessIds", () => {
|
|
|
129
174
|
});
|
|
130
175
|
|
|
131
176
|
expect(inspectLivePiProcesses()).toEqual({
|
|
177
|
+
state: "inconclusive",
|
|
178
|
+
processIds: [],
|
|
179
|
+
inconclusivePids: [41001],
|
|
180
|
+
});
|
|
181
|
+
expect(calls).toEqual(["powershell", "tasklist"]);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("#411 Windows OMP/Pi live-process scan", () => {
|
|
186
|
+
test("(a) tasklist-only fallback skips a parent omp.exe as an ancestor", () => {
|
|
187
|
+
const calls: string[] = [];
|
|
188
|
+
__setRpcIdentityTestHooks({
|
|
189
|
+
platform: "win32",
|
|
190
|
+
processListExecFileSync: windowsProcessListExec((file) => {
|
|
191
|
+
calls.push(file);
|
|
192
|
+
if (file === "powershell") {
|
|
193
|
+
throw new Error("powershell unavailable");
|
|
194
|
+
}
|
|
195
|
+
return tasklistOutput([
|
|
196
|
+
[process.ppid, "omp.exe"],
|
|
197
|
+
[WINDOWS_FOREIGN_PID, "notepad.exe"],
|
|
198
|
+
]);
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const discovery = inspectLivePiProcesses();
|
|
203
|
+
expect(discovery.processIds).not.toContain(process.ppid);
|
|
204
|
+
expect(discovery.skippedAncestorPids).toContain(process.ppid);
|
|
205
|
+
expect(calls[0]).toBe("powershell");
|
|
206
|
+
expect(calls).toContain("tasklist");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("(a) CIM parent omp.exe with a Pi arc is skipped as an ancestor", () => {
|
|
210
|
+
__setRpcIdentityTestHooks({
|
|
211
|
+
platform: "win32",
|
|
212
|
+
nowMs: () => PLUGIN_BUILD_MS,
|
|
213
|
+
processListExecFileSync: windowsProcessListExec((file) => {
|
|
214
|
+
if (file !== "powershell") {
|
|
215
|
+
throw new Error(`${file} must not run when CIM succeeds`);
|
|
216
|
+
}
|
|
217
|
+
return cimOutput([
|
|
218
|
+
{
|
|
219
|
+
ProcessId: process.pid,
|
|
220
|
+
ParentProcessId: WINDOWS_LAUNCHER_PID,
|
|
221
|
+
CommandLine: BUN_PI_CHILD_COMMAND,
|
|
222
|
+
CreationDate: wmiCreationDate(PLUGIN_BUILD_MS),
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
ProcessId: WINDOWS_LAUNCHER_PID,
|
|
226
|
+
ParentProcessId: 4,
|
|
227
|
+
CommandLine: OMP_PI_ARC_COMMAND,
|
|
228
|
+
CreationDate: wmiCreationDate(OLDER_THAN_PLUGIN_MS),
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const discovery = inspectLivePiProcesses();
|
|
235
|
+
expect(discovery.processIds).not.toContain(WINDOWS_LAUNCHER_PID);
|
|
236
|
+
expect(discovery.processIds).not.toContain(process.pid);
|
|
237
|
+
expect(discovery.skippedAncestorPids).toContain(WINDOWS_LAUNCHER_PID);
|
|
238
|
+
expect(discovery.state).toBe("known");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("(b) CIM unrelated omp.exe with a Pi arc and older CreationDate is a verified blocker", () => {
|
|
242
|
+
__setRpcIdentityTestHooks({
|
|
243
|
+
platform: "win32",
|
|
244
|
+
nowMs: () => PLUGIN_BUILD_MS,
|
|
245
|
+
processListExecFileSync: windowsProcessListExec((file) => {
|
|
246
|
+
if (file !== "powershell") {
|
|
247
|
+
throw new Error(`${file} must not run when CIM succeeds`);
|
|
248
|
+
}
|
|
249
|
+
return cimOutput([
|
|
250
|
+
{
|
|
251
|
+
ProcessId: process.pid,
|
|
252
|
+
ParentProcessId: process.ppid,
|
|
253
|
+
CommandLine: BUN_PI_CHILD_COMMAND,
|
|
254
|
+
CreationDate: wmiCreationDate(PLUGIN_BUILD_MS),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
ProcessId: WINDOWS_FOREIGN_PID,
|
|
258
|
+
ParentProcessId: 8,
|
|
259
|
+
CommandLine: OMP_PI_ARC_COMMAND,
|
|
260
|
+
CreationDate: wmiCreationDate(OLDER_THAN_PLUGIN_MS, 60),
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const discovery = inspectLivePiProcesses();
|
|
267
|
+
expect(discovery.state).toBe("known");
|
|
268
|
+
expect(discovery.processIds).toEqual([WINDOWS_FOREIGN_PID]);
|
|
269
|
+
expect(readProcessProbeEvidence(WINDOWS_FOREIGN_PID)).toEqual({
|
|
270
|
+
startTime: OLDER_THAN_PLUGIN_MS,
|
|
271
|
+
commandLine: OMP_PI_ARC_COMMAND,
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("(c) CIM omp.exe with no Pi arc is inconclusive", () => {
|
|
276
|
+
__setRpcIdentityTestHooks({
|
|
277
|
+
platform: "win32",
|
|
278
|
+
processListExecFileSync: windowsProcessListExec((file) => {
|
|
279
|
+
if (file !== "powershell") {
|
|
280
|
+
throw new Error(`${file} must not run when CIM succeeds`);
|
|
281
|
+
}
|
|
282
|
+
return cimOutput([
|
|
283
|
+
{
|
|
284
|
+
ProcessId: WINDOWS_FOREIGN_PID,
|
|
285
|
+
ParentProcessId: 8,
|
|
286
|
+
CommandLine: BARE_OMP_COMMAND,
|
|
287
|
+
CreationDate: wmiCreationDate(OLDER_THAN_PLUGIN_MS),
|
|
288
|
+
},
|
|
289
|
+
]);
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const discovery = inspectLivePiProcesses();
|
|
294
|
+
expect(discovery.state).toBe("inconclusive");
|
|
295
|
+
expect(discovery.processIds).toEqual([]);
|
|
296
|
+
expect(discovery.inconclusivePids).toEqual([WINDOWS_FOREIGN_PID]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("(d) PowerShell unavailable falls back to tasklist as inconclusive", () => {
|
|
300
|
+
const calls: string[] = [];
|
|
301
|
+
__setRpcIdentityTestHooks({
|
|
302
|
+
platform: "win32",
|
|
303
|
+
processListExecFileSync: windowsProcessListExec((file) => {
|
|
304
|
+
calls.push(file);
|
|
305
|
+
if (file === "powershell") {
|
|
306
|
+
throw new Error("powershell unavailable");
|
|
307
|
+
}
|
|
308
|
+
return tasklistOutput([
|
|
309
|
+
[WINDOWS_FOREIGN_PID, "omp.exe"],
|
|
310
|
+
[41002, "opencode.exe"],
|
|
311
|
+
]);
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const discovery = inspectLivePiProcesses();
|
|
316
|
+
expect(discovery.state).toBe("inconclusive");
|
|
317
|
+
expect(discovery.processIds).toEqual([]);
|
|
318
|
+
expect(discovery.inconclusivePids).toEqual([WINDOWS_FOREIGN_PID]);
|
|
319
|
+
expect(calls).toEqual(["powershell", "tasklist"]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("(e) POSIX ancestor exclusion skips the parent Pi harness", () => {
|
|
323
|
+
__setRpcIdentityTestHooks({
|
|
324
|
+
platform: "darwin",
|
|
325
|
+
processListExecFileSync: windowsProcessListExec((file, args) => {
|
|
326
|
+
if (file === "ps" && args.includes("ppid=")) {
|
|
327
|
+
const pidIndex = args.indexOf("-p");
|
|
328
|
+
const pid = Number(args[pidIndex + 1]);
|
|
329
|
+
if (pid === process.pid) return ` ${process.ppid}\n`;
|
|
330
|
+
if (pid === process.ppid) return " 1\n";
|
|
331
|
+
return " 1\n";
|
|
332
|
+
}
|
|
333
|
+
return [
|
|
334
|
+
` ${process.pid} bun /opt/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js`,
|
|
335
|
+
` ${process.ppid} bun /opt/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js`,
|
|
336
|
+
" 41001 bun /opt/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js",
|
|
337
|
+
].join("\n");
|
|
338
|
+
}),
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const discovery = inspectLivePiProcesses();
|
|
342
|
+
expect(discovery.processIds).toEqual([41001]);
|
|
343
|
+
expect(discovery.skippedAncestorPids).toContain(process.ppid);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("(f) CIM single-object JSON shape parses", () => {
|
|
347
|
+
__setRpcIdentityTestHooks({
|
|
348
|
+
platform: "win32",
|
|
349
|
+
processListExecFileSync: windowsProcessListExec((file) => {
|
|
350
|
+
if (file !== "powershell") {
|
|
351
|
+
throw new Error(`${file} must not run when CIM succeeds`);
|
|
352
|
+
}
|
|
353
|
+
return cimOutput([
|
|
354
|
+
{
|
|
355
|
+
ProcessId: WINDOWS_FOREIGN_PID,
|
|
356
|
+
ParentProcessId: 8,
|
|
357
|
+
CommandLine: OMP_PI_ARC_COMMAND,
|
|
358
|
+
CreationDate: wmiCreationDate(OLDER_THAN_PLUGIN_MS),
|
|
359
|
+
},
|
|
360
|
+
]);
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(inspectLivePiProcesses()).toMatchObject({
|
|
132
365
|
state: "known",
|
|
133
|
-
processIds: [
|
|
366
|
+
processIds: [WINDOWS_FOREIGN_PID],
|
|
134
367
|
});
|
|
135
|
-
expect(calls).toEqual(["tasklist"]);
|
|
136
368
|
});
|
|
137
369
|
});
|
|
138
370
|
|
|
@@ -369,6 +601,15 @@ describe("isPidIdentityPlausible", () => {
|
|
|
369
601
|
|
|
370
602
|
calls.length = 0;
|
|
371
603
|
expect(isPidIdentityPlausible(record(NOW_MS))).toBe("inconclusive");
|
|
372
|
-
expect(calls).toEqual([
|
|
604
|
+
expect(calls).toEqual([
|
|
605
|
+
{
|
|
606
|
+
file: "powershell",
|
|
607
|
+
args: [
|
|
608
|
+
"-NoProfile",
|
|
609
|
+
"-Command",
|
|
610
|
+
"Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress",
|
|
611
|
+
],
|
|
612
|
+
},
|
|
613
|
+
]);
|
|
373
614
|
});
|
|
374
615
|
});
|