pi-subagents 0.36.0 → 0.37.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 +17 -0
- package/README.md +93 -8
- package/agents/delegate.md +2 -0
- package/agents/worker.md +2 -0
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +20 -4
- package/src/agents/agent-management.ts +7 -1
- package/src/agents/agents.ts +99 -12
- package/src/api/capability-ceiling.ts +17 -0
- package/src/api/delegation.ts +3 -1
- package/src/api/preflight.ts +399 -0
- package/src/extension/index.ts +2 -0
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +8 -2
- package/src/extension/tool-description.ts +2 -0
- package/src/runs/background/async-execution.ts +125 -14
- package/src/runs/background/async-resume.ts +24 -7
- package/src/runs/background/async-status.ts +18 -0
- package/src/runs/background/process-terminal.ts +280 -0
- package/src/runs/background/run-status.ts +7 -1
- package/src/runs/background/scheduled-runs.ts +6 -1
- package/src/runs/background/stale-run-reconciler.ts +6 -0
- package/src/runs/background/subagent-runner.ts +182 -12
- package/src/runs/foreground/chain-execution.ts +5 -0
- package/src/runs/foreground/execution.ts +59 -13
- package/src/runs/foreground/subagent-executor.ts +32 -2
- package/src/runs/shared/acceptance.ts +41 -30
- package/src/runs/shared/capability-ceiling.ts +177 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/src/runs/shared/nested-events.ts +8 -1
- package/src/runs/shared/parallel-utils.ts +5 -0
- package/src/runs/shared/pi-args.ts +141 -58
- package/src/runs/shared/session-lease.ts +25 -5
- package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
- package/src/runs/shared/tool-availability.ts +18 -2
- package/src/shared/launch-contract.ts +123 -0
- package/src/shared/types.ts +104 -7
- package/src/shared/utils.ts +17 -42
- package/src/slash/delegation-adapters.ts +1 -1
- package/src/slash/slash-commands.ts +1 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { writeAtomicJson, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
+
import {
|
|
5
|
+
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
6
|
+
type AsyncStatus,
|
|
7
|
+
type CanonicalSessionTerminalV1,
|
|
8
|
+
type ProcessInstanceExitV1,
|
|
9
|
+
type ProcessTerminalReason,
|
|
10
|
+
type ProcessTerminalV1,
|
|
11
|
+
} from "../../shared/types.ts";
|
|
12
|
+
import { canonicalSessionId, inspectSessionLease } from "../shared/session-lease.ts";
|
|
13
|
+
|
|
14
|
+
export interface ProcessTerminalCandidate {
|
|
15
|
+
version: 1;
|
|
16
|
+
runId: string;
|
|
17
|
+
runnerProcessInstanceId: string;
|
|
18
|
+
writers: Record<string, ProcessInstanceExitV1[]>;
|
|
19
|
+
expectedWriters?: Record<string, number>;
|
|
20
|
+
sessionFile?: string;
|
|
21
|
+
revivalLeaseToken?: string;
|
|
22
|
+
revivalLeaseReleaseAcknowledged?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RunnerCloseObservation {
|
|
26
|
+
processInstanceId: string;
|
|
27
|
+
closeObservedAt: number;
|
|
28
|
+
exitCode: number | null;
|
|
29
|
+
signal: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validProcessInstance(value: unknown, kind?: "runner" | "pi-writer"): value is ProcessInstanceExitV1 {
|
|
37
|
+
if (!isRecord(value)) return false;
|
|
38
|
+
if (typeof value.processInstanceId !== "string" || value.processInstanceId.length === 0) return false;
|
|
39
|
+
if (kind ? value.kind !== kind : (value.kind !== "runner" && value.kind !== "pi-writer")) return false;
|
|
40
|
+
if (typeof value.closeObservedAt !== "number" || !Number.isFinite(value.closeObservedAt)) return false;
|
|
41
|
+
if (typeof value.exitCode !== "number" && value.exitCode !== null) return false;
|
|
42
|
+
if (typeof value.signal !== "string" && value.signal !== null) return false;
|
|
43
|
+
return value.kind === "runner"
|
|
44
|
+
? value.attempt === undefined
|
|
45
|
+
: typeof value.attempt === "number" && Number.isInteger(value.attempt) && value.attempt >= 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validInstance(value: unknown): value is ProcessInstanceExitV1 {
|
|
49
|
+
return validProcessInstance(value, "pi-writer");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function processTerminalCandidatePath(asyncDir: string): string {
|
|
53
|
+
return path.join(asyncDir, "process-terminal-candidate.json");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function processTerminalPath(asyncDir: string): string {
|
|
57
|
+
return path.join(asyncDir, "process-terminal.json");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function errorMessage(error: unknown): string {
|
|
61
|
+
return error instanceof Error ? error.message : String(error);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readProcessTerminalCandidate(asyncDir: string): ProcessTerminalCandidate | undefined {
|
|
65
|
+
try {
|
|
66
|
+
const raw = JSON.parse(fs.readFileSync(processTerminalCandidatePath(asyncDir), "utf-8")) as unknown;
|
|
67
|
+
if (!isRecord(raw) || raw.version !== 1 || typeof raw.runId !== "string" || typeof raw.runnerProcessInstanceId !== "string" || !isRecord(raw.writers)) {
|
|
68
|
+
throw new Error(`Invalid process-terminal candidate in '${asyncDir}'.`);
|
|
69
|
+
}
|
|
70
|
+
const writers: Record<string, ProcessInstanceExitV1[]> = {};
|
|
71
|
+
for (const [index, entries] of Object.entries(raw.writers)) {
|
|
72
|
+
if (!Array.isArray(entries) || !entries.every(validInstance)) throw new Error(`Invalid writer process records for child '${index}'.`);
|
|
73
|
+
writers[index] = entries;
|
|
74
|
+
}
|
|
75
|
+
let expectedWriters: Record<string, number> | undefined;
|
|
76
|
+
if (raw.expectedWriters !== undefined) {
|
|
77
|
+
if (!isRecord(raw.expectedWriters)) throw new Error("Invalid expected writer process records.");
|
|
78
|
+
expectedWriters = {};
|
|
79
|
+
for (const [index, count] of Object.entries(raw.expectedWriters)) {
|
|
80
|
+
if (typeof count !== "number" || !Number.isInteger(count) || count < 0) throw new Error(`Invalid expected writer count for child '${index}'.`);
|
|
81
|
+
expectedWriters[index] = count;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (raw.sessionFile !== undefined && typeof raw.sessionFile !== "string") throw new Error("Invalid process-terminal candidate sessionFile.");
|
|
85
|
+
if (raw.revivalLeaseToken !== undefined && typeof raw.revivalLeaseToken !== "string") throw new Error("Invalid process-terminal candidate lease token.");
|
|
86
|
+
if (raw.revivalLeaseReleaseAcknowledged !== undefined && typeof raw.revivalLeaseReleaseAcknowledged !== "boolean") throw new Error("Invalid process-terminal lease release acknowledgement.");
|
|
87
|
+
return {
|
|
88
|
+
version: 1,
|
|
89
|
+
runId: raw.runId,
|
|
90
|
+
runnerProcessInstanceId: raw.runnerProcessInstanceId,
|
|
91
|
+
writers,
|
|
92
|
+
...(expectedWriters ? { expectedWriters } : {}),
|
|
93
|
+
...(raw.sessionFile ? { sessionFile: raw.sessionFile } : {}),
|
|
94
|
+
...(raw.revivalLeaseToken ? { revivalLeaseToken: raw.revivalLeaseToken } : {}),
|
|
95
|
+
...(raw.revivalLeaseReleaseAcknowledged !== undefined ? { revivalLeaseReleaseAcknowledged: raw.revivalLeaseReleaseAcknowledged } : {}),
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function writeProcessTerminalCandidate(asyncDir: string, candidate: ProcessTerminalCandidate): void {
|
|
104
|
+
writePrivateAtomicJson(processTerminalCandidatePath(asyncDir), candidate);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function markProcessTerminalCandidateLeaseRelease(asyncDir: string, token: string, acknowledged: boolean): void {
|
|
108
|
+
const candidate = readProcessTerminalCandidate(asyncDir);
|
|
109
|
+
if (!candidate || candidate.revivalLeaseToken !== token) return;
|
|
110
|
+
writeProcessTerminalCandidate(asyncDir, { ...candidate, revivalLeaseReleaseAcknowledged: acknowledged });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function unknownProof(runId: string, runnerProcessInstanceId: string, reason: ProcessTerminalReason, diagnostic?: string): ProcessTerminalV1 {
|
|
114
|
+
return { version: 1, state: "unknown", runId, runnerProcessInstanceId, reason, ...(diagnostic ? { diagnostic } : {}) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resumeDisposition(state: string | undefined, sessionFile: string | undefined): "resumable" | "non-resumable" | "unavailable" {
|
|
118
|
+
if (state === "stopped") return "non-resumable";
|
|
119
|
+
if (state !== "complete" && state !== "completed" && state !== "failed" && state !== "paused") return "unavailable";
|
|
120
|
+
return sessionFile && fs.existsSync(sessionFile) ? "resumable" : "unavailable";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sessionProjection(candidate: ProcessTerminalCandidate, lease: ReturnType<typeof inspectSessionLease>): CanonicalSessionTerminalV1 | undefined {
|
|
124
|
+
if (!candidate.sessionFile || lease.state !== "free") return undefined;
|
|
125
|
+
if (candidate.revivalLeaseToken && candidate.revivalLeaseReleaseAcknowledged !== true) return undefined;
|
|
126
|
+
return {
|
|
127
|
+
canonicalSessionId: canonicalSessionId(candidate.sessionFile),
|
|
128
|
+
leaseDisposition: candidate.revivalLeaseToken ? "released" : "not-held",
|
|
129
|
+
freeAtObservation: true,
|
|
130
|
+
...(candidate.revivalLeaseToken ? { canonicalSessionLeaseReleased: true } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateProof(raw: unknown, asyncDir: string, fallback?: { runId?: string; runnerProcessInstanceId?: string }): raw is ProcessTerminalV1 {
|
|
135
|
+
if (!isRecord(raw) || raw.version !== 1 || !["pending", "observed", "unknown", "not-started"].includes(String(raw.state)) || typeof raw.runId !== "string" || !raw.runId || typeof raw.runnerProcessInstanceId !== "string" || !raw.runnerProcessInstanceId) {
|
|
136
|
+
throw new Error(`Invalid process-terminal proof in '${asyncDir}'.`);
|
|
137
|
+
}
|
|
138
|
+
if (fallback?.runId && raw.runId !== fallback.runId) throw new Error(`Process-terminal proof in '${asyncDir}' belongs to run '${raw.runId}', expected '${fallback.runId}'.`);
|
|
139
|
+
if (fallback?.runnerProcessInstanceId && raw.runnerProcessInstanceId !== fallback.runnerProcessInstanceId) throw new Error(`Process-terminal proof in '${asyncDir}' belongs to runner '${raw.runnerProcessInstanceId}', expected '${fallback.runnerProcessInstanceId}'.`);
|
|
140
|
+
if (raw.instances !== undefined && (!Array.isArray(raw.instances) || !raw.instances.every((entry) => validProcessInstance(entry)))) {
|
|
141
|
+
throw new Error(`Invalid process-terminal instances in '${asyncDir}'.`);
|
|
142
|
+
}
|
|
143
|
+
if (raw.state === "observed") {
|
|
144
|
+
if (typeof raw.observedAt !== "number" || !Number.isFinite(raw.observedAt)) throw new Error(`Observed process-terminal proof in '${asyncDir}' is missing observedAt.`);
|
|
145
|
+
if (!Array.isArray(raw.instances)) throw new Error(`Observed process-terminal proof in '${asyncDir}' is missing instances.`);
|
|
146
|
+
const runner = raw.instances.find((entry) => isRecord(entry) && entry.kind === "runner");
|
|
147
|
+
if (!validProcessInstance(runner, "runner") || runner.processInstanceId !== raw.runnerProcessInstanceId) throw new Error(`Observed process-terminal proof in '${asyncDir}' has no matching runner instance.`);
|
|
148
|
+
}
|
|
149
|
+
if (raw.resumeDisposition !== undefined && !["resumable", "non-resumable", "unavailable"].includes(String(raw.resumeDisposition))) throw new Error(`Invalid process-terminal resume disposition in '${asyncDir}'.`);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function sanitizeProcessTerminal(value: unknown, fallback: { runId?: string; runnerProcessInstanceId?: string }, label = "status"): ProcessTerminalV1 | undefined {
|
|
154
|
+
if (value === undefined) return undefined;
|
|
155
|
+
try {
|
|
156
|
+
validateProof(value, label, fallback);
|
|
157
|
+
return value;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return unknownProof(fallback.runId ?? label, fallback.runnerProcessInstanceId ?? "unknown", "proof-write-failed", errorMessage(error));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function readProcessTerminal(asyncDir: string, fallback?: { runId?: string; runnerProcessInstanceId?: string }): ProcessTerminalV1 | undefined {
|
|
164
|
+
try {
|
|
165
|
+
const raw = JSON.parse(fs.readFileSync(processTerminalPath(asyncDir), "utf-8")) as unknown;
|
|
166
|
+
validateProof(raw, asyncDir, fallback);
|
|
167
|
+
return raw;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
170
|
+
return unknownProof(fallback?.runId ?? path.basename(asyncDir), fallback?.runnerProcessInstanceId ?? "unknown", "proof-write-failed", errorMessage(error));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function stepProcessTerminalProof(
|
|
175
|
+
proof: ProcessTerminalV1,
|
|
176
|
+
childIndex: number,
|
|
177
|
+
state: ProcessTerminalV1["state"],
|
|
178
|
+
records: ProcessInstanceExitV1[],
|
|
179
|
+
resumeDispositionValue: ProcessTerminalV1["resumeDisposition"],
|
|
180
|
+
): ProcessTerminalV1 {
|
|
181
|
+
const base = {
|
|
182
|
+
version: 1 as const,
|
|
183
|
+
runId: proof.runId,
|
|
184
|
+
childIndex,
|
|
185
|
+
runnerProcessInstanceId: proof.runnerProcessInstanceId,
|
|
186
|
+
...(resumeDispositionValue ? { resumeDisposition: resumeDispositionValue } : {}),
|
|
187
|
+
};
|
|
188
|
+
if (state === "observed") {
|
|
189
|
+
return { ...base, state, observedAt: proof.state === "observed" ? proof.observedAt : Date.now(), instances: records };
|
|
190
|
+
}
|
|
191
|
+
if (state === "unknown") {
|
|
192
|
+
return { ...base, state, reason: proof.state === "unknown" ? proof.reason : "writer-close-unverified" };
|
|
193
|
+
}
|
|
194
|
+
return { ...base, state };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function overlayStatus(asyncDir: string, proof: ProcessTerminalV1, candidate?: ProcessTerminalCandidate): void {
|
|
198
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
199
|
+
try {
|
|
200
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
201
|
+
status.processTerminal = proof;
|
|
202
|
+
if (status.steps) {
|
|
203
|
+
for (const [index, step] of status.steps.entries()) {
|
|
204
|
+
const records = candidate?.writers[String(index)] ?? [];
|
|
205
|
+
const expected = candidate?.expectedWriters?.[String(index)] ?? (records.length > 0 ? records.length : 0);
|
|
206
|
+
const stepState = expected === 0 ? "not-started" : proof.state === "observed" && records.length === expected ? "observed" : proof.state === "pending" ? "pending" : "unknown";
|
|
207
|
+
step.processTerminal = stepProcessTerminalProof(proof, index, stepState, records, resumeDisposition(step.status, step.sessionFile ?? candidate?.sessionFile));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
writeAtomicJson(statusPath, status);
|
|
211
|
+
} catch {
|
|
212
|
+
// The proof sidecar remains authoritative when terminal status is unavailable.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function finalizeProcessTerminal(
|
|
217
|
+
asyncDir: string,
|
|
218
|
+
runId: string,
|
|
219
|
+
runnerClose: RunnerCloseObservation,
|
|
220
|
+
): ProcessTerminalV1 {
|
|
221
|
+
const existing = readProcessTerminal(asyncDir, { runId, runnerProcessInstanceId: runnerClose.processInstanceId });
|
|
222
|
+
if (existing && fs.existsSync(processTerminalPath(asyncDir))) {
|
|
223
|
+
if (existing.state === "observed" && existing.runId === runId && existing.runnerProcessInstanceId === runnerClose.processInstanceId) return existing;
|
|
224
|
+
if (existing.state === "unknown") return existing;
|
|
225
|
+
}
|
|
226
|
+
let proof: ProcessTerminalV1;
|
|
227
|
+
let candidateForOverlay: ProcessTerminalCandidate | undefined;
|
|
228
|
+
try {
|
|
229
|
+
const candidate = readProcessTerminalCandidate(asyncDir);
|
|
230
|
+
candidateForOverlay = candidate;
|
|
231
|
+
if (!candidate) proof = unknownProof(runId, runnerClose.processInstanceId, "runner-candidate-missing");
|
|
232
|
+
else if (candidate.runId !== runId || candidate.runnerProcessInstanceId !== runnerClose.processInstanceId) proof = unknownProof(runId, runnerClose.processInstanceId, "runner-instance-mismatch");
|
|
233
|
+
else {
|
|
234
|
+
const allWriters = Object.values(candidate.writers).flat();
|
|
235
|
+
const status = (() => {
|
|
236
|
+
try { return JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatus; } catch { return undefined; }
|
|
237
|
+
})();
|
|
238
|
+
const session = candidate.sessionFile ? inspectSessionLease(candidate.sessionFile) : undefined;
|
|
239
|
+
const writerEntries = Object.entries(candidate.writers);
|
|
240
|
+
const expectedWriters = candidate.expectedWriters ?? Object.fromEntries(writerEntries.map(([index, records]) => [index, records.length]));
|
|
241
|
+
const expectedEntries = Object.entries(expectedWriters);
|
|
242
|
+
const expectedIndexes = new Set(expectedEntries.map(([index]) => index));
|
|
243
|
+
const writerIndexes = new Set(writerEntries.map(([index]) => index));
|
|
244
|
+
const inconsistentWriters = writerEntries.some(([index, records]) => !expectedIndexes.has(index) || records.length !== expectedWriters[index])
|
|
245
|
+
|| expectedEntries.some(([index, expected]) => !writerIndexes.has(index) && expected !== 0);
|
|
246
|
+
if (session && session.state !== "free") {
|
|
247
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, session.state === "owned" ? "canonical-session-lease-active" : "canonical-session-unavailable");
|
|
248
|
+
} else if (candidate.revivalLeaseToken && candidate.revivalLeaseReleaseAcknowledged !== true) {
|
|
249
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, "canonical-session-release-unverified");
|
|
250
|
+
} else if (inconsistentWriters || (allWriters.length === 0 && expectedEntries.length === 0)) {
|
|
251
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, "writer-close-unverified");
|
|
252
|
+
} else {
|
|
253
|
+
const runner: ProcessInstanceExitV1 = { kind: "runner", ...runnerClose };
|
|
254
|
+
const canonicalSession = session && sessionProjection(candidate, session);
|
|
255
|
+
proof = {
|
|
256
|
+
version: 1,
|
|
257
|
+
state: "observed",
|
|
258
|
+
runId,
|
|
259
|
+
runnerProcessInstanceId: runnerClose.processInstanceId,
|
|
260
|
+
observedAt: runnerClose.closeObservedAt,
|
|
261
|
+
instances: [runner, ...allWriters],
|
|
262
|
+
resumeDisposition: resumeDisposition(status?.state, candidate.sessionFile ?? status?.sessionFile),
|
|
263
|
+
...(canonicalSession ? { canonicalSession } : {}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
proof = unknownProof(runId, runnerClose.processInstanceId, "proof-write-failed", errorMessage(error));
|
|
269
|
+
}
|
|
270
|
+
let durable = false;
|
|
271
|
+
try {
|
|
272
|
+
writeAtomicJson(processTerminalPath(asyncDir), proof);
|
|
273
|
+
durable = true;
|
|
274
|
+
overlayStatus(asyncDir, proof, candidateForOverlay);
|
|
275
|
+
fs.appendFileSync(path.join(asyncDir, "events.jsonl"), `${JSON.stringify({ type: "subagent.run.process_terminal", lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, ts: Date.now(), runId, processTerminal: proof })}\n`, "utf-8");
|
|
276
|
+
} catch {
|
|
277
|
+
// Do not emit a process-terminal event when the proof sidecar was not durable.
|
|
278
|
+
}
|
|
279
|
+
return durable ? proof : unknownProof(runId, runnerClose.processInstanceId, "proof-write-failed", "Failed to persist process-terminal proof.");
|
|
280
|
+
}
|
|
@@ -8,6 +8,7 @@ import { formatModelThinking } from "../../shared/formatters.ts";
|
|
|
8
8
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
9
9
|
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details, type ForegroundResumeRun, type NestedRunSummary, type SteeringStatus, type SubagentState } from "../../shared/types.ts";
|
|
10
10
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
11
|
+
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
11
12
|
import { resolveAsyncRunLocation } from "./async-resume.ts";
|
|
12
13
|
import { resolveSubagentRunId } from "./run-id-resolver.ts";
|
|
13
14
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
@@ -351,10 +352,15 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
351
352
|
const updated = status.lastUpdate ? new Date(status.lastUpdate).toISOString() : "n/a";
|
|
352
353
|
const statusActivityText = status.state === "running" ? formatActivityLabel(status.lastActivityAt, status.activityState) : undefined;
|
|
353
354
|
const steeringText = formatSteeringSummary(status);
|
|
355
|
+
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
|
|
356
|
+
?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
|
|
354
357
|
|
|
355
358
|
const lines = [
|
|
356
359
|
`Run: ${status.runId}`,
|
|
357
360
|
`State: ${status.state}`,
|
|
361
|
+
processTerminal ? `Process terminal: ${processTerminal.state}${processTerminal.reason ? ` (${processTerminal.reason})` : ""}` : undefined,
|
|
362
|
+
status.capabilityCeiling ? `Capability ceiling: ${status.capabilityCeiling.allowedTools === undefined ? "names unrestricted" : status.capabilityCeiling.allowedTools.length === 0 ? "none" : status.capabilityCeiling.allowedTools.join(", ")}\nExtensions denied: ${status.capabilityCeiling.denyExtensions ? "yes" : "no"} (sources: ${status.capabilityCeiling.sources.join(", ")})` : undefined,
|
|
363
|
+
status.capabilityAudit ? `Capability audit: ${status.capabilityAudit.removedTools.length} tools removed, ${status.capabilityAudit.removedExtensionCount} extension entries removed` : undefined,
|
|
358
364
|
status.error ? `Error: ${status.error}` : undefined,
|
|
359
365
|
statusActivityText ? `Activity: ${statusActivityText}` : undefined,
|
|
360
366
|
steeringText ? `Steering: ${steeringText}` : undefined,
|
|
@@ -402,7 +408,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
402
408
|
if (fs.existsSync(logPath)) lines.push(`Log: ${logPath}`);
|
|
403
409
|
if (fs.existsSync(eventsPath)) lines.push(`Events: ${eventsPath}`);
|
|
404
410
|
|
|
405
|
-
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
|
|
411
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
|
|
406
412
|
}
|
|
407
413
|
}
|
|
408
414
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../../shared/types.ts";
|
|
14
14
|
import type { SubagentParamsLike } from "../foreground/subagent-executor.ts";
|
|
15
15
|
import { validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
16
|
+
import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
16
17
|
|
|
17
18
|
export const SCHEDULED_RUNS_DIR = path.join(TEMP_ROOT_DIR, "scheduled-subagent-runs");
|
|
18
19
|
export const SCHEDULED_RUN_ACTIONS = ["schedule", "schedule-list", "schedule-status", "schedule-cancel"] as const;
|
|
@@ -57,6 +58,7 @@ type ScheduledRunManagerDeps = {
|
|
|
57
58
|
storeRoot?: string;
|
|
58
59
|
now?: () => number;
|
|
59
60
|
randomId?: () => string;
|
|
61
|
+
resolveCapabilityCeiling?: (sessionId: string) => ResolvedSubagentCapabilityCeiling | undefined;
|
|
60
62
|
timers?: ScheduledRunTimers;
|
|
61
63
|
};
|
|
62
64
|
|
|
@@ -310,12 +312,15 @@ export class ScheduledRunManager {
|
|
|
310
312
|
const sanitized = sanitizeScheduledParams(params);
|
|
311
313
|
if (sanitized.error) return textResult(sanitized.error, true);
|
|
312
314
|
const scheduleInput = params.schedule!.trim();
|
|
315
|
+
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
316
|
+
if (this.deps.resolveCapabilityCeiling?.(sessionId)) {
|
|
317
|
+
return textResult("Cannot schedule a capability-ceiling-restricted run because this store does not yet persist ceilings. Remove the active parent restriction or run it immediately.", true);
|
|
318
|
+
}
|
|
313
319
|
const runAt = parseScheduledRunTime(scheduleInput, this.now());
|
|
314
320
|
const pendingCount = store.list().filter((job) => job.state === "scheduled" || job.state === "running").length;
|
|
315
321
|
const maxPending = resolveMaxPending(this.deps.config);
|
|
316
322
|
if (pendingCount >= maxPending) return textResult(`Scheduled subagent limit reached (${pendingCount}/${maxPending} pending or running). Cancel an existing scheduled run before adding another.`, true);
|
|
317
323
|
const id = this.randomId();
|
|
318
|
-
const sessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
319
324
|
const scheduleName = params.scheduleName?.trim();
|
|
320
325
|
const executionParams = sanitized.params!;
|
|
321
326
|
const now = this.now();
|
|
@@ -175,6 +175,9 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
|
|
|
175
175
|
return {
|
|
176
176
|
...status,
|
|
177
177
|
state: repair.state,
|
|
178
|
+
...(status.lifecycleArtifactVersion === 3 && (!status.processTerminal || status.processTerminal.state === "pending") ? {
|
|
179
|
+
processTerminal: { version: 1 as const, state: "unknown" as const, runId: status.runId, runnerProcessInstanceId: "observer-unavailable", reason: "observer-unavailable" as const },
|
|
180
|
+
} : {}),
|
|
178
181
|
...(repair.state === "stopped" ? { stopped: true } : {}),
|
|
179
182
|
activityState: undefined,
|
|
180
183
|
lastUpdate: now,
|
|
@@ -231,6 +234,9 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
231
234
|
const repairedStatus: AsyncStatus = {
|
|
232
235
|
...status,
|
|
233
236
|
state: "failed",
|
|
237
|
+
...(status.lifecycleArtifactVersion === 3 && (!status.processTerminal || status.processTerminal.state === "pending") ? {
|
|
238
|
+
processTerminal: { version: 1 as const, state: "unknown" as const, runId, runnerProcessInstanceId: "observer-unavailable", reason: "stale-repair" as const },
|
|
239
|
+
} : {}),
|
|
234
240
|
activityState: undefined,
|
|
235
241
|
lastUpdate: now,
|
|
236
242
|
endedAt: now,
|