@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.
Files changed (176) hide show
  1. package/README.md +2 -0
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/config/project-security.d.ts +2 -1
  4. package/dist/config/project-security.d.ts.map +1 -1
  5. package/dist/config/schema/magic-context.d.ts +10 -0
  6. package/dist/config/schema/magic-context.d.ts.map +1 -1
  7. package/dist/{conflict-warning-hook-mt0ttc4x.js → conflict-warning-hook-an4fxhwf.js} +3 -3
  8. package/dist/{derive-budgets-88mxk2w1.js → derive-budgets-da4yg3tz.js} +3 -2
  9. package/dist/features/magic-context/context-authority.d.ts.map +1 -1
  10. package/dist/features/magic-context/dreamer/classify-prompt.d.ts +5 -1
  11. package/dist/features/magic-context/dreamer/classify-prompt.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  14. package/dist/features/magic-context/dreamer/manifest-parser.d.ts +8 -0
  15. package/dist/features/magic-context/dreamer/manifest-parser.d.ts.map +1 -1
  16. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +7 -1
  17. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/map-memories.d.ts +18 -0
  19. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +5 -0
  21. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -1
  24. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  26. package/dist/features/magic-context/fail-closed-block.d.ts +6 -1
  27. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  28. package/dist/features/magic-context/memory/embedding-openai.d.ts +3 -1
  29. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/project-identity.d.ts +6 -0
  31. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  32. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  33. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  34. package/dist/features/magic-context/reclaim-protection.d.ts +13 -0
  35. package/dist/features/magic-context/reclaim-protection.d.ts.map +1 -0
  36. package/dist/features/magic-context/smart-notes/runner.d.ts.map +1 -1
  37. package/dist/features/magic-context/smart-notes/wake-plane.d.ts +21 -0
  38. package/dist/features/magic-context/smart-notes/wake-plane.d.ts.map +1 -0
  39. package/dist/features/magic-context/storage-db.d.ts +19 -3
  40. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  41. package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -0
  42. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  43. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  44. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  45. package/dist/features/magic-context/storage-primers.d.ts +11 -0
  46. package/dist/features/magic-context/storage-primers.d.ts.map +1 -1
  47. package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
  48. package/dist/features/magic-context/storage-tags.d.ts +13 -5
  49. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  50. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +7 -0
  51. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  52. package/dist/features/magic-context/window-report-ledger.d.ts +70 -0
  53. package/dist/features/magic-context/window-report-ledger.d.ts.map +1 -0
  54. package/dist/hooks/auto-update-checker/checker.d.ts +8 -1
  55. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  56. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/channel2-cycle.d.ts +16 -0
  58. package/dist/hooks/magic-context/channel2-cycle.d.ts.map +1 -0
  59. package/dist/hooks/magic-context/channel2-delivery.d.ts +3 -9
  60. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/command-handler.d.ts +2 -0
  62. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +33 -103
  64. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/event-resolvers.d.ts +4 -0
  68. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/execute-status.d.ts +3 -1
  70. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/fold-execution-gate.d.ts +2 -0
  72. package/dist/hooks/magic-context/fold-execution-gate.d.ts.map +1 -0
  73. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  74. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  76. package/dist/hooks/magic-context/live-session-state.d.ts +3 -0
  77. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/lkg-slot.d.ts +20 -0
  80. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/module-state-sync.d.ts +8 -0
  82. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/module-wire.d.ts +6 -1
  85. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/rust-mode-transform.d.ts +21 -2
  87. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/strip-content.d.ts +27 -0
  89. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +78 -0
  93. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -0
  94. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +14 -3
  96. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/transform.d.ts +10 -0
  98. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
  100. package/dist/{index-nsx2n9eg.js → index-4cz5nfxf.js} +3 -3
  101. package/dist/{index-f1tnb3vm.js → index-8pqfcghm.js} +21 -9
  102. package/dist/index-9zhhvr4w.js +733 -0
  103. package/dist/{index-zp6x6s6a.js → index-cae89eas.js} +25 -6
  104. package/dist/index-cgyfn1s2.js +53 -0
  105. package/dist/{index-3zqcdwqr.js → index-crke4z5j.js} +7 -4
  106. package/dist/{index-1skzp4dc.js → index-mmvhrqqb.js} +1991 -274
  107. package/dist/{index-tsgs7td8.js → index-pck9vadr.js} +1 -1
  108. package/dist/{index-3tnkymva.js → index-sw8gnq34.js} +1 -1
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +1714 -988
  111. package/dist/plugin/hooks/create-session-hooks.d.ts +3 -0
  112. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  113. package/dist/plugin/messages-transform.d.ts.map +1 -1
  114. package/dist/plugin/rpc-handlers.d.ts +2 -0
  115. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  116. package/dist/{recomp-orchestrator-a0g9b52q.js → recomp-orchestrator-jsde4ygw.js} +7 -6
  117. package/dist/{safe-notification-target-c11fdda2.js → safe-notification-target-b693f9x3.js} +2 -2
  118. package/dist/{send-session-notification-bgtf4853.js → send-session-notification-dxske5zm.js} +3 -3
  119. package/dist/shared/conflict-detector.d.ts +63 -0
  120. package/dist/shared/conflict-detector.d.ts.map +1 -1
  121. package/dist/shared/harness-provider-map.d.ts +8 -0
  122. package/dist/shared/harness-provider-map.d.ts.map +1 -1
  123. package/dist/shared/logger.d.ts +8 -0
  124. package/dist/shared/logger.d.ts.map +1 -1
  125. package/dist/shared/models-dev-cache.d.ts +7 -0
  126. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  127. package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
  128. package/dist/shared/prompt-surface.d.ts +2 -0
  129. package/dist/shared/prompt-surface.d.ts.map +1 -1
  130. package/dist/shared/rpc-client.d.ts.map +1 -1
  131. package/dist/shared/rpc-server.d.ts.map +1 -1
  132. package/dist/shared/rpc-types.d.ts +29 -0
  133. package/dist/shared/rpc-types.d.ts.map +1 -1
  134. package/dist/shared/rpc-utils.d.ts +28 -9
  135. package/dist/shared/rpc-utils.d.ts.map +1 -1
  136. package/dist/shared/tail-hygiene-status.d.ts +15 -0
  137. package/dist/shared/tail-hygiene-status.d.ts.map +1 -0
  138. package/dist/shared/window-geometry.d.ts +90 -0
  139. package/dist/shared/window-geometry.d.ts.map +1 -0
  140. package/dist/{storage-meta-session-df9tstzr.js → storage-meta-session-mk4gsnke.js} +4 -3
  141. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  142. package/dist/tools/ctx-note/tools.d.ts.map +1 -1
  143. package/dist/tui/data/context-db.d.ts.map +1 -1
  144. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  145. package/package.json +3 -1
  146. package/src/config/index.ts +2 -0
  147. package/src/config/latch-permanence-guard.test.ts +218 -0
  148. package/src/config/project-security.ts +10 -1
  149. package/src/config/schema/magic-context.ts +12 -2
  150. package/src/shared/__fixtures__/window-overlay.v1.json +337 -0
  151. package/src/shared/conflict-detector.test.ts +176 -1
  152. package/src/shared/conflict-detector.ts +126 -3
  153. package/src/shared/harness-provider-map.test.ts +20 -0
  154. package/src/shared/harness-provider-map.ts +19 -0
  155. package/src/shared/logger.test.ts +119 -0
  156. package/src/shared/logger.ts +33 -15
  157. package/src/shared/models-dev-cache.test.ts +42 -0
  158. package/src/shared/models-dev-cache.ts +62 -9
  159. package/src/shared/prompt-surface-runtime.ts +5 -3
  160. package/src/shared/prompt-surface.test.ts +38 -0
  161. package/src/shared/prompt-surface.ts +15 -2
  162. package/src/shared/rpc-client.ts +4 -2
  163. package/src/shared/rpc-server.ts +1 -0
  164. package/src/shared/rpc-types.ts +30 -0
  165. package/src/shared/rpc-utils.test.ts +162 -13
  166. package/src/shared/rpc-utils.ts +224 -31
  167. package/src/shared/tail-hygiene-status.ts +57 -0
  168. package/src/shared/window-geometry.test.ts +342 -0
  169. package/src/shared/window-geometry.ts +626 -0
  170. package/src/tui/data/context-db.ts +5 -0
  171. package/src/tui/index.tsx +33 -1
  172. package/src/tui/slots/sidebar-content.tsx +17 -5
  173. package/src/tui-compiled/data/context-db.ts +5 -0
  174. package/src/tui-compiled/index.tsx +359 -273
  175. package/src/tui-compiled/slots/sidebar-content.tsx +316 -287
  176. package/dist/index-wgvxkqpx.js +0 -342
@@ -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 function isPidAlive(pid: number): boolean {
54
- if (!Number.isInteger(pid) || pid <= 0) return false;
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
- process.kill(pid, 0);
57
- return true;
58
- } catch (err) {
59
- return (err as NodeJS.ErrnoException).code === "EPERM";
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; other platforms use `ps`
157
- * only on this cold database-open guard path. Legacy records without a start
158
- * time use a weaker command-name check, and every probe failure stays live so
159
- * this identity check cannot weaken the migration guard on uncertainty.
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 function isPidIdentityPlausible(record: RpcPortFileRecord): boolean {
162
- if (!Number.isInteger(record.pid) || record.pid <= 0) return false;
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
- : readPsProcessStartTime(record.pid);
169
- if (processStartTime === null) return true;
170
- return processStartTime <= record.started_at + RPC_IDENTITY_SKEW_TOLERANCE_MS;
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
- : readPsProcessCommand(record.pid);
177
- if (command === null) return true;
178
- return commandLooksLikeOpenCode(command);
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. Destructive maintenance callers
232
- * use the unreadable state to fail closed; ordinary migration guards retain
233
- * their historical best-effort process list through discoverLivePiProcessIds().
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("ps", ["-axo", "pid=,command="], {
242
- encoding: "utf8",
243
- timeout: PS_PROBE_TIMEOUT_MS,
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
- for (const line of output.split(/\r?\n/)) {
248
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
249
- if (!match) continue;
250
- const pid = Number(match[1]);
251
- if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) continue;
252
- if (commandLooksLikePi(match[2])) pids.add(pid);
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
+ }