@cortexkit/opencode-magic-context 0.38.1 → 0.40.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 +57 -4
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/profiles.d.ts +22 -0
- package/dist/config/profiles.d.ts.map +1 -0
- package/dist/config/project-security.d.ts +14 -6
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/raw-loader.d.ts +42 -0
- package/dist/config/raw-loader.d.ts.map +1 -0
- package/dist/config/schema/magic-context.d.ts +966 -531
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts +3 -2
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts +9 -2
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +4 -2
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +21 -5
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +3 -2
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts +2 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts +0 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -2
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts +6 -3
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +11 -1
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-failure.d.ts +11 -0
- package/dist/features/magic-context/memory/embedding-failure.d.ts.map +1 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts +5 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-provider.d.ts +3 -0
- package/dist/features/magic-context/memory/embedding-provider.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/mural/compress-cues.d.ts +3 -2
- package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +2 -0
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts +3 -2
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/condition-compiler.d.ts +2 -0
- package/dist/features/magic-context/smart-notes/condition-compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -2
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +7 -2
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +1 -0
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/tool-definition-tokens.d.ts +10 -0
- package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +11 -0
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts +3 -2
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts +3 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +4 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +4 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +4 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +14 -2
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts +7 -0
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts.map +1 -1
- package/dist/hooks/magic-context/format-embed-failure.d.ts +3 -0
- package/dist/hooks/magic-context/format-embed-failure.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 +13 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-persist.d.ts +14 -0
- package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts +13 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts +1 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts +12 -0
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/raw-fallback-context-limit.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +4 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +6 -0
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +12 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +5 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +9 -1
- 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-2y0ybv5x.js → index-3dkzzh0t.js} +15 -17
- package/dist/{index-5znjkjws.js → index-axpym70t.js} +879 -170
- package/dist/index-ehpzdy0v.js +126 -0
- package/dist/{index-22mw8km4.js → index-rw0qrkvq.js} +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18466 -16177
- package/dist/model-resolution-r9jbcz86.js +15 -0
- package/dist/plugin/dream-timer.d.ts +3 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +2 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +14 -6
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-p030kxtc.js → recomp-orchestrator-dmyvqekw.js} +2 -3
- package/dist/shared/jsonc-edit.d.ts +6 -1
- package/dist/shared/jsonc-edit.d.ts.map +1 -1
- package/dist/shared/model-resolution.d.ts +53 -0
- package/dist/shared/model-resolution.d.ts.map +1 -0
- package/dist/shared/model-suggestion-retry.d.ts +4 -1
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/resolve-fallbacks.d.ts +5 -1
- package/dist/shared/resolve-fallbacks.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +2 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +12 -1
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/status-detail-text.d.ts +4 -0
- package/dist/shared/status-detail-text.d.ts.map +1 -0
- package/dist/shared/subagent-runner.d.ts +2 -1
- package/dist/shared/subagent-runner.d.ts.map +1 -1
- package/dist/{storage-meta-session-2306t3j7.js → storage-meta-session-yvtbaw7c.js} +1 -1
- package/dist/{task-config-at3adc59.js → task-config-a1qmj9bx.js} +2 -2
- 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 +4 -2
- package/src/config/index.test.ts +434 -19
- package/src/config/index.ts +67 -33
- package/src/config/profiles.ts +106 -0
- package/src/config/project-security.hostile-matrix.test.ts +480 -0
- package/src/config/project-security.test.ts +43 -5
- package/src/config/project-security.ts +151 -22
- package/src/config/raw-loader.test.ts +424 -0
- package/src/config/raw-loader.ts +490 -0
- package/src/config/schema/magic-context.test.ts +272 -2
- package/src/config/schema/magic-context.ts +342 -49
- package/src/shared/jsonc-edit.ts +74 -1
- package/src/shared/model-resolution.test.ts +170 -0
- package/src/shared/model-resolution.ts +227 -0
- package/src/shared/model-suggestion-retry.test.ts +27 -0
- package/src/shared/model-suggestion-retry.ts +19 -10
- package/src/shared/resolve-fallbacks.test.ts +13 -1
- package/src/shared/resolve-fallbacks.ts +19 -4
- package/src/shared/rpc-types.ts +2 -0
- package/src/shared/rpc-utils.test.ts +32 -0
- package/src/shared/rpc-utils.ts +38 -13
- package/src/shared/status-detail-text.test.ts +82 -0
- package/src/shared/status-detail-text.ts +57 -0
- package/src/shared/subagent-runner.ts +3 -1
- package/src/tui/data/context-db.ts +1 -0
- package/src/tui-compiled/data/context-db.ts +1 -0
- package/dist/index-td9depx5.js +0 -46
- package/dist/resolve-fallbacks-qf0ekmz9.js +0 -11
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalizeModelEntry,
|
|
5
|
+
resolveDreamerTaskModel,
|
|
6
|
+
resolveFallbackEntries,
|
|
7
|
+
resolveHistorianModel,
|
|
8
|
+
} from "./model-resolution";
|
|
9
|
+
|
|
10
|
+
describe("model-resolution", () => {
|
|
11
|
+
test("normalizes string and object entries to the same model identity", () => {
|
|
12
|
+
expect(normalizeModelEntry("anthropic/sonnet", "opencode")).toEqual({
|
|
13
|
+
model: "anthropic/sonnet",
|
|
14
|
+
});
|
|
15
|
+
expect(
|
|
16
|
+
normalizeModelEntry({ model: "anthropic/sonnet", variant: "high" }, "opencode"),
|
|
17
|
+
).toEqual({ model: "anthropic/sonnet", qualifier: "high" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("keeps ordered qualifier-distinct fallbacks and removes exact duplicates", () => {
|
|
21
|
+
expect(
|
|
22
|
+
resolveFallbackEntries(
|
|
23
|
+
[
|
|
24
|
+
{ model: "anthropic/sonnet", variant: "low" },
|
|
25
|
+
{ model: "anthropic/sonnet", variant: "high" },
|
|
26
|
+
{ model: "anthropic/sonnet", variant: "low" },
|
|
27
|
+
"google/flash",
|
|
28
|
+
"google/flash",
|
|
29
|
+
],
|
|
30
|
+
"opencode",
|
|
31
|
+
),
|
|
32
|
+
).toEqual([
|
|
33
|
+
{ model: "anthropic/sonnet", qualifier: "low" },
|
|
34
|
+
{ model: "anthropic/sonnet", qualifier: "high" },
|
|
35
|
+
{ model: "google/flash" },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("does not inherit a block qualifier into unqualified fallbacks", () => {
|
|
40
|
+
const config = {
|
|
41
|
+
historian: {
|
|
42
|
+
opencode: {
|
|
43
|
+
model: "open/primary",
|
|
44
|
+
fallback_models: ["open/fallback"],
|
|
45
|
+
variant: "primary-only",
|
|
46
|
+
},
|
|
47
|
+
pi: {
|
|
48
|
+
model: "pi/primary",
|
|
49
|
+
fallback_models: ["pi/fallback"],
|
|
50
|
+
thinking_level: "high",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Inheritance is deliberately absent because a fallback model may not support the
|
|
56
|
+
// qualifier selected for the primary model.
|
|
57
|
+
expect(resolveHistorianModel(config, "opencode")).toEqual({
|
|
58
|
+
primary: { model: "open/primary", qualifier: "primary-only" },
|
|
59
|
+
fallbacks: [{ model: "open/fallback" }],
|
|
60
|
+
});
|
|
61
|
+
expect(resolveHistorianModel(config, "pi")).toEqual({
|
|
62
|
+
primary: { model: "pi/primary", qualifier: "high" },
|
|
63
|
+
fallbacks: [{ model: "pi/fallback" }],
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("reads historian attempts only from the requested harness", () => {
|
|
68
|
+
const config = {
|
|
69
|
+
historian: {
|
|
70
|
+
model: "flat/ignored",
|
|
71
|
+
opencode: {
|
|
72
|
+
model: { model: "open/code", variant: "oc-primary" },
|
|
73
|
+
fallback_models: [{ model: "open/fallback", variant: "oc-fallback" }],
|
|
74
|
+
},
|
|
75
|
+
pi: {
|
|
76
|
+
model: { model: "pi/model", thinking_level: "high" },
|
|
77
|
+
fallback_models: [{ model: "pi/fallback", thinking_level: "max" }],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
expect(resolveHistorianModel(config, "opencode")).toEqual({
|
|
83
|
+
primary: { model: "open/code", qualifier: "oc-primary" },
|
|
84
|
+
fallbacks: [{ model: "open/fallback", qualifier: "oc-fallback" }],
|
|
85
|
+
});
|
|
86
|
+
expect(resolveHistorianModel(config, "pi")).toEqual({
|
|
87
|
+
primary: { model: "pi/model", qualifier: "high" },
|
|
88
|
+
fallbacks: [{ model: "pi/fallback", qualifier: "max" }],
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("resolves ordinary task model and scheduling without crossing harnesses", () => {
|
|
93
|
+
const config = {
|
|
94
|
+
dreamer: {
|
|
95
|
+
tasks: {
|
|
96
|
+
curate: { schedule: "0 4 * * *", promotion_threshold: 3 },
|
|
97
|
+
},
|
|
98
|
+
opencode: {
|
|
99
|
+
model: { model: "open/default", variant: "default-variant" },
|
|
100
|
+
tasks: {
|
|
101
|
+
curate: {
|
|
102
|
+
model: { model: "open/task", variant: "task-variant" },
|
|
103
|
+
fallback_models: [
|
|
104
|
+
"open/bare",
|
|
105
|
+
{ model: "open/qualified", variant: "fb" },
|
|
106
|
+
],
|
|
107
|
+
timeout_minutes: 12,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
pi: {
|
|
112
|
+
model: { model: "pi/default", thinking_level: "high" },
|
|
113
|
+
tasks: {
|
|
114
|
+
curate: {
|
|
115
|
+
model: { model: "pi/task", thinking_level: "max" },
|
|
116
|
+
fallback_models: [{ model: "pi/fallback", thinking_level: "minimal" }],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
expect(resolveDreamerTaskModel({ config, harness: "opencode", task: "curate" })).toEqual({
|
|
124
|
+
primary: { model: "open/task", qualifier: "task-variant" },
|
|
125
|
+
fallbacks: [{ model: "open/bare" }, { model: "open/qualified", qualifier: "fb" }],
|
|
126
|
+
schedule: "0 4 * * *",
|
|
127
|
+
timeoutMinutes: 12,
|
|
128
|
+
promotionThreshold: 3,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("uses mural model between compress-cues task and harness default", () => {
|
|
133
|
+
const config = {
|
|
134
|
+
dreamer: {
|
|
135
|
+
tasks: { "compress-cues": { schedule: "0 4 * * *" } },
|
|
136
|
+
opencode: {
|
|
137
|
+
model: { model: "open/default", variant: "default-variant" },
|
|
138
|
+
tasks: { "compress-cues": { variant: "task-local-must-not-leak" } },
|
|
139
|
+
},
|
|
140
|
+
pi: {
|
|
141
|
+
model: { model: "pi/default", thinking_level: "high" },
|
|
142
|
+
tasks: { "compress-cues": {} },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
expect(
|
|
148
|
+
resolveDreamerTaskModel({
|
|
149
|
+
config,
|
|
150
|
+
harness: "opencode",
|
|
151
|
+
task: "compress-cues",
|
|
152
|
+
muralModel: "mural/model",
|
|
153
|
+
}),
|
|
154
|
+
).toMatchObject({
|
|
155
|
+
primary: { model: "mural/model", qualifier: "default-variant" },
|
|
156
|
+
fallbacks: [],
|
|
157
|
+
});
|
|
158
|
+
expect(
|
|
159
|
+
resolveDreamerTaskModel({
|
|
160
|
+
config,
|
|
161
|
+
harness: "pi",
|
|
162
|
+
task: "compress-cues",
|
|
163
|
+
muralModel: "mural/model",
|
|
164
|
+
}),
|
|
165
|
+
).toMatchObject({
|
|
166
|
+
primary: { model: "mural/model", qualifier: "high" },
|
|
167
|
+
fallbacks: [],
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
export type ModelHarness = "opencode" | "pi";
|
|
2
|
+
|
|
3
|
+
export interface ResolvedModelEntry {
|
|
4
|
+
/** Canonical provider/model reference used for identity and model selection. */
|
|
5
|
+
model: string;
|
|
6
|
+
/** OpenCode variant or Pi thinking level, selected for the active entry only. */
|
|
7
|
+
qualifier?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ModelInput = string | ResolvedModelEntry;
|
|
11
|
+
|
|
12
|
+
type ConfigRecord = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
function asRecord(value: unknown): ConfigRecord | undefined {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
16
|
+
? (value as ConfigRecord)
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readString(value: unknown): string | undefined {
|
|
21
|
+
if (typeof value !== "string") return undefined;
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasOwn(record: ConfigRecord, key: string): boolean {
|
|
27
|
+
return Object.hasOwn(record, key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Collapses the string and object spellings of a harness model entry into the
|
|
32
|
+
* same model identity. Qualifiers stay separate because they alter an attempt,
|
|
33
|
+
* not the configured model identity.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeModelEntry(
|
|
36
|
+
entry: unknown,
|
|
37
|
+
harness: ModelHarness,
|
|
38
|
+
): ResolvedModelEntry | undefined {
|
|
39
|
+
if (typeof entry === "string") {
|
|
40
|
+
const model = readString(entry);
|
|
41
|
+
return model ? { model } : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const objectEntry = asRecord(entry);
|
|
45
|
+
if (!objectEntry) return undefined;
|
|
46
|
+
const model = readString(objectEntry.model);
|
|
47
|
+
if (!model) return undefined;
|
|
48
|
+
|
|
49
|
+
const qualifier = readString(
|
|
50
|
+
objectEntry[harness === "opencode" ? "variant" : "thinking_level"],
|
|
51
|
+
);
|
|
52
|
+
return qualifier ? { model, qualifier } : { model };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isValidModelReference(entry: ResolvedModelEntry): boolean {
|
|
56
|
+
const slash = entry.model.indexOf("/");
|
|
57
|
+
return slash > 0 && slash < entry.model.length - 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sameAttempt(a: ResolvedModelEntry, b: ResolvedModelEntry): boolean {
|
|
61
|
+
return a.model === b.model && a.qualifier === b.qualifier;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolves a configured fallback list in declaration order. Attempts are
|
|
66
|
+
* deduplicated by model plus qualifier: the same model with distinct
|
|
67
|
+
* qualifiers remains two intentional attempts.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveFallbackEntries(
|
|
70
|
+
value: unknown,
|
|
71
|
+
harness: ModelHarness,
|
|
72
|
+
): ResolvedModelEntry[] {
|
|
73
|
+
const values = Array.isArray(value) ? value : value === undefined ? [] : [value];
|
|
74
|
+
const resolved: ResolvedModelEntry[] = [];
|
|
75
|
+
for (const value of values) {
|
|
76
|
+
const entry = normalizeModelEntry(value, harness);
|
|
77
|
+
if (!entry || !isValidModelReference(entry)) continue;
|
|
78
|
+
if (resolved.some((candidate) => sameAttempt(candidate, entry))) continue;
|
|
79
|
+
resolved.push(entry);
|
|
80
|
+
}
|
|
81
|
+
return resolved;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolvePrimaryEntry(args: {
|
|
85
|
+
entry: unknown;
|
|
86
|
+
defaultQualifier: unknown;
|
|
87
|
+
harness: ModelHarness;
|
|
88
|
+
}): ResolvedModelEntry | undefined {
|
|
89
|
+
const entry = normalizeModelEntry(args.entry, args.harness);
|
|
90
|
+
if (!entry) return undefined;
|
|
91
|
+
const qualifier = entry.qualifier ?? readString(args.defaultQualifier);
|
|
92
|
+
return qualifier ? { ...entry, qualifier } : entry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ResolvedHistorianModel {
|
|
96
|
+
primary?: ResolvedModelEntry;
|
|
97
|
+
fallbacks: ResolvedModelEntry[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve historian model attempts from exactly one harness block. Flat keys
|
|
102
|
+
* and the other harness are deliberately never consulted after cutover.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveHistorianModel(
|
|
105
|
+
config: unknown,
|
|
106
|
+
harness: ModelHarness,
|
|
107
|
+
): ResolvedHistorianModel {
|
|
108
|
+
const historian = asRecord(asRecord(config)?.historian);
|
|
109
|
+
const block = asRecord(historian?.[harness]);
|
|
110
|
+
if (!block) return { fallbacks: [] };
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
primary: resolvePrimaryEntry({
|
|
114
|
+
entry: block.model,
|
|
115
|
+
defaultQualifier: block[harness === "opencode" ? "variant" : "thinking_level"],
|
|
116
|
+
harness,
|
|
117
|
+
}),
|
|
118
|
+
fallbacks: resolveFallbackEntries(block.fallback_models, harness),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ResolvedDreamerTaskModel {
|
|
123
|
+
primary?: ResolvedModelEntry;
|
|
124
|
+
fallbacks: ResolvedModelEntry[];
|
|
125
|
+
schedule?: string;
|
|
126
|
+
timeoutMinutes?: number;
|
|
127
|
+
promotionThreshold?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve a dreamer task from one harness subtree. Model selection is task →
|
|
132
|
+
* harness default, except compress-cues inserts its harness-independent mural
|
|
133
|
+
* string between those two rungs. Scheduling remains at dreamer.tasks.<task>.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveDreamerTaskModel(args: {
|
|
136
|
+
config: unknown;
|
|
137
|
+
harness: ModelHarness;
|
|
138
|
+
task: string;
|
|
139
|
+
muralModel?: unknown;
|
|
140
|
+
}): ResolvedDreamerTaskModel {
|
|
141
|
+
const dreamer = asRecord(asRecord(args.config)?.dreamer);
|
|
142
|
+
const harnessBlock = asRecord(dreamer?.[args.harness]);
|
|
143
|
+
const taskBlock = asRecord(asRecord(harnessBlock?.tasks)?.[args.task]);
|
|
144
|
+
const schedulingTask = asRecord(asRecord(dreamer?.tasks)?.[args.task]);
|
|
145
|
+
if (!harnessBlock) {
|
|
146
|
+
return {
|
|
147
|
+
fallbacks: [],
|
|
148
|
+
schedule: readString(schedulingTask?.schedule),
|
|
149
|
+
timeoutMinutes:
|
|
150
|
+
typeof taskBlock?.timeout_minutes === "number"
|
|
151
|
+
? taskBlock.timeout_minutes
|
|
152
|
+
: undefined,
|
|
153
|
+
promotionThreshold:
|
|
154
|
+
typeof schedulingTask?.promotion_threshold === "number"
|
|
155
|
+
? schedulingTask.promotion_threshold
|
|
156
|
+
: undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const taskQualifier = taskBlock?.[args.harness === "opencode" ? "variant" : "thinking_level"];
|
|
161
|
+
// A mural string borrows the harness default's qualifier whether that
|
|
162
|
+
// qualifier is written beside `model` or inside the default entry object.
|
|
163
|
+
const harnessQualifier =
|
|
164
|
+
harnessBlock[args.harness === "opencode" ? "variant" : "thinking_level"] ??
|
|
165
|
+
normalizeModelEntry(harnessBlock.model, args.harness)?.qualifier;
|
|
166
|
+
const taskHasModel = taskBlock !== undefined && hasOwn(taskBlock, "model");
|
|
167
|
+
const usesMuralModel =
|
|
168
|
+
!taskHasModel && args.task === "compress-cues" && Boolean(readString(args.muralModel));
|
|
169
|
+
const primarySource = taskHasModel
|
|
170
|
+
? taskBlock?.model
|
|
171
|
+
: usesMuralModel
|
|
172
|
+
? args.muralModel
|
|
173
|
+
: harnessBlock.model;
|
|
174
|
+
const fallbackSource =
|
|
175
|
+
taskBlock !== undefined && hasOwn(taskBlock, "fallback_models")
|
|
176
|
+
? taskBlock.fallback_models
|
|
177
|
+
: harnessBlock.fallback_models;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
primary: resolvePrimaryEntry({
|
|
181
|
+
entry: primarySource,
|
|
182
|
+
// mural.model is a plain string, so its qualifier is always the
|
|
183
|
+
// executing harness default rather than a task-local default.
|
|
184
|
+
defaultQualifier: usesMuralModel
|
|
185
|
+
? harnessQualifier
|
|
186
|
+
: (taskQualifier ?? harnessQualifier),
|
|
187
|
+
harness: args.harness,
|
|
188
|
+
}),
|
|
189
|
+
fallbacks: resolveFallbackEntries(fallbackSource, args.harness),
|
|
190
|
+
schedule: readString(schedulingTask?.schedule),
|
|
191
|
+
timeoutMinutes:
|
|
192
|
+
typeof taskBlock?.timeout_minutes === "number" ? taskBlock.timeout_minutes : undefined,
|
|
193
|
+
promotionThreshold:
|
|
194
|
+
typeof schedulingTask?.promotion_threshold === "number"
|
|
195
|
+
? schedulingTask.promotion_threshold
|
|
196
|
+
: undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Keep non-model agent settings at their original level while replacing only
|
|
202
|
+
* OpenCode's model-resolution fields with the executing harness's primary.
|
|
203
|
+
*/
|
|
204
|
+
export function resolveOpenCodeAgentOverrides(agent: unknown): Record<string, unknown> {
|
|
205
|
+
const source = asRecord(agent);
|
|
206
|
+
if (!source) return {};
|
|
207
|
+
const {
|
|
208
|
+
model: _model,
|
|
209
|
+
fallback_models: _fallbackModels,
|
|
210
|
+
variant: _variant,
|
|
211
|
+
thinking_level: _thinking,
|
|
212
|
+
opencode,
|
|
213
|
+
pi,
|
|
214
|
+
tasks,
|
|
215
|
+
...rest
|
|
216
|
+
} = source;
|
|
217
|
+
const primary = resolvePrimaryEntry({
|
|
218
|
+
entry: asRecord(opencode)?.model,
|
|
219
|
+
defaultQualifier: asRecord(opencode)?.variant,
|
|
220
|
+
harness: "opencode",
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
...rest,
|
|
224
|
+
...(primary ? { model: primary.model } : {}),
|
|
225
|
+
...(primary?.qualifier ? { variant: primary.qualifier } : {}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -56,6 +56,33 @@ describe("promptSyncWithModelSuggestionRetry", () => {
|
|
|
56
56
|
expect(prompt).toHaveBeenCalledTimes(1);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
test("each active fallback sends only its own variant", async () => {
|
|
60
|
+
const prompt = mock(async () => {
|
|
61
|
+
if (prompt.mock.calls.length <= 2) throw new Error("attempt failed");
|
|
62
|
+
return {};
|
|
63
|
+
});
|
|
64
|
+
const client = createClient(prompt);
|
|
65
|
+
|
|
66
|
+
await promptSyncWithModelSuggestionRetry(
|
|
67
|
+
client,
|
|
68
|
+
{
|
|
69
|
+
path: { id: "ses-test" },
|
|
70
|
+
body: { model: { providerID: "anthropic", modelID: "primary" }, variant: "high" },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
fallbackModels: [{ model: "anthropic/fallback", qualifier: "low" }, "google/bare"],
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect((prompt.mock.calls[1]?.[0] as PromptCall).body).toMatchObject({
|
|
78
|
+
model: { providerID: "anthropic", modelID: "fallback" },
|
|
79
|
+
variant: "low",
|
|
80
|
+
});
|
|
81
|
+
const bareFallbackBody = (prompt.mock.calls[2]?.[0] as PromptCall).body;
|
|
82
|
+
expect(bareFallbackBody.model).toEqual({ providerID: "google", modelID: "bare" });
|
|
83
|
+
expect(bareFallbackBody.variant).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
59
86
|
test("primary fails, fallback[0] succeeds", async () => {
|
|
60
87
|
const prompt = mock(async () => {
|
|
61
88
|
if (prompt.mock.calls.length === 1) throw new Error("primary failed");
|
|
@@ -2,7 +2,8 @@ import type { createOpencodeClient } from "@opencode-ai/sdk";
|
|
|
2
2
|
|
|
3
3
|
import { detectOverflow } from "../features/magic-context/overflow-detection";
|
|
4
4
|
import { log } from "./logger";
|
|
5
|
-
import {
|
|
5
|
+
import type { ModelInput } from "./model-resolution";
|
|
6
|
+
import { parseProviderModel, toModelEntry } from "./resolve-fallbacks";
|
|
6
7
|
|
|
7
8
|
type Client = ReturnType<typeof createOpencodeClient>;
|
|
8
9
|
|
|
@@ -13,6 +14,8 @@ const ABORT_CALL_TIMEOUT_MS = 3000;
|
|
|
13
14
|
|
|
14
15
|
export type PromptBody = {
|
|
15
16
|
model?: { providerID: string; modelID: string };
|
|
17
|
+
/** OpenCode variant; Pi's facade also uses it as the active thinking level. */
|
|
18
|
+
variant?: string;
|
|
16
19
|
[key: string]: unknown;
|
|
17
20
|
};
|
|
18
21
|
|
|
@@ -64,7 +67,7 @@ export interface PromptRetryOptions {
|
|
|
64
67
|
* - On all-failed, the LAST error is thrown (matches legacy behavior when
|
|
65
68
|
* `fallbackModels` is empty).
|
|
66
69
|
*/
|
|
67
|
-
fallbackModels?: readonly
|
|
70
|
+
fallbackModels?: readonly ModelInput[];
|
|
68
71
|
/**
|
|
69
72
|
* Identifier for structured logging (e.g. "dreamer:consolidate",
|
|
70
73
|
* "historian", "compressor", "sidekick"). Helps correlate fallback
|
|
@@ -394,16 +397,19 @@ export async function promptSyncWithModelSuggestionRetry(
|
|
|
394
397
|
|
|
395
398
|
// Iterate fallbacks.
|
|
396
399
|
for (let i = 0; i < fallbacks.length; i += 1) {
|
|
397
|
-
const
|
|
400
|
+
const fallback = toModelEntry(fallbacks[i]);
|
|
401
|
+
const parsed = fallback ? parseProviderModel(fallback.model) : null;
|
|
398
402
|
if (!parsed) {
|
|
399
|
-
log(`[${callContext}] skipping invalid fallback spec: ${fallbacks[i]}`);
|
|
403
|
+
log(`[${callContext}] skipping invalid fallback spec: ${String(fallbacks[i])}`);
|
|
400
404
|
continue;
|
|
401
405
|
}
|
|
402
406
|
|
|
403
407
|
const label = `${parsed.providerID}/${parsed.modelID}`;
|
|
408
|
+
const { variant: _primaryVariant, ...bodyWithoutPrimaryVariant } = baseBody;
|
|
404
409
|
const attemptArgs = copyPromptArgs(baseArgs, {
|
|
405
|
-
...
|
|
410
|
+
...bodyWithoutPrimaryVariant,
|
|
406
411
|
model: parsed,
|
|
412
|
+
...(fallback?.qualifier ? { variant: fallback.qualifier } : {}),
|
|
407
413
|
});
|
|
408
414
|
|
|
409
415
|
try {
|
|
@@ -429,7 +435,7 @@ export async function promptSyncWithModelSuggestionRetry(
|
|
|
429
435
|
// caller's report (e.g. /ctx-dream tasks_json) still surfaces a real
|
|
430
436
|
// diagnostic.
|
|
431
437
|
log(
|
|
432
|
-
`[${callContext}] all models exhausted; tried: ${[explicitPrimaryLabel, ...fallbacks].join(", ")}; last error: ${shortErr(lastError)}`,
|
|
438
|
+
`[${callContext}] all models exhausted; tried: ${[explicitPrimaryLabel, ...fallbacks.map((fallback) => toModelEntry(fallback)?.model ?? String(fallback))].join(", ")}; last error: ${shortErr(lastError)}`,
|
|
433
439
|
);
|
|
434
440
|
throw lastError ?? new Error("All fallback models failed");
|
|
435
441
|
}
|
|
@@ -515,16 +521,19 @@ export async function promptSyncWithValidatedOutputRetry<TOutput, TValidated = T
|
|
|
515
521
|
}
|
|
516
522
|
|
|
517
523
|
for (let i = 0; i < fallbacks.length; i += 1) {
|
|
518
|
-
const
|
|
524
|
+
const fallback = toModelEntry(fallbacks[i]);
|
|
525
|
+
const parsed = fallback ? parseProviderModel(fallback.model) : null;
|
|
519
526
|
if (!parsed) {
|
|
520
|
-
log(`[${callContext}] skipping invalid fallback spec: ${fallbacks[i]}`);
|
|
527
|
+
log(`[${callContext}] skipping invalid fallback spec: ${String(fallbacks[i])}`);
|
|
521
528
|
continue;
|
|
522
529
|
}
|
|
523
530
|
|
|
524
531
|
const label = `${parsed.providerID}/${parsed.modelID}`;
|
|
532
|
+
const { variant: _primaryVariant, ...bodyWithoutPrimaryVariant } = baseBody;
|
|
525
533
|
const attemptArgs = copyPromptArgs(baseArgs, {
|
|
526
|
-
...
|
|
534
|
+
...bodyWithoutPrimaryVariant,
|
|
527
535
|
model: parsed,
|
|
536
|
+
...(fallback?.qualifier ? { variant: fallback.qualifier } : {}),
|
|
528
537
|
});
|
|
529
538
|
const attempt: PromptAttemptInfo = {
|
|
530
539
|
label,
|
|
@@ -563,7 +572,7 @@ export async function promptSyncWithValidatedOutputRetry<TOutput, TValidated = T
|
|
|
563
572
|
}
|
|
564
573
|
|
|
565
574
|
log(
|
|
566
|
-
`[${callContext}] all models exhausted; tried: ${[explicitPrimaryLabel, ...fallbacks].join(", ")}; original error: ${shortErr(firstError)}; last error: ${shortErr(lastError)}`,
|
|
575
|
+
`[${callContext}] all models exhausted; tried: ${[explicitPrimaryLabel, ...fallbacks.map((fallback) => toModelEntry(fallback)?.model ?? String(fallback))].join(", ")}; original error: ${shortErr(firstError)}; last error: ${shortErr(lastError)}`,
|
|
567
576
|
);
|
|
568
577
|
throw firstError ?? lastError ?? new Error("All fallback models failed validation");
|
|
569
578
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { parseProviderModel, resolveFallbackChain } from "./resolve-fallbacks";
|
|
3
|
+
import { modelBodyField, parseProviderModel, resolveFallbackChain } from "./resolve-fallbacks";
|
|
4
4
|
|
|
5
5
|
describe("resolveFallbackChain", () => {
|
|
6
6
|
// Policy: user-config-only. There is NO builtin provider-agnostic chain —
|
|
@@ -62,6 +62,18 @@ describe("resolveFallbackChain", () => {
|
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
describe("modelBodyField", () => {
|
|
66
|
+
test("sends the active entry variant and leaves a bare entry unqualified", () => {
|
|
67
|
+
expect(modelBodyField({ model: "anthropic/claude-sonnet", qualifier: "high" })).toEqual({
|
|
68
|
+
model: { providerID: "anthropic", modelID: "claude-sonnet" },
|
|
69
|
+
variant: "high",
|
|
70
|
+
});
|
|
71
|
+
expect(modelBodyField("google/gemini-flash")).toEqual({
|
|
72
|
+
model: { providerID: "google", modelID: "gemini-flash" },
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
65
77
|
describe("parseProviderModel", () => {
|
|
66
78
|
test("parses standard provider/model", () => {
|
|
67
79
|
expect(parseProviderModel("anthropic/claude-sonnet-4-6")).toEqual({
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ModelInput, ResolvedModelEntry } from "./model-resolution";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Resolve the fallback model list to attempt for a hidden-agent (historian /
|
|
3
5
|
* dreamer / sidekick) call when its configured primary fails.
|
|
@@ -71,10 +73,23 @@ export function parseProviderModel(spec: string): { providerID: string; modelID:
|
|
|
71
73
|
* unparseable (the session falls back to its default model). Spread into a
|
|
72
74
|
* `client.session.prompt` body.
|
|
73
75
|
*/
|
|
74
|
-
export function modelBodyField(spec:
|
|
76
|
+
export function modelBodyField(spec: ModelInput | undefined): {
|
|
75
77
|
model?: { providerID: string; modelID: string };
|
|
78
|
+
variant?: string;
|
|
76
79
|
} {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
const entry = toModelEntry(spec);
|
|
81
|
+
if (!entry) return {};
|
|
82
|
+
const parsed = parseProviderModel(entry.model);
|
|
83
|
+
return parsed
|
|
84
|
+
? { model: parsed, ...(entry.qualifier ? { variant: entry.qualifier } : {}) }
|
|
85
|
+
: {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Convert legacy string call sites and resolved entries into one active attempt shape. */
|
|
89
|
+
export function toModelEntry(spec: ModelInput | undefined): ResolvedModelEntry | undefined {
|
|
90
|
+
if (typeof spec === "string") {
|
|
91
|
+
const model = spec.trim();
|
|
92
|
+
return model ? { model } : undefined;
|
|
93
|
+
}
|
|
94
|
+
return spec;
|
|
80
95
|
}
|
package/src/shared/rpc-types.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface SidebarSnapshot {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
export interface StatusDetail extends SidebarSnapshot {
|
|
144
|
+
/** User-owned model profile selected for this project, or null for the base config. */
|
|
145
|
+
activeProfile: string | null;
|
|
144
146
|
tagCounter: number;
|
|
145
147
|
activeTags: number;
|
|
146
148
|
droppedTags: number;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
4
4
|
import type { execFileSync } from "node:child_process";
|
|
5
5
|
import type { readFileSync } from "node:fs";
|
|
6
|
+
|
|
6
7
|
import {
|
|
7
8
|
__resetRpcIdentityTestHooks,
|
|
8
9
|
__setRpcIdentityTestHooks,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
isPidAlive,
|
|
13
14
|
isPidIdentityPlausible,
|
|
14
15
|
type RpcPortFileRecord,
|
|
16
|
+
readProcessProbeEvidence,
|
|
15
17
|
} from "./rpc-utils";
|
|
16
18
|
|
|
17
19
|
const PID = 1234;
|
|
@@ -210,6 +212,36 @@ describe("isPidAlive", () => {
|
|
|
210
212
|
});
|
|
211
213
|
});
|
|
212
214
|
|
|
215
|
+
describe("readProcessProbeEvidence", () => {
|
|
216
|
+
test("captures the same Linux start-time and command probes used by the guard", () => {
|
|
217
|
+
__setRpcIdentityTestHooks({
|
|
218
|
+
platform: "linux",
|
|
219
|
+
nowMs: () => NOW_MS,
|
|
220
|
+
readFileSync: linuxFiles({
|
|
221
|
+
[`/proc/${PID}/stat`]: procStat(10_000),
|
|
222
|
+
"/proc/uptime": `${UPTIME_SECONDS}.0 0.0`,
|
|
223
|
+
[`/proc/${PID}/cmdline`]:
|
|
224
|
+
"/usr/local/bin/opencode\u0000--directory\u0000/home/alice/project",
|
|
225
|
+
}),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(readProcessProbeEvidence(PID)).toEqual({
|
|
229
|
+
startTime: 1_100_000,
|
|
230
|
+
commandLine: "/usr/local/bin/opencode\u0000--directory\u0000/home/alice/project",
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("returns unavailable fields instead of throwing when probes fail", () => {
|
|
235
|
+
__setRpcIdentityTestHooks({
|
|
236
|
+
platform: "linux",
|
|
237
|
+
readFileSync: linuxFiles({}),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(() => readProcessProbeEvidence(PID)).not.toThrow();
|
|
241
|
+
expect(readProcessProbeEvidence(PID)).toEqual({ startTime: null, commandLine: null });
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
213
245
|
describe("isPidIdentityPlausible", () => {
|
|
214
246
|
test("rejects a reused Linux PID when proc start time is substantially newer", () => {
|
|
215
247
|
const readPaths: string[] = [];
|