Skip to content

fix: preserve subagent task text on model fallback retry#36273

Closed
mitchmcalister wants to merge 6 commits intoopenclaw:mainfrom
mitchmcalister:fix/subagent-fallback-preserves-task
Closed

fix: preserve subagent task text on model fallback retry#36273
mitchmcalister wants to merge 6 commits intoopenclaw:mainfrom
mitchmcalister:fix/subagent-fallback-preserves-task

Conversation

@mitchmcalister
Copy link
Copy Markdown
Contributor

Summary

  • When a model attempt times out and fallback kicks in, resolveFallbackRetryPrompt unconditionally replaces the message body with a generic "Continue where you left off" continuation prompt
  • For subagent spawns, the first message body is the task — replacing it drops all task context, causing the subagent to hallucinate or fail
  • This adds an isSubagent guard that preserves the original body when the caller is a subagent lane, reusing the existing isSubagentLane detection already computed in agentCommand

Test plan

  • Existing agent.test.ts tests pass (29/29 verified locally)
  • Spawn a subagent via sessions_spawn with a specific task, force model fallback (e.g. primary model timeout), verify subagent session log contains the original task text

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added commands Command implementations size: XS labels Mar 5, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 5, 2026

Greptile Summary

This PR fixes a bug where resolveFallbackRetryPrompt unconditionally replaced the message body with a generic continuation prompt on model fallback retries, which caused subagent sessions to lose their entire task context. The fix adds an isSubagent guard so that when the caller is a subagent lane, the original body is preserved on retry.

  • The isSubagentLane boolean — already computed from opts.lane at the top of agentCommandInternal — is now threaded through the runAgentAttempt parameter object and forwarded to resolveFallbackRetryPrompt as isSubagent. The logic is correct and minimal.
  • The three-way logic in resolveFallbackRetryPrompt (no fallback → preserve, subagent fallback → preserve, non-subagent fallback → generic continuation) is easy to follow and aligns with the documented intent.
  • No automated test covers the new isFallbackRetry=true && isSubagent=true branch; the only validation is a manual test plan. A regression in this path would be invisible to CI.

Confidence Score: 4/5

  • This PR is safe to merge — the logic change is small, correct, and well-scoped; the only concern is missing automated test coverage for the new branch.
  • The fix is minimal and targets a clearly identified bug: one new parameter threaded through two function signatures and one additional early-return guard. The pre-existing isSubagentLane detection is reused correctly, there are no side effects on the non-subagent path, and the change is backward-compatible. Score is 4 rather than 5 solely because the new code path has no automated regression test.
  • src/commands/agent.ts — the new isSubagent branch in resolveFallbackRetryPrompt lacks an automated test.

Last reviewed commit: a5717b0

Comment thread src/commands/agent.ts
Comment on lines 127 to 144
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.";
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/commands/agent.ts Outdated
Comment on lines +140 to +141
if (params.isSubagent) {
return params.body;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@mitchmcalister
Copy link
Copy Markdown
Contributor Author

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 prompt still contains the original task body rather than the generic continuation text.

mitchmcalister and others added 2 commits March 5, 2026 09:27
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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/commands/agent.ts Outdated
const effectivePrompt = resolveFallbackRetryPrompt({
body: params.body,
isFallbackRetry: params.isFallbackRetry,
isInitialSubagentSpawn: params.isSubagentLane && params.isNewSession,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/commands/agent.ts Outdated
const effectivePrompt = resolveFallbackRetryPrompt({
body: params.body,
isFallbackRetry: params.isFallbackRetry,
isInitialSubagentSpawn: params.isSubagentLane && params.isNewSession,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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>
@mitchmcalister
Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands Command implementations size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant