@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
@@ -47,6 +47,44 @@ describe("prompt-surface resolution", () => {
47
47
  }
48
48
  });
49
49
 
50
+ it("resolves Pi-native and canonical model keys with canonical precedence", () => {
51
+ const nativeKey = "openai-codex/gpt-5.6-sol";
52
+ const canonicalKey = "openai/gpt-5.6-sol";
53
+
54
+ expect(resolveCacheTtl({ default: "5m", [nativeKey]: "60m" }, canonicalKey)).toBe("60m");
55
+ expect(
56
+ resolvePromptSurface(
57
+ { default: "full", models: { [nativeKey]: "light" } },
58
+ canonicalKey,
59
+ ),
60
+ ).toEqual({ preset: "light", source: "exact" });
61
+ expect(
62
+ resolveCacheTtl(
63
+ { default: "5m", [nativeKey]: "60m", [canonicalKey]: "30m" },
64
+ nativeKey,
65
+ ),
66
+ ).toBe("30m");
67
+ expect(
68
+ resolvePromptSurface(
69
+ { default: "full", models: { [nativeKey]: "light", [canonicalKey]: "full" } },
70
+ nativeKey,
71
+ ),
72
+ ).toEqual({ preset: "full", source: "exact" });
73
+ });
74
+
75
+ it("leaves unknown provider keys unchanged", () => {
76
+ expect(
77
+ resolveCacheTtl(
78
+ { default: "5m", "custom-provider/model": "1m" },
79
+ "custom-provider/model",
80
+ ),
81
+ ).toBe("1m");
82
+ expect(modelKeyLookupOrder("custom-provider/model")[0]).toEqual({
83
+ key: "custom-provider/model",
84
+ source: "exact",
85
+ });
86
+ });
87
+
50
88
  it("derives a stable config identity independent of object key order", () => {
51
89
  const left = promptSurfaceConfigIdentity({
52
90
  default: "full",
@@ -1,3 +1,5 @@
1
+ import { modelRefLookupOrder } from "./harness-provider-map";
2
+
1
3
  /** The built-in prompt-surface variants. */
2
4
  export type PromptSurfacePreset = "full" | "light";
3
5
 
@@ -69,6 +71,8 @@ export interface ModelKeyCandidate {
69
71
  * provider/model boundary is the first slash; the rest of the string remains
70
72
  * the model ID, including additional slashes. Candidates are case-sensitive.
71
73
  *
74
+ * Known harness provider aliases are checked canonical-first at each specificity,
75
+ * so one shared config works on every harness and canonical wins on collisions.
72
76
  * Provider wildcards are checked after progressively less-specific model keys,
73
77
  * but before the caller's default. That keeps an exact or base-model override
74
78
  * authoritative while still allowing `provider/*` to cover otherwise-unlisted
@@ -82,10 +86,15 @@ export function modelKeyLookupOrder(modelKey: string | undefined): ModelKeyCandi
82
86
 
83
87
  const provider = modelKey.slice(0, slash);
84
88
  let modelID = modelKey.slice(slash + 1);
89
+ const providerRefs = modelRefLookupOrder(`${provider}/${modelID}`);
85
90
  const candidates: ModelKeyCandidate[] = [];
86
91
 
87
92
  while (modelID.length > 0) {
88
- candidates.push({ key: `${provider}/${modelID}`, source: "exact" });
93
+ for (const providerRef of providerRefs) {
94
+ const providerSlash = providerRef.indexOf("/");
95
+ const providerPrefix = providerRef.slice(0, providerSlash);
96
+ candidates.push({ key: `${providerPrefix}/${modelID}`, source: "exact" });
97
+ }
89
98
  candidates.push({ key: modelID, source: "bare" });
90
99
 
91
100
  const lastDash = modelID.lastIndexOf("-");
@@ -93,7 +102,11 @@ export function modelKeyLookupOrder(modelKey: string | undefined): ModelKeyCandi
93
102
  modelID = modelID.slice(0, lastDash);
94
103
  }
95
104
 
96
- candidates.push({ key: `${provider}/*`, source: "wildcard" });
105
+ for (const providerRef of providerRefs) {
106
+ const providerSlash = providerRef.indexOf("/");
107
+ const providerPrefix = providerRef.slice(0, providerSlash);
108
+ candidates.push({ key: `${providerPrefix}/*`, source: "wildcard" });
109
+ }
97
110
 
98
111
  const seen = new Set<string>();
99
112
  return candidates.filter((candidate) => {
@@ -149,7 +149,9 @@ export class MagicContextRpcClient {
149
149
  for (const entry of readdirSync(this.portDir)) {
150
150
  if (!entry.startsWith("port-") || !entry.endsWith(".json")) continue;
151
151
  const record = parseRpcPortFile(readFileSync(join(this.portDir, entry), "utf-8"));
152
- if (!record || !isPidAlive(record.pid)) continue;
152
+ // A denied liveness probe still leaves this as a candidate; the
153
+ // mandatory health check below confirms whether its RPC server is reachable.
154
+ if (!record || isPidAlive(record.pid) === "dead") continue;
153
155
  records.push(record);
154
156
  }
155
157
  } catch {
@@ -158,7 +160,7 @@ export class MagicContextRpcClient {
158
160
 
159
161
  try {
160
162
  const legacy = parseRpcPortFile(readFileSync(this.legacyPortFilePath, "utf-8"));
161
- if (legacy && (!legacy.pid || isPidAlive(legacy.pid))) records.push(legacy);
163
+ if (legacy && (!legacy.pid || isPidAlive(legacy.pid) !== "dead")) records.push(legacy);
162
164
  } catch {
163
165
  // Legacy discovery is optional.
164
166
  }
@@ -185,6 +185,7 @@ export class MagicContextRpcServer {
185
185
  port: this.port,
186
186
  pid: process.pid,
187
187
  started_at: this.startedAt,
188
+ kind: "OpenCode server",
188
189
  token: this.token,
189
190
  instance_id: this.instanceId,
190
191
  }),
@@ -7,6 +7,21 @@ import type {
7
7
  DreamTaskBacklogMap,
8
8
  DreamTaskProgress,
9
9
  } from "../features/magic-context/dreamer/task-registry";
10
+ import type { LoggerDiagnostics } from "./logger";
11
+
12
+ export interface TailHygieneStatus {
13
+ /** Tokens in active, non-protected tail content that the agent can reclaim. */
14
+ u: number;
15
+ /** Tokens in rendered-tail content eligible for hygiene accounting in the same scan. */
16
+ t: number;
17
+ /** Reclaimable-to-eligible token ratio, clamped to 0–1 and shared by both nudge mechanisms. */
18
+ severity: number;
19
+ /** False until a fresh scan runs after existing tail content changes, preventing stale measurements. */
20
+ evaluable: boolean;
21
+ generationInvalidated: boolean;
22
+ baselineGeneration: number;
23
+ computedAt: number;
24
+ }
10
25
 
11
26
  export interface SidebarSnapshot {
12
27
  sessionId: string;
@@ -77,6 +92,8 @@ export interface SidebarSnapshot {
77
92
  * shows this as "Tool Definitions".
78
93
  */
79
94
  toolDefinitionTokens: number;
95
+ /** Persisted reclaimable (U) and eligible (T) token counts used by both nudge mechanisms. */
96
+ tailHygiene?: TailHygieneStatus;
80
97
  /**
81
98
  * Effective execute-threshold percentage for this session's active model,
82
99
  * after per-model resolution and the tokens→percentage conversion (when
@@ -135,6 +152,17 @@ export interface StatusDetail extends SidebarSnapshot {
135
152
  isSubagent: boolean;
136
153
  pendingOps: Array<{ tagId: number; operation: string }>;
137
154
  contextLimit: number;
155
+ windowGeometry?: {
156
+ usableSoft: number;
157
+ usableHard: number;
158
+ geometry: "shared_upfront" | "shared_truncating" | "separate";
159
+ derivation: {
160
+ window: number;
161
+ reserve: number;
162
+ reserveSource: "output_catalog" | "output_config" | "wall_margin" | "none";
163
+ geometry: "shared_upfront" | "shared_truncating" | "separate";
164
+ };
165
+ };
138
166
  /**
139
167
  * Parsed cache TTL in ms. -1 = never expires (cacheTtl "never"; Infinity
140
168
  * cannot ride JSON-RPC and 0 would be indistinguishable from unset). The
@@ -170,6 +198,8 @@ export interface StatusDetail extends SidebarSnapshot {
170
198
  toastDurationMs: number;
171
199
  /** One-line status data for the experimental memory mural. */
172
200
  mural?: { present: boolean; ageMs: number | null };
201
+ /** Runtime logger write failures observed by this plugin process. */
202
+ loggerDiagnostics: LoggerDiagnostics;
173
203
  /**
174
204
  * Stable storage-version probe: "which schema is the DB at, which fence does
175
205
  * this binary carry". Field names are deliberately snake_case, mirroring the
@@ -6,8 +6,10 @@ import type { readFileSync } from "node:fs";
6
6
  import {
7
7
  __resetRpcIdentityTestHooks,
8
8
  __setRpcIdentityTestHooks,
9
+ classifyProcessKind,
9
10
  discoverLivePiProcessIds,
10
11
  inspectLivePiProcesses,
12
+ isPidAlive,
11
13
  isPidIdentityPlausible,
12
14
  type RpcPortFileRecord,
13
15
  } from "./rpc-utils";
@@ -42,10 +44,41 @@ function psOutput(output: string | Error): typeof execFileSync {
42
44
  }) as typeof execFileSync;
43
45
  }
44
46
 
47
+ function tasklistOutput(entries: Array<[number, string]>): string {
48
+ return [
49
+ '"Image Name","PID","Session Name","Session#","Mem Usage"',
50
+ ...entries.map(([pid, command]) => `"${command}","${pid}","Console","1","10,000 K"`),
51
+ ].join("\r\n");
52
+ }
53
+
45
54
  afterEach(() => {
46
55
  __resetRpcIdentityTestHooks();
47
56
  });
48
57
 
58
+ describe("classifyProcessKind", () => {
59
+ test("classifies OpenCode server, OpenCode instance, Pi, and unknown commands", () => {
60
+ expect(classifyProcessKind("/usr/local/bin/opencode serve --hostname 127.0.0.1")).toBe(
61
+ "OpenCode server",
62
+ );
63
+ expect(classifyProcessKind("node /opt/opencode/bin/opencode --continue")).toBe(
64
+ "OpenCode instance (TUI/CLI)",
65
+ );
66
+ expect(
67
+ classifyProcessKind(
68
+ "bun /workspace/node_modules/@mariozechner/pi-coding-agent/dist/cli.js",
69
+ ),
70
+ ).toBe("Pi");
71
+ expect(classifyProcessKind("/usr/bin/other --flag")).toBe("process");
72
+ expect(classifyProcessKind(null)).toBe("process");
73
+ });
74
+
75
+ test("recognizes serve flags and Windows-style executable paths", () => {
76
+ expect(classifyProcessKind("opencode --serve=true")).toBe("OpenCode server");
77
+ expect(classifyProcessKind("C:\\Tools\\opencode.exe")).toBe("OpenCode instance (TUI/CLI)");
78
+ expect(classifyProcessKind("pi.cmd --model test")).toBe("Pi");
79
+ });
80
+ });
81
+
49
82
  describe("discoverLivePiProcessIds", () => {
50
83
  test("finds Pi-family harness commands while excluding the current process", () => {
51
84
  __setRpcIdentityTestHooks({
@@ -78,6 +111,103 @@ describe("discoverLivePiProcessIds", () => {
78
111
  error: "ps unavailable",
79
112
  });
80
113
  });
114
+
115
+ test("uses tasklist instead of ps on Windows", () => {
116
+ const calls: string[] = [];
117
+ __setRpcIdentityTestHooks({
118
+ platform: "win32",
119
+ processListExecFileSync: ((file: string | URL) => {
120
+ calls.push(String(file));
121
+ return tasklistOutput([
122
+ [process.pid, "pi.exe"],
123
+ [41001, "pi.exe"],
124
+ [41002, "opencode.exe"],
125
+ ]);
126
+ }) as typeof execFileSync,
127
+ });
128
+
129
+ expect(inspectLivePiProcesses()).toEqual({
130
+ state: "known",
131
+ processIds: [41001],
132
+ });
133
+ expect(calls).toEqual(["tasklist"]);
134
+ });
135
+ });
136
+
137
+ describe("isPidAlive", () => {
138
+ test("distinguishes confirmed, dead, and inaccessible PID probes", () => {
139
+ const probeFailure = (code: string): NodeJS.ErrnoException => {
140
+ const error = new Error(`kill failed: ${code}`) as NodeJS.ErrnoException;
141
+ error.code = code;
142
+ return error;
143
+ };
144
+
145
+ __setRpcIdentityTestHooks({
146
+ processKill: (() => true) as typeof process.kill,
147
+ });
148
+ expect(isPidAlive(PID)).toBe("alive");
149
+
150
+ __setRpcIdentityTestHooks({
151
+ processKill: (() => {
152
+ throw probeFailure("ESRCH");
153
+ }) as typeof process.kill,
154
+ });
155
+ expect(isPidAlive(PID)).toBe("dead");
156
+
157
+ __setRpcIdentityTestHooks({
158
+ processKill: (() => {
159
+ throw probeFailure("EPERM");
160
+ }) as typeof process.kill,
161
+ });
162
+ expect(isPidAlive(PID)).toBe("inconclusive");
163
+ });
164
+
165
+ test("uses tasklist for Windows liveness and captures probe stderr", () => {
166
+ const calls: Array<{ file: string; args: readonly string[]; stdio: unknown }> = [];
167
+ __setRpcIdentityTestHooks({
168
+ platform: "win32",
169
+ execFileSync: ((
170
+ file: string | URL,
171
+ args: readonly string[] = [],
172
+ options: { stdio?: unknown } = {},
173
+ ) => {
174
+ calls.push({ file: String(file), args, stdio: options.stdio });
175
+ if (String(file) === "ps") throw new Error("ps must not run on Windows");
176
+ return tasklistOutput([[PID, "OpenCode.exe"]]);
177
+ }) as typeof execFileSync,
178
+ });
179
+
180
+ expect(isPidAlive(PID)).toBe("alive");
181
+ expect(calls).toEqual([
182
+ {
183
+ file: "tasklist",
184
+ args: ["/FO", "CSV", "/FI", `PID eq ${PID}`],
185
+ stdio: ["ignore", "pipe", "pipe"],
186
+ },
187
+ ]);
188
+ });
189
+
190
+ test("treats a successful tasklist no-match response as dead", () => {
191
+ __setRpcIdentityTestHooks({
192
+ platform: "win32",
193
+ execFileSync: (() =>
194
+ "INFO: No tasks are running which match the specified criteria.") as typeof execFileSync,
195
+ });
196
+
197
+ expect(isPidAlive(PID)).toBe("dead");
198
+ });
199
+
200
+ test("returns inconclusive when the Windows tasklist probe cannot spawn", () => {
201
+ __setRpcIdentityTestHooks({
202
+ platform: "win32",
203
+ execFileSync: (() => {
204
+ throw new Error("tasklist unavailable");
205
+ }) as typeof execFileSync,
206
+ });
207
+
208
+ expect(isPidAlive(PID)).toBe("inconclusive");
209
+ expect(isPidIdentityPlausible(record(0))).toBe("inconclusive");
210
+ });
81
211
  });
82
212
 
83
213
  describe("isPidIdentityPlausible", () => {
@@ -99,7 +229,7 @@ describe("isPidIdentityPlausible", () => {
99
229
  }) as typeof execFileSync,
100
230
  });
101
231
 
102
- expect(isPidIdentityPlausible(record(500_000))).toBe(false);
232
+ expect(isPidIdentityPlausible(record(500_000))).toBe("implausible");
103
233
  expect(readPaths).toEqual([`/proc/${PID}/stat`, "/proc/uptime"]);
104
234
  });
105
235
 
@@ -115,10 +245,10 @@ describe("isPidIdentityPlausible", () => {
115
245
 
116
246
  // The mocked process start is 1,100,000ms. The 120s tolerance is part of
117
247
  // the contract because port-file creation follows process startup.
118
- expect(isPidIdentityPlausible(record(1_000_000))).toBe(true);
248
+ expect(isPidIdentityPlausible(record(1_000_000))).toBe("plausible");
119
249
  });
120
250
 
121
- test("treats an unreadable Linux start-time probe as live", () => {
251
+ test("reports an unreadable Linux start-time probe as inconclusive", () => {
122
252
  __setRpcIdentityTestHooks({
123
253
  platform: "linux",
124
254
  readFileSync: linuxFiles({
@@ -126,17 +256,17 @@ describe("isPidIdentityPlausible", () => {
126
256
  }),
127
257
  });
128
258
 
129
- expect(isPidIdentityPlausible(record(500_000))).toBe(true);
259
+ expect(isPidIdentityPlausible(record(500_000))).toBe("inconclusive");
130
260
  });
131
261
 
132
- test("uses the legacy Linux command fallback and fails closed on probe errors", () => {
262
+ test("uses the legacy Linux command fallback and distinguishes probe errors", () => {
133
263
  __setRpcIdentityTestHooks({
134
264
  platform: "linux",
135
265
  readFileSync: linuxFiles({
136
266
  [`/proc/${PID}/cmdline`]: "/usr/sbin/opendkim --config /etc/opendkim.conf",
137
267
  }),
138
268
  });
139
- expect(isPidIdentityPlausible(record(0))).toBe(false);
269
+ expect(isPidIdentityPlausible(record(0))).toBe("implausible");
140
270
 
141
271
  __setRpcIdentityTestHooks({
142
272
  platform: "linux",
@@ -144,7 +274,7 @@ describe("isPidIdentityPlausible", () => {
144
274
  [`/proc/${PID}/cmdline`]: "/usr/local/bin/opencode serve",
145
275
  }),
146
276
  });
147
- expect(isPidIdentityPlausible(record(0))).toBe(true);
277
+ expect(isPidIdentityPlausible(record(0))).toBe("plausible");
148
278
 
149
279
  __setRpcIdentityTestHooks({
150
280
  platform: "linux",
@@ -152,7 +282,7 @@ describe("isPidIdentityPlausible", () => {
152
282
  [`/proc/${PID}/cmdline`]: new Error("procfs unavailable"),
153
283
  }),
154
284
  });
155
- expect(isPidIdentityPlausible(record(0))).toBe(true);
285
+ expect(isPidIdentityPlausible(record(0))).toBe("inconclusive");
156
286
  });
157
287
 
158
288
  test("uses ps start time and command probes on non-Linux platforms", () => {
@@ -162,7 +292,7 @@ describe("isPidIdentityPlausible", () => {
162
292
  });
163
293
  expect(
164
294
  isPidIdentityPlausible(record(Date.parse("Mon Aug 7 00:00:00 1970") - 121_000)),
165
- ).toBe(false);
295
+ ).toBe("implausible");
166
296
 
167
297
  __setRpcIdentityTestHooks({
168
298
  platform: "darwin",
@@ -170,24 +300,43 @@ describe("isPidIdentityPlausible", () => {
170
300
  });
171
301
  expect(
172
302
  isPidIdentityPlausible(record(Date.parse("Mon Aug 7 00:00:00 1970") - 120_000)),
173
- ).toBe(true);
303
+ ).toBe("plausible");
174
304
 
175
305
  __setRpcIdentityTestHooks({
176
306
  platform: "darwin",
177
307
  execFileSync: psOutput("/usr/sbin/opendkim -f"),
178
308
  });
179
- expect(isPidIdentityPlausible(record(0))).toBe(false);
309
+ expect(isPidIdentityPlausible(record(0))).toBe("implausible");
180
310
 
181
311
  __setRpcIdentityTestHooks({
182
312
  platform: "darwin",
183
313
  execFileSync: psOutput("/Applications/OpenCode.app/Contents/MacOS/opencode"),
184
314
  });
185
- expect(isPidIdentityPlausible(record(0))).toBe(true);
315
+ expect(isPidIdentityPlausible(record(0))).toBe("plausible");
186
316
 
187
317
  __setRpcIdentityTestHooks({
188
318
  platform: "darwin",
189
319
  execFileSync: psOutput(new Error("ps unavailable")),
190
320
  });
191
- expect(isPidIdentityPlausible(record(0))).toBe(true);
321
+ expect(isPidIdentityPlausible(record(0))).toBe("inconclusive");
322
+ });
323
+
324
+ test("uses tasklist for the Windows command fallback and skips unavailable start time", () => {
325
+ const calls: Array<{ file: string; args: readonly string[] }> = [];
326
+ __setRpcIdentityTestHooks({
327
+ platform: "win32",
328
+ execFileSync: ((file: string | URL, args: readonly string[] = []) => {
329
+ calls.push({ file: String(file), args });
330
+ if (String(file) === "ps") throw new Error("ps must not run on Windows");
331
+ return tasklistOutput([[PID, "OpenCode.exe"]]);
332
+ }) as typeof execFileSync,
333
+ });
334
+
335
+ expect(isPidIdentityPlausible(record(0))).toBe("plausible");
336
+ expect(calls).toEqual([{ file: "tasklist", args: ["/FO", "CSV", "/FI", `PID eq ${PID}`] }]);
337
+
338
+ calls.length = 0;
339
+ expect(isPidIdentityPlausible(record(NOW_MS))).toBe("inconclusive");
340
+ expect(calls).toEqual([]);
192
341
  });
193
342
  });