pi-subagents 0.66.0 → 0.68.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/CHANGELOG.md +138 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +43 -15
- package/docs/configuration.md +67 -23
- package/docs/extension-api.md +38 -19
- package/docs/missions.md +10 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +22 -12
- package/docs/standalone-background.md +59 -0
- package/docs/tool-reference.md +33 -20
- package/docs/watchdog.md +39 -10
- package/docs/workflows.md +37 -13
- package/index.ts +5 -2
- package/inspector-runner.mjs +2 -2
- package/package.json +4 -2
- package/prompts/parallel-review.md +1 -1
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +32 -0
- package/skills/pi-subagents/SKILL.md +32 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +11 -9
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +190 -71
- package/src/agents/builtin-names.ts +1 -0
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +94 -59
- package/src/api/required-child-extensions.ts +6 -0
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +66 -4
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +62 -39
- package/src/extension/public-execution.ts +7 -5
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +81 -81
- package/src/extension/tool-description.ts +30 -82
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +44 -31
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +348 -176
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +17 -12
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +23 -10
- package/src/runs/background/binary-bootstrap.ts +38 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +105 -7
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +81 -33
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +12 -33
- package/src/runs/background/runner-child-launch.ts +6 -1
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +51 -18
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +441 -304
- package/src/runs/background/subagent-wait.ts +176 -28
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +134 -248
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +374 -178
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +38 -11
- package/src/runs/shared/async-status-projection.ts +127 -33
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +28 -5
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +8 -1
- package/src/runs/shared/child-session.ts +127 -52
- package/src/runs/shared/child-tool-plan.ts +142 -11
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +45 -15
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +68 -8
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +74 -30
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -7
- package/src/slash/subagents-admin.ts +24 -13
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +168 -37
- package/src/watchdog/child-status.ts +28 -28
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +21 -1
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +10 -2
- package/src/watchdog/register-main.ts +39 -35
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +123 -74
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +100 -27
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +3 -0
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +5 -3
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +279 -10
- package/src/workflows/workflow-checklist.ts +2 -2
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/runner-server-preload.mjs +0 -13
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
4
|
-
import { TEMP_ROOT_DIR } from "../../shared/types.ts";
|
|
5
|
-
import { getAgentDir } from "../../shared/utils.ts";
|
|
6
|
-
|
|
7
|
-
export const EXCLUSIONS_PATH_ENV = "PI_MODEL_EXCLUSIONS_PATH";
|
|
8
|
-
|
|
9
|
-
type ModelExclusionTarget = { modelId: string; provider?: string } | { provider: string; modelId?: never };
|
|
10
|
-
|
|
11
|
-
export type ModelExclusion = ModelExclusionTarget & {
|
|
12
|
-
reason?: string;
|
|
13
|
-
recordedAt: number;
|
|
14
|
-
expiresAt: number;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type RecordModelFailureOptions = ModelExclusionTarget & {
|
|
18
|
-
reason?: string;
|
|
19
|
-
ttlMs?: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
let exclusions: ModelExclusion[] = [];
|
|
23
|
-
let loaded = false;
|
|
24
|
-
/** Default duration for a new model exclusion when no per-record TTL is supplied. */
|
|
25
|
-
export const DEFAULT_MODEL_EXCLUSION_TTL_MS = 24 * 60 * 60_000;
|
|
26
|
-
/** Keeps a new expiry safely below JavaScript's maximum Date timestamp. */
|
|
27
|
-
export const MAX_MODEL_EXCLUSION_TTL_MS = 8_000_000_000_000_000;
|
|
28
|
-
const MAX_DATE_TIMESTAMP_MS = 8_640_000_000_000_000;
|
|
29
|
-
let defaultTTLMs = DEFAULT_MODEL_EXCLUSION_TTL_MS;
|
|
30
|
-
let loadedTTLCeilingMs: number | undefined;
|
|
31
|
-
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
32
|
-
let persistSeq = 0;
|
|
33
|
-
|
|
34
|
-
const AUTH_FAILURE_PATTERNS = [
|
|
35
|
-
/auth(?:entication)?/i,
|
|
36
|
-
/unauthori[sz]ed/i,
|
|
37
|
-
/forbidden/i,
|
|
38
|
-
/api key/i,
|
|
39
|
-
/token expired/i,
|
|
40
|
-
/invalid key/i,
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
function isAuthModelExclusion(entry: ModelExclusion): boolean {
|
|
44
|
-
const reason = entry.reason;
|
|
45
|
-
return typeof reason === "string" && AUTH_FAILURE_PATTERNS.some((pattern) => pattern.test(reason));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getAuthStoreMtimeMs(): number | undefined {
|
|
49
|
-
const authStorePath = path.join(getAgentDir(), "auth.json");
|
|
50
|
-
try {
|
|
51
|
-
const stats = fs.statSync(authStorePath);
|
|
52
|
-
return stats.isFile() && Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : undefined;
|
|
53
|
-
} catch (error) {
|
|
54
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
55
|
-
console.error(`[model-exclusions] Failed to stat Pi auth store at ${authStorePath}; preserving auth-related exclusions:`, error);
|
|
56
|
-
}
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function invalidateAuthExclusions(): void {
|
|
62
|
-
if (!exclusions.some(isAuthModelExclusion)) return;
|
|
63
|
-
const authStoreMtimeMs = getAuthStoreMtimeMs();
|
|
64
|
-
if (authStoreMtimeMs === undefined) return;
|
|
65
|
-
const retained = exclusions.filter((entry) => !isAuthModelExclusion(entry) || authStoreMtimeMs <= entry.recordedAt);
|
|
66
|
-
if (retained.length === exclusions.length) return;
|
|
67
|
-
exclusions = retained;
|
|
68
|
-
schedulePersist();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Override the default TTL applied to newly recorded model exclusions.
|
|
73
|
-
*
|
|
74
|
-
* @param ms Duration in milliseconds. Must be finite and positive.
|
|
75
|
-
* @returns Nothing.
|
|
76
|
-
*/
|
|
77
|
-
// TEST:test/unit/model-exclusions.test.ts[model exclusions — TTL expiry]
|
|
78
|
-
export function setDefaultTTL(ms: number, options?: { shortenExisting?: boolean }): void {
|
|
79
|
-
if (!Number.isFinite(ms) || ms <= 0 || ms > MAX_MODEL_EXCLUSION_TTL_MS) {
|
|
80
|
-
throw new Error(`Default model exclusion TTL must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}.`);
|
|
81
|
-
}
|
|
82
|
-
defaultTTLMs = ms;
|
|
83
|
-
loadedTTLCeilingMs = options?.shortenExisting ? ms : undefined;
|
|
84
|
-
if (loaded && loadedTTLCeilingMs !== undefined && shortenExclusionsToTTL(exclusions, loadedTTLCeilingMs, Date.now())) schedulePersist();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Resolve the persistence path. Honors PI_MODEL_EXCLUSIONS_PATH; defaults to
|
|
89
|
-
* <TEMP_ROOT_DIR>/model-exclusions.json. Resolved lazily so tests can point the
|
|
90
|
-
* store at an isolated location after module load.
|
|
91
|
-
*/
|
|
92
|
-
export function getExclusionsFilePath(): string {
|
|
93
|
-
const envPath = process.env[EXCLUSIONS_PATH_ENV];
|
|
94
|
-
if (typeof envPath === "string" && envPath.trim()) return envPath.trim();
|
|
95
|
-
return path.join(TEMP_ROOT_DIR, "model-exclusions.json");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Persist exclusions to disk immediately (atomic write via tmp + rename).
|
|
100
|
-
* The store otherwise debounces writes; call this when durability matters
|
|
101
|
-
* (and in tests).
|
|
102
|
-
*/
|
|
103
|
-
export function flushPersist(): void {
|
|
104
|
-
const file = getExclusionsFilePath();
|
|
105
|
-
try {
|
|
106
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
107
|
-
const tmpPath = `${file}.${process.pid}.${persistSeq++}.tmp`;
|
|
108
|
-
fs.writeFileSync(tmpPath, JSON.stringify({
|
|
109
|
-
version: 1,
|
|
110
|
-
exclusions: deduplicate(exclusions),
|
|
111
|
-
}, null, 2), "utf-8");
|
|
112
|
-
fs.renameSync(tmpPath, file);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error(`[model-exclusions] Failed to persist exclusions to ${file}:`, error);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function schedulePersist(): void {
|
|
119
|
-
if (persistTimer) clearTimeout(persistTimer);
|
|
120
|
-
persistTimer = setTimeout(() => {
|
|
121
|
-
persistTimer = null;
|
|
122
|
-
flushPersist();
|
|
123
|
-
}, 5000);
|
|
124
|
-
// Never hold the process open just to flush exclusions.
|
|
125
|
-
persistTimer.unref?.();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function ensureLoaded(): void {
|
|
129
|
-
if (loaded) return;
|
|
130
|
-
loaded = true;
|
|
131
|
-
try {
|
|
132
|
-
const raw = fs.readFileSync(getExclusionsFilePath(), "utf-8");
|
|
133
|
-
const data = JSON.parse(raw);
|
|
134
|
-
if (data.version === 1) {
|
|
135
|
-
if (!Array.isArray(data.exclusions)) throw new Error("Model exclusion store version 1 must contain an exclusions array.");
|
|
136
|
-
const now = Date.now();
|
|
137
|
-
let droppedInvalid = false;
|
|
138
|
-
exclusions = data.exclusions.flatMap((entry: unknown, index: number) => {
|
|
139
|
-
const result = readPersistedExclusion(entry, index);
|
|
140
|
-
if (!result.ok) {
|
|
141
|
-
droppedInvalid = true;
|
|
142
|
-
console.warn(`[model-exclusions] Ignoring invalid exclusion: ${result.message}`);
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
return [result.exclusion];
|
|
146
|
-
}).filter((e: ModelExclusion) => e.expiresAt > now);
|
|
147
|
-
const shortened = loadedTTLCeilingMs !== undefined && shortenExclusionsToTTL(exclusions, loadedTTLCeilingMs, now);
|
|
148
|
-
exclusions = deduplicate(exclusions);
|
|
149
|
-
if (droppedInvalid) flushPersist();
|
|
150
|
-
else if (shortened) schedulePersist();
|
|
151
|
-
}
|
|
152
|
-
invalidateAuthExclusions();
|
|
153
|
-
} catch (error) {
|
|
154
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
155
|
-
console.error(`[model-exclusions] Failed to load exclusions from ${getExclusionsFilePath()}:`, error);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
type PersistedExclusionRead =
|
|
161
|
-
| { ok: true; exclusion: ModelExclusion }
|
|
162
|
-
| { ok: false; message: string };
|
|
163
|
-
|
|
164
|
-
function invalidPersistedExclusion(index: number, message: string): PersistedExclusionRead {
|
|
165
|
-
return { ok: false, message: `Model exclusion store entry ${index} ${message}.` };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function readPersistedExclusion(entry: unknown, index: number): PersistedExclusionRead {
|
|
169
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return invalidPersistedExclusion(index, "must be an object");
|
|
170
|
-
const candidate = entry as { modelId?: unknown; provider?: unknown; reason?: unknown; recordedAt?: unknown; expiresAt?: unknown };
|
|
171
|
-
const { modelId, provider, reason } = candidate;
|
|
172
|
-
if (modelId !== undefined && (typeof modelId !== "string" || modelId.length === 0)) return invalidPersistedExclusion(index, "has an invalid modelId");
|
|
173
|
-
if (provider !== undefined && (typeof provider !== "string" || provider.length === 0)) return invalidPersistedExclusion(index, "has an invalid provider");
|
|
174
|
-
if (reason !== undefined && typeof reason !== "string") return invalidPersistedExclusion(index, "has an invalid reason");
|
|
175
|
-
const { recordedAt, expiresAt } = candidate;
|
|
176
|
-
if (typeof recordedAt !== "number" || !Number.isFinite(recordedAt) || recordedAt <= 0 || recordedAt > MAX_DATE_TIMESTAMP_MS) return invalidPersistedExclusion(index, "has an invalid recordedAt");
|
|
177
|
-
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt) || expiresAt <= 0 || expiresAt > MAX_DATE_TIMESTAMP_MS) return invalidPersistedExclusion(index, "has an invalid expiresAt");
|
|
178
|
-
const metadata = { ...(reason === undefined ? {} : { reason }), recordedAt, expiresAt };
|
|
179
|
-
if (modelId === undefined) {
|
|
180
|
-
if (provider === undefined) return invalidPersistedExclusion(index, "must include modelId or provider");
|
|
181
|
-
return { ok: true, exclusion: { provider, ...metadata } };
|
|
182
|
-
}
|
|
183
|
-
return { ok: true, exclusion: { modelId, ...(provider === undefined ? {} : { provider }), ...metadata } };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function dedupKey(entry: ModelExclusion): string {
|
|
187
|
-
return `${entry.provider ?? ""}|${entry.modelId ?? ""}`;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function deduplicate(items: ModelExclusion[]): ModelExclusion[] {
|
|
191
|
-
const map = new Map<string, ModelExclusion>();
|
|
192
|
-
for (const entry of items) {
|
|
193
|
-
const key = dedupKey(entry);
|
|
194
|
-
const existing = map.get(key);
|
|
195
|
-
if (!existing || entry.recordedAt > existing.recordedAt) {
|
|
196
|
-
map.set(key, entry);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return Array.from(map.values());
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Record a model failure as a temporary exclusion. While the exclusion is
|
|
204
|
-
* active, {@link isExcluded} returns true for the model (or for every model of
|
|
205
|
-
* the provider when modelId is omitted), and {@link filterFallbackCandidates}
|
|
206
|
-
* removes matching candidates from fallback lists.
|
|
207
|
-
*/
|
|
208
|
-
export function recordModelFailure(options: RecordModelFailureOptions): void {
|
|
209
|
-
ensureLoaded();
|
|
210
|
-
const ttl = options.ttlMs ?? defaultTTLMs;
|
|
211
|
-
const now = Date.now();
|
|
212
|
-
const target: ModelExclusionTarget = options.modelId !== undefined
|
|
213
|
-
? { modelId: options.modelId, ...(options.provider ? { provider: options.provider } : {}) }
|
|
214
|
-
: { provider: options.provider };
|
|
215
|
-
const exclusion: ModelExclusion = {
|
|
216
|
-
...target,
|
|
217
|
-
reason: options.reason ?? "runtime-failure",
|
|
218
|
-
recordedAt: now,
|
|
219
|
-
expiresAt: now + ttl,
|
|
220
|
-
};
|
|
221
|
-
exclusions.unshift(exclusion);
|
|
222
|
-
exclusions = deduplicate(exclusions);
|
|
223
|
-
if (exclusions.length > 200) exclusions.length = 200;
|
|
224
|
-
flushPersist();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Drop all expired exclusions from memory and schedule a persist.
|
|
229
|
-
*/
|
|
230
|
-
export function clearExpiredExclusions(): void {
|
|
231
|
-
ensureLoaded();
|
|
232
|
-
invalidateAuthExclusions();
|
|
233
|
-
prune(exclusions, Date.now());
|
|
234
|
-
schedulePersist();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Remove every exclusion (e.g. after the operator fixes credentials).
|
|
239
|
-
*/
|
|
240
|
-
export function clearExclusions(): void {
|
|
241
|
-
ensureLoaded();
|
|
242
|
-
exclusions.length = 0;
|
|
243
|
-
schedulePersist();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Whether an exclusion entry matches a candidate.
|
|
248
|
-
*
|
|
249
|
-
* Semantics:
|
|
250
|
-
* - Entry with modelId: model-specific exclusion. Matches only that modelId;
|
|
251
|
-
* when both the entry and the candidate carry a provider, the providers must
|
|
252
|
-
* also agree so `openai/gpt-4` does not exclude `github-copilot/gpt-4`.
|
|
253
|
-
* - Entry without modelId: provider-wide exclusion (e.g. quota or auth failure).
|
|
254
|
-
* Matches every model of that provider.
|
|
255
|
-
*/
|
|
256
|
-
function entryMatches(entry: ModelExclusion, candidateModelId: string, candidateProvider: string | undefined, now: number): boolean {
|
|
257
|
-
if (entry.expiresAt <= now) return false;
|
|
258
|
-
if (entry.modelId !== undefined) {
|
|
259
|
-
if (entry.modelId !== candidateModelId) return false;
|
|
260
|
-
return !entry.provider || !candidateProvider || entry.provider === candidateProvider;
|
|
261
|
-
}
|
|
262
|
-
return Boolean(entry.provider) && entry.provider === candidateProvider;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Whether a model (or its provider) is currently excluded.
|
|
267
|
-
*/
|
|
268
|
-
export function isExcluded(modelId: string, provider: string): boolean {
|
|
269
|
-
ensureLoaded();
|
|
270
|
-
invalidateAuthExclusions();
|
|
271
|
-
return exclusions.some((entry) => entryMatches(entry, modelId, provider, Date.now()));
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Return the active exclusion matching a full model id, if any.
|
|
276
|
-
*
|
|
277
|
-
* The caller uses this for hard-fail diagnostics; fallback filtering should
|
|
278
|
-
* continue to use {@link filterFallbackCandidates}.
|
|
279
|
-
*/
|
|
280
|
-
export function findModelExclusion(fullId: string, now = Date.now()): Readonly<ModelExclusion> | undefined {
|
|
281
|
-
ensureLoaded();
|
|
282
|
-
invalidateAuthExclusions();
|
|
283
|
-
const { provider, modelId } = parseModelKey(fullId);
|
|
284
|
-
return exclusions.find((entry) => entryMatches(entry, modelId, provider, now));
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Number of live (non-expired) exclusions.
|
|
289
|
-
*/
|
|
290
|
-
export function getExcludedCount(): number {
|
|
291
|
-
ensureLoaded();
|
|
292
|
-
clearExpiredExclusions();
|
|
293
|
-
return exclusions.length;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Split a candidate fullId into its provider + modelId components.
|
|
298
|
-
*
|
|
299
|
-
* A fullId may carry a thinking suffix (`provider/model:thinking`) which is
|
|
300
|
-
* stripped before parsing, and the modelId itself may contain slashes
|
|
301
|
-
* (e.g. `openrouter/google/gemini-flash`). The first `/`-segment is the
|
|
302
|
-
* provider; everything after is the modelId. This MUST stay in lock-step with
|
|
303
|
-
* the matching inside {@link isExcluded} so that a failure recorded via
|
|
304
|
-
* {@link recordModelFailure} is later recognised by the candidate filter.
|
|
305
|
-
*/
|
|
306
|
-
export function parseModelKey(fullId: string): { provider?: string; modelId: string } {
|
|
307
|
-
const base = splitKnownThinkingSuffix(fullId).baseModel;
|
|
308
|
-
if (!base.includes("/")) return { modelId: base };
|
|
309
|
-
const slash = base.indexOf("/");
|
|
310
|
-
return { provider: base.slice(0, slash), modelId: base.slice(slash + 1) };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Filter a list of candidate fullIds, removing excluded models/providers and
|
|
315
|
-
* duplicates while preserving order.
|
|
316
|
-
*/
|
|
317
|
-
export function filterFallbackCandidates(candidates: string[], opts?: {
|
|
318
|
-
now?: number;
|
|
319
|
-
onExcluded?: (candidate: string, exclusion: Readonly<ModelExclusion>) => void;
|
|
320
|
-
ignoreExclusion?: (candidate: string, exclusion: Readonly<ModelExclusion>) => boolean;
|
|
321
|
-
}): string[] {
|
|
322
|
-
ensureLoaded();
|
|
323
|
-
invalidateAuthExclusions();
|
|
324
|
-
const timestamp = opts?.now ?? Date.now();
|
|
325
|
-
const seen = new Set<string>();
|
|
326
|
-
const filtered: string[] = [];
|
|
327
|
-
for (const raw of candidates) {
|
|
328
|
-
if (!raw || seen.has(raw)) continue;
|
|
329
|
-
const { provider: candidateProvider, modelId: candidateModelId } = parseModelKey(raw);
|
|
330
|
-
const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp) && opts?.ignoreExclusion?.(raw, entry) !== true);
|
|
331
|
-
if (exclusion) {
|
|
332
|
-
opts?.onExcluded?.(raw, exclusion);
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
seen.add(raw);
|
|
336
|
-
filtered.push(raw);
|
|
337
|
-
}
|
|
338
|
-
return filtered;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Reload exclusions from disk (for tests and config hot-reload).
|
|
343
|
-
* Discards any in-memory-only exclusions that were not yet persisted.
|
|
344
|
-
*/
|
|
345
|
-
export function reloadFromDisk(): void {
|
|
346
|
-
loaded = false;
|
|
347
|
-
exclusions = [];
|
|
348
|
-
ensureLoaded();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function prune(items: ModelExclusion[], now: number): void {
|
|
352
|
-
let write = 0;
|
|
353
|
-
for (let i = 0; i < items.length; i++) {
|
|
354
|
-
const entry = items[i]!;
|
|
355
|
-
if (entry.expiresAt > now) {
|
|
356
|
-
items[write++] = entry;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
items.length = write;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function shortenExclusionsToTTL(items: ModelExclusion[], ttlMs: number, now: number): boolean {
|
|
363
|
-
let changed = false;
|
|
364
|
-
for (const entry of items) {
|
|
365
|
-
const configuredExpiry = entry.recordedAt + ttlMs;
|
|
366
|
-
if (entry.expiresAt > configuredExpiry) {
|
|
367
|
-
entry.expiresAt = configuredExpiry;
|
|
368
|
-
changed = true;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
const previousLength = items.length;
|
|
372
|
-
prune(items, now);
|
|
373
|
-
return changed || items.length !== previousLength;
|
|
374
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { ChildSession } from "./child-session.ts";
|
|
2
|
-
import { getReadonlySessionEvidence, type SettledReadonlyEvidence } from "./readonly-session-evidence.ts";
|
|
3
|
-
|
|
4
|
-
/** Owned by the logical host run, shared with abort recovery; never reset per attempt. */
|
|
5
|
-
export type LogicalRecoveryState = "unused" | "abort-recovery" | "readonly-continuation";
|
|
6
|
-
|
|
7
|
-
export interface ReadonlyContinuationCandidate {
|
|
8
|
-
/** Actual resolved identity, not an alias or a parsed display reference. */
|
|
9
|
-
readonly resolved: { readonly provider: string; readonly model: string; readonly api: string } | undefined;
|
|
10
|
-
readonly tried: boolean;
|
|
11
|
-
/** Host assessment of retained input support AND context capacity, including prompt overhead. */
|
|
12
|
-
readonly compatibility: "compatible" | "incompatible" | "unknown";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ReadonlyContinuationInput {
|
|
16
|
-
/** Retain the source child privately: its live accessor detects revoked receipts. */
|
|
17
|
-
readonly source: ChildSession | undefined;
|
|
18
|
-
readonly recoveryState: LogicalRecoveryState;
|
|
19
|
-
/** Ordered, already authorized and exclusion-filtered. This planner never resolves models. */
|
|
20
|
-
readonly candidates: readonly ReadonlyContinuationCandidate[];
|
|
21
|
-
readonly currentIndex: number;
|
|
22
|
-
/** False includes success, stop/interrupt/detach/handoff, deadline, or workflow-permit veto. */
|
|
23
|
-
readonly lifecycleAllowsContinuation: boolean;
|
|
24
|
-
/** False includes completion/structured/acceptance failures, pending input or other effects. */
|
|
25
|
-
readonly effectsAllowContinuation: boolean;
|
|
26
|
-
/** Configured tool budgets are unsupported; unknown authoritative usage allowance denies. */
|
|
27
|
-
readonly budget: "unconfigured" | "available" | "exhausted" | "unknown" | "tool-budget-configured";
|
|
28
|
-
readonly knownContextOverflow: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const READONLY_CONTINUATION_PROMPT = "The previous provider request failed with HTTP 429 after read-only progress. Continue from the retained transcript and completed tool results. Do not restart or repeat completed work. Use only the existing read-only tools and finish the requested response.";
|
|
32
|
-
|
|
33
|
-
export type ReadonlyContinuationPlan =
|
|
34
|
-
| { readonly kind: "deny"; readonly reason: "recovery-consumed" | "veto" | "no-evidence" | "unresolved-identity" | "incompatible" | "no-sibling" }
|
|
35
|
-
| { readonly kind: "continue"; readonly candidateIndex: number; readonly expected: SettledReadonlyEvidence;
|
|
36
|
-
readonly prompt: typeof READONLY_CONTINUATION_PROMPT; readonly recoveryState: "readonly-continuation" };
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Pure decision: no disk reads, dispatch, receipt minting, or state mutation.
|
|
40
|
-
* The host MUST recheck live source proof and lifecycle/budget at handoff, then store the
|
|
41
|
-
* returned consumed state BEFORE creation (even if creation subsequently fails),
|
|
42
|
-
* and pass expected to requestReadonlySessionEvidence on the exact-file sibling launch.
|
|
43
|
-
* That factory guard owns checkpoint/configured-provider revalidation before open/prompt/dispatch.
|
|
44
|
-
* The host must also verify the actual sibling model matches the selected resolved identity;
|
|
45
|
-
* a plan is not a dispatch authorization and any create/guard failure terminates recovery.
|
|
46
|
-
*/
|
|
47
|
-
export function planReadonlyModelContinuation(input: ReadonlyContinuationInput): ReadonlyContinuationPlan {
|
|
48
|
-
if (input.recoveryState !== "unused") return { kind: "deny", reason: "recovery-consumed" };
|
|
49
|
-
if (!input.lifecycleAllowsContinuation || !input.effectsAllowContinuation || input.knownContextOverflow
|
|
50
|
-
|| (input.budget !== "unconfigured" && input.budget !== "available")) return { kind: "deny", reason: "veto" };
|
|
51
|
-
const expected = input.source && getReadonlySessionEvidence(input.source);
|
|
52
|
-
if (!expected || input.source?.detached || input.source?.shutDown) return { kind: "deny", reason: "no-evidence" };
|
|
53
|
-
const current = Number.isInteger(input.currentIndex) && input.currentIndex >= 0 ? input.candidates[input.currentIndex]?.resolved : undefined;
|
|
54
|
-
if (!current || current.provider !== expected.provider || current.model !== expected.model || current.api !== expected.api) {
|
|
55
|
-
return { kind: "deny", reason: "unresolved-identity" };
|
|
56
|
-
}
|
|
57
|
-
for (let index = input.currentIndex + 1; index < input.candidates.length; index++) {
|
|
58
|
-
const candidate = input.candidates[index];
|
|
59
|
-
if (!candidate) return { kind: "deny", reason: "unresolved-identity" };
|
|
60
|
-
if (candidate.tried) continue;
|
|
61
|
-
const resolved = candidate.resolved;
|
|
62
|
-
if (!resolved?.provider || !resolved.model || !resolved.api) return { kind: "deny", reason: "unresolved-identity" };
|
|
63
|
-
if (resolved.provider !== expected.provider || resolved.model === expected.model) continue;
|
|
64
|
-
if (input.candidates.some((other) => other.tried && other.resolved?.provider === resolved.provider && other.resolved.model === resolved.model)) continue;
|
|
65
|
-
if (resolved.api !== expected.api || candidate.compatibility !== "compatible") return { kind: "deny", reason: "incompatible" };
|
|
66
|
-
return { kind: "continue", candidateIndex: index, expected, prompt: READONLY_CONTINUATION_PROMPT, recoveryState: "readonly-continuation" };
|
|
67
|
-
}
|
|
68
|
-
return { kind: "deny", reason: "no-sibling" };
|
|
69
|
-
}
|