@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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) => {
|
package/src/shared/rpc-client.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/shared/rpc-server.ts
CHANGED
package/src/shared/rpc-types.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
248
|
+
expect(isPidIdentityPlausible(record(1_000_000))).toBe("plausible");
|
|
119
249
|
});
|
|
120
250
|
|
|
121
|
-
test("
|
|
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(
|
|
259
|
+
expect(isPidIdentityPlausible(record(500_000))).toBe("inconclusive");
|
|
130
260
|
});
|
|
131
261
|
|
|
132
|
-
test("uses the legacy Linux command fallback and
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
});
|