fix: preserve subagent task text on model fallback retry#36273
fix: preserve subagent task text on model fallback retry#36273mitchmcalister wants to merge 6 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes a bug where
Confidence Score: 4/5
Last reviewed commit: a5717b0 |
| params.sessionStore[params.sessionKey] = persisted; | ||
| } | ||
|
|
||
| function resolveFallbackRetryPrompt(params: { body: string; isFallbackRetry: boolean }): string { | ||
| function resolveFallbackRetryPrompt(params: { | ||
| body: string; | ||
| isFallbackRetry: boolean; | ||
| isSubagent?: boolean; | ||
| }): string { | ||
| if (!params.isFallbackRetry) { | ||
| return params.body; | ||
| } | ||
| // Subagents carry their task in the first message body — replacing it with a | ||
| // generic continuation prompt would drop the task context entirely. | ||
| if (params.isSubagent) { | ||
| return params.body; | ||
| } | ||
| return "Continue where you left off. The previous model attempt failed or timed out."; | ||
| } |
There was a problem hiding this comment.
Missing automated test for new subagent fallback branch
The new isSubagent branch in resolveFallbackRetryPrompt is not covered by any automated test. The existing agent.test.ts suite never inspects the prompt argument forwarded to runCliAgent, so the case isFallbackRetry=true && isSubagentLane=true is only validated by the manual test plan in the PR description.
Because this is the only meaningful code path changed by the fix, a regression here could go undetected. Consider adding a test (either by exporting the function for unit-testing, or by asserting on the prompt captured by the runCliAgentSpy when lane is set to AGENT_LANE_SUBAGENT and the fallback run is triggered):
it("preserves body on model fallback retry for subagent lane", async () => {
// configure runCliAgent to throw FailoverError on first call, succeed on second
runCliAgentSpy
.mockRejectedValueOnce(new FailoverError("timeout", "timeout"))
.mockResolvedValue(/* ... */);
await agentCommand({ body: "Do the task", lane: AGENT_LANE_SUBAGENT, ... });
// second call (fallback) must still carry the original task, not the generic prompt
const secondCallPrompt = runCliAgentSpy.mock.calls[1]![0].prompt;
expect(secondCallPrompt).toBe("Do the task");
});Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/agent.ts
Line: 127-144
Comment:
**Missing automated test for new subagent fallback branch**
The new `isSubagent` branch in `resolveFallbackRetryPrompt` is not covered by any automated test. The existing `agent.test.ts` suite never inspects the `prompt` argument forwarded to `runCliAgent`, so the case `isFallbackRetry=true && isSubagentLane=true` is only validated by the manual test plan in the PR description.
Because this is the only meaningful code path changed by the fix, a regression here could go undetected. Consider adding a test (either by exporting the function for unit-testing, or by asserting on the `prompt` captured by the `runCliAgentSpy` when `lane` is set to `AGENT_LANE_SUBAGENT` and the fallback run is triggered):
```ts
it("preserves body on model fallback retry for subagent lane", async () => {
// configure runCliAgent to throw FailoverError on first call, succeed on second
runCliAgentSpy
.mockRejectedValueOnce(new FailoverError("timeout", "timeout"))
.mockResolvedValue(/* ... */);
await agentCommand({ body: "Do the task", lane: AGENT_LANE_SUBAGENT, ... });
// second call (fallback) must still carry the original task, not the generic prompt
const secondCallPrompt = runCliAgentSpy.mock.calls[1]![0].prompt;
expect(secondCallPrompt).toBe("Do the task");
});
```
How can I resolve this? If you propose a fix, please make it concise.Verifies that when a subagent lane triggers model fallback, the original task body is preserved instead of being replaced with the generic "Continue where you left off" continuation prompt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bf460137c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (params.isSubagent) { | ||
| return params.body; |
There was a problem hiding this comment.
Limit subagent body replay to first fallback turn
resolveFallbackRetryPrompt now returns the full body for every fallback attempt when isSubagent is true, but the subagent lane is used beyond initial spawn (persistent follow-ups/steer reruns), not only for first-task bootstrapping. In cases where the first model attempt already recorded the user turn before failing (the same duplicate-turn scenario guarded by fallbackAttemptIndex in agentCommandInternal), this change re-injects the same instruction on retry and can repeat side-effecting tool actions; the body replay should be scoped to initial subagent task bootstrapping rather than all subagent retries.
Useful? React with 👍 / 👎.
|
Fair point on missing test coverage. Added in the follow-up commit (bf46013): triggers a model fallback on a subagent lane and asserts the second call's |
Limit resolveFallbackRetryPrompt body preservation to the first fallback turn of a new subagent session (isNewSession && isSubagentLane). Follow-up messages on existing sessions use the standard continuation prompt to avoid re-injecting the user turn and repeating side-effecting tool actions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 92b7aed66b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const effectivePrompt = resolveFallbackRetryPrompt({ | ||
| body: params.body, | ||
| isFallbackRetry: params.isFallbackRetry, | ||
| isInitialSubagentSpawn: params.isSubagentLane && params.isNewSession, |
There was a problem hiding this comment.
Detect initial subagent spawn independently of session freshness
isInitialSubagentSpawn is derived from isSubagentLane && isNewSession, but real sessions_spawn runs pre-create the child session before invoking agent (they call sessions.patch first in src/agents/subagent-spawn.ts, and applySessionsPatchToStore creates a fresh entry with sessionId/updatedAt in src/gateway/sessions-patch.ts). That makes resolveSession report isNewSession = false, so fallback retries still use the generic continuation prompt and drop the original [Subagent Task] text when the first model attempt times out. In other words, this condition misses the production spawn path and leaves the reported fallback-context loss unresolved.
Useful? React with 👍 / 👎.
…-preserves-task # Conflicts: # src/commands/agent.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f0fc558243
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const effectivePrompt = resolveFallbackRetryPrompt({ | ||
| body: params.body, | ||
| isFallbackRetry: params.isFallbackRetry, | ||
| isInitialSubagentSpawn: params.isSubagentLane && params.isNewSession, |
There was a problem hiding this comment.
Identify subagent first turn without freshness gating
resolveFallbackRetryPrompt now preserves the original body only when isInitialSubagentSpawn is true, but this flag is computed as isSubagentLane && isNewSession; in the real sessions_spawn flow, src/agents/subagent-spawn.ts calls sessions.patch before invoking agent, and applySessionsPatchToStore creates a fresh session entry, so resolveSession reports isNewSession = false even on the first subagent task. That means fallback retries still switch to the generic continuation prompt and drop [Subagent Task] text in the production spawn path this change was meant to fix.
Useful? React with 👍 / 👎.
isNewSession was false for subagent spawns because sessions.patch pre-creates the session entry. Instead, check whether the resolved session entry already has a sessionFile field — sessions.patch never sets it, so its absence reliably indicates the first run for this session, while follow-ups (where a prior run persisted the transcript path) correctly use the continuation prompt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Superseded by #43331, which applies the same prompt-preservation fix broadly to all agent fallback retries (not just subagents) and adds failure-mode-aware image handling. |
Summary
resolveFallbackRetryPromptunconditionally replaces the message body with a generic "Continue where you left off" continuation promptisSubagentguard that preserves the original body when the caller is a subagent lane, reusing the existingisSubagentLanedetection already computed inagentCommandTest plan
agent.test.tstests pass (29/29 verified locally)sessions_spawnwith a specific task, force model fallback (e.g. primary model timeout), verify subagent session log contains the original task text🤖 Generated with Claude Code