brl-subagent 2.3.9 → 2.5.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/AGENT.md +5 -0
- package/README.md +20 -8
- package/package.json +11 -9
- package/src/concurrency.ts +1 -0
- package/src/diff.ts +1 -0
- package/src/event-bus.ts +8 -2
- package/src/git.ts +68 -0
- package/src/history.ts +59 -1
- package/src/index.ts +244 -406
- package/src/kill-escalation.ts +94 -0
- package/src/logging.ts +58 -4
- package/src/messaging.ts +1 -0
- package/src/metrics.ts +1 -0
- package/src/model-availability.ts +1 -0
- package/src/notify-completion.ts +1 -0
- package/src/params.ts +1 -0
- package/src/paths.ts +1 -0
- package/src/preflight.ts +30 -14
- package/src/prelude.ts +1 -0
- package/src/presets.ts +1 -0
- package/src/proc.ts +140 -0
- package/src/prompt.ts +1 -0
- package/src/recovery-engine.ts +262 -0
- package/src/recovery.ts +325 -0
- package/src/reports.ts +64 -8
- package/src/router.ts +1 -0
- package/src/run-registry.ts +269 -0
- package/src/runner.ts +207 -16
- package/src/sanitize.ts +54 -3
- package/src/scheduler.ts +1 -0
- package/src/schema.ts +254 -0
- package/src/session-manager.ts +142 -111
- package/src/state.ts +52 -7
- package/src/templates.ts +1 -0
- package/src/transcript-path.ts +41 -0
- package/src/transcript-tail.ts +1 -0
- package/src/transcript.ts +14 -12
- package/src/tui-format.ts +1 -0
- package/src/tui.ts +231 -296
- package/src/types.ts +130 -2
- package/src/unit-run.ts +9 -0
- package/src/validate.ts +16 -2
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Purpose: Entry point: tool/command registration, the delegate_task handlers, and the execution-mode runners.
|
|
1
2
|
/**
|
|
2
3
|
* brl-subagent — multi-agent orchestration for pi.
|
|
3
4
|
* Entry point: tool registrations, command surface, and the multi-mode runners.
|
|
@@ -29,6 +30,7 @@ import type {
|
|
|
29
30
|
GraphDetails,
|
|
30
31
|
GraphWave,
|
|
31
32
|
DelegateTaskDetails,
|
|
33
|
+
DelegateTaskParams,
|
|
32
34
|
Priority,
|
|
33
35
|
TaskTemplate,
|
|
34
36
|
ToolResult,
|
|
@@ -56,25 +58,34 @@ import { pkgPath } from "./paths";
|
|
|
56
58
|
|
|
57
59
|
import { sanitizeTask, validateCwd, validateOutputFile, stripAnsi, capOutput, getCurrentDepth, sanitizeErrorMessage, buildCrashResult } from "./sanitize";
|
|
58
60
|
import {
|
|
59
|
-
getCurrentBranch,
|
|
60
61
|
hasUncommittedChanges,
|
|
61
62
|
createWorkBranch,
|
|
62
63
|
captureDiff,
|
|
63
|
-
|
|
64
|
+
getHeadState,
|
|
65
|
+
restoreHead,
|
|
64
66
|
deleteBranch,
|
|
65
67
|
mergeWorkBranch,
|
|
68
|
+
type HeadState,
|
|
66
69
|
} from "./git";
|
|
67
70
|
import { preflightCheck } from "./preflight";
|
|
68
71
|
import { gateSessionCost, rejectApprovalAlwaysInBackground, runPreTaskValidation, validateDelegationTargets, formatValidationWarnings } from "./prelude";
|
|
69
72
|
import { loadBuiltinPresets, loadCustomPresets, getAllPresets, writePresetFile, formatPresetRestriction, formatToolRestriction } from "./presets";
|
|
70
73
|
import { modelIsAvailable } from "./model-availability";
|
|
71
|
-
import { validatePreTask, diagnoseFailure } from "./validate";
|
|
74
|
+
import { validatePreTask, diagnoseFailure, DEFAULT_BACKGROUND_DEADLINE_MS } from "./validate";
|
|
72
75
|
import { findUnknownParams, KNOWN_DELEGATE_KEYS, resolveSubagentParams, snapshotOriginalParams } from "./params";
|
|
76
|
+
import { delegateTaskParamsSchema } from "./schema";
|
|
73
77
|
import { createSessionState } from "./state";
|
|
74
78
|
import { makeLiveOnUpdate, createUnitRun, finalizeUnitRun, finalizeUnitRunCrash, pruneHistoryIfNeeded, registerLiveRun } from "./unit-run";
|
|
75
79
|
import { buildSubagentPrompt, describePromptMode } from "./prompt";
|
|
76
|
-
import { runSubagent, cleanupTempDirs } from "./runner";
|
|
80
|
+
import { runSubagent, cleanupTempDirs, reapActiveChildren } from "./runner";
|
|
81
|
+
import {
|
|
82
|
+
recoverProduction,
|
|
83
|
+
currentProcessOwner,
|
|
84
|
+
newDispatchIdentity,
|
|
85
|
+
} from "./recovery";
|
|
86
|
+
import { markInterrupted } from "./run-registry";
|
|
77
87
|
import { acquireSlot, releaseSlot, updateStatus, updateProgressStatus } from "./concurrency";
|
|
88
|
+
import { transcriptDisplayPath } from "./transcript-path";
|
|
78
89
|
import {
|
|
79
90
|
finalizeRunRecord,
|
|
80
91
|
resolveRetryParams,
|
|
@@ -100,6 +111,7 @@ import {
|
|
|
100
111
|
showRunHistory,
|
|
101
112
|
showMonitor,
|
|
102
113
|
showDashboard,
|
|
114
|
+
showComplianceMenu,
|
|
103
115
|
showRetryMenu,
|
|
104
116
|
renderDelegateCall,
|
|
105
117
|
renderDelegateResult,
|
|
@@ -119,35 +131,6 @@ import {
|
|
|
119
131
|
// Helpers
|
|
120
132
|
// ---------------------------------------------------------------------------
|
|
121
133
|
|
|
122
|
-
/**
|
|
123
|
-
* Sanitize a text preview for markdown-rendered notifications: strip code
|
|
124
|
-
* fences (``` and ~~~) and backticks, keep whole non-empty lines with
|
|
125
|
-
* mid-line truncation for over-long lines, and never split surrogate pairs
|
|
126
|
-
* at the boundary.
|
|
127
|
-
*/
|
|
128
|
-
function sanitizePreview(text: string, maxLen = 500): string {
|
|
129
|
-
// Strip markdown fences and backticks
|
|
130
|
-
const cleaned = text.replace(/```/g, '').replace(/~~~+/g, '').replace(/`/g, '');
|
|
131
|
-
const lines = cleaned.split('\n').filter(l => l.trim());
|
|
132
|
-
let out = '';
|
|
133
|
-
for (const line of lines) {
|
|
134
|
-
if (out.length + line.length + 1 > maxLen) {
|
|
135
|
-
// Mid-line truncation: keep as much of this line as fits
|
|
136
|
-
const remaining = maxLen - out.length - 1;
|
|
137
|
-
if (remaining > 10) {
|
|
138
|
-
out += line.slice(0, remaining - 3) + '...\n';
|
|
139
|
-
}
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
out += line + '\n';
|
|
143
|
-
}
|
|
144
|
-
// Avoid splitting surrogate pairs at the boundary (reachable now via slice above)
|
|
145
|
-
const safe = out.slice(0, maxLen);
|
|
146
|
-
const lastChar = safe[safe.length - 1];
|
|
147
|
-
const stripped = lastChar && /[\uD800-\uDBFF]/.test(lastChar) ? safe.slice(0, -1) : safe;
|
|
148
|
-
return stripped.trimEnd();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
134
|
/**
|
|
152
135
|
* DRY helper (issue #154 review): shape an inventory of items into a single
|
|
153
136
|
* comma-delimited summary line for the LLM-facing delegation guidance. Both
|
|
@@ -192,6 +175,17 @@ export function buildTemplateGuideline(templateSummary: string): string {
|
|
|
192
175
|
// Extension entry point
|
|
193
176
|
// ---------------------------------------------------------------------------
|
|
194
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Compile-time coverage (issue #229 item 4): fails to compile if
|
|
180
|
+
* DelegateTaskParams omits any KNOWN_DELEGATE_KEYS entry. Runtime drift is
|
|
181
|
+
* invisible (the schema passes the key through, the handler ignores it), so
|
|
182
|
+
* only the compiler can catch it. Together with per-step-model.test.ts's
|
|
183
|
+
* runtime ratchet this closes the chain: registered schema <-> KNOWN_DELEGATE_KEYS <-> DelegateTaskParams.
|
|
184
|
+
*/
|
|
185
|
+
type _KnownDelegateKey = (typeof KNOWN_DELEGATE_KEYS extends Set<infer K> ? K : never) & string;
|
|
186
|
+
type _ExpectNever<T extends never> = T;
|
|
187
|
+
type _DelegateParamCoverage = _ExpectNever<Exclude<_KnownDelegateKey, keyof DelegateTaskParams>>;
|
|
188
|
+
|
|
195
189
|
export default function (pi: ExtensionAPI) {
|
|
196
190
|
const log = createLogger("brl-subagent");
|
|
197
191
|
|
|
@@ -413,12 +407,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
413
407
|
// -------------------------------------------------------------------
|
|
414
408
|
|
|
415
409
|
async function runChainMode(
|
|
416
|
-
params:
|
|
410
|
+
params: DelegateTaskParams,
|
|
417
411
|
signal: AbortSignal | undefined,
|
|
418
412
|
onUpdate: AgentToolUpdateCallback<DelegateTaskDetails> | undefined,
|
|
419
413
|
ctx: ExtensionContext,
|
|
420
414
|
): Promise<ToolResult<SubagentResult | ChainDetails | undefined>> {
|
|
421
|
-
const chainSteps = params.chain
|
|
415
|
+
const chainSteps: SubTaskParams[] = params.chain!;
|
|
422
416
|
|
|
423
417
|
// Issue #133: dispatch-start timestamp — the aggregate run entry's
|
|
424
418
|
// startedAt (captured once, at mode entry, not at completion).
|
|
@@ -714,7 +708,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
714
708
|
// foreground live entry (the #130 invariant gap that swept
|
|
715
709
|
// graph/chain entries immediately).
|
|
716
710
|
const { runId, run } = createUnitRun(merged, stepModel, chainPriority, globalParams.resolvedPreset?.name);
|
|
717
|
-
state.persistRun(pi, run);
|
|
711
|
+
state.persistRun(pi, run, "unit");
|
|
718
712
|
|
|
719
713
|
// R2: Prune old run entries if history exceeds limit
|
|
720
714
|
pruneHistoryIfNeeded(state, ctx, log);
|
|
@@ -754,6 +748,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
754
748
|
getFinalOutput,
|
|
755
749
|
log,
|
|
756
750
|
currentDepth + 1,
|
|
751
|
+
undefined, // intercom
|
|
752
|
+
undefined, // subagentId
|
|
753
|
+
run.childMarker,
|
|
757
754
|
);
|
|
758
755
|
} catch (err) {
|
|
759
756
|
// Issue #133 (mirrors the #119 R1/C1 discipline): finalize the
|
|
@@ -937,12 +934,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
937
934
|
// -------------------------------------------------------------------
|
|
938
935
|
|
|
939
936
|
async function runParallelMode(
|
|
940
|
-
params:
|
|
937
|
+
params: DelegateTaskParams,
|
|
941
938
|
signal: AbortSignal | undefined,
|
|
942
939
|
onUpdate: AgentToolUpdateCallback<DelegateTaskDetails> | undefined,
|
|
943
940
|
ctx: ExtensionContext,
|
|
944
941
|
): Promise<ToolResult<SubagentResult | ParallelDetails | undefined>> {
|
|
945
|
-
const taskList = params.tasks
|
|
942
|
+
const taskList: SubTaskParams[] = params.tasks!;
|
|
946
943
|
|
|
947
944
|
// R5: Check session cost limit before spawning
|
|
948
945
|
const costError = gateSessionCost({
|
|
@@ -1166,7 +1163,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1166
1163
|
// monitor drill-in shows per-unit priority and a post-hoc audit
|
|
1167
1164
|
// trail exists per subtask (mirrors single-mode run persistence).
|
|
1168
1165
|
const { runId, run } = createUnitRun(merged, stepModel, parallelPriority, globalParams.resolvedPreset?.name);
|
|
1169
|
-
state.persistRun(pi, run);
|
|
1166
|
+
state.persistRun(pi, run, "unit");
|
|
1170
1167
|
|
|
1171
1168
|
// R2: Prune old run entries if history exceeds limit
|
|
1172
1169
|
pruneHistoryIfNeeded(state, ctx, log);
|
|
@@ -1224,6 +1221,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1224
1221
|
currentDepth + 1,
|
|
1225
1222
|
intercom,
|
|
1226
1223
|
subagentId,
|
|
1224
|
+
run.childMarker,
|
|
1227
1225
|
);
|
|
1228
1226
|
|
|
1229
1227
|
// Fill SubTaskResult
|
|
@@ -1407,12 +1405,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1407
1405
|
// -------------------------------------------------------------------
|
|
1408
1406
|
|
|
1409
1407
|
async function runGraphMode(
|
|
1410
|
-
params:
|
|
1408
|
+
params: DelegateTaskParams,
|
|
1411
1409
|
signal: AbortSignal | undefined,
|
|
1412
1410
|
onUpdate: AgentToolUpdateCallback<DelegateTaskDetails> | undefined,
|
|
1413
1411
|
ctx: ExtensionContext,
|
|
1414
1412
|
): Promise<ToolResult<SubagentResult | GraphDetails | undefined>> {
|
|
1415
|
-
const graphTasks = params.graph
|
|
1413
|
+
const graphTasks: GraphTask[] = params.graph!;
|
|
1416
1414
|
|
|
1417
1415
|
// Issue #133: dispatch-start timestamp — the aggregate run entry's
|
|
1418
1416
|
// startedAt (captured once, at mode entry, not at completion).
|
|
@@ -1802,7 +1800,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1802
1800
|
// foreground live entry (the #130 invariant gap that swept
|
|
1803
1801
|
// graph/chain entries immediately).
|
|
1804
1802
|
const { runId, run } = createUnitRun(merged, stepModel, graphPriority, globalParams.resolvedPreset?.name);
|
|
1805
|
-
state.persistRun(pi, run);
|
|
1803
|
+
state.persistRun(pi, run, "unit");
|
|
1806
1804
|
|
|
1807
1805
|
// R2: Prune old run entries if history exceeds limit
|
|
1808
1806
|
pruneHistoryIfNeeded(state, ctx, log);
|
|
@@ -1839,6 +1837,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1839
1837
|
currentDepth + 1,
|
|
1840
1838
|
intercom,
|
|
1841
1839
|
subagentId,
|
|
1840
|
+
run.childMarker,
|
|
1842
1841
|
);
|
|
1843
1842
|
|
|
1844
1843
|
const subTaskResult: SubTaskResult = {
|
|
@@ -2012,10 +2011,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2012
2011
|
async function runBackgroundFanOut(
|
|
2013
2012
|
pi: ExtensionAPI,
|
|
2014
2013
|
ctx: ExtensionContext,
|
|
2015
|
-
params:
|
|
2014
|
+
params: DelegateTaskParams,
|
|
2016
2015
|
signal: AbortSignal | undefined,
|
|
2017
2016
|
): Promise<ToolResult<SubagentResult | undefined>> {
|
|
2018
|
-
const taskList = params.tasks
|
|
2017
|
+
const taskList: SubTaskParams[] = params.tasks!;
|
|
2019
2018
|
|
|
2020
2019
|
// Session cost limit gates the WHOLE batch before any spawn — same
|
|
2021
2020
|
// estimate/limit logic as runParallelMode, scaled by task count.
|
|
@@ -2253,10 +2252,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2253
2252
|
// startBackgroundAgent — the shared background-spawn tail.
|
|
2254
2253
|
// Used by BOTH spawnBackgroundRun (single task) and runBackgroundFanOut
|
|
2255
2254
|
// (tasks fan-out): the session-manager import, the spawn call, live-monitor
|
|
2256
|
-
// registration, footer counters, the 2s progress poller and the
|
|
2257
|
-
// timer live here exactly once. Callers keep their own prelude,
|
|
2258
|
-
// validations and result formatting. THROWS on spawn failure —
|
|
2259
|
-
// translate the error into their own result shape.
|
|
2255
|
+
// registration, footer counters, the 2s progress poller and the default
|
|
2256
|
+
// deadline timer live here exactly once. Callers keep their own prelude,
|
|
2257
|
+
// checks, validations and result formatting. THROWS on spawn failure —
|
|
2258
|
+
// callers translate the error into their own result shape.
|
|
2260
2259
|
// -------------------------------------------------------------------
|
|
2261
2260
|
|
|
2262
2261
|
async function startBackgroundAgent(
|
|
@@ -2277,6 +2276,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2277
2276
|
originalParams?: SubagentRun["originalParams"];
|
|
2278
2277
|
/** cwd used to redact paths from crash notifications. */
|
|
2279
2278
|
sanitizeCwd: string;
|
|
2279
|
+
// Option B U1: durability identity threaded onto the background records.
|
|
2280
|
+
dispatchId?: string;
|
|
2281
|
+
resumeOf?: string;
|
|
2282
|
+
attempt?: number;
|
|
2280
2283
|
},
|
|
2281
2284
|
): Promise<BackgroundAgent> {
|
|
2282
2285
|
const { spawnBackgroundSession, setAgentFinalOutput, extractFinalOutput, updateAgentStatus } = await import('./session-manager');
|
|
@@ -2303,6 +2306,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2303
2306
|
// Issue #98/#108: snapshotOriginalParams is the single source of truth
|
|
2304
2307
|
// for the caller params a later retry restores.
|
|
2305
2308
|
originalParams: spawn.originalParams,
|
|
2309
|
+
// Option B U1: durability identity for the agent record + run entry.
|
|
2310
|
+
dispatchId: spawn.dispatchId,
|
|
2311
|
+
resumeOf: spawn.resumeOf,
|
|
2312
|
+
attempt: spawn.attempt,
|
|
2306
2313
|
});
|
|
2307
2314
|
|
|
2308
2315
|
// Register for live monitor
|
|
@@ -2412,7 +2419,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2412
2419
|
}, { deliverAs: "followUp" });
|
|
2413
2420
|
} else if (agent.status === 'stopped') {
|
|
2414
2421
|
// User-initiated stop (stop_subagent) or deadline abort
|
|
2415
|
-
// (timeout/
|
|
2422
|
+
// (timeout/deadline) — not a failure.
|
|
2416
2423
|
updateProgressStatus(state, ctx);
|
|
2417
2424
|
} else {
|
|
2418
2425
|
state.completedSubagents++;
|
|
@@ -2447,20 +2454,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
2447
2454
|
}
|
|
2448
2455
|
}, 2000);
|
|
2449
2456
|
|
|
2450
|
-
//
|
|
2451
|
-
// W2 (issue #28): previously this only stopped the poller —
|
|
2452
|
-
// session kept running forever (orphaned). Now it pre-sets
|
|
2453
|
-
// 'stopped' (so the .then keeps it, per W1) and aborts the
|
|
2454
|
-
//
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
+
// Default deadline: stop polling AND abort the session after the
|
|
2458
|
+
// deadline. W2 (issue #28): previously this only stopped the poller —
|
|
2459
|
+
// the pi session kept running forever (orphaned). Now it pre-sets
|
|
2460
|
+
// 'stopped' (so the .then keeps it, per W1) and aborts the session.
|
|
2461
|
+
// Issue #240: an explicit per-agent timeout is HONORED verbatim; this
|
|
2462
|
+
// deadline DEFAULTS to 30m only when no timeout is given. It is a
|
|
2463
|
+
// default, not a ceiling. Issue #244: exactly ONE deadline timer per
|
|
2464
|
+
// run — an explicit timeout is owned by the session-manager W3 timer,
|
|
2465
|
+
// so this index timer arms ONLY on the no-timeout path, mirroring
|
|
2466
|
+
// spawnBackgroundSession's `params.timeout && params.timeout > 0` guard
|
|
2467
|
+
// (with no timeout, it owns the 30m default, #28).
|
|
2468
|
+
const hasExplicitDeadline = spawn.timeout !== undefined && spawn.timeout > 0;
|
|
2469
|
+
const hardCapMs = spawn.timeout ?? DEFAULT_BACKGROUND_DEADLINE_MS;
|
|
2470
|
+
const hardCapHandle: ReturnType<typeof setTimeout> | undefined =
|
|
2471
|
+
hasExplicitDeadline ? undefined : setTimeout(() => {
|
|
2457
2472
|
if (!completed && !agent.completedAt) {
|
|
2458
2473
|
try {
|
|
2459
2474
|
completed = true;
|
|
2460
2475
|
clearInterval(pollInterval);
|
|
2461
2476
|
// Pre-set stopped BEFORE aborting so the .then in
|
|
2462
2477
|
// spawnBackgroundSession keeps the stopped state.
|
|
2463
|
-
updateAgentStatus(agent.id, 'stopped', `Timed out (${hardCapMs}ms
|
|
2478
|
+
updateAgentStatus(agent.id, 'stopped', `Timed out (${hardCapMs}ms deadline)`);
|
|
2464
2479
|
agent._sessionRef?.abort().catch(() => {
|
|
2465
2480
|
// Abort may reject if the session is mid-dispose;
|
|
2466
2481
|
// the status flip above is already recorded.
|
|
@@ -2480,7 +2495,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2480
2495
|
// W3/poller stopped path (no completedSubagents increment).
|
|
2481
2496
|
updateProgressStatus(state, ctx);
|
|
2482
2497
|
} catch (err) {
|
|
2483
|
-
// Defensive: never let the
|
|
2498
|
+
// Defensive: never let the deadline timer throw uncaught — that would
|
|
2484
2499
|
// skip finalizeLiveSubagent, the counter decrement, and the notification.
|
|
2485
2500
|
// Do all fallible work first (output capture), then mutate counters,
|
|
2486
2501
|
// then notify — a throw mid-path can't double-fire mutations.
|
|
@@ -2491,7 +2506,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2491
2506
|
// get_subagent_result doesn't report 'running' and the W3 timer
|
|
2492
2507
|
// guard (!agent.completedAt) can't re-fire later.
|
|
2493
2508
|
try {
|
|
2494
|
-
updateAgentStatus(agent.id, 'stopped', `Timed out (${hardCapMs}ms
|
|
2509
|
+
updateAgentStatus(agent.id, 'stopped', `Timed out (${hardCapMs}ms deadline)`);
|
|
2495
2510
|
} catch { /* ignore */ }
|
|
2496
2511
|
try {
|
|
2497
2512
|
setAgentFinalOutput(agent.id, extractAgentFinalOutput(agent));
|
|
@@ -2528,6 +2543,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2528
2543
|
priority?: string;
|
|
2529
2544
|
},
|
|
2530
2545
|
singleTask: string,
|
|
2546
|
+
// Option B U1: the spawn entry a retryRunId resolved to, so a retried
|
|
2547
|
+
// background run inherits dispatchId/resumeOf/attempt (record identity,
|
|
2548
|
+
// not a delegate param — D4).
|
|
2549
|
+
retrySourceRun?: SubagentRun,
|
|
2531
2550
|
): Promise<ToolResult<undefined>> {
|
|
2532
2551
|
// Phase 6.5: Background execution — spawn session and return ID immediately.
|
|
2533
2552
|
// Resolve the preset and its model BEFORE the background branch so the
|
|
@@ -2608,6 +2627,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2608
2627
|
bgResolvedPreset?.promptGuideline,
|
|
2609
2628
|
);
|
|
2610
2629
|
|
|
2630
|
+
// Option B U1: durability identity (retry inherits the original dispatch).
|
|
2631
|
+
const bgIdentity = newDispatchIdentity(retrySourceRun);
|
|
2611
2632
|
const agent = await startBackgroundAgent(pi, ctx, {
|
|
2612
2633
|
task: singleTask,
|
|
2613
2634
|
type: params.preset || 'general-purpose',
|
|
@@ -2622,6 +2643,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2622
2643
|
gitMode: bgResolved.resolvedGitMode,
|
|
2623
2644
|
originalParams: snapshotOriginalParams(params),
|
|
2624
2645
|
sanitizeCwd: bgResolved.effectiveCwd,
|
|
2646
|
+
dispatchId: bgIdentity.dispatchId,
|
|
2647
|
+
resumeOf: bgIdentity.resumeOf,
|
|
2648
|
+
attempt: bgIdentity.attempt,
|
|
2625
2649
|
});
|
|
2626
2650
|
|
|
2627
2651
|
// B2: Surface auto-route decisions in background spawn result too —
|
|
@@ -2696,6 +2720,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2696
2720
|
retry: () => showRetryMenu(ctx, state),
|
|
2697
2721
|
sla: () => showSLAConfig(ctx, state, applyConfig),
|
|
2698
2722
|
"sla-stats": () => showSLAStats(ctx, state),
|
|
2723
|
+
// E5: the compliance report menu (file access / secrets exposure /
|
|
2724
|
+
// full summary) shipped in tui.ts but had no handler key, so
|
|
2725
|
+
// `/brl-subagent compliance` fell through to the config menu and the
|
|
2726
|
+
// feature was unreachable (broken wiring, not dead code).
|
|
2727
|
+
compliance: () => showComplianceMenu(ctx, state),
|
|
2699
2728
|
};
|
|
2700
2729
|
|
|
2701
2730
|
if (trimmed && trimmed in handlers) {
|
|
@@ -2755,13 +2784,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
2755
2784
|
"You can customize per-call via inheritSystemPrompt and systemPrompt: set inheritSystemPrompt: false to save context, provide a systemPrompt for custom instructions, or use both to add instructions on top of inheritance.",
|
|
2756
2785
|
"Set thinkingLevel per call to match task complexity. The level is capped at the user's configured maximum. Map tasks to levels using this heuristic: off = file listing, grep, simple read. minimal = file diff, syntax check, find-and-replace. low = refactoring, test generation, documentation. medium = default — code review, debugging, moderate analysis. high = security audit, architecture review, complex debugging. xhigh = multi-step causal reasoning, research, novel problem solving. Default to 'off' or 'minimal' for trivial tasks — do not waste the user's budget.",
|
|
2757
2786
|
"Use outputFile to have the subagent write full findings to disk and return only a structured summary — saves context tokens for large investigations.",
|
|
2758
|
-
"Set timeout (in ms) to limit how long a subagent can run. Useful for tasks that might hang or get stuck. Background runs
|
|
2787
|
+
"Set timeout (in ms) to limit how long a subagent can run. Useful for tasks that might hang or get stuck. Background runs default to a 30-minute deadline when no timeout is given; an explicit timeout is honored as given (30 minutes is a default, not a ceiling).",
|
|
2759
2788
|
"Set cwd to override the subagent's working directory. Defaults to the current project directory.",
|
|
2760
2789
|
"Set label to give the subagent a human-readable name (e.g., 'security-audit' or 'docs-review'). Labels appear in the status bar and tool call display.",
|
|
2761
2790
|
`Use preset to apply a delegation configuration (built-in or custom via /brl-subagent preset). Preset values are defaults — explicit parameters override them. IMPORTANT: some presets restrict tools — e.g. outputFile requires the subagent's write tool, which security-auditor and code-reviewer exclude. Built-in presets: ${presetRestrictionSummary}. Custom presets are NOT listed here — inspect them via /brl-subagent preset before combining with outputFile or tool-dependent work. When combining a preset with outputFile or tool-dependent work, verify the preset allows the required tools.`,
|
|
2762
2791
|
buildTemplateGuideline(templateSummary),
|
|
2763
2792
|
"To retry a failed subagent, pass its run ID as retryRunId. The retry restores the recorded params - including background, gitMode, approvalMode, force - with explicit values on this call winning. Multi-step runs degrade to a single task. See the retryRunId description for the full field list.",
|
|
2764
|
-
"Set retryOnTimeout: true to automatically retry a subagent that times out. Only retries once — the second timeout is treated as a final failure.",
|
|
2793
|
+
"Set retryOnTimeout: true to automatically retry a FOREGROUND subagent that times out. Only retries once — the second timeout is treated as a final failure. Background runs are not auto-retried; re-dispatch with retryRunId.",
|
|
2765
2794
|
"Set background: true to run the subagent in the background without blocking. The tool returns immediately with an agent ID. With tasks, background fans out: every task starts as its own background agent, the call returns one ID per task in task order, and the conductor is woken once per agent as each finishes; chain and graph cannot be combined with background (rejected). Background runs wake the conductor with a structured completion message when they finish — do not poll: polling is only correct when completion notifications are disabled (completionNotify \"off\"); one status check as a stall check is legitimate.",
|
|
2766
2795
|
"",
|
|
2767
2796
|
"## Conductor Guardrails",
|
|
@@ -2771,7 +2800,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2771
2800
|
"2. **Thinking level**: Match thinking level to task complexity: off/minimal for trivial tasks (file listing, grep), low for refactoring/docs, medium for code review/debugging, high for security audits/complex debugging, xhigh for multi-step reasoning/novel problems.",
|
|
2772
2801
|
"3. **Git mode**: Use gitMode='branch' for tasks that create commits or PRs. Use gitMode='none' for read-only tasks.",
|
|
2773
2802
|
"4. **Tools**: Verify the subagent has the tools it needs. If the task writes files, ensure write and edit are not excluded. If the task runs commands, ensure bash is not excluded.",
|
|
2774
|
-
"5. **Timeout**: Set timeout based on task complexity. Simple: 30s. Medium: 60s. Complex: 120s+. xhigh thinking: at least 120s. Background runs
|
|
2803
|
+
"5. **Timeout**: Set timeout based on task complexity. Simple: 30s. Medium: 60s. Complex: 120s+. xhigh thinking: at least 120s. Background runs default to a 30-minute deadline when no timeout is set; an explicit timeout is honored.",
|
|
2775
2804
|
"",
|
|
2776
2805
|
"These guardrails prevent common misconfigurations. The extension also validates configuration before spawning (H1): tool warnings are surfaced in the returned result, but two mismatches are HARD errors that reject the delegation — outputFile with the write tool excluded, and a capability-critical task/toolset mismatch (a run/execute/test/compile/benchmark task without bash, or an exploration task with none of find/ls/grep/bash; pass force: true to override the capability class). Getting it right the first time is faster and more efficient.",
|
|
2777
2806
|
"",
|
|
@@ -2779,328 +2808,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
2779
2808
|
"The autoRoutePreset() function can automatically select the best preset based on task keywords. Consider using it for preset selection.",
|
|
2780
2809
|
"Prefer a preset when the delegation matches a standard shape — review/audit/test/docs; override per-call deliberately.",
|
|
2781
2810
|
],
|
|
2782
|
-
parameters:
|
|
2783
|
-
task: Type.Optional(Type.String({
|
|
2784
|
-
description: "Detailed description of the task for the subagent to complete (required for single mode, optional for chain/tasks/graph)",
|
|
2785
|
-
})),
|
|
2786
|
-
systemPrompt: Type.Optional(
|
|
2787
|
-
Type.String({
|
|
2788
|
-
description:
|
|
2789
|
-
"Custom system prompt or additional instructions for the subagent. " +
|
|
2790
|
-
"When inheritSystemPrompt is true (default), this is appended after the inherited prompt. " +
|
|
2791
|
-
"When inheritSystemPrompt is false, this replaces the inherited prompt entirely.",
|
|
2792
|
-
}),
|
|
2793
|
-
),
|
|
2794
|
-
inheritSystemPrompt: Type.Optional(
|
|
2795
|
-
Type.Boolean({
|
|
2796
|
-
description:
|
|
2797
|
-
"Whether to inherit the main agent's system prompt. " +
|
|
2798
|
-
"Default: true. Set to false to use only your custom systemPrompt, " +
|
|
2799
|
-
"or to avoid passing a large inherited prompt to the subagent.",
|
|
2800
|
-
}),
|
|
2801
|
-
),
|
|
2802
|
-
thinkingLevel: Type.Optional(
|
|
2803
|
-
Type.String({
|
|
2804
|
-
description:
|
|
2805
|
-
"Requested thinking level for this subagent call. " +
|
|
2806
|
-
"One of: off, minimal, low, medium, high, xhigh. " +
|
|
2807
|
-
"Capped at the user's configured maximum. If omitted, the user's configured level is used.",
|
|
2808
|
-
}),
|
|
2809
|
-
),
|
|
2810
|
-
outputFile: Type.Optional(
|
|
2811
|
-
Type.String({
|
|
2812
|
-
description:
|
|
2813
|
-
"Project-relative path where the subagent should write its full findings. " +
|
|
2814
|
-
"When provided, the subagent is instructed to write complete output to this file " +
|
|
2815
|
-
"and return only a structured summary.",
|
|
2816
|
-
}),
|
|
2817
|
-
),
|
|
2818
|
-
label: Type.Optional(
|
|
2819
|
-
Type.String({
|
|
2820
|
-
description:
|
|
2821
|
-
"Human-readable label for this subagent (e.g., 'security-audit', 'docs-review'). " +
|
|
2822
|
-
"Appears in the status bar and tool call display. " +
|
|
2823
|
-
"Omit to use the default anonymous counter.",
|
|
2824
|
-
}),
|
|
2825
|
-
),
|
|
2826
|
-
model: Type.Optional(Type.String({ description: "Model override (provider/model-id). Defaults to the global subagent model." })),
|
|
2827
|
-
timeout: Type.Optional(
|
|
2828
|
-
Type.Number({
|
|
2829
|
-
description:
|
|
2830
|
-
"Maximum time in milliseconds the subagent is allowed to run. " +
|
|
2831
|
-
"If exceeded, the subagent is killed and an error is returned. " +
|
|
2832
|
-
"Background runs are additionally hard-capped at 30 minutes, so a larger value is silently shortened there.",
|
|
2833
|
-
}),
|
|
2834
|
-
),
|
|
2835
|
-
cwd: Type.Optional(
|
|
2836
|
-
Type.String({
|
|
2837
|
-
description:
|
|
2838
|
-
"Working directory for the subagent. Must be an existing directory. " +
|
|
2839
|
-
"Defaults to the conductor's current working directory.",
|
|
2840
|
-
}),
|
|
2841
|
-
),
|
|
2842
|
-
tools: Type.Optional(
|
|
2843
|
-
Type.Array(Type.String(), {
|
|
2844
|
-
description:
|
|
2845
|
-
"Explicit allowlist of tool names for the subagent. Maps to pi's --tools flag.",
|
|
2846
|
-
}),
|
|
2847
|
-
),
|
|
2848
|
-
excludeTools: Type.Optional(
|
|
2849
|
-
Type.Array(Type.String(), {
|
|
2850
|
-
description:
|
|
2851
|
-
"Tool names to disable for the subagent. Maps to pi's --exclude-tools flag.",
|
|
2852
|
-
}),
|
|
2853
|
-
),
|
|
2854
|
-
noBuiltinTools: Type.Optional(
|
|
2855
|
-
Type.Boolean({
|
|
2856
|
-
description:
|
|
2857
|
-
"Disable all built-in tools for the subagent. Maps to pi's --no-builtin-tools flag.",
|
|
2858
|
-
}),
|
|
2859
|
-
),
|
|
2860
|
-
preset: Type.Optional(
|
|
2861
|
-
Type.String({
|
|
2862
|
-
description:
|
|
2863
|
-
"Name of a saved delegation preset (created via /brl-subagent preset). " +
|
|
2864
|
-
"Preset values are used as defaults; explicit parameters on this call override them.",
|
|
2865
|
-
}),
|
|
2866
|
-
),
|
|
2867
|
-
template: Type.Optional(
|
|
2868
|
-
Type.String({
|
|
2869
|
-
description:
|
|
2870
|
-
"Name of a saved task template. Use with params to fill template slots. " +
|
|
2871
|
-
"Templates are file-backed: 9 builtin templates ship with the extension " +
|
|
2872
|
-
"(browse via /brl-subagent templates); override or add via .md files in " +
|
|
2873
|
-
"~/.pi/agent/brl-subagent/templates/ or .pi/brl-subagent/templates/ " +
|
|
2874
|
-
"(project-local wins over user-global over builtin).",
|
|
2875
|
-
}),
|
|
2876
|
-
),
|
|
2877
|
-
params: Type.Optional(
|
|
2878
|
-
Type.Record(Type.String(), Type.String(), {
|
|
2879
|
-
description:
|
|
2880
|
-
"Parameter values for template ${param} slots. " +
|
|
2881
|
-
"Keys are param names, values are the substitution text.",
|
|
2882
|
-
}),
|
|
2883
|
-
),
|
|
2884
|
-
retryRunId: Type.Optional(
|
|
2885
|
-
Type.String({
|
|
2886
|
-
description:
|
|
2887
|
-
"ID of a previously failed subagent run to retry. " +
|
|
2888
|
-
"The retry rebuilds the parameter object from a fixed field set, falling back to the " +
|
|
2889
|
-
"original run's recorded values, with explicit values on this call winning: task, label, " +
|
|
2890
|
-
"model, preset, systemPrompt, inheritSystemPrompt, thinkingLevel, priority, outputFile, " +
|
|
2891
|
-
"timeout, cwd, tools, excludeTools, noBuiltinTools, background, gitMode, approvalMode, force. " +
|
|
2892
|
-
"The execution-shape fields ARE restored: a retried background run stays background (with its " +
|
|
2893
|
-
"completion wake), and a retried branch-mode run keeps its work branch, its approvalMode " +
|
|
2894
|
-
"gating and its force override. Qualify that by the effective gitMode - when neither the record " +
|
|
2895
|
-
"nor this call carries one, the configured default applies, which may still be 'branch'. " +
|
|
2896
|
-
"Not restored: chain/tasks/graph and params, so a retried multi-step run silently degrades to a " +
|
|
2897
|
-
"single task - re-issue it fresh. retryOnTimeout is explicit-only: never restored, honoured when passed. " +
|
|
2898
|
-
"template is asymmetric: the original's template is NOT restored, but a template passed on the " +
|
|
2899
|
-
"retry call DOES take effect (it is resolved before the retry merge). " +
|
|
2900
|
-
"Fan-out units (parallel/chain/graph origin) record no background, so retrying one is a single " +
|
|
2901
|
-
"foreground run - pass background: true for a background retry. " +
|
|
2902
|
-
"Only works with runs that ended in failure (exitCode != 0, timeout, error, or abort).",
|
|
2903
|
-
}),
|
|
2904
|
-
),
|
|
2905
|
-
gitMode: Type.Optional(
|
|
2906
|
-
Type.String({
|
|
2907
|
-
description:
|
|
2908
|
-
"Git integration mode for this subagent call. " +
|
|
2909
|
-
"'branch' creates a work branch, captures the diff, and switches back. " +
|
|
2910
|
-
"'none' (default) does nothing. Falls back to the configured default.",
|
|
2911
|
-
}),
|
|
2912
|
-
),
|
|
2913
|
-
retryOnTimeout: Type.Optional(
|
|
2914
|
-
Type.Boolean({
|
|
2915
|
-
description:
|
|
2916
|
-
"If true and the subagent times out, automatically retry with the same parameters. " +
|
|
2917
|
-
"Only retries once — the second timeout is treated as a final failure.",
|
|
2918
|
-
}),
|
|
2919
|
-
),
|
|
2920
|
-
approvalMode: Type.Optional(
|
|
2921
|
-
Type.String({
|
|
2922
|
-
description:
|
|
2923
|
-
"Change approval mode: auto (never ask), writes (ask when files changed), " +
|
|
2924
|
-
"always (ask every time). Default is user config (/brl-subagent approval).",
|
|
2925
|
-
}),
|
|
2926
|
-
),
|
|
2927
|
-
force: Type.Optional(
|
|
2928
|
-
Type.Boolean({
|
|
2929
|
-
description:
|
|
2930
|
-
"Override for capability pre-flight errors (default false). A dispatch is blocked when the task clearly needs a capability the resolved toolset lacks — " +
|
|
2931
|
-
"a run/execute/test/compile/benchmark task with no bash, or an exploration task (search/grep/find/list/locate/glob) with none of find/ls/grep/bash. " +
|
|
2932
|
-
"Set force: true to dispatch anyway: the mismatch is then delivered as a warning in the result instead of rejecting the call. " +
|
|
2933
|
-
"Warnings are surfaced either way. force never suppresses outputFile-without-write conflicts.",
|
|
2934
|
-
}),
|
|
2935
|
-
),
|
|
2936
|
-
background: Type.Optional(
|
|
2937
|
-
Type.Boolean({
|
|
2938
|
-
description:
|
|
2939
|
-
"Run the subagent in the background without blocking the conductor. " +
|
|
2940
|
-
"When true, the tool returns immediately with an agent ID. " +
|
|
2941
|
-
"The conductor is woken with a completion message; use get_subagent_result for post-wake retrieval and stall checks. " +
|
|
2942
|
-
"Supports a single task or the tasks array (a fan-out that starts one background agent per task; with tasks the call returns one ID per task and the conductor is woken once per agent as each finishes); combining background with chain or graph is rejected. " +
|
|
2943
|
-
"Default: false (blocking mode).",
|
|
2944
|
-
}),
|
|
2945
|
-
),
|
|
2946
|
-
priority: Type.Optional(
|
|
2947
|
-
Type.Union([
|
|
2948
|
-
Type.Literal("critical"),
|
|
2949
|
-
Type.Literal("high"),
|
|
2950
|
-
Type.Literal("normal"),
|
|
2951
|
-
Type.Literal("low"),
|
|
2952
|
-
], {
|
|
2953
|
-
description: "Concurrency priority for this delegation: critical, high, normal, or low. Higher-priority delegations queue ahead. Defaults to normal.",
|
|
2954
|
-
})
|
|
2955
|
-
),
|
|
2956
|
-
// Issue #114: NO per-step `priority` on chain[] — chain steps never
|
|
2957
|
-
// compete for concurrency slots (the chain holds ONE slot for its whole
|
|
2958
|
-
// duration); array order IS the priority.
|
|
2959
|
-
chain: Type.Optional(Type.Array(Type.Object({
|
|
2960
|
-
task: Type.String({ description: "Task description. Use {previous} to reference the previous step output." }),
|
|
2961
|
-
label: Type.Optional(Type.String({})),
|
|
2962
|
-
model: Type.Optional(Type.String({ description: "Model override for this step (provider/model-id). Defaults to the global subagent model." })),
|
|
2963
|
-
thinkingLevel: Type.Optional(Type.String({})),
|
|
2964
|
-
cwd: Type.Optional(Type.String({})),
|
|
2965
|
-
timeout: Type.Optional(Type.Number({})),
|
|
2966
|
-
outputFile: Type.Optional(Type.String({})),
|
|
2967
|
-
tools: Type.Optional(Type.Array(Type.String({}))),
|
|
2968
|
-
excludeTools: Type.Optional(Type.Array(Type.String({}))),
|
|
2969
|
-
noBuiltinTools: Type.Optional(Type.Boolean({})),
|
|
2970
|
-
systemPrompt: Type.Optional(Type.String({})),
|
|
2971
|
-
inheritSystemPrompt: Type.Optional(Type.Boolean({})),
|
|
2972
|
-
}), {
|
|
2973
|
-
description: "Sequential chain of tasks. Each step receives the previous step output via {previous} placeholder in the task string. Chain stops at the first failure. Max " + MAX_CHAIN_STEPS + " steps."
|
|
2974
|
-
})),
|
|
2975
|
-
tasks: Type.Optional(Type.Array(Type.Object({
|
|
2976
|
-
task: Type.String({ description: "Task description for this parallel subtask" }),
|
|
2977
|
-
label: Type.Optional(Type.String({})),
|
|
2978
|
-
model: Type.Optional(Type.String({ description: "Model override for this step (provider/model-id). Defaults to the global subagent model." })),
|
|
2979
|
-
thinkingLevel: Type.Optional(Type.String({})),
|
|
2980
|
-
priority: Type.Optional(
|
|
2981
|
-
Type.Union([
|
|
2982
|
-
Type.Literal("critical"),
|
|
2983
|
-
Type.Literal("high"),
|
|
2984
|
-
Type.Literal("normal"),
|
|
2985
|
-
Type.Literal("low"),
|
|
2986
|
-
], { description: "Concurrency priority for this subtask (overrides the call-level default)." })
|
|
2987
|
-
),
|
|
2988
|
-
cwd: Type.Optional(Type.String({})),
|
|
2989
|
-
timeout: Type.Optional(Type.Number({})),
|
|
2990
|
-
outputFile: Type.Optional(Type.String({})),
|
|
2991
|
-
tools: Type.Optional(Type.Array(Type.String({}))),
|
|
2992
|
-
excludeTools: Type.Optional(Type.Array(Type.String({}))),
|
|
2993
|
-
noBuiltinTools: Type.Optional(Type.Boolean({})),
|
|
2994
|
-
systemPrompt: Type.Optional(Type.String({})),
|
|
2995
|
-
inheritSystemPrompt: Type.Optional(Type.Boolean({})),
|
|
2996
|
-
}), {
|
|
2997
|
-
description: "Parallel tasks to execute concurrently. All tasks run regardless of individual failures. Max " + MAX_PARALLEL_TASKS + " tasks."
|
|
2998
|
-
})),
|
|
2999
|
-
graph: Type.Optional(Type.Array(Type.Object({
|
|
3000
|
-
id: Type.String({ description: "Unique identifier for this task node" }),
|
|
3001
|
-
task: Type.String({ description: "Task description. Use {<nodeId>} to reference output from another task (the referenced node's own id, which must be a word-character id - letters, digits, underscore; ids like 'step-1' or 'node.a' are not matched and the braces are left as literal text)." }),
|
|
3002
|
-
label: Type.Optional(Type.String({})),
|
|
3003
|
-
model: Type.Optional(Type.String({ description: "Model override for this step (provider/model-id). Defaults to the global subagent model." })),
|
|
3004
|
-
dependsOn: Type.Optional(Type.Array(Type.String({}), { description: "IDs of tasks that must complete before this one starts" })),
|
|
3005
|
-
thinkingLevel: Type.Optional(Type.String({})),
|
|
3006
|
-
priority: Type.Optional(
|
|
3007
|
-
Type.Union([
|
|
3008
|
-
Type.Literal("critical"),
|
|
3009
|
-
Type.Literal("high"),
|
|
3010
|
-
Type.Literal("normal"),
|
|
3011
|
-
Type.Literal("low"),
|
|
3012
|
-
], { description: "Concurrency priority for this subtask (overrides the call-level default)." })
|
|
3013
|
-
),
|
|
3014
|
-
cwd: Type.Optional(Type.String({})),
|
|
3015
|
-
timeout: Type.Optional(Type.Number({})),
|
|
3016
|
-
outputFile: Type.Optional(Type.String({})),
|
|
3017
|
-
tools: Type.Optional(Type.Array(Type.String({}))),
|
|
3018
|
-
excludeTools: Type.Optional(Type.Array(Type.String({}))),
|
|
3019
|
-
noBuiltinTools: Type.Optional(Type.Boolean({})),
|
|
3020
|
-
systemPrompt: Type.Optional(Type.String({})),
|
|
3021
|
-
inheritSystemPrompt: Type.Optional(Type.Boolean({})),
|
|
3022
|
-
}), {
|
|
3023
|
-
description: "Declare tasks with dependencies. The scheduler parallelizes independent tasks and sequences dependent ones. Max " + MAX_GRAPH_TASKS + " tasks."
|
|
3024
|
-
})),
|
|
3025
|
-
}),
|
|
2811
|
+
parameters: delegateTaskParamsSchema,
|
|
3026
2812
|
|
|
3027
2813
|
async execute(
|
|
3028
2814
|
_toolCallId: string,
|
|
3029
|
-
params:
|
|
3030
|
-
// Schema registers task as Type.Optional — required for single mode only,
|
|
3031
|
-
// omitted by chain/tasks/graph calls. Aligned with Static<TParams>.
|
|
3032
|
-
task?: string;
|
|
3033
|
-
label?: string;
|
|
3034
|
-
model?: string;
|
|
3035
|
-
preset?: string;
|
|
3036
|
-
systemPrompt?: string;
|
|
3037
|
-
inheritSystemPrompt?: boolean;
|
|
3038
|
-
thinkingLevel?: string;
|
|
3039
|
-
outputFile?: string;
|
|
3040
|
-
timeout?: number;
|
|
3041
|
-
cwd?: string;
|
|
3042
|
-
tools?: string[];
|
|
3043
|
-
excludeTools?: string[];
|
|
3044
|
-
noBuiltinTools?: boolean;
|
|
3045
|
-
template?: string;
|
|
3046
|
-
params?: Record<string, string>;
|
|
3047
|
-
retryRunId?: string;
|
|
3048
|
-
retryOnTimeout?: boolean;
|
|
3049
|
-
background?: boolean;
|
|
3050
|
-
force?: boolean;
|
|
3051
|
-
gitMode?: string;
|
|
3052
|
-
priority?: string;
|
|
3053
|
-
chain?: Array<{
|
|
3054
|
-
task: string;
|
|
3055
|
-
label?: string;
|
|
3056
|
-
model?: string;
|
|
3057
|
-
thinkingLevel?: string;
|
|
3058
|
-
cwd?: string;
|
|
3059
|
-
timeout?: number;
|
|
3060
|
-
outputFile?: string;
|
|
3061
|
-
tools?: string[];
|
|
3062
|
-
excludeTools?: string[];
|
|
3063
|
-
noBuiltinTools?: boolean;
|
|
3064
|
-
systemPrompt?: string;
|
|
3065
|
-
inheritSystemPrompt?: boolean;
|
|
3066
|
-
}>;
|
|
3067
|
-
tasks?: Array<{
|
|
3068
|
-
task: string;
|
|
3069
|
-
label?: string;
|
|
3070
|
-
model?: string;
|
|
3071
|
-
thinkingLevel?: string;
|
|
3072
|
-
priority?: string;
|
|
3073
|
-
cwd?: string;
|
|
3074
|
-
timeout?: number;
|
|
3075
|
-
outputFile?: string;
|
|
3076
|
-
tools?: string[];
|
|
3077
|
-
excludeTools?: string[];
|
|
3078
|
-
noBuiltinTools?: boolean;
|
|
3079
|
-
systemPrompt?: string;
|
|
3080
|
-
inheritSystemPrompt?: boolean;
|
|
3081
|
-
}>;
|
|
3082
|
-
graph?: Array<{
|
|
3083
|
-
id: string;
|
|
3084
|
-
task: string;
|
|
3085
|
-
label?: string;
|
|
3086
|
-
model?: string;
|
|
3087
|
-
dependsOn?: string[];
|
|
3088
|
-
thinkingLevel?: string;
|
|
3089
|
-
priority?: string;
|
|
3090
|
-
cwd?: string;
|
|
3091
|
-
timeout?: number;
|
|
3092
|
-
outputFile?: string;
|
|
3093
|
-
tools?: string[];
|
|
3094
|
-
excludeTools?: string[];
|
|
3095
|
-
noBuiltinTools?: boolean;
|
|
3096
|
-
systemPrompt?: string;
|
|
3097
|
-
inheritSystemPrompt?: boolean;
|
|
3098
|
-
}>;
|
|
3099
|
-
},
|
|
2815
|
+
params: DelegateTaskParams,
|
|
3100
2816
|
signal: AbortSignal | undefined,
|
|
3101
2817
|
onUpdate: AgentToolUpdateCallback<DelegateTaskDetails> | undefined,
|
|
3102
2818
|
ctx: ExtensionContext,
|
|
3103
2819
|
) {
|
|
2820
|
+
// Option B U1: the run a retryRunId resolves to (spawn entry). Internal
|
|
2821
|
+
// record identity — dispatchId/resumeOf/attempt are threaded onto the new
|
|
2822
|
+
// record, NOT through the delegate-param surface (D4).
|
|
2823
|
+
let retrySourceRun: SubagentRun | undefined;
|
|
3104
2824
|
// Issue #99: warn on unknown params — typebox allows additional properties
|
|
3105
2825
|
// by default (pi's validateToolArguments passes unknown keys through), so a
|
|
3106
2826
|
// typo like `thinkinglevel` or a schema-drifted param is otherwise silently
|
|
@@ -3204,7 +2924,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3204
2924
|
if (params.retryRunId) {
|
|
3205
2925
|
const runEntry = state.findSpawnRunById(ctx, params.retryRunId);
|
|
3206
2926
|
if (runEntry) {
|
|
3207
|
-
|
|
2927
|
+
retrySourceRun = runEntry;
|
|
2928
|
+
params = resolveRetryParams(params, runEntry) as DelegateTaskParams;
|
|
3208
2929
|
} else {
|
|
3209
2930
|
// Issue #98: a silent no-op here made retries of background runs
|
|
3210
2931
|
// (which previously never wrote run entries) start as FRESH runs
|
|
@@ -3323,7 +3044,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3323
3044
|
const singleTask = params.task!; // non-empty (sanitizer guarantees; isSingle confirms)
|
|
3324
3045
|
|
|
3325
3046
|
if (params.background) {
|
|
3326
|
-
return spawnBackgroundRun(pi, ctx, params, singleTask);
|
|
3047
|
+
return spawnBackgroundRun(pi, ctx, params, singleTask, retrySourceRun);
|
|
3327
3048
|
}
|
|
3328
3049
|
|
|
3329
3050
|
// R5: Check session cost limit before spawning
|
|
@@ -3470,6 +3191,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3470
3191
|
|
|
3471
3192
|
// Create run record
|
|
3472
3193
|
const runId = crypto.randomUUID();
|
|
3194
|
+
// Option B U1: durability identity — a retry inherits the original
|
|
3195
|
+
// dispatchId and records resumeOf/attempt; a fresh marker keys the
|
|
3196
|
+
// child process for boot-time orphan discovery.
|
|
3197
|
+
const identity = newDispatchIdentity(retrySourceRun);
|
|
3473
3198
|
const run: SubagentRun = {
|
|
3474
3199
|
id: runId,
|
|
3475
3200
|
task,
|
|
@@ -3482,8 +3207,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3482
3207
|
priority: params.priority,
|
|
3483
3208
|
startedAt: new Date().toISOString(),
|
|
3484
3209
|
originalParams: snapshotOriginalParams(params),
|
|
3210
|
+
dispatchId: identity.dispatchId,
|
|
3211
|
+
resumeOf: identity.resumeOf,
|
|
3212
|
+
attempt: identity.attempt,
|
|
3213
|
+
owner: currentProcessOwner(),
|
|
3214
|
+
childMarker: crypto.randomUUID(),
|
|
3485
3215
|
};
|
|
3486
|
-
state.persistRun(pi, run);
|
|
3216
|
+
state.persistRun(pi, run, "foreground");
|
|
3487
3217
|
|
|
3488
3218
|
// R2: Prune old run entries if history exceeds limit
|
|
3489
3219
|
pruneHistoryIfNeeded(state, ctx, log);
|
|
@@ -3492,11 +3222,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
3492
3222
|
registerLiveRun(state, run, ctx);
|
|
3493
3223
|
|
|
3494
3224
|
// P3: Git integration — set up work branch if gitMode is "branch"
|
|
3495
|
-
let
|
|
3225
|
+
let headState: HeadState | undefined;
|
|
3496
3226
|
let workBranchName: string | undefined;
|
|
3497
3227
|
if (resolvedGitMode === "branch") {
|
|
3498
3228
|
try {
|
|
3499
|
-
|
|
3229
|
+
headState = getHeadState(resolvedCwd);
|
|
3500
3230
|
|
|
3501
3231
|
if (hasUncommittedChanges(resolvedCwd)) {
|
|
3502
3232
|
log.warn("Uncommitted changes detected; proceeding with branch-based workflow anyway", {
|
|
@@ -3504,40 +3234,56 @@ export default function (pi: ExtensionAPI) {
|
|
|
3504
3234
|
});
|
|
3505
3235
|
}
|
|
3506
3236
|
|
|
3507
|
-
const branchResult = createWorkBranch(resolvedCwd,
|
|
3237
|
+
const branchResult = createWorkBranch(resolvedCwd, headState.sha);
|
|
3508
3238
|
if (branchResult.ok) {
|
|
3509
3239
|
workBranchName = branchResult.branch;
|
|
3510
3240
|
log.info("Created work branch for subagent", {
|
|
3511
3241
|
branch: workBranchName,
|
|
3512
|
-
base:
|
|
3242
|
+
base: headState.branch,
|
|
3513
3243
|
});
|
|
3514
3244
|
} else {
|
|
3515
3245
|
log.error("Failed to create work branch, falling back to 'none'", {
|
|
3516
3246
|
error: branchResult.error,
|
|
3517
3247
|
});
|
|
3518
|
-
|
|
3248
|
+
headState = undefined;
|
|
3519
3249
|
}
|
|
3520
3250
|
} catch (err) {
|
|
3521
3251
|
log.warn("Not a git repository or git error; falling back to gitMode 'none'", {
|
|
3522
3252
|
error: (err as Error).message,
|
|
3523
3253
|
});
|
|
3524
|
-
|
|
3254
|
+
headState = undefined;
|
|
3525
3255
|
workBranchName = undefined;
|
|
3526
3256
|
}
|
|
3527
3257
|
}
|
|
3528
3258
|
|
|
3529
|
-
// Helper to
|
|
3259
|
+
// Helper to restore the starting HEAD and delete the work branch. The
|
|
3260
|
+
// delete is NOT gated on the restore succeeding: the branch must never
|
|
3261
|
+
// survive teardown. A still-checked-out branch simply makes deleteBranch
|
|
3262
|
+
// fail (logged) rather than silently leaking the ref.
|
|
3530
3263
|
const cleanupGitBranch = () => {
|
|
3531
|
-
if (workBranchName
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3264
|
+
if (!workBranchName) return;
|
|
3265
|
+
if (headState) {
|
|
3266
|
+
const restoreResult = restoreHead(resolvedCwd, headState);
|
|
3267
|
+
if (restoreResult.ok) {
|
|
3268
|
+
log.info("Restored original HEAD", {
|
|
3269
|
+
branch: headState.branch,
|
|
3270
|
+
detached: headState.detached,
|
|
3271
|
+
});
|
|
3272
|
+
} else {
|
|
3273
|
+
log.warn("Failed to restore original HEAD during cleanup", {
|
|
3274
|
+
branch: headState.branch,
|
|
3275
|
+
error: restoreResult.error,
|
|
3276
|
+
});
|
|
3539
3277
|
}
|
|
3540
3278
|
}
|
|
3279
|
+
// Attempt to delete the work branch (non-critical)
|
|
3280
|
+
const deleteResult = deleteBranch(resolvedCwd, workBranchName);
|
|
3281
|
+
if (!deleteResult.ok) {
|
|
3282
|
+
log.warn("Failed to delete work branch during cleanup", {
|
|
3283
|
+
branch: workBranchName,
|
|
3284
|
+
error: deleteResult.error,
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3541
3287
|
};
|
|
3542
3288
|
|
|
3543
3289
|
// R1: Circuit breaker check — reject if circuit is open
|
|
@@ -3635,6 +3381,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3635
3381
|
childDepth,
|
|
3636
3382
|
undefined, // intercom
|
|
3637
3383
|
undefined, // subagentId
|
|
3384
|
+
run.childMarker,
|
|
3638
3385
|
);
|
|
3639
3386
|
|
|
3640
3387
|
// Auto-retry on timeout
|
|
@@ -3682,6 +3429,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3682
3429
|
childDepth,
|
|
3683
3430
|
undefined, // intercom
|
|
3684
3431
|
undefined, // subagentId
|
|
3432
|
+
run.childMarker,
|
|
3685
3433
|
);
|
|
3686
3434
|
}
|
|
3687
3435
|
|
|
@@ -3694,17 +3442,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
3694
3442
|
result.label = label;
|
|
3695
3443
|
|
|
3696
3444
|
// P3: Capture git diff if we created a work branch
|
|
3697
|
-
if (workBranchName &&
|
|
3698
|
-
|
|
3445
|
+
if (workBranchName && headState) {
|
|
3446
|
+
// Diff against the captured starting sha rather than the branch
|
|
3447
|
+
// name: a detached start's branch label is the "HEAD" pseudo-ref,
|
|
3448
|
+
// for which `git diff HEAD...HEAD` is always empty.
|
|
3449
|
+
const diffResult = captureDiff(resolvedCwd, headState.sha);
|
|
3699
3450
|
if (diffResult.ok) {
|
|
3700
3451
|
result.gitBranch = workBranchName;
|
|
3701
3452
|
result.gitDiff = diffResult.diff;
|
|
3702
3453
|
}
|
|
3703
3454
|
|
|
3704
|
-
//
|
|
3705
|
-
const
|
|
3706
|
-
if (
|
|
3707
|
-
log.info("
|
|
3455
|
+
// Restore the starting HEAD (branch, or detached sha)
|
|
3456
|
+
const restoreResult = restoreHead(resolvedCwd, headState);
|
|
3457
|
+
if (restoreResult.ok) {
|
|
3458
|
+
log.info("Restored original HEAD", {
|
|
3459
|
+
branch: headState.branch,
|
|
3460
|
+
detached: headState.detached,
|
|
3461
|
+
});
|
|
3708
3462
|
|
|
3709
3463
|
// P4: Change approval workflow — let user review the diff
|
|
3710
3464
|
const diffContent = (result.gitDiff ?? "").trim();
|
|
@@ -3748,13 +3502,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
3748
3502
|
error: mergeResult.error,
|
|
3749
3503
|
});
|
|
3750
3504
|
}
|
|
3751
|
-
|
|
3505
|
+
// #308: a DETACHED start has no branch of its own to hold the
|
|
3506
|
+
// merge result — teardown re-detached at the captured sha and
|
|
3507
|
+
// the merge fast-forwarded THAT detached HEAD, so deleting the
|
|
3508
|
+
// work branch would leave the committed work reachable only
|
|
3509
|
+
// from the detached HEAD (silently lost on the next checkout).
|
|
3510
|
+
// Keep the branch as the durable ref and log where the work
|
|
3511
|
+
// lives. An ATTACHED start merged onto its own branch, which
|
|
3512
|
+
// already retains the commit, so the temporary work branch is
|
|
3513
|
+
// safe to drop.
|
|
3514
|
+
if (headState.detached) {
|
|
3515
|
+
log.info("detached checkout: work preserved on branch", {
|
|
3516
|
+
branch: workBranchName,
|
|
3517
|
+
});
|
|
3518
|
+
} else {
|
|
3519
|
+
deleteBranch(resolvedCwd, workBranchName);
|
|
3520
|
+
}
|
|
3752
3521
|
workBranchName = undefined;
|
|
3753
3522
|
} else {
|
|
3754
3523
|
// No changes — just delete the empty branch
|
|
3755
3524
|
deleteBranch(resolvedCwd, workBranchName);
|
|
3756
3525
|
workBranchName = undefined;
|
|
3757
3526
|
}
|
|
3527
|
+
} else {
|
|
3528
|
+
log.warn("Failed to restore original HEAD after work-branch run", {
|
|
3529
|
+
branch: headState.branch,
|
|
3530
|
+
error: restoreResult.error,
|
|
3531
|
+
});
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// Nothing may leave teardown with the work branch still present: the
|
|
3535
|
+
// approval flow above clears workBranchName on every terminal choice
|
|
3536
|
+
// (including "apply", where keeping the branch is intentional), so a
|
|
3537
|
+
// still-set name means the restore failed and the flow was skipped.
|
|
3538
|
+
if (workBranchName) {
|
|
3539
|
+
deleteBranch(resolvedCwd, workBranchName);
|
|
3540
|
+
workBranchName = undefined;
|
|
3758
3541
|
}
|
|
3759
3542
|
}
|
|
3760
3543
|
|
|
@@ -4018,8 +3801,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4018
3801
|
}
|
|
4019
3802
|
|
|
4020
3803
|
// Include transcript path
|
|
4021
|
-
|
|
4022
|
-
resultText += `\nTranscript: ${getTranscriptPath(params.agent_id)}`;
|
|
3804
|
+
resultText += `\nTranscript: ${transcriptDisplayPath(params.agent_id)}`;
|
|
4023
3805
|
|
|
4024
3806
|
return {
|
|
4025
3807
|
content: [{ type: "text" as const, text: resultText }],
|
|
@@ -4038,7 +3820,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4038
3820
|
label: "Steer Subagent",
|
|
4039
3821
|
description: [
|
|
4040
3822
|
"Send a steering message to a running background agent.",
|
|
4041
|
-
"The message
|
|
3823
|
+
"The message is delivered after the agent's current tool call finishes, before its next model call.",
|
|
4042
3824
|
"Use this to redirect an agent's work without restarting it.",
|
|
4043
3825
|
].join(" "),
|
|
4044
3826
|
parameters: Type.Object({
|
|
@@ -4053,7 +3835,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4053
3835
|
const { steerAgent } = await import('./session-manager');
|
|
4054
3836
|
|
|
4055
3837
|
try {
|
|
4056
|
-
const agent = steerAgent(params.agent_id, params.message);
|
|
3838
|
+
const agent = await steerAgent(params.agent_id, params.message);
|
|
4057
3839
|
|
|
4058
3840
|
if (!agent) {
|
|
4059
3841
|
return {
|
|
@@ -4138,6 +3920,50 @@ export default function (pi: ExtensionAPI) {
|
|
|
4138
3920
|
// Session lifecycle — F7: Session-bound state initialization
|
|
4139
3921
|
// -------------------------------------------------------------------
|
|
4140
3922
|
|
|
3923
|
+
// -------------------------------------------------------------------
|
|
3924
|
+
// Option B U1: boot recovery — identity/liveness detection + child reap
|
|
3925
|
+
// -------------------------------------------------------------------
|
|
3926
|
+
// Runs AFTER restoreFromSession so config is ready. Candidate runs come from
|
|
3927
|
+
// the DURABLE registry (`.pi/run-registry/`), NOT from this session's run
|
|
3928
|
+
// entries: a crashed session's foreground/unit records are invisible to a
|
|
3929
|
+
// default fresh startup, and the full `.pi/subagents/` store is too costly to
|
|
3930
|
+
// parse (only the one agent record a background entry names is read). For
|
|
3931
|
+
// each in-flight run whose owner is dead, its orphaned marker-verified child
|
|
3932
|
+
// is reaped and the record is marked: the registry entry always, plus the
|
|
3933
|
+
// agent record for background runs. SILENT in the UI — the notice/offer
|
|
3934
|
+
// surface is U3. Platforms without /proc skip the whole scan (one log line):
|
|
3935
|
+
// pid-only liveness cannot rule out pid reuse, so nothing is killed or
|
|
3936
|
+
// marked unverified.
|
|
3937
|
+
async function performBootRecovery(): Promise<void> {
|
|
3938
|
+
try {
|
|
3939
|
+
const { getAgent, markAgentInterrupted } = await import("./session-manager");
|
|
3940
|
+
// `recoverProduction` owns the /proc platform guard (nothing is marked or
|
|
3941
|
+
// killed without start-token verification) — see src/recovery.ts.
|
|
3942
|
+
const summary = await recoverProduction({
|
|
3943
|
+
log,
|
|
3944
|
+
// Read THAT ONE agent record by id — never the whole store.
|
|
3945
|
+
readAgentRecord: (id) => getAgent(id),
|
|
3946
|
+
mark(record, interruptedAt) {
|
|
3947
|
+
// Registry entry = the durable mark for every kind. Issue #304: its
|
|
3948
|
+
// boolean is the durable-write result — do NOT drop it, or the boot
|
|
3949
|
+
// summary would claim a mark that never landed. The scan counts a
|
|
3950
|
+
// `false` as `markFailures` and warns with the id.
|
|
3951
|
+
const durable = markInterrupted(record.id, interruptedAt);
|
|
3952
|
+
if (record.kind === "agent") markAgentInterrupted(record.id, interruptedAt);
|
|
3953
|
+
return durable;
|
|
3954
|
+
},
|
|
3955
|
+
});
|
|
3956
|
+
// Report the scan whenever it did anything durable OR attempted a mark
|
|
3957
|
+
// that failed — a failure-only boot must not be silent (issue #304).
|
|
3958
|
+
if (summary.marked > 0 || summary.markFailures > 0) {
|
|
3959
|
+
log.warn("Boot recovery marked interrupted runs", { ...summary });
|
|
3960
|
+
}
|
|
3961
|
+
} catch (err) {
|
|
3962
|
+
// Recovery is best-effort hygiene — it must never break session start.
|
|
3963
|
+
log.warn("Boot recovery scan failed", { error: (err as Error).message });
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
|
|
4141
3967
|
pi.on("session_start", async (_event, ctx) => {
|
|
4142
3968
|
// Issue #147: capture the session context for the completion-push subscriber.
|
|
4143
3969
|
sessionCtx = ctx;
|
|
@@ -4188,6 +4014,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
4188
4014
|
// F5/F9: Safe state restoration with type guards
|
|
4189
4015
|
state.restoreFromSession(ctx);
|
|
4190
4016
|
|
|
4017
|
+
// Option B U1: reap a dead conductor's orphaned children and mark the
|
|
4018
|
+
// records interrupted BEFORE anything else touches their worktrees.
|
|
4019
|
+
await performBootRecovery();
|
|
4020
|
+
|
|
4191
4021
|
updateStatus(state, ctx);
|
|
4192
4022
|
});
|
|
4193
4023
|
|
|
@@ -4197,6 +4027,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
4197
4027
|
activeSubagents: state.activeSubagents,
|
|
4198
4028
|
});
|
|
4199
4029
|
|
|
4030
|
+
// Option B U1: reap in-flight foreground children first, so a clean exit
|
|
4031
|
+
// never orphans a subprocess (the crash case is covered by boot recovery).
|
|
4032
|
+
try {
|
|
4033
|
+
await reapActiveChildren(log);
|
|
4034
|
+
} catch (err) {
|
|
4035
|
+
log.warn("Shutdown reap failed", { error: (err as Error).message });
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4200
4038
|
// Clear all live subagent sessions
|
|
4201
4039
|
state.subagentSessions.clear();
|
|
4202
4040
|
// Drop pending finalize claims too (PR #71 review: the claim set must
|