Skip to content

Commit 3aae8b0

Browse files
Merge pull request #853 from corbitsdev/cl-7538-retry-failed-fleet-workers-instead-of-idling-the
Retry a failed fleet worker with a changed brief
2 parents f425e3f + fcdd49d commit 3aae8b0

18 files changed

Lines changed: 561 additions & 43 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
3535
workflow rails. Frozen-prefix matching ignores model-emitted copies of the
3636
marker.
3737

38+
### Changed
39+
40+
- Skywalker may spawn one successor with a changed brief after a failed or
41+
incomplete-report fleet worker. A parent-initiated interrupt
42+
(`stop_reason: interrupted`) is a resumable pause — `resume_agent` or
43+
re-wait, not a successor — unless the session is no longer resumable.
44+
Operator-cancelled salvage still waits for the operator. Identical briefs
45+
stay refused at the prompt / spawn-handoff layer. `wait_agents` JSON
46+
includes `stop_reason` so interrupt vs cancelled vs incomplete-report stay
47+
distinct. This reverses the 0.3.15 fail-path idle, not operator-cancel.
48+
3849
## [0.3.18] - 2026-09-08
3950

4051
### Added

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Two directors, selected by role:
114114

115115
- **ChatDirector** (interactive, `src/agent/director.ts`) — Extends `DefaultDirector` with task list tracking, workflow nudges, LSP auto-activation, and multi-turn chat semantics. It never terminates the session: operator declines are surfaced as replies and the reactor stays alive for the next message. Auto mode is toggled by CLI flags (`--auto` / `--no-auto`); there is currently no in-session key to toggle it (default on; constrained envelope — workspace writes and unconstrained shell auto-allow; installs, recursive rm, force/uncontained worktree changes, sensitive-path and opaque-wrapper shell still ask; contained non-force `git worktree add`/`remove`/`prune` and `list` auto-allow; shell file-mutation denied). It is not a separate edit/plan mode.
116116
- **SubAgentDirector** (delegated work, `src/subagent/index.ts`) — Drives a dispatched worker until a turn arrives with no tool calls, then replies with the final assistant text and ends the run. A tool-less turn **after tools** completes only with the four-heading envelope (Summary, Findings, Blockers, Paths); a missing envelope nudges once (**incomplete-report**) and a second tool-less turn still without the envelope salvages as **incomplete-report-stop**. Explore/read-only workers that used tools then replied with findings remain normal completes; `requireEvidence` (off by default, set per director) additionally requires at least one read before a tool-less spawn-only reply can complete. Reads done through `run_shell` count as evidence too — `src/subagent/shell-evidence.ts` classifies shell reads (`cat`, `grep`, `sed` without `-i`, …) over the same subject expansion the auto-shell policy uses — but there is no corresponding shell-write evidence or file-write requirement: a run that never touches a file still completes normally once it replies with the envelope. There is no turn budget. Operator/parent cancel after any progress returns a **cancelled** salvage report (partial findings + tool activity) instead of a bare cancel string; cancel before progress still surfaces as cancelled-by-operator. There is no repetition/no-progress/never-acted/never-edited hard stop and no fingerprint-based re-dispatch block — a genuinely stuck worker runs until it completes, stalls, hits an opt-in wall-clock deadline, or is cancelled.
117-
`spawn_agent` starts each worker and records it in the caller's fleet mailbox; `wait_agents` collects terminal reports from that mailbox. Deadline salvage prepends an advisory parent hint suggesting continuation plus a longer deadline if more wall-clock time is warranted. Cancelled salvage asks the parent to synthesize Findings and Paths and wait for the operator instead of auto-starting another specialist. Deadline hints are advisory only — an identical re-dispatch is still admitted.
117+
`spawn_agent` starts each worker and records it in the caller's fleet mailbox; `wait_agents` collects terminal reports from that mailbox. Wait JSON includes `stop_reason` from the session when present so a salvage that is wait-`done` is not mistaken for a clean complete, and so parent-initiated interrupt (`interrupted`) is not mistaken for operator-cancel (`cancelled`). Deadline salvage prepends an advisory parent hint suggesting continuation plus a longer deadline if more wall-clock time is warranted. Failed and incomplete-report salvage tell the parent to diagnose from the report or error and MAY spawn one successor with a changed brief. A parent-initiated interrupt is a resumable pause: wait unblocks with `stop_reason: interrupted` (often while the session is still running and has no report); the parent should `resume_agent` or re-wait, and must not spawn a successor against a still-live worker. Successor only if that session is no longer resumable. Operator-cancelled salvage asks the parent to synthesize Findings and Paths and wait for the operator instead of auto-starting another specialist. Identical re-dispatch of the same brief stays refused at the prompt / spawn-handoff layer; there is no fingerprint-based re-dispatch hard-block. Deadline hints are advisory only — an identical re-dispatch is still admitted at runtime. Parent hints are prepended on salvage reports returned to the parent. The runtime does not auto-spawn successors.
118118

119119
#### Model-family policy (`src/agent/model-family-policy.ts`)
120120

docs/TELEMETRY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ a truthy value (`1`, `true`, …) to restore per-call spans for debugging.
111111
worker ending during an active parent turn carries that turn's `parent_trace_id`.
112112
Pre-progress operator aborts settle with `status=cancelled` and
113113
`stop_reason=cancelled` even when the worker promise rejects. An interrupt that
114-
keeps a worker resumable settles with `status=interrupted` and the same
115-
`stop_reason=cancelled`; terminal events never report a still-running status.
114+
keeps a worker resumable settles with `status=interrupted` and
115+
`stop_reason=interrupted`; terminal events never report a still-running status.
116116

117117
A deterministic synthetic fixture captures 10 parent generations, 80 parent
118118
tool spans, and 4 worker start/end pairs. The comparable former shape is 98

src/agent/directors/skywalker/package.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,30 @@ describe("skywalkerPackage", () => {
146146
expect(p).not.toContain("if the job still needs doing");
147147
});
148148

149+
test("systemPrompt fail-then-successor is distinct from operator-cancel wait", () => {
150+
const p = skywalkerPackage.systemPrompt;
151+
expect(p).toContain("incomplete-report");
152+
expect(p).toContain("MAY `spawn_agent` **one** successor");
153+
expect(p).toContain("changed** brief");
154+
expect(p).toContain("wait for the operator");
155+
expect(p).toContain("Do not auto-retry");
156+
expect(p).toContain("Identical re-dispatch of the same brief stays refused");
157+
expect(p).toContain("Operator-cancel is not a re-dispatch");
158+
expect(p).not.toContain("Then start the next worker");
159+
expect(p).not.toContain("if the job still needs doing");
160+
expect(p).not.toContain("interrupted-incomplete");
161+
});
162+
163+
test("systemPrompt treats parent interrupt as resume, not successor spawn", () => {
164+
const p = skywalkerPackage.systemPrompt;
165+
expect(p).toContain("Parent-initiated interrupt");
166+
expect(p).toContain("resume_agent");
167+
expect(p).toContain("still-live worker");
168+
expect(p).toContain("no longer resumable");
169+
expect(p).toContain("interrupt_agent");
170+
expect(p).toContain("stop_reason: interrupted");
171+
});
172+
149173
test("systemPrompt simple path skips explorer+critic for tiny work", () => {
150174
const p = skywalkerPackage.systemPrompt;
151175
expect(p).toContain("DIY on the parent");

src/agent/directors/skywalker/package.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ Do **not** turn a "why is this stalled / why no thinking / spawn looks broken" d
7676
- Classify digs, screenshots of worker rows, and "why/how does X work" as COMMUNICATION first.
7777
- Answer from mounted tools + known architecture; at most **one** explorer worker if a single unknown path blocks the answer.
7878
- Never spawn parallel "parent UI / child UI / stream events / prompt guardrail / session dig" waves for the same question.
79-
- When workers stall, loop, or come back unfinished: synthesize what returned, report Blockers, and change approach — do **not** re-fan-out another diagnostic wave on the same topic.
79+
- When workers stall or loop: synthesize what returned, report Blockers, and change approach — do **not** re-fan-out another diagnostic wave on the same topic.
80+
- Failed wait (\`status: failed\` plus \`error\`) or salvage \`incomplete-report\`: diagnose from the wait report or error; MAY \`spawn_agent\` **one** successor with a **changed** brief (new \`success_criteria\` / \`do_not\` / continuation from Findings). Cap is one successor for that stall. Spawn the successor — do not search the repo as a substitute.
81+
- Parent-initiated interrupt (\`interrupt_agent\` / \`send_input\` with \`interrupt:true\`): wait unblocks with \`status: interrupted\` and \`stop_reason: interrupted\`. That is a resumable pause, not fail or incomplete-report. The worker is often still running and often has no report. Call \`resume_agent\` (changed follow-up into retained context) or re-wait. Do **not** \`spawn_agent\` a successor against a still-live worker. Successor only if the session is no longer resumable.
82+
- Operator-cancel (\`stop_reason\` cancelled, or Blockers that say wait for the operator): synthesize Findings and Paths, report Blockers, and **wait for the operator**. Do not auto-retry. Do not spawn a successor because the worker was cancelled.
8083
- Do **not** search the repo yourself after a worker stops without finishing.
8184
- Permission asks and long run_shell clocks on worker rows are not a signal to spawn more diggers.
8285
@@ -85,6 +88,9 @@ Do **not** turn a "why is this stalled / why no thinking / spawn looks broken" d
8588
Child starts blank. Parent writes a complete packet: Goal, contracts copied verbatim, Scope/do_not, Done-when/success_criteria, What to report.
8689
Runtime requires success_criteria for implement/review and their default directors; recommended otherwise.
8790
Re-dispatch after a blocker is a new handoff (new criteria / new do_not), not a retry of the old one-liner.
91+
Identical re-dispatch of the same brief stays refused.
92+
Operator-cancel is not a re-dispatch — wait for the operator.
93+
Parent-initiated interrupt is not a re-dispatch — resume_agent (or re-wait). Successor only if the session is no longer resumable.
8894
When the operator brief states a function signature or return shape, put that **verbatim** into implement success_criteria (including sync vs Promise if stated or implied by existing code/tests).
8995
9096
# Verify after ship

src/agent/prompts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export function buildGuidelines(
170170
"Orchestration:",
171171
"- Break multi-step or parallel work into focused worker dispatches with distinct lenses; prefer `spawn_agent` (fire several in one turn when jobs are independent), then reply with who is running and end the turn — workers keep running while you are idle, and `wait_agents` / `list_agents` on a later turn collect their reports without holding this conversation blocked.",
172172
"- Pass the typed spawn contract: `intent`, `success_criteria` (done-when; required for implement/review and their default directors), `do_not` (scope fence), and `report_focus`. Free-form `prompt` without `success_criteria` fail-closes for implement/review and their default directors.",
173-
"- After workers return, merge their Summary/Findings into a coherent answer for the operator; do not paste raw fleet-agent dumps.",
173+
"- After workers return, classify fail / incomplete-report vs parent-initiated interrupt vs operator-cancel vs clean complete. Fail-path (`status: failed` or salvage `incomplete-report`): diagnose from the report or error and MAY spawn one successor with a changed brief. Parent-initiated interrupt (`interrupt_agent` / `send_input` with `interrupt:true` unblocks wait with `stop_reason: interrupted`): the worker is often still running and often has no report — `resume_agent` or re-wait; do not `spawn_agent` a successor against a still-live worker. Successor only if that session is no longer resumable. Operator-cancel (`stop_reason` cancelled): wait for the operator; do not auto-retry. Identical brief: refuse. Merge Summary/Findings into a coherent answer for the operator; do not paste raw fleet-agent dumps.",
174174
"- Use manage_tasks for your own coordination checklist; spawning workers is `spawn_agent` / `wait_agents`, not manage_tasks.",
175175
"- If context is compacted automatically, do not stop tasks early due to token fear; persist progress via manage_tasks and worker reports.",
176176
]),

src/prompts.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,20 @@ test("orchestrator guidelines teach the typed task spawn contract", () => {
129129
expect(guidelines).not.toContain("weaker");
130130
});
131131

132-
test("primary chat prompt does not invite auto-starting the next worker after unfinished specialists", () => {
132+
test("primary chat prompt classifies fail-path successor vs interrupt resume vs operator-cancel wait", () => {
133133
const prompt = buildChatSystemPrompt();
134134
const guidelines = buildGuidelines({ sessionMode: "orchestrator" });
135-
expect(guidelines).not.toContain("change the brief rather than repeating it");
135+
expect(guidelines).toContain("MAY spawn one successor with a changed brief");
136+
expect(guidelines).toContain("wait for the operator");
137+
expect(guidelines).toContain("do not auto-retry");
138+
expect(guidelines).toContain("Identical brief: refuse");
139+
expect(guidelines).toContain("resume_agent");
140+
expect(guidelines).toContain("still-live worker");
141+
expect(guidelines).not.toContain("interrupted-incomplete");
136142
expect(guidelines).not.toContain("start the next worker");
143+
expect(prompt).toContain("MAY `spawn_agent` **one** successor");
144+
expect(prompt).toContain("wait for the operator");
145+
expect(prompt).toContain("Identical re-dispatch of the same brief stays refused");
137146
expect(prompt).not.toContain("Then start the next worker");
138147
expect(prompt).not.toContain("if the job still needs doing");
139148
});

0 commit comments

Comments
 (0)