brl-subagent 2.5.0 → 2.5.1
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 +9 -2
- package/README.md +6 -3
- package/package.json +6 -6
- package/src/history.ts +19 -9
- package/src/index.ts +174 -34
- package/src/kill-escalation.ts +338 -6
- package/src/metrics.ts +5 -3
- package/src/notify-completion.ts +23 -0
- package/src/proc.ts +270 -20
- package/src/recovery-engine.ts +100 -14
- package/src/recovery-surface.ts +374 -0
- package/src/recovery.ts +19 -1
- package/src/run-registry.ts +64 -3
- package/src/runner.ts +216 -13
- package/src/session-manager.ts +129 -7
- package/src/state.ts +32 -2
- package/src/tui-format.ts +57 -1
- package/src/tui.ts +102 -22
- package/src/types.ts +66 -1
- package/src/unit-run.ts +8 -1
package/src/tui.ts
CHANGED
|
@@ -32,11 +32,13 @@ import type {
|
|
|
32
32
|
UsageStats,
|
|
33
33
|
FileDiff,
|
|
34
34
|
CompletionNotifyMode,
|
|
35
|
+
RecoveryMode,
|
|
35
36
|
} from "./types";
|
|
36
37
|
import {
|
|
37
38
|
THINKING_LEVELS,
|
|
38
39
|
NAV_FOOTER,
|
|
39
40
|
COMPLETION_NOTIFY_MODES,
|
|
41
|
+
RECOVERY_MODES,
|
|
40
42
|
TASK_PREVIEW_MAX_LENGTH,
|
|
41
43
|
COLLAPSED_OUTPUT_LINES,
|
|
42
44
|
COLLAPSED_DIFF_FILES_PREVIEW,
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
isMultiSubagentDetails,
|
|
52
54
|
isGraphDetails,
|
|
53
55
|
isUnsettledPartial,
|
|
56
|
+
isInterruptedRun,
|
|
54
57
|
} from "./types";
|
|
55
58
|
import { buildFileAccessReport, buildSecretsExposureReport, generateComplianceSummary } from "./reports";
|
|
56
59
|
import { extractParamNames } from "./templates";
|
|
@@ -59,10 +62,11 @@ import { formatPresetSummary, getPreset, loadCustomPresets } from "./presets";
|
|
|
59
62
|
import { buildOutputHonestyLines, formatRunDuration } from "./history";
|
|
60
63
|
import * as fs from "node:fs";
|
|
61
64
|
import * as path from "node:path";
|
|
62
|
-
import { collapseRunsForHistory, sweepStaleLiveSubagents, type SessionState } from "./state";
|
|
65
|
+
import { collapseRunsForHistory, retryableRuns, sweepStaleLiveSubagents, type SessionState } from "./state";
|
|
63
66
|
import { getAgent } from "./session-manager";
|
|
67
|
+
import { visibleRunEntriesForSession } from "./recovery-surface";
|
|
64
68
|
import { computeSLAMetrics, computeCostTrend, formatSparkline } from "./metrics";
|
|
65
|
-
import { formatElapsed, liveRowName, liveSpinner, formatLiveRowDim } from "./tui-format";
|
|
69
|
+
import { formatElapsed, liveRowName, liveSpinner, formatLiveRowDim, runHistoryStatus, runHistoryStatusLabel, buildRetryOfferMessage, runDisplayName } from "./tui-format";
|
|
66
70
|
import {
|
|
67
71
|
renderTranscriptMessages,
|
|
68
72
|
type TranscriptLineStyle,
|
|
@@ -336,6 +340,32 @@ export async function showCompletionNotifySelector(
|
|
|
336
340
|
onConfigChanged(ctx, `Completion notify mode set to ${result}`);
|
|
337
341
|
}
|
|
338
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Option B U3 (D4): recovery posture selector. Modeled on the completionNotify
|
|
345
|
+
* selector; `RECOVERY_MODES` excludes "auto" by construction.
|
|
346
|
+
*/
|
|
347
|
+
export async function showRecoverySelector(
|
|
348
|
+
ctx: ExtensionContext,
|
|
349
|
+
state: SessionState,
|
|
350
|
+
onConfigChanged: (ctx: ExtensionContext, msg: string) => void,
|
|
351
|
+
): Promise<void> {
|
|
352
|
+
const modeDescriptions: Record<RecoveryMode, string> = {
|
|
353
|
+
offer: "Surface interrupted runs (Run History row + one boot notice) - default",
|
|
354
|
+
off: "Do not surface interrupted runs",
|
|
355
|
+
};
|
|
356
|
+
const items: SelectItem[] = RECOVERY_MODES.map((mode) => ({
|
|
357
|
+
value: mode,
|
|
358
|
+
label: mode,
|
|
359
|
+
description: modeDescriptions[mode],
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
const result = await showSelectList(ctx, "Recovery Posture", items, 5);
|
|
363
|
+
if (!result) return;
|
|
364
|
+
|
|
365
|
+
state.config.recovery = result as RecoveryMode;
|
|
366
|
+
onConfigChanged(ctx, `Recovery posture set to ${result}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
339
369
|
// ---------------------------------------------------------------------------
|
|
340
370
|
// E4: SLA tracking configuration
|
|
341
371
|
// ---------------------------------------------------------------------------
|
|
@@ -938,6 +968,13 @@ export function getConfigMenuItems(state: SessionState): SelectItem[] {
|
|
|
938
968
|
? "Wake only on failed/stopped"
|
|
939
969
|
: "Never wake (passive delivery)",
|
|
940
970
|
},
|
|
971
|
+
{
|
|
972
|
+
value: "recovery",
|
|
973
|
+
label: "Set Recovery Posture",
|
|
974
|
+
description: state.config.recovery === "offer"
|
|
975
|
+
? "Surface interrupted runs (default)"
|
|
976
|
+
: "Do not surface interrupted runs",
|
|
977
|
+
},
|
|
941
978
|
|
|
942
979
|
{
|
|
943
980
|
value: "historyentries",
|
|
@@ -1085,6 +1122,17 @@ export async function showConfigMenu(
|
|
|
1085
1122
|
// Run history viewer
|
|
1086
1123
|
// ---------------------------------------------------------------------------
|
|
1087
1124
|
|
|
1125
|
+
/**
|
|
1126
|
+
* U3 Fix A: the recovery visibility bridge wired for a TUI surface. Session
|
|
1127
|
+
* run entries UNIONED with synthetic durable interrupted records the session
|
|
1128
|
+
* has never seen (a genuine fresh boot), deduped by id. Both the history list
|
|
1129
|
+
* and the retry menu read THIS, so they can never disagree about an interrupted
|
|
1130
|
+
* run that exists only in the durable registry.
|
|
1131
|
+
*/
|
|
1132
|
+
function recoverableRunEntries(ctx: ExtensionContext, state: SessionState): SubagentRun[] {
|
|
1133
|
+
return visibleRunEntriesForSession(state.getRunEntries(ctx), getAgent);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1088
1136
|
export async function showRunHistory(
|
|
1089
1137
|
ctx: ExtensionContext,
|
|
1090
1138
|
state: SessionState,
|
|
@@ -1094,6 +1142,7 @@ export async function showRunHistory(
|
|
|
1094
1142
|
running: ctx.ui.theme.fg("accent", "◉"),
|
|
1095
1143
|
done: ctx.ui.theme.fg("success", "✓"),
|
|
1096
1144
|
failed: ctx.ui.theme.fg("error", "✗"),
|
|
1145
|
+
interrupted: ctx.ui.theme.fg("warning", "◌"),
|
|
1097
1146
|
};
|
|
1098
1147
|
|
|
1099
1148
|
// Browse loop (issue #260): list → detail → list … until the LIST is exited
|
|
@@ -1102,7 +1151,7 @@ export async function showRunHistory(
|
|
|
1102
1151
|
// One row per SETTLED run (issue #259): collapse each run's spawn+terminal
|
|
1103
1152
|
// entry pair to its terminal-preferred record; in-flight runs are omitted
|
|
1104
1153
|
// (the live monitor owns running state).
|
|
1105
|
-
const runs = collapseRunsForHistory(
|
|
1154
|
+
const runs = collapseRunsForHistory(recoverableRunEntries(ctx, state));
|
|
1106
1155
|
|
|
1107
1156
|
if (runs.length === 0) {
|
|
1108
1157
|
ctx.ui.notify("No subagent runs recorded yet. Delegate a task to see history.", "info");
|
|
@@ -1110,10 +1159,15 @@ export async function showRunHistory(
|
|
|
1110
1159
|
}
|
|
1111
1160
|
|
|
1112
1161
|
const items: SelectItem[] = runs.map((r) => {
|
|
1113
|
-
const
|
|
1114
|
-
const
|
|
1162
|
+
const status = runHistoryStatus(r);
|
|
1163
|
+
const icon = statusIcon[status] || "·";
|
|
1164
|
+
const name = runDisplayName(r);
|
|
1115
1165
|
const model = r.model.split("/").pop() || r.model;
|
|
1116
|
-
const when = r.finishedAt
|
|
1166
|
+
const when = r.finishedAt
|
|
1167
|
+
? formatRunDuration(r.durationMs || 0)
|
|
1168
|
+
: status === "interrupted"
|
|
1169
|
+
? "interrupted"
|
|
1170
|
+
: "running...";
|
|
1117
1171
|
const cost = r.cost ? ` $${r.cost.toFixed(4)}` : "";
|
|
1118
1172
|
const desc = `${r.thinkingLevel} · ${model} · ${when}${cost}`;
|
|
1119
1173
|
return { value: r.id, label: `${icon} ${name}`, description: desc };
|
|
@@ -1218,8 +1272,7 @@ async function showRunDetail(ctx: ExtensionContext, run: SubagentRun): Promise<v
|
|
|
1218
1272
|
const container = new Container();
|
|
1219
1273
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1220
1274
|
|
|
1221
|
-
const statusLabel =
|
|
1222
|
-
run.status === "done" ? "Completed" : run.status === "failed" ? "Failed" : "Running";
|
|
1275
|
+
const statusLabel = runHistoryStatusLabel(run);
|
|
1223
1276
|
container.addChild(
|
|
1224
1277
|
new Text(
|
|
1225
1278
|
theme.fg(
|
|
@@ -1993,32 +2046,59 @@ export async function showDashboard(
|
|
|
1993
2046
|
// Retry menu
|
|
1994
2047
|
// ---------------------------------------------------------------------------
|
|
1995
2048
|
|
|
1996
|
-
export async function showRetryMenu(
|
|
1997
|
-
|
|
2049
|
+
export async function showRetryMenu(
|
|
2050
|
+
ctx: ExtensionContext,
|
|
2051
|
+
state: SessionState,
|
|
2052
|
+
// U3 (D5): the abandon action is injected by the extension entry point (it
|
|
2053
|
+
// owns the session-manager/registry wiring). Omitted in read-only contexts.
|
|
2054
|
+
onAbandon?: (id: string) => void | Promise<void>,
|
|
2055
|
+
): Promise<void> {
|
|
2056
|
+
// U3 (D3): failed AND unresolved interrupted runs share this surface, one row
|
|
2057
|
+
// per run, built on the same terminal-preferred collapse the history uses.
|
|
2058
|
+
const candidates = retryableRuns(recoverableRunEntries(ctx, state));
|
|
1998
2059
|
|
|
1999
|
-
if (
|
|
2000
|
-
ctx.ui.notify("No failed subagent runs to retry.", "info");
|
|
2060
|
+
if (candidates.length === 0) {
|
|
2061
|
+
ctx.ui.notify("No failed or interrupted subagent runs to retry.", "info");
|
|
2001
2062
|
return;
|
|
2002
2063
|
}
|
|
2003
2064
|
|
|
2004
|
-
const items: SelectItem[] =
|
|
2005
|
-
const
|
|
2065
|
+
const items: SelectItem[] = candidates.map((r) => {
|
|
2066
|
+
const interrupted = isInterruptedRun(r);
|
|
2067
|
+
const name = runDisplayName(r);
|
|
2006
2068
|
const model = r.model.split("/").pop() || r.model;
|
|
2007
|
-
const err = r.errorMessage ? r.errorMessage.slice(0, 40) : "error";
|
|
2069
|
+
const err = r.errorMessage ? r.errorMessage.slice(0, 40) : interrupted ? "interrupted" : "error";
|
|
2008
2070
|
return {
|
|
2009
2071
|
value: r.id,
|
|
2010
|
-
label:
|
|
2011
|
-
description: `${model} · ${err}`,
|
|
2072
|
+
label: `${interrupted ? "◌" : "✗"} ${name}`,
|
|
2073
|
+
description: `${interrupted ? "interrupted · " : ""}${model} · ${err}`,
|
|
2012
2074
|
};
|
|
2013
2075
|
});
|
|
2014
2076
|
|
|
2015
|
-
const selectedId = await showSelectList(ctx, "Retry Failed Run", items, 10);
|
|
2077
|
+
const selectedId = await showSelectList(ctx, "Retry Failed or Interrupted Run", items, 10);
|
|
2016
2078
|
if (!selectedId) return;
|
|
2017
2079
|
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
)
|
|
2080
|
+
const run = candidates.find((r) => r.id === selectedId);
|
|
2081
|
+
if (!run) return;
|
|
2082
|
+
|
|
2083
|
+
if (isInterruptedRun(run) && onAbandon) {
|
|
2084
|
+
const action = await showSelectList(
|
|
2085
|
+
ctx,
|
|
2086
|
+
"Interrupted Run",
|
|
2087
|
+
[
|
|
2088
|
+
{ value: "retry", label: "Retry", description: "Re-dispatch with its recorded params (retryRunId)" },
|
|
2089
|
+
{ value: "abandon", label: "Abandon", description: "Finalize as stopped and prune its session file" },
|
|
2090
|
+
],
|
|
2091
|
+
5,
|
|
2092
|
+
);
|
|
2093
|
+
if (!action) return;
|
|
2094
|
+
if (action === "abandon") {
|
|
2095
|
+
await onAbandon(run.id);
|
|
2096
|
+
ctx.ui.notify(`Abandoned interrupted run ${run.id.slice(0, 8)}.`, "info");
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
ctx.ui.notify(buildRetryOfferMessage(run), "info");
|
|
2022
2102
|
}
|
|
2023
2103
|
|
|
2024
2104
|
// ---------------------------------------------------------------------------
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,15 @@ export type CompletionNotifyMode = (typeof COMPLETION_NOTIFY_MODES)[number];
|
|
|
22
22
|
|
|
23
23
|
export const DEFAULT_COMPLETION_NOTIFY: CompletionNotifyMode = "all";
|
|
24
24
|
|
|
25
|
+
// Option B U3 (D4): boot recovery posture. "offer" surfaces interrupted runs
|
|
26
|
+
// (Run History row + one boot notice); "off" silences the notice and marks
|
|
27
|
+
// newly dispatched runs so they are never offered. "auto" is deliberately NOT
|
|
28
|
+
// a member (U4 territory), enforced by RECOVERY_MODES being the shape guard.
|
|
29
|
+
export const RECOVERY_MODES = ["offer", "off"] as const;
|
|
30
|
+
export type RecoveryMode = (typeof RECOVERY_MODES)[number];
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_RECOVERY: RecoveryMode = "offer";
|
|
33
|
+
|
|
25
34
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
26
35
|
|
|
27
36
|
export const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
@@ -238,6 +247,9 @@ export interface SubagentState {
|
|
|
238
247
|
// state, "failed" wakes only on failed/stopped, "off" never wakes (still
|
|
239
248
|
// delivers passively at the next user prompt).
|
|
240
249
|
completionNotify: CompletionNotifyMode;
|
|
250
|
+
// Option B U3 (D4): recovery posture for the session. Applies to newly
|
|
251
|
+
// dispatched runs (snapshotted per run) and to the boot notice.
|
|
252
|
+
recovery: RecoveryMode;
|
|
241
253
|
maxHistoryEntries: number; // 0 = unlimited
|
|
242
254
|
sessionCostLimit: number; // 0 = unlimited
|
|
243
255
|
perTaskCostEstimate: number; // 0 = no estimate, use default
|
|
@@ -327,6 +339,9 @@ export interface SubagentRun {
|
|
|
327
339
|
interruptedAt?: string;
|
|
328
340
|
owner?: ProcessOwner;
|
|
329
341
|
childMarker?: string;
|
|
342
|
+
// Option B U3 (D4): the recovery posture in force when this run was
|
|
343
|
+
// dispatched. Additive: a pre-U3 record lacks it and reads as "offer".
|
|
344
|
+
recoveryPosture?: RecoveryMode;
|
|
330
345
|
startedAt: string;
|
|
331
346
|
finishedAt?: string;
|
|
332
347
|
durationMs?: number;
|
|
@@ -707,7 +722,7 @@ export const RESERVED_NAME_PATTERN = /^__.*__$/;
|
|
|
707
722
|
export const RESERVED_COMMAND_NAMES = new Set([
|
|
708
723
|
"model", "thinking", "concurrency", "depth", "history", "monitor",
|
|
709
724
|
"preset", "retry", "reset", "priority", "templates", "dashboard", "approval",
|
|
710
|
-
"costlimit", "historyentries", "sla", "completionnotify",
|
|
725
|
+
"costlimit", "historyentries", "sla", "completionnotify", "recovery",
|
|
711
726
|
"graph", "sla-stats", "compliance",
|
|
712
727
|
]);
|
|
713
728
|
|
|
@@ -735,6 +750,13 @@ export interface BackgroundAgent {
|
|
|
735
750
|
finalOutput?: string;
|
|
736
751
|
/** @internal — session reference for live monitor polling */
|
|
737
752
|
_sessionRef?: import('@earendil-works/pi-coding-agent').AgentSession;
|
|
753
|
+
/**
|
|
754
|
+
* @internal — Option B U2: the on-disk session file for this run, captured
|
|
755
|
+
* from `SessionManager.getSessionFile()` at spawn. Used by the settle path to
|
|
756
|
+
* prune the EXACT per-run directory. Live-only: stripped by `persistAgent`,
|
|
757
|
+
* never a persisted record field (D4).
|
|
758
|
+
*/
|
|
759
|
+
_sessionFile?: string;
|
|
738
760
|
// Option B U1 (D3/D4): additive durability identity, mirroring SubagentRun.
|
|
739
761
|
// Background records carry dispatchId/owner/attempt. `childMarker` is
|
|
740
762
|
// declared for round-trip tolerance with the run-record/agent-record JSON
|
|
@@ -746,6 +768,9 @@ export interface BackgroundAgent {
|
|
|
746
768
|
interruptedAt?: string;
|
|
747
769
|
owner?: ProcessOwner;
|
|
748
770
|
childMarker?: string;
|
|
771
|
+
// Option B U3 (D4): the recovery posture at dispatch, mirrored from the
|
|
772
|
+
// run record. Additive (absent reads as "offer").
|
|
773
|
+
recoveryPosture?: RecoveryMode;
|
|
749
774
|
}
|
|
750
775
|
|
|
751
776
|
// ---------------------------------------------------------------------------
|
|
@@ -1010,6 +1035,11 @@ export function isSubagentStateShape(value: unknown): value is SubagentState {
|
|
|
1010
1035
|
if (!COMPLETION_NOTIFY_MODES.includes(v.completionNotify as CompletionNotifyMode)) return false;
|
|
1011
1036
|
}
|
|
1012
1037
|
|
|
1038
|
+
// recovery must be one of RECOVERY_MODES if present ("auto" is rejected)
|
|
1039
|
+
if (v.recovery !== undefined) {
|
|
1040
|
+
if (!RECOVERY_MODES.includes(v.recovery as RecoveryMode)) return false;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1013
1043
|
// maxHistoryEntries must be a non-negative number if present
|
|
1014
1044
|
if (v.maxHistoryEntries !== undefined) {
|
|
1015
1045
|
if (typeof v.maxHistoryEntries !== "number" || v.maxHistoryEntries < 0) return false;
|
|
@@ -1045,9 +1075,25 @@ export function isSubagentRunShape(value: unknown): value is SubagentRun {
|
|
|
1045
1075
|
if (typeof v.id !== "string") return false;
|
|
1046
1076
|
if (typeof v.task !== "string") return false;
|
|
1047
1077
|
if (!["running", "done", "failed"].includes(v.status as string)) return false;
|
|
1078
|
+
// U3 (D4): a malformed snapshot is rejected like any other bad field; an
|
|
1079
|
+
// absent one is fine (pre-U3 records) and reads as "offer" via the helper.
|
|
1080
|
+
if (v.recoveryPosture !== undefined) {
|
|
1081
|
+
if (!RECOVERY_MODES.includes(v.recoveryPosture as RecoveryMode)) return false;
|
|
1082
|
+
}
|
|
1048
1083
|
return true;
|
|
1049
1084
|
}
|
|
1050
1085
|
|
|
1086
|
+
/**
|
|
1087
|
+
* Option B U3 (D4): the recovery posture recorded for a run. Absent (pre-U3
|
|
1088
|
+
* records) or an unrecognized value reads as the default "offer" — the
|
|
1089
|
+
* additive-field contract (no migration).
|
|
1090
|
+
*/
|
|
1091
|
+
export function runRecoveryPosture(
|
|
1092
|
+
run: Pick<SubagentRun, "recoveryPosture">,
|
|
1093
|
+
): RecoveryMode {
|
|
1094
|
+
return run.recoveryPosture === "off" ? "off" : DEFAULT_RECOVERY;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1051
1097
|
/**
|
|
1052
1098
|
* Option B U1 (D3): the ONE interrupted-state predicate. Status deliberately
|
|
1053
1099
|
* stays "running" (a new status value would be dropped by old readers and
|
|
@@ -1059,6 +1105,25 @@ export function isInterruptedRun(run: SubagentRun): boolean {
|
|
|
1059
1105
|
return run.status === "running" && !!run.interruptedAt;
|
|
1060
1106
|
}
|
|
1061
1107
|
|
|
1108
|
+
/**
|
|
1109
|
+
* Fix B (item 2): TRUE when a terminal run record represents an ABORT rather
|
|
1110
|
+
* than a genuine failure. Abandon writes the store's ONLY stopped-outcome
|
|
1111
|
+
* shape — `failed` + `stopReason`/`errorCategory` "aborted" (D5 mapping) — and
|
|
1112
|
+
* a user `stop_subagent` writes the same. Failure tallies (SLA) must exclude
|
|
1113
|
+
* these. The run record carries no dedicated "abandoned" marker and D5 forbids
|
|
1114
|
+
* adding one, so a user-ABANDONED run and a user-STOPPED run are
|
|
1115
|
+
* indistinguishable here; both are treated as non-failures.
|
|
1116
|
+
*/
|
|
1117
|
+
export function isAbortedRun(
|
|
1118
|
+
run: Pick<SubagentRun, "stopReason" | "errorCategory" | "originalParams">,
|
|
1119
|
+
): boolean {
|
|
1120
|
+
return (
|
|
1121
|
+
run.stopReason === "aborted" ||
|
|
1122
|
+
run.errorCategory === "aborted" ||
|
|
1123
|
+
run.originalParams?.errorCategory === "aborted"
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1062
1127
|
/**
|
|
1063
1128
|
* Type guard: checks if a value is a valid MultiSubagentDetails shape.
|
|
1064
1129
|
*/
|
package/src/unit-run.ts
CHANGED
|
@@ -27,8 +27,9 @@ import type {
|
|
|
27
27
|
SubagentRun,
|
|
28
28
|
SubagentToolOptions,
|
|
29
29
|
Priority,
|
|
30
|
+
RecoveryMode,
|
|
30
31
|
} from "./types";
|
|
31
|
-
import { getFinalOutput, classifyError } from "./types";
|
|
32
|
+
import { getFinalOutput, classifyError, DEFAULT_RECOVERY } from "./types";
|
|
32
33
|
import { snapshotOriginalParams } from "./params";
|
|
33
34
|
import { currentProcessOwner } from "./recovery";
|
|
34
35
|
import { capOutput, stripAnsi, buildCrashResult } from "./sanitize";
|
|
@@ -106,6 +107,9 @@ export function createUnitRun(
|
|
|
106
107
|
stepModel: { provider: string; id: string },
|
|
107
108
|
priorityFloor: Priority,
|
|
108
109
|
presetName?: string,
|
|
110
|
+
// U3 (D4): the session recovery posture at dispatch. Optional so existing
|
|
111
|
+
// callers/tests stay valid; production passes `state.config.recovery`.
|
|
112
|
+
recoveryPosture: RecoveryMode = DEFAULT_RECOVERY,
|
|
109
113
|
): { runId: string; run: SubagentRun } {
|
|
110
114
|
const runId = crypto.randomUUID();
|
|
111
115
|
const run: SubagentRun = {
|
|
@@ -131,6 +135,9 @@ export function createUnitRun(
|
|
|
131
135
|
attempt: 1,
|
|
132
136
|
owner: currentProcessOwner(),
|
|
133
137
|
childMarker: crypto.randomUUID(),
|
|
138
|
+
// U3 (D4): snapshot the posture so the boot notice/offer reads what was
|
|
139
|
+
// in force at dispatch.
|
|
140
|
+
recoveryPosture,
|
|
134
141
|
// The execution-shape fields (background/gitMode/approvalMode/force) are
|
|
135
142
|
// deliberately omitted: a fan-out unit retries as a SINGLE run, so it must
|
|
136
143
|
// not inherit the fan-out's background-ness. A retry that wants background
|