@cortexkit/opencode-magic-context 0.36.1 → 0.38.0
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 +2 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +2 -1
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +10 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-mt0ttc4x.js → conflict-warning-hook-an4fxhwf.js} +3 -3
- package/dist/{derive-budgets-88mxk2w1.js → derive-budgets-da4yg3tz.js} +3 -2
- package/dist/features/magic-context/context-authority.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify-prompt.d.ts +5 -1
- package/dist/features/magic-context/dreamer/classify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/manifest-parser.d.ts +8 -0
- package/dist/features/magic-context/dreamer/manifest-parser.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +7 -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 +18 -0
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +5 -0
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +6 -1
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts +3 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +6 -0
- package/dist/features/magic-context/memory/project-identity.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.map +1 -1
- package/dist/features/magic-context/reclaim-protection.d.ts +13 -0
- package/dist/features/magic-context/reclaim-protection.d.ts.map +1 -0
- package/dist/features/magic-context/smart-notes/runner.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/wake-plane.d.ts +21 -0
- package/dist/features/magic-context/smart-notes/wake-plane.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +19 -3
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-primers.d.ts +11 -0
- package/dist/features/magic-context/storage-primers.d.ts.map +1 -1
- package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +13 -5
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +7 -0
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/features/magic-context/window-report-ledger.d.ts +70 -0
- package/dist/features/magic-context/window-report-ledger.d.ts.map +1 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +8 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-cycle.d.ts +16 -0
- package/dist/hooks/magic-context/channel2-cycle.d.ts.map +1 -0
- package/dist/hooks/magic-context/channel2-delivery.d.ts +3 -9
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +2 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +33 -103
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +4 -0
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts +3 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/fold-execution-gate.d.ts +2 -0
- package/dist/hooks/magic-context/fold-execution-gate.d.ts.map +1 -0
- 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/live-session-state.d.ts +3 -0
- package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +20 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts +8 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-wire.d.ts +6 -1
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +21 -2
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +27 -0
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +78 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +14 -3
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +10 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
- package/dist/{index-nsx2n9eg.js → index-4cz5nfxf.js} +3 -3
- package/dist/{index-f1tnb3vm.js → index-8pqfcghm.js} +21 -9
- package/dist/index-9zhhvr4w.js +733 -0
- package/dist/{index-zp6x6s6a.js → index-cae89eas.js} +25 -6
- package/dist/index-cgyfn1s2.js +53 -0
- package/dist/{index-3zqcdwqr.js → index-crke4z5j.js} +7 -4
- package/dist/{index-1skzp4dc.js → index-mmvhrqqb.js} +1991 -274
- package/dist/{index-tsgs7td8.js → index-pck9vadr.js} +1 -1
- package/dist/{index-3tnkymva.js → index-sw8gnq34.js} +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1714 -988
- package/dist/plugin/hooks/create-session-hooks.d.ts +3 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +2 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-a0g9b52q.js → recomp-orchestrator-jsde4ygw.js} +7 -6
- package/dist/{safe-notification-target-c11fdda2.js → safe-notification-target-b693f9x3.js} +2 -2
- package/dist/{send-session-notification-bgtf4853.js → send-session-notification-dxske5zm.js} +3 -3
- package/dist/shared/conflict-detector.d.ts +63 -0
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/harness-provider-map.d.ts +8 -0
- package/dist/shared/harness-provider-map.d.ts.map +1 -1
- package/dist/shared/logger.d.ts +8 -0
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts +7 -0
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
- package/dist/shared/prompt-surface.d.ts +2 -0
- package/dist/shared/prompt-surface.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +29 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +28 -9
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/tail-hygiene-status.d.ts +15 -0
- package/dist/shared/tail-hygiene-status.d.ts.map +1 -0
- package/dist/shared/window-geometry.d.ts +90 -0
- package/dist/shared/window-geometry.d.ts.map +1 -0
- package/dist/{storage-meta-session-df9tstzr.js → storage-meta-session-mk4gsnke.js} +4 -3
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/config/index.ts +2 -0
- package/src/config/latch-permanence-guard.test.ts +218 -0
- package/src/config/project-security.ts +10 -1
- package/src/config/schema/magic-context.ts +12 -2
- package/src/shared/__fixtures__/window-overlay.v1.json +337 -0
- package/src/shared/conflict-detector.test.ts +176 -1
- package/src/shared/conflict-detector.ts +126 -3
- package/src/shared/harness-provider-map.test.ts +20 -0
- package/src/shared/harness-provider-map.ts +19 -0
- package/src/shared/logger.test.ts +119 -0
- package/src/shared/logger.ts +33 -15
- package/src/shared/models-dev-cache.test.ts +42 -0
- package/src/shared/models-dev-cache.ts +62 -9
- package/src/shared/prompt-surface-runtime.ts +5 -3
- package/src/shared/prompt-surface.test.ts +38 -0
- package/src/shared/prompt-surface.ts +15 -2
- package/src/shared/rpc-client.ts +4 -2
- package/src/shared/rpc-server.ts +1 -0
- package/src/shared/rpc-types.ts +30 -0
- package/src/shared/rpc-utils.test.ts +162 -13
- package/src/shared/rpc-utils.ts +224 -31
- package/src/shared/tail-hygiene-status.ts +57 -0
- package/src/shared/window-geometry.test.ts +342 -0
- package/src/shared/window-geometry.ts +626 -0
- package/src/tui/data/context-db.ts +5 -0
- package/src/tui/index.tsx +33 -1
- package/src/tui/slots/sidebar-content.tsx +17 -5
- package/src/tui-compiled/data/context-db.ts +5 -0
- package/src/tui-compiled/index.tsx +359 -273
- package/src/tui-compiled/slots/sidebar-content.tsx +316 -287
- package/dist/index-wgvxkqpx.js +0 -342
package/src/shared/rpc-utils.ts
CHANGED
|
@@ -3,10 +3,16 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
+
export type ProcessKind = "OpenCode server" | "OpenCode instance (TUI/CLI)" | "Pi" | "process";
|
|
7
|
+
|
|
6
8
|
export interface RpcPortFileRecord {
|
|
7
9
|
port: number;
|
|
8
10
|
pid: number;
|
|
9
11
|
started_at: number;
|
|
12
|
+
/** Optional producer-provided kind; older records omit it. */
|
|
13
|
+
kind?: string;
|
|
14
|
+
/** Compatibility with discovery records that used the harness name. */
|
|
15
|
+
harness?: string;
|
|
10
16
|
/**
|
|
11
17
|
* Per-process bearer token. The server requires it on all non-health RPC
|
|
12
18
|
* calls so a random local process or browser-origin script that merely
|
|
@@ -50,13 +56,22 @@ export function legacyRpcPortFilePath(storageDir: string, directory: string): st
|
|
|
50
56
|
return join(rpcPortDir(storageDir, directory), "port");
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
export
|
|
54
|
-
|
|
59
|
+
export type PidLiveness = "alive" | "dead" | "inconclusive";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check whether the platform confirms a PID is live without treating a denied
|
|
63
|
+
* probe as confirmation. Windows uses tasklist because MSYS2/Cygwin ps does not
|
|
64
|
+
* support the options used by the Unix probe. Sandboxes commonly reject
|
|
65
|
+
* `kill(pid, 0)` with EPERM even when the PID does not exist outside their view.
|
|
66
|
+
*/
|
|
67
|
+
export function isPidAlive(pid: number): PidLiveness {
|
|
68
|
+
if (!Number.isInteger(pid) || pid <= 0) return "dead";
|
|
69
|
+
if (rpcIdentityPlatform === "win32") return readWindowsProcess(pid).state;
|
|
55
70
|
try {
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
-
} catch (
|
|
59
|
-
return (
|
|
71
|
+
rpcIdentityProcessKill(pid, 0);
|
|
72
|
+
return "alive";
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return (error as NodeJS.ErrnoException).code === "ESRCH" ? "dead" : "inconclusive";
|
|
60
75
|
}
|
|
61
76
|
}
|
|
62
77
|
|
|
@@ -64,9 +79,12 @@ const RPC_IDENTITY_SKEW_TOLERANCE_MS = 120_000;
|
|
|
64
79
|
const LINUX_CLOCK_TICKS_PER_SECOND = 100;
|
|
65
80
|
const PS_PROBE_TIMEOUT_MS = 1_000;
|
|
66
81
|
const OPEN_CODE_COMMAND_MARKERS = ["opencode", "node", "bun", "electron"];
|
|
82
|
+
const TASKLIST_NO_TASKS_PATTERN =
|
|
83
|
+
/^INFO:\s+No tasks are running which match the specified criteria\.?$/im;
|
|
67
84
|
|
|
68
85
|
let rpcIdentityReadFileSync: typeof readFileSync = readFileSync;
|
|
69
86
|
let rpcIdentityExecFileSync: typeof execFileSync = execFileSync;
|
|
87
|
+
let rpcIdentityProcessKill: typeof process.kill = process.kill;
|
|
70
88
|
let rpcProcessListExecFileSync: typeof execFileSync = execFileSync;
|
|
71
89
|
let rpcProcessListTestOverride = false;
|
|
72
90
|
let rpcIdentityPlatform: NodeJS.Platform = process.platform;
|
|
@@ -116,6 +134,7 @@ function readPsProcessStartTime(pid: number): number | null {
|
|
|
116
134
|
const output = rpcIdentityExecFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
117
135
|
encoding: "utf8",
|
|
118
136
|
timeout: PS_PROBE_TIMEOUT_MS,
|
|
137
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
119
138
|
});
|
|
120
139
|
const processStartTime = Date.parse(String(output).trim());
|
|
121
140
|
return Number.isFinite(processStartTime) ? processStartTime : null;
|
|
@@ -124,6 +143,72 @@ function readPsProcessStartTime(pid: number): number | null {
|
|
|
124
143
|
}
|
|
125
144
|
}
|
|
126
145
|
|
|
146
|
+
interface ProcessListEntry {
|
|
147
|
+
pid: number;
|
|
148
|
+
command: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseCsvLine(line: string): string[] | null {
|
|
152
|
+
const fields: string[] = [];
|
|
153
|
+
let field = "";
|
|
154
|
+
let quoted = false;
|
|
155
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
156
|
+
const character = line[index];
|
|
157
|
+
if (character === '"') {
|
|
158
|
+
if (quoted && line[index + 1] === '"') {
|
|
159
|
+
field += '"';
|
|
160
|
+
index += 1;
|
|
161
|
+
} else {
|
|
162
|
+
quoted = !quoted;
|
|
163
|
+
}
|
|
164
|
+
} else if (character === "," && !quoted) {
|
|
165
|
+
fields.push(field);
|
|
166
|
+
field = "";
|
|
167
|
+
} else {
|
|
168
|
+
field += character;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (quoted) return null;
|
|
172
|
+
fields.push(field);
|
|
173
|
+
return fields;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseTasklistOutput(output: string): ProcessListEntry[] | null {
|
|
177
|
+
const entries: ProcessListEntry[] = [];
|
|
178
|
+
let sawHeader = false;
|
|
179
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
180
|
+
const line = rawLine.trim();
|
|
181
|
+
if (!line) continue;
|
|
182
|
+
if (TASKLIST_NO_TASKS_PATTERN.test(line)) return [];
|
|
183
|
+
const fields = parseCsvLine(line);
|
|
184
|
+
if (!fields) continue;
|
|
185
|
+
if (fields[1]?.trim().toLowerCase() === "pid") {
|
|
186
|
+
sawHeader = true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const pid = Number(fields[1]);
|
|
190
|
+
if (!Number.isInteger(pid) || pid <= 0 || !fields[0]) continue;
|
|
191
|
+
entries.push({ pid, command: fields[0] });
|
|
192
|
+
}
|
|
193
|
+
return entries.length > 0 || sawHeader ? entries : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readWindowsProcess(pid: number): { state: PidLiveness; command?: string } {
|
|
197
|
+
try {
|
|
198
|
+
const output = rpcIdentityExecFileSync("tasklist", ["/FO", "CSV", "/FI", `PID eq ${pid}`], {
|
|
199
|
+
encoding: "utf8",
|
|
200
|
+
timeout: PS_PROBE_TIMEOUT_MS,
|
|
201
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
202
|
+
});
|
|
203
|
+
const entries = parseTasklistOutput(String(output));
|
|
204
|
+
if (entries === null) return { state: "inconclusive" };
|
|
205
|
+
const process = entries.find((entry) => entry.pid === pid);
|
|
206
|
+
return process ? { state: "alive", command: process.command } : { state: "dead" };
|
|
207
|
+
} catch {
|
|
208
|
+
return { state: "inconclusive" };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
127
212
|
function readLinuxProcessCommand(pid: number): string | null {
|
|
128
213
|
try {
|
|
129
214
|
return String(rpcIdentityReadFileSync(`/proc/${pid}/cmdline`, "utf8"));
|
|
@@ -137,6 +222,7 @@ function readPsProcessCommand(pid: number): string | null {
|
|
|
137
222
|
const output = rpcIdentityExecFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
138
223
|
encoding: "utf8",
|
|
139
224
|
timeout: PS_PROBE_TIMEOUT_MS,
|
|
225
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
140
226
|
});
|
|
141
227
|
return String(output);
|
|
142
228
|
} catch {
|
|
@@ -144,6 +230,78 @@ function readPsProcessCommand(pid: number): string | null {
|
|
|
144
230
|
}
|
|
145
231
|
}
|
|
146
232
|
|
|
233
|
+
/** Reuse the platform-gated command probes used by PID identity checks. */
|
|
234
|
+
export function readProcessCommand(pid: number): string | null {
|
|
235
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
236
|
+
return rpcIdentityPlatform === "linux"
|
|
237
|
+
? readLinuxProcessCommand(pid)
|
|
238
|
+
: rpcIdentityPlatform === "win32"
|
|
239
|
+
? (readWindowsProcess(pid).command ?? null)
|
|
240
|
+
: readPsProcessCommand(pid);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function executableName(token: string | undefined): string {
|
|
244
|
+
return (
|
|
245
|
+
(token ?? "")
|
|
246
|
+
.replace(/^['"]|['"]$/g, "")
|
|
247
|
+
.split("/")
|
|
248
|
+
.at(-1) ?? ""
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function commandTokens(command: string): string[] {
|
|
253
|
+
return command
|
|
254
|
+
.toLowerCase()
|
|
255
|
+
.replaceAll("\\", "/")
|
|
256
|
+
.replaceAll("\u0000", " ")
|
|
257
|
+
.split(/\s+/)
|
|
258
|
+
.map((token) => token.replace(/^['"]|['"]$/g, ""))
|
|
259
|
+
.filter(Boolean);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function commandHasOpenCodeExecutable(tokens: readonly string[]): number {
|
|
263
|
+
return tokens.findIndex((token) => {
|
|
264
|
+
const executable = executableName(token).replace(/\.(?:exe|cmd)$/, "");
|
|
265
|
+
return executable === "opencode" || executable.endsWith("/opencode");
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function commandHasPiExecutable(tokens: readonly string[]): boolean {
|
|
270
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
271
|
+
const executable = executableName(tokens[index]).replace(/\.(?:exe|cmd)$/, "");
|
|
272
|
+
if (["pi", "omp", "oh-my-pi"].includes(executable)) return true;
|
|
273
|
+
if (["node", "bun", "deno"].includes(executable)) {
|
|
274
|
+
const script = executableName(tokens[index + 1]).replace(/\.(?:exe|cmd)$/, "");
|
|
275
|
+
if (
|
|
276
|
+
["pi", "pi.js", "pi.mjs", "pi.cjs"].includes(script) ||
|
|
277
|
+
tokens[index + 1]?.includes("pi-coding-agent")
|
|
278
|
+
) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Classify a process command without changing the liveness decision. */
|
|
287
|
+
export function classifyProcessKind(command: string | null | undefined): ProcessKind {
|
|
288
|
+
if (!command) return "process";
|
|
289
|
+
const tokens = commandTokens(command);
|
|
290
|
+
const openCodeIndex = commandHasOpenCodeExecutable(tokens);
|
|
291
|
+
if (openCodeIndex >= 0) {
|
|
292
|
+
const args = tokens.slice(openCodeIndex + 1);
|
|
293
|
+
if (
|
|
294
|
+
args.some(
|
|
295
|
+
(token) => token === "serve" || token === "--serve" || token.startsWith("--serve="),
|
|
296
|
+
)
|
|
297
|
+
) {
|
|
298
|
+
return "OpenCode server";
|
|
299
|
+
}
|
|
300
|
+
return "OpenCode instance (TUI/CLI)";
|
|
301
|
+
}
|
|
302
|
+
return commandHasPiExecutable(tokens) ? "Pi" : "process";
|
|
303
|
+
}
|
|
304
|
+
|
|
147
305
|
function commandLooksLikeOpenCode(command: string): boolean {
|
|
148
306
|
const normalized = command.toLowerCase();
|
|
149
307
|
return OPEN_CODE_COMMAND_MARKERS.some((marker) => normalized.includes(marker));
|
|
@@ -153,40 +311,51 @@ function commandLooksLikeOpenCode(command: string): boolean {
|
|
|
153
311
|
* Verify that a live PID still belongs to the process that wrote a port record.
|
|
154
312
|
*
|
|
155
313
|
* A PID can be reused after its original process exits. On Linux, procfs gives
|
|
156
|
-
* us a process start time without spawning a helper;
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
314
|
+
* us a process start time without spawning a helper; macOS and other Unix-like
|
|
315
|
+
* platforms use `ps`, while Windows uses `tasklist`, only on this cold
|
|
316
|
+
* database-open guard path. Legacy records without a start time use a weaker
|
|
317
|
+
* command-name check. A failed filesystem or process probe is inconclusive,
|
|
318
|
+
* not proof that this port record still belongs to OpenCode.
|
|
160
319
|
*/
|
|
161
|
-
export
|
|
162
|
-
|
|
320
|
+
export type PidIdentityPlausibility = "plausible" | "implausible" | "inconclusive";
|
|
321
|
+
|
|
322
|
+
export function isPidIdentityPlausible(record: RpcPortFileRecord): PidIdentityPlausibility {
|
|
323
|
+
if (!Number.isInteger(record.pid) || record.pid <= 0) return "implausible";
|
|
163
324
|
|
|
164
325
|
if (Number.isFinite(record.started_at) && record.started_at > 0) {
|
|
165
326
|
const processStartTime =
|
|
166
327
|
rpcIdentityPlatform === "linux"
|
|
167
328
|
? readLinuxProcessStartTime(record.pid)
|
|
168
|
-
:
|
|
169
|
-
|
|
170
|
-
|
|
329
|
+
: rpcIdentityPlatform === "win32"
|
|
330
|
+
? null
|
|
331
|
+
: readPsProcessStartTime(record.pid);
|
|
332
|
+
if (processStartTime === null) return "inconclusive";
|
|
333
|
+
return processStartTime <= record.started_at + RPC_IDENTITY_SKEW_TOLERANCE_MS
|
|
334
|
+
? "plausible"
|
|
335
|
+
: "implausible";
|
|
171
336
|
}
|
|
172
337
|
|
|
173
338
|
const command =
|
|
174
339
|
rpcIdentityPlatform === "linux"
|
|
175
340
|
? readLinuxProcessCommand(record.pid)
|
|
176
|
-
:
|
|
177
|
-
|
|
178
|
-
|
|
341
|
+
: rpcIdentityPlatform === "win32"
|
|
342
|
+
? (readWindowsProcess(record.pid).command ?? null)
|
|
343
|
+
: readPsProcessCommand(record.pid);
|
|
344
|
+
if (command === null) return "inconclusive";
|
|
345
|
+
return commandLooksLikeOpenCode(command) ? "plausible" : "implausible";
|
|
179
346
|
}
|
|
180
347
|
|
|
181
348
|
export function __setRpcIdentityTestHooks(hooks: {
|
|
182
349
|
readFileSync?: typeof readFileSync;
|
|
183
350
|
execFileSync?: typeof execFileSync;
|
|
351
|
+
processKill?: typeof process.kill;
|
|
184
352
|
processListExecFileSync?: typeof execFileSync;
|
|
185
353
|
platform?: NodeJS.Platform;
|
|
186
354
|
nowMs?: () => number;
|
|
187
355
|
}): void {
|
|
188
356
|
rpcIdentityReadFileSync = hooks.readFileSync ?? readFileSync;
|
|
189
357
|
rpcIdentityExecFileSync = hooks.execFileSync ?? execFileSync;
|
|
358
|
+
rpcIdentityProcessKill = hooks.processKill ?? process.kill;
|
|
190
359
|
rpcProcessListExecFileSync = hooks.processListExecFileSync ?? execFileSync;
|
|
191
360
|
rpcProcessListTestOverride = hooks.processListExecFileSync !== undefined;
|
|
192
361
|
rpcIdentityPlatform = hooks.platform ?? process.platform;
|
|
@@ -196,6 +365,7 @@ export function __setRpcIdentityTestHooks(hooks: {
|
|
|
196
365
|
export function __resetRpcIdentityTestHooks(): void {
|
|
197
366
|
rpcIdentityReadFileSync = readFileSync;
|
|
198
367
|
rpcIdentityExecFileSync = execFileSync;
|
|
368
|
+
rpcIdentityProcessKill = process.kill;
|
|
199
369
|
rpcProcessListExecFileSync = execFileSync;
|
|
200
370
|
rpcProcessListTestOverride = false;
|
|
201
371
|
rpcIdentityPlatform = process.platform;
|
|
@@ -228,28 +398,49 @@ export interface PiProcessDiscovery {
|
|
|
228
398
|
|
|
229
399
|
/**
|
|
230
400
|
* Inspect Pi/OMP processes without converting a failed process-list probe into
|
|
231
|
-
* false evidence that no harness is running.
|
|
232
|
-
*
|
|
233
|
-
*
|
|
401
|
+
* false evidence that no harness is running. Callers choose their own policy
|
|
402
|
+
* for the unreadable state: destructive maintenance can fail closed, while a
|
|
403
|
+
* migration guard can proceed after reporting that no live Pi process was confirmed.
|
|
234
404
|
*/
|
|
235
405
|
export function inspectLivePiProcesses(): PiProcessDiscovery {
|
|
236
406
|
if (process.env.NODE_ENV === "test" && !rpcProcessListTestOverride) {
|
|
237
407
|
return { state: "known", processIds: [] };
|
|
238
408
|
}
|
|
239
409
|
try {
|
|
410
|
+
const isWindows = rpcIdentityPlatform === "win32";
|
|
240
411
|
const output = String(
|
|
241
|
-
rpcProcessListExecFileSync(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
412
|
+
rpcProcessListExecFileSync(
|
|
413
|
+
isWindows ? "tasklist" : "ps",
|
|
414
|
+
isWindows ? ["/FO", "CSV"] : ["-axo", "pid=,command="],
|
|
415
|
+
{
|
|
416
|
+
encoding: "utf8",
|
|
417
|
+
timeout: PS_PROBE_TIMEOUT_MS,
|
|
418
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
419
|
+
},
|
|
420
|
+
),
|
|
245
421
|
);
|
|
246
422
|
const pids = new Set<number>();
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
423
|
+
if (isWindows) {
|
|
424
|
+
const entries = parseTasklistOutput(output);
|
|
425
|
+
if (entries === null) {
|
|
426
|
+
return {
|
|
427
|
+
state: "unreadable",
|
|
428
|
+
processIds: [],
|
|
429
|
+
error: "tasklist output unavailable",
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
for (const entry of entries) {
|
|
433
|
+
if (entry.pid === process.pid) continue;
|
|
434
|
+
if (commandLooksLikePi(entry.command)) pids.add(entry.pid);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
for (const line of output.split(/\r?\n/)) {
|
|
438
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
439
|
+
if (!match) continue;
|
|
440
|
+
const pid = Number(match[1]);
|
|
441
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) continue;
|
|
442
|
+
if (commandLooksLikePi(match[2])) pids.add(pid);
|
|
443
|
+
}
|
|
253
444
|
}
|
|
254
445
|
return { state: "known", processIds: [...pids].sort((left, right) => left - right) };
|
|
255
446
|
} catch (error) {
|
|
@@ -281,6 +472,8 @@ export function parseRpcPortFile(content: string, fallbackPid = 0): RpcPortFileR
|
|
|
281
472
|
port,
|
|
282
473
|
pid,
|
|
283
474
|
started_at: Number.isFinite(startedAt) ? startedAt : 0,
|
|
475
|
+
kind: typeof parsed.kind === "string" ? parsed.kind : undefined,
|
|
476
|
+
harness: typeof parsed.harness === "string" ? parsed.harness : undefined,
|
|
284
477
|
token: typeof parsed.token === "string" ? parsed.token : undefined,
|
|
285
478
|
instance_id:
|
|
286
479
|
typeof parsed.instance_id === "string" ? parsed.instance_id : undefined,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Channel1State } from "../hooks/magic-context/ctx-reduce-nudge";
|
|
2
|
+
import type { TailHygieneStatus } from "./rpc-types";
|
|
3
|
+
|
|
4
|
+
export interface WireTailHygieneBaseline {
|
|
5
|
+
u?: number;
|
|
6
|
+
t?: number;
|
|
7
|
+
severity?: number;
|
|
8
|
+
evaluable?: boolean;
|
|
9
|
+
generation_invalidated?: boolean;
|
|
10
|
+
baseline_generation?: number;
|
|
11
|
+
computed_at_ms?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function finiteNumber(value: unknown, fallback = 0): number {
|
|
15
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Normalize either renderer authority's persisted baseline without dropping valid zeros. */
|
|
19
|
+
export function resolveTailHygieneStatus(
|
|
20
|
+
tsBaseline: Channel1State | undefined,
|
|
21
|
+
rustBaseline?: WireTailHygieneBaseline | null,
|
|
22
|
+
): TailHygieneStatus | undefined {
|
|
23
|
+
if (rustBaseline !== undefined && rustBaseline !== null) {
|
|
24
|
+
const t = Math.max(0, finiteNumber(rustBaseline.t));
|
|
25
|
+
const u = Math.min(t, Math.max(0, finiteNumber(rustBaseline.u)));
|
|
26
|
+
return {
|
|
27
|
+
u,
|
|
28
|
+
t,
|
|
29
|
+
severity: Math.min(
|
|
30
|
+
1,
|
|
31
|
+
Math.max(0, finiteNumber(rustBaseline.severity, u / Math.max(t, 1))),
|
|
32
|
+
),
|
|
33
|
+
evaluable: rustBaseline.evaluable === true,
|
|
34
|
+
generationInvalidated: rustBaseline.generation_invalidated === true,
|
|
35
|
+
baselineGeneration: Math.max(0, finiteNumber(rustBaseline.baseline_generation)),
|
|
36
|
+
computedAt: Math.max(0, finiteNumber(rustBaseline.computed_at_ms)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (tsBaseline === undefined) return undefined;
|
|
40
|
+
const t = Math.max(0, tsBaseline.baselineT + tsBaseline.turnDeltaT);
|
|
41
|
+
const u = Math.min(t, Math.max(0, tsBaseline.baselineU + tsBaseline.turnDeltaU));
|
|
42
|
+
return {
|
|
43
|
+
u,
|
|
44
|
+
t,
|
|
45
|
+
severity: Math.min(1, Math.max(0, u / Math.max(t, 1))),
|
|
46
|
+
evaluable: tsBaseline.evaluable && !tsBaseline.generationInvalidated,
|
|
47
|
+
generationInvalidated: tsBaseline.generationInvalidated,
|
|
48
|
+
baselineGeneration: Math.max(0, tsBaseline.baselineGeneration),
|
|
49
|
+
computedAt: Math.max(0, tsBaseline.computedAt),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatTailHygiene(status: TailHygieneStatus): string {
|
|
54
|
+
const percentage = (status.severity * 100).toFixed(1);
|
|
55
|
+
const state = status.evaluable ? "" : " · held until baseline refresh";
|
|
56
|
+
return `${percentage}% · ${status.u.toLocaleString()} / ${status.t.toLocaleString()} tok${state}`;
|
|
57
|
+
}
|