@ferris1225/pi-subagents 4.3.5 → 4.3.7
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 +54 -0
- package/README.md +106 -34
- package/agents/artisan.md +4 -2
- package/agents/scout.md +4 -2
- package/agents/steward.md +4 -3
- package/package.json +1 -1
- package/src/delegation/dispatch.ts +6 -3
- package/src/delegation/prompt.ts +40 -13
- package/src/execution/rpc-control.ts +40 -0
- package/src/execution/rpc-run.ts +24 -26
- package/src/execution/spawn.ts +15 -7
- package/src/isolation/managed-paths.ts +145 -0
- package/src/isolation/recovery.ts +41 -12
- package/src/isolation/temp-hygiene.ts +7 -7
- package/src/isolation/worktree.ts +1 -11
- package/src/lifecycle/durable.ts +98 -24
- package/src/lifecycle/runtime.ts +5 -5
- package/src/lifecycle/thread-lifecycle.ts +32 -19
- package/src/lifecycle/thread-restore.ts +4 -1
- package/src/lifecycle/thread-shared.ts +0 -1
- package/src/lifecycle/tools.ts +154 -27
package/src/lifecycle/runtime.ts
CHANGED
|
@@ -107,7 +107,7 @@ export interface SubagentRuntime {
|
|
|
107
107
|
* one-time session-start notice. */
|
|
108
108
|
restoredRunIds: number[];
|
|
109
109
|
restoredNotified: boolean;
|
|
110
|
-
/** Deliver a batch
|
|
110
|
+
/** Deliver a batch at the next safe parent turn boundary and wake an idle parent. */
|
|
111
111
|
sendCompletionGroup: (items: CompletionMessageItem[]) => void;
|
|
112
112
|
/** Claim the sole delivery route before a generation can settle. */
|
|
113
113
|
claimRunDelivery: (runId: number, route: "background" | "await") => void;
|
|
@@ -189,10 +189,10 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
|
|
|
189
189
|
content: formatCompletionMessage(items) + formatActiveRunsFooter(active),
|
|
190
190
|
display: true,
|
|
191
191
|
};
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
pi.sendMessage(message, { deliverAs: "
|
|
192
|
+
// Steer delivers after the current assistant turn's tool calls and before
|
|
193
|
+
// the next model call. A follow-up would wait for the whole parent run to
|
|
194
|
+
// settle, allowing completions and stop results to arrive after its final reply.
|
|
195
|
+
pi.sendMessage(message, { deliverAs: "steer", triggerTurn: true });
|
|
196
196
|
},
|
|
197
197
|
claimRunDelivery: (runId, route) => {
|
|
198
198
|
runDeliveries.set(runId, { route, immediate: false });
|
|
@@ -27,11 +27,12 @@ import {
|
|
|
27
27
|
queuedResult,
|
|
28
28
|
} from "../presentation/format.ts";
|
|
29
29
|
import { monitor } from "../presentation/monitor.ts";
|
|
30
|
-
import {
|
|
30
|
+
import { findDuplicateDispatch } from "../delegation/prompt.ts";
|
|
31
31
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "../isolation/recovery.ts";
|
|
32
32
|
import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
|
|
33
33
|
import { forkRetainedSession } from "../execution/session-fork.ts";
|
|
34
34
|
import {
|
|
35
|
+
buildAppendedObjectivePrompt,
|
|
35
36
|
buildResumePrompt,
|
|
36
37
|
getProjectRoot,
|
|
37
38
|
RpcRunControl,
|
|
@@ -135,12 +136,21 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
135
136
|
|
|
136
137
|
const originalCwd = resolve(cwd ?? runCtx.cwd);
|
|
137
138
|
if (!existingThread) {
|
|
138
|
-
const duplicate =
|
|
139
|
-
if (duplicate) {
|
|
139
|
+
const duplicate = findDuplicateDispatch(runtime.threads.values(), task, originalCwd);
|
|
140
|
+
if (duplicate?.kind === "active") {
|
|
140
141
|
return failedStartResult(
|
|
141
142
|
agentName,
|
|
142
143
|
task,
|
|
143
|
-
`Duplicate active dispatch matches run #${duplicate.id} (${duplicate.agentName}). Use that logical thread instead; resume #${duplicate.id} when it is eligible.`,
|
|
144
|
+
`Duplicate active dispatch matches run #${duplicate.source.id} (${duplicate.source.agentName}). Use that logical thread instead; resume #${duplicate.source.id} when it is eligible.`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (duplicate?.kind === "settled") {
|
|
148
|
+
// The same brief on the same tree would re-buy work whose result main
|
|
149
|
+
// already holds; the retained session continues it for a fraction.
|
|
150
|
+
return failedStartResult(
|
|
151
|
+
agentName,
|
|
152
|
+
task,
|
|
153
|
+
`Run #${duplicate.source.id} (${duplicate.source.agentName}) already ${duplicate.source.state} this exact brief and kept its context; its result was delivered. Resume #${duplicate.source.id} with an appended objective instead of paying for a second run, or restate the brief with what changed.`,
|
|
144
154
|
);
|
|
145
155
|
}
|
|
146
156
|
}
|
|
@@ -333,9 +343,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
333
343
|
? {
|
|
334
344
|
sessionId: priorSessionId,
|
|
335
345
|
sessionDir: priorSessionDir,
|
|
336
|
-
stdinText:
|
|
337
|
-
? task
|
|
338
|
-
: buildResumePrompt(priorTask ?? task, "the retained thread was resumed")
|
|
346
|
+
stdinText: appendedObjectiveOnResume
|
|
347
|
+
? buildAppendedObjectivePrompt(priorTask ?? task, task)
|
|
348
|
+
: buildResumePrompt(priorTask ?? task, "the retained thread was resumed"),
|
|
339
349
|
}
|
|
340
350
|
: {}),
|
|
341
351
|
},
|
|
@@ -375,17 +385,19 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
375
385
|
|
|
376
386
|
const lifecycleInterrupted = (): boolean =>
|
|
377
387
|
thread.lifecycleOperation === "stop" ||
|
|
388
|
+
thread.lifecycleOperation === "park" ||
|
|
378
389
|
thread.state === "stopped";
|
|
379
|
-
// Destructive stop
|
|
380
|
-
// the lifecycle. Leave the partial result/session
|
|
381
|
-
// stop
|
|
382
|
-
// exactly one aborted result
|
|
383
|
-
|
|
390
|
+
// Destructive stop and park own publication once they have
|
|
391
|
+
// synchronously claimed the lifecycle. Leave the partial result/session
|
|
392
|
+
// on the thread; stop waits for this queue task, finalizes isolation,
|
|
393
|
+
// and emits exactly one aborted result, while park records the
|
|
394
|
+
// checkpoint and answers through its own tool result.
|
|
395
|
+
if (lifecycleInterrupted()) return;
|
|
384
396
|
|
|
385
397
|
// A shutdown can win in the microtask gap after the child RPC
|
|
386
398
|
// settles. Never replace the stable top-level session with an
|
|
387
399
|
// aborted partial.
|
|
388
|
-
if (backgroundSignal.aborted ||
|
|
400
|
+
if (backgroundSignal.aborted || !runtime.sessionActive) return;
|
|
389
401
|
|
|
390
402
|
if (thread.retireOnSettle) runtime.retireThreadSession(thread);
|
|
391
403
|
// Claim terminal settlement synchronously before the first slow await.
|
|
@@ -471,9 +483,9 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
471
483
|
() => {
|
|
472
484
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
473
485
|
// A destructive stop owns publication and may still be finalizing an
|
|
474
|
-
// isolated worktree. Do not expose a
|
|
475
|
-
// that owner records
|
|
476
|
-
if (thread.lifecycleOperation === "stop") return;
|
|
486
|
+
// isolated worktree; a park owns the checkpoint. Do not expose a
|
|
487
|
+
// terminal monitor state before that owner records its outcome.
|
|
488
|
+
if (thread.lifecycleOperation === "stop" || thread.lifecycleOperation === "park") return;
|
|
477
489
|
runtime.runControllers.delete(runId);
|
|
478
490
|
thread.queueController = undefined;
|
|
479
491
|
thread.state = "stopped";
|
|
@@ -487,9 +499,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
487
499
|
async (error) => {
|
|
488
500
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
489
501
|
// Queue-level crashes use the same settlement reservation as ordinary
|
|
490
|
-
// results. A concurrent destructive stop may supersede it while
|
|
491
|
-
// worktree finalization is running, in which case
|
|
492
|
-
|
|
502
|
+
// results. A concurrent destructive stop or park may supersede it while
|
|
503
|
+
// slow worktree finalization is running, in which case that owner
|
|
504
|
+
// publishes once.
|
|
505
|
+
if (thread.lifecycleOperation === "stop" || thread.lifecycleOperation === "park") return;
|
|
493
506
|
const settlementVersion = ++thread.lifecycleVersion;
|
|
494
507
|
thread.lifecycleOperation = "settle";
|
|
495
508
|
const ownsSettlement = (): boolean =>
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type SingleResult,
|
|
25
25
|
} from "../execution/spawn.ts";
|
|
26
26
|
import { isProcessAlive, killProcessTree, sweepProjectDurableDirs, sweepProjectTempDirs } from "../isolation/temp-hygiene.ts";
|
|
27
|
+
import { readRecoveryRecords, referencedRecoveryPaths } from "../isolation/recovery.ts";
|
|
27
28
|
import {
|
|
28
29
|
isPathInside,
|
|
29
30
|
restoreWorktreeIsolation,
|
|
@@ -225,9 +226,11 @@ export function bootstrapDurableState(runtime: SubagentRuntime): Promise<void> {
|
|
|
225
226
|
try {
|
|
226
227
|
// Sessions and worktrees outlive their process on purpose, so only
|
|
227
228
|
// ownership separates state a live pi still resumes from state a crash
|
|
228
|
-
// abandoned.
|
|
229
|
+
// abandoned. Valid thread and recovery records always keep their paths.
|
|
229
230
|
const records = await readThreadRecords(runtime.configPath);
|
|
231
|
+
const recoveryRecords = await readRecoveryRecords(runtime.configPath);
|
|
230
232
|
const referenced = [...referencedDurablePaths(records)];
|
|
233
|
+
referenced.push(...await referencedRecoveryPaths(runtime.configPath, recoveryRecords));
|
|
231
234
|
sweepProjectDurableDirs(projectRoots, {
|
|
232
235
|
keep: (path) => referenced.some((claimed) => isPathInside(path, claimed)),
|
|
233
236
|
});
|
package/src/lifecycle/tools.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Thread controls around the subagent runtime: subagent_control
|
|
3
|
-
* destructive subagent_stop. There is no status/poll
|
|
4
|
-
* each result (with an on-disk artifact when
|
|
5
|
-
* model, so waiting is never a tool call; the only
|
|
6
|
-
* true` on a dispatch, for one-shot parents that exit
|
|
2
|
+
* Thread controls around the subagent runtime: subagent_control
|
|
3
|
+
* (steer/resume/park) and destructive subagent_stop. There is no status/poll
|
|
4
|
+
* tool — completions carry each result (with an on-disk artifact when
|
|
5
|
+
* truncated) and wake the main model, so waiting is never a tool call; the only
|
|
6
|
+
* in-turn block is `wait: true` on a dispatch, for one-shot parents that exit
|
|
7
|
+
* at end of turn.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
@@ -15,10 +16,10 @@ import { DEFAULT_MAX_RESULT_LINES, loadConfig } from "../configuration/config.ts
|
|
|
15
16
|
import { removeThreadRecord } from "./durable.ts";
|
|
16
17
|
import { formatCompletionBlock, matchRunIds } from "../presentation/format.ts";
|
|
17
18
|
import { emptyUsage } from "../execution/rpc-control.ts";
|
|
18
|
-
import { formatTaskSummary, monitor } from "../presentation/monitor.ts";
|
|
19
|
+
import { formatTaskSummary, formatUsageCompact, monitor } from "../presentation/monitor.ts";
|
|
19
20
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "../isolation/recovery.ts";
|
|
20
21
|
import type { SubagentRuntime, SubagentThread } from "./runtime.ts";
|
|
21
|
-
import { CONTROL_QUIESCE_TIMEOUT_MS, projectResultsRoot, quiesced } from "./thread-shared.ts";
|
|
22
|
+
import { CONTROL_QUIESCE_TIMEOUT_MS, persistThreadCheckpoint, projectResultsRoot, quiesced } from "./thread-shared.ts";
|
|
22
23
|
import { getResultOutput, type SingleResult } from "../execution/spawn.ts";
|
|
23
24
|
import type { WorktreeFinalization } from "../isolation/worktree.ts";
|
|
24
25
|
|
|
@@ -34,19 +35,24 @@ function renderFirstLine(result: { content?: unknown }, label: string, theme: an
|
|
|
34
35
|
|
|
35
36
|
export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
36
37
|
const SubagentControlParams = Type.Object({
|
|
37
|
-
action: StringEnum(["resume"] as const, {
|
|
38
|
-
description:
|
|
38
|
+
action: StringEnum(["steer", "resume", "park"] as const, {
|
|
39
|
+
description:
|
|
40
|
+
"steer: send guidance to the running attempt (a settled or parked thread continues with it); resume: continue a parked or settled thread; park: pause a running thread at a stable checkpoint, keeping its session and worktree for a later resume.",
|
|
39
41
|
}),
|
|
40
42
|
id: Type.Integer({ minimum: 1, description: "Stable run id shown by subagent dispatch output." }),
|
|
41
43
|
objective: Type.Optional(
|
|
42
|
-
Type.String({ description: "
|
|
44
|
+
Type.String({ description: "Guidance for steer (required and nonblank), or an optional appended objective for resume. Ignored by park." }),
|
|
43
45
|
),
|
|
44
46
|
});
|
|
45
47
|
|
|
48
|
+
/** A thread that a steer can continue instead of reject: it is not live, but
|
|
49
|
+
* its retained session can absorb the guidance as an appended objective. */
|
|
50
|
+
type ContinuableState = "completed" | "failed" | "parked";
|
|
51
|
+
|
|
46
52
|
pi.registerTool({
|
|
47
53
|
name: "subagent_control",
|
|
48
54
|
label: "Subagent Control",
|
|
49
|
-
description: "
|
|
55
|
+
description: "Steer a running child with additional guidance (continuing it if it has settled or is parked), resume a parked/settled thread, or park a running thread at a stable checkpoint, by stable run id.",
|
|
50
56
|
parameters: SubagentControlParams,
|
|
51
57
|
|
|
52
58
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -61,31 +67,152 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
61
67
|
const trimmed = value?.trim();
|
|
62
68
|
return trimmed ? trimmed : undefined;
|
|
63
69
|
};
|
|
70
|
+
const textResult = (text: string) => ({ content: [{ type: "text" as const, text }], details: {} });
|
|
71
|
+
/** Why the thread has no steerable/parkable running RPC attempt right now. */
|
|
72
|
+
const inactiveReason = (): string | undefined => {
|
|
73
|
+
// Park drives the control through `stopped` before it records the
|
|
74
|
+
// checkpoint, so the operation is named ahead of the transient state.
|
|
75
|
+
if (thread.lifecycleOperation === "park") return "parking";
|
|
76
|
+
if (thread.lifecycleOperation === "stop" || thread.state === "stopped") return "stopped";
|
|
77
|
+
if (thread.lifecycleOperation === "resume") return "resuming";
|
|
78
|
+
if (thread.lifecycleOperation === "settle" || thread.state === "completed" || thread.state === "failed") {
|
|
79
|
+
return `settled (${thread.state})`;
|
|
80
|
+
}
|
|
81
|
+
if (thread.state === "queued" || thread.state === "resuming") {
|
|
82
|
+
const phase = thread.control.getPhase();
|
|
83
|
+
return phase === "starting" || phase === "retrying" ? phase : thread.state;
|
|
84
|
+
}
|
|
85
|
+
return thread.state === "running" ? undefined : thread.state;
|
|
86
|
+
};
|
|
87
|
+
const resumeThread = async (
|
|
88
|
+
objective: string | undefined,
|
|
89
|
+
continuedFrom?: ContinuableState,
|
|
90
|
+
) => {
|
|
91
|
+
if (thread.retired) {
|
|
92
|
+
return textResult(`Run #${thread.id} was retired by subagent_stop and has no resumable session.`);
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
thread.state !== "parked" &&
|
|
96
|
+
thread.state !== "completed" &&
|
|
97
|
+
thread.state !== "failed"
|
|
98
|
+
) {
|
|
99
|
+
return textResult(`Run #${thread.id} is ${thread.state}; it must be parked or settled before resume.`);
|
|
100
|
+
}
|
|
101
|
+
const requestedObjective = objective === undefined ? undefined : nonBlank(objective);
|
|
102
|
+
if (objective !== undefined && !requestedObjective) {
|
|
103
|
+
return textResult("resume objective must be non-blank when provided.");
|
|
104
|
+
}
|
|
105
|
+
const hadRetainedSession = Boolean(thread.sessionId && thread.sessionDir);
|
|
106
|
+
const pending = await thread.resume(requestedObjective, ctx);
|
|
107
|
+
if (pending.exitCode !== -1) return textResult(getResultOutput(pending));
|
|
108
|
+
const currentObjective = formatTaskSummary(requestedObjective ?? thread.task, 80, false);
|
|
109
|
+
const mode = requestedObjective
|
|
110
|
+
? `appended objective: ${currentObjective}`
|
|
111
|
+
: `continuing current objective: ${currentObjective}`;
|
|
112
|
+
const context = hadRetainedSession ? "retained context reused" : "no prior child context";
|
|
113
|
+
const prefix = continuedFrom
|
|
114
|
+
? `Run #${thread.id} was already ${continuedFrom} before steering; resumed the same thread`
|
|
115
|
+
: `Resumed run #${thread.id}`;
|
|
116
|
+
return textResult(`${prefix}: ${mode}; ${context}.`);
|
|
117
|
+
};
|
|
118
|
+
/** Steering guidance for a thread that is no longer live continues the
|
|
119
|
+
* same thread with that guidance instead of being dropped, so the
|
|
120
|
+
* evidence is never re-bought by a second dispatch. */
|
|
121
|
+
const continueSteer = async (objective: string) => {
|
|
122
|
+
const continuable = (): ContinuableState | undefined =>
|
|
123
|
+
thread.state === "completed" || thread.state === "failed" || thread.state === "parked"
|
|
124
|
+
? thread.state
|
|
125
|
+
: undefined;
|
|
126
|
+
if (thread.lifecycleOperation === "settle" || (!continuable() && thread.control.getPhase() === "settled")) {
|
|
127
|
+
if (!(await quiesced(thread.generationCompletion))) return undefined;
|
|
128
|
+
}
|
|
129
|
+
const state = continuable();
|
|
130
|
+
if (!state || thread.lifecycleOperation) return undefined;
|
|
131
|
+
return resumeThread(objective, state);
|
|
132
|
+
};
|
|
64
133
|
|
|
65
134
|
try {
|
|
66
135
|
switch (params.action) {
|
|
136
|
+
case "steer": {
|
|
137
|
+
const objective = nonBlank(params.objective);
|
|
138
|
+
if (!objective) {
|
|
139
|
+
return textResult("steer objective must be non-blank.");
|
|
140
|
+
}
|
|
141
|
+
if (thread.retired) {
|
|
142
|
+
return textResult(`Run #${thread.id} was retired by subagent_stop and cannot be steered.`);
|
|
143
|
+
}
|
|
144
|
+
const continued = await continueSteer(objective);
|
|
145
|
+
if (continued) return continued;
|
|
146
|
+
const unavailable = inactiveReason();
|
|
147
|
+
if (unavailable) {
|
|
148
|
+
return textResult(`Run #${thread.id} is ${unavailable}; only an active running RPC attempt can be steered. No guidance was sent.`);
|
|
149
|
+
}
|
|
150
|
+
const steered = await thread.control.steer(objective);
|
|
151
|
+
if (!steered.accepted) {
|
|
152
|
+
const resumed = await continueSteer(objective);
|
|
153
|
+
if (resumed) return resumed;
|
|
154
|
+
if (steered.reason === "no-active-attempt") {
|
|
155
|
+
return textResult(`Run #${thread.id} is marked running but has no active RPC attempt; no guidance was sent.`);
|
|
156
|
+
}
|
|
157
|
+
return textResult(`Run #${thread.id} is ${steered.phase}; only an active running RPC attempt can be steered. No guidance was sent.`);
|
|
158
|
+
}
|
|
159
|
+
return textResult(`Steered run #${thread.id} with additional in-scope guidance; its original objective is unchanged.`);
|
|
160
|
+
}
|
|
67
161
|
case "resume": {
|
|
162
|
+
return resumeThread(params.objective);
|
|
163
|
+
}
|
|
164
|
+
case "park": {
|
|
68
165
|
if (thread.retired) {
|
|
69
|
-
return
|
|
166
|
+
return textResult(`Run #${thread.id} was retired by subagent_stop and cannot be parked.`);
|
|
70
167
|
}
|
|
71
|
-
|
|
72
|
-
|
|
168
|
+
const unavailable = inactiveReason();
|
|
169
|
+
if (unavailable) {
|
|
170
|
+
return textResult(`Run #${thread.id} is ${unavailable}; only an active running RPC attempt can be parked. Use subagent_stop to discard a run that has not started.`);
|
|
73
171
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return { content: [{ type: "text", text: "resume objective must be non-blank when provided." }], details: {} };
|
|
172
|
+
if (!thread.sessionId || !thread.sessionDir) {
|
|
173
|
+
return textResult(`Run #${thread.id} has no retained session yet; steer it or let it settle instead.`);
|
|
77
174
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
175
|
+
// Park interrupts the child at its next safe point but keeps the
|
|
176
|
+
// session and worktree, so the thread returns to `parked`, not to a
|
|
177
|
+
// failure. Claim synchronously like stop; the generation body sees
|
|
178
|
+
// the claim and leaves publication to this path.
|
|
179
|
+
const parkVersion = ++thread.lifecycleVersion;
|
|
180
|
+
thread.lifecycleOperation = "park";
|
|
181
|
+
const generation = thread.generation;
|
|
182
|
+
const controller = thread.queueController;
|
|
183
|
+
const completion = thread.generationCompletion;
|
|
184
|
+
const ownsPark = (): boolean =>
|
|
185
|
+
runtime.threads.get(thread.id) === thread &&
|
|
186
|
+
thread.generation === generation &&
|
|
187
|
+
thread.lifecycleVersion === parkVersion &&
|
|
188
|
+
thread.lifecycleOperation === "park" &&
|
|
189
|
+
!thread.retired;
|
|
190
|
+
try {
|
|
191
|
+
await quiesced(thread.control.stop("Parked by subagent_control at a stable checkpoint.").catch(() => undefined));
|
|
192
|
+
if (!(await quiesced(completion))) runtime.backgroundQueue.cancel(controller);
|
|
193
|
+
if (!ownsPark()) {
|
|
194
|
+
return textResult(`Run #${thread.id} changed while it was being parked; no checkpoint was recorded by this call.`);
|
|
195
|
+
}
|
|
196
|
+
if (runtime.runControllers.get(thread.id) === controller) runtime.runControllers.delete(thread.id);
|
|
197
|
+
if (thread.queueController === controller) thread.queueController = undefined;
|
|
198
|
+
thread.state = "parked";
|
|
199
|
+
monitor.setStatus(thread.id, "parked");
|
|
200
|
+
thread.elapsedMs = monitor.getElapsedMs(thread.id) ?? thread.elapsedMs;
|
|
201
|
+
persistThreadCheckpoint(runtime, thread, "parked");
|
|
202
|
+
const run = monitor.findRun(thread.id);
|
|
203
|
+
const usage = run ? formatUsageCompact(run.usage) : "";
|
|
204
|
+
if (runtime.sessionActive) {
|
|
205
|
+
ctx.ui.notify(`■ #${thread.id} ${run ? monitor.summarize(run) : thread.agentName} · parked`, "info");
|
|
206
|
+
}
|
|
207
|
+
const retained = thread.isolation === "worktree" ? "session and worktree" : "session";
|
|
208
|
+
return textResult(
|
|
209
|
+
`Parked run #${thread.id} (${thread.agentName}) at a stable checkpoint${usage ? ` after ${usage}` : ""}. Its retained ${retained} continue on subagent_control resume (optionally with an appended objective) or on steer; subagent_stop discards them.`,
|
|
210
|
+
);
|
|
211
|
+
} finally {
|
|
212
|
+
if (thread.lifecycleVersion === parkVersion && thread.lifecycleOperation === "park") {
|
|
213
|
+
thread.lifecycleOperation = undefined;
|
|
214
|
+
}
|
|
82
215
|
}
|
|
83
|
-
const currentObjective = formatTaskSummary(objective ?? thread.task, 80, false);
|
|
84
|
-
const mode = objective
|
|
85
|
-
? `appended objective: ${currentObjective}`
|
|
86
|
-
: `continuing current objective: ${currentObjective}`;
|
|
87
|
-
const context = hadRetainedSession ? "retained context reused" : "no prior child context";
|
|
88
|
-
return { content: [{ type: "text", text: `Resumed run #${thread.id}: ${mode}; ${context}.` }], details: {} };
|
|
89
216
|
}
|
|
90
217
|
}
|
|
91
218
|
} catch (error) {
|