Skip to content

fix(agents): unified fallback retry safety — images, partial execution, system context#44188

Closed
mitchmcalister wants to merge 2 commits intoopenclaw:mainfrom
mitchmcalister:fix/unified-fallback-retry-safety
Closed

fix(agents): unified fallback retry safety — images, partial execution, system context#44188
mitchmcalister wants to merge 2 commits intoopenclaw:mainfrom
mitchmcalister:fix/unified-fallback-retry-safety

Conversation

@mitchmcalister
Copy link
Copy Markdown
Contributor

@mitchmcalister mitchmcalister commented Mar 12, 2026

Summary

Consolidates three draft PRs (#43331, #43485, #43507) into a single focused change that handles fallback retry safety:

  • Cross-provider image privacy: Strip explicitly-supplied images when fallback targets a different provider. Suppress prompt-based image detection (detectAndLoadPromptImages) on cross-provider retry to prevent local image files from being loaded and sent.
  • Partial execution detection: Capture tool execution metadata (toolMetas) from the embedded runner, attach sanitized tool names to FailoverError, and thread through the fallback loop with lastPartialExecution ?? previous preservation for multi-hop chains.
  • System context injection: Partial execution warnings are injected via extraSystemPrompt (system-level context) rather than prepended to the user message. This keeps the warning in the proper prompt layer and avoids models treating [System: ...] markers as user text.

Closes #43481, closes #43482, closes #43492

Architectural constraints

The pi-coding-agent library's AgentSession.prompt() always adds a new user turn — there is no "retry last turn" API. The library's internal auto-retry uses agent.continue() which avoids duplicate turns, but this operates within a single runEmbeddedPiAgent() lifecycle. OpenClaw's model fallback system creates a new embedded runner invocation per candidate (new session setup, new streamFn), so continue() cannot be used across fallback candidates without restructuring the runner lifecycle or modifying the library.

The orphaned-user-message repair in attempt.ts correctly handles the re-submission pattern: it branches the session tree (pointer move, not deletion) to prevent consecutive user turns, then the original prompt is re-sent via prompt(). This is working as designed.

Alternatives explored

  • agent.continue() across fallback candidates: Not viable — continue() uses live streamFn and model references, but the embedded runner configures these per-invocation. Would require sharing a session across candidates.
  • sendCustomMessage({ deliverAs: "nextTurn" }): Public API for context injection, but queues for the next prompt() call — timing doesn't align with the fallback retry flow.
  • Session-level retry coordination: Would require the embedded runner to support a "retry mode" that skips prompt() and reuses the orphaned user turn. Feasible but requires fundamental runner restructuring.

Non-goals

Test plan

  • resolveRetryImages unit tests: cross-provider strip, same-provider preserve, format error strip, undefined failureReason
  • resolveRetryImages integration test: images stripped through full agentCommandrunEmbeddedPiAgent chain
  • suppressPromptImageDetection integration test: flag passed on cross-provider retry
  • FailoverError.sanitizeToolNames unit tests: character stripping, truncation, limit, empty filtering
  • buildPartialExecutionSystemContext unit tests: undefined/empty/with-tools/with-messaging-warning
  • Partial execution forwarding in runWithModelFallback: single-hop and multi-hop preservation
  • pnpm check clean
  • pnpm build succeeds

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 12, 2026

Greptile Summary

This PR consolidates three draft PRs into a focused change that hardens the model fallback retry path across three dimensions: cross-provider image privacy, partial execution detection, and system context injection.

Key changes:

  • Image handling: Replaces the blunt "strip images on any fallback" policy with resolveRetryImages, which strips only on cross-provider retries (privacy) or same-provider format errors (modality). Adds suppressPromptImageDetection to also block prompt-referenced local images from being loaded and forwarded cross-provider.
  • Partial execution tracking: FailoverError now carries a sanitized partialExecution field populated from attempt.toolMetas in the embedded runner. The fallback loop in runWithModelFallback preserves the earliest partial-execution record across multi-hop chains (lastPartialExecution ?? previous).
  • System context injection: Partial-execution warnings are delivered via extraSystemPrompt rather than prepended to the user message, keeping the signal in the correct prompt layer and avoiding models treating [System: ...] markers as user text.
  • Prompt forwarding fix: Drops the "Continue where you left off" prompt substitution — the original user message is now always re-sent on fallback, with the orphaned-user-message repair in attempt.ts handling any duplicate-turn cleanup.

Two minor style observations were flagged: (1) sanitizeToolNames applies the 20-name cap before filtering empty names, so the output can be fewer than 20 valid entries even when more valid names exist beyond index 19 — reversing the slice/filter order would fix this if the intent is "up to 20 valid names". (2) The isCrossProviderRetry guard (primaryProvider !== providerOverride) uses strict !== while resolveRetryImages guards with primaryProvider && currentProvider, creating a subtle asymmetry when primaryProvider is undefined that could leave explicit images unstripped while still suppressing prompt image detection.

Confidence Score: 4/5

  • Safe to merge — the changes are well-tested and the logic is correct; two minor style/consistency notes do not affect production behavior.
  • The core logic is sound: image stripping, partial-execution capture, and system-prompt injection all work as described. Test coverage is comprehensive (unit + integration across all three feature areas). The two flagged items are edge cases (undefined primaryProvider is not reachable in production; the sanitizeToolNames ordering only matters when tool names contain only special characters) that do not affect the happy path.
  • src/agents/failover-error.ts (sanitizeToolNames slice/filter order) and src/commands/agent.ts (isCrossProviderRetry guard alignment with resolveRetryImages).

Comments Outside Diff (1)

  1. src/agents/failover-error.ts, line 98-101 (link)

    slice applied before filter may yield fewer than 20 valid names

    The 20-item limit is applied to the input array before invalid names are stripped. If any of the first 20 names sanitize to an empty string (e.g. "!!!"""), the result will have fewer than 20 valid entries even when more valid names exist at indices ≥ 20.

    The current test passes 25 all-valid names, so it doesn't expose this gap. If the intent is "return up to 20 sanitized names", the order should be reversed:

    If the intent is instead "process only the first 20 tool executions regardless of validity", the current order is intentional — but a short comment would make that explicit.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/failover-error.ts
    Line: 98-101
    
    Comment:
    **`slice` applied before `filter` may yield fewer than 20 valid names**
    
    The 20-item limit is applied to the *input* array before invalid names are stripped. If any of the first 20 names sanitize to an empty string (e.g. `"!!!"``""`), the result will have fewer than 20 valid entries even when more valid names exist at indices ≥ 20.
    
    The current test passes 25 all-valid names, so it doesn't expose this gap. If the intent is "return up to 20 sanitized names", the order should be reversed:
    
    
    
    If the intent is instead "process only the first 20 tool executions regardless of validity", the current order is intentional — but a short comment would make that explicit.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/failover-error.ts
Line: 98-101

Comment:
**`slice` applied before `filter` may yield fewer than 20 valid names**

The 20-item limit is applied to the *input* array before invalid names are stripped. If any of the first 20 names sanitize to an empty string (e.g. `"!!!"``""`), the result will have fewer than 20 valid entries even when more valid names exist at indices ≥ 20.

The current test passes 25 all-valid names, so it doesn't expose this gap. If the intent is "return up to 20 sanitized names", the order should be reversed:

```suggestion
  static sanitizeToolNames(names: string[]): string[] {
    return names
      .map((n) => n.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 100))
      .filter(Boolean)
      .slice(0, 20);
  }
```

If the intent is instead "process only the first 20 tool executions regardless of validity", the current order is intentional — but a short comment would make that explicit.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/commands/agent.ts
Line: 499-500

Comment:
**`isCrossProviderRetry` guard differs from `resolveRetryImages` guard**

`isCrossProviderRetry` uses strict `!==`, so it evaluates to `true` whenever `primaryProvider` is `undefined` and `providerOverride` is set (`undefined !== "openai"``true`). In that scenario `suppressPromptImageDetection` would be `true`, yet `resolveRetryImages` would *not* strip explicit images because its own guard requires both providers to be truthy:

```ts
// resolveRetryImages — strips only when both providers are known
if (primaryProvider && currentProvider && primaryProvider !== currentProvider) {
```

The outcome would be: explicit images forwarded (not stripped) but prompt-based image detection suppressed — an inconsistent mix. `primaryProvider` should always be defined in practice (it comes from the resolved model config), but aligning the guard to mirror `resolveRetryImages` would make the invariant explicit and avoid silent mismatches if the code path ever changes:

```suggestion
  const isCrossProviderRetry =
    params.isFallbackRetry &&
    !!params.primaryProvider &&
    params.primaryProvider !== params.providerOverride;
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 8491415

Comment thread src/commands/agent.ts Outdated
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: 84914158a8

ℹ️ 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/agents/pi-embedded-runner/run/attempt.ts
@mitchmcalister mitchmcalister force-pushed the fix/unified-fallback-retry-safety branch from 240769d to 350de99 Compare March 12, 2026 18:26
@mitchmcalister
Copy link
Copy Markdown
Contributor Author

Fixed in 350de99 — gated partialExecContext on same-provider retry using the existing isCrossProviderRetry check, matching the suggested fix exactly.

The isCrossProviderRetry computation was hoisted earlier in runAgentAttempt (from line 507 to line 393) so it's now shared by all three cross-provider safety gates: image stripping, prompt image detection suppression, and partial execution context suppression.

Cross-provider retries will now skip buildPartialExecutionSystemContext entirely, so no tool names or messaging status leak to a different LLM provider.

@katoue

This comment was marked as spam.

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: 350de99765

ℹ️ 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 +396 to +398
params.isFallbackRetry &&
!!params.primaryProvider &&
params.primaryProvider !== params.providerOverride;
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 Apply cross-provider image guard when primary is skipped

isCrossProviderRetry is gated on params.isFallbackRetry, but isFallbackRetry is derived from the number of executed runs, not the fallback candidate index. In runWithModelFallback, the primary candidate can be skipped entirely (for example, all profiles in cooldown), so the first executed cross-provider candidate is treated as non-retry and both resolveRetryImages(...) and suppressPromptImageDetection stay disabled, allowing local/attached images to be sent to a different provider despite the new privacy guard.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verified the mechanism: fallbackAttemptIndex only increments when run() is called, so a skipped primary (cooldown) leaves isFallbackRetry = false on the first executed candidate.

However, this is the pre-existing behavior — before this PR, images always went to whatever provider ran first. This PR added protection for the retry case: images already sent to provider A should not be re-sent to provider B. The "primary skipped" scenario is a distinct design question about whether images should ever go cross-provider when the primary was never attempted — that is about the fallback trust model, not retry safety.

isFallbackRetry correctly reflects "a prior execution failed" vs "first attempt." A skip is not a failed execution — no data was sent to any provider.

Out of scope for this PR; worth tracking as a follow-up if the privacy posture requires it.

Comment on lines +1478 to +1482
partialExecution: (() => {
if (attempt.toolMetas.length === 0) {
return undefined;
}
const sanitizedToolNames = FailoverError.sanitizeToolNames(
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 Preserve partial execution across profile-rotation attempts

This partialExecution payload is built only from the current attempt.toolMetas, so tool activity from earlier in-run attempts is dropped when auth-profile rotation (advanceAuthProfile() + continue) occurred before final failover. In that case, if an earlier rotated attempt already executed side-effecting tools (especially messaging) and the last attempt failed before tools, the fallback model receives no partial-execution warning and can repeat actions that already ran.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verified: runEmbeddedAttempt() creates a fresh subscription (and fresh toolMetas) on each loop iteration, so tools from an earlier profile-rotation attempt are indeed not carried into the final FailoverError.partialExecution.

This requires tool execution followed by an auth/rate-limit/timeout error within the same runEmbeddedPiAgent call — an edge case. The current behavior is strictly additive: we capture partial execution when the final attempt had tools, and when it did not (profile rotation reset it), the fallback model gets no warning, which is the same behavior as before this PR.

Accumulating toolMetas across profile rotations would require a loop-scoped accumulator. Valid enhancement, out of scope for this safety-focused PR.

@mitchmcalister
Copy link
Copy Markdown
Contributor Author

The remaining check CI failure is a pre-existing oxfmt formatting issue on main — 3 CSS files in ui/src/styles/ (grouped.css, layout.css, chat/layout.css). Confirmed the same check job fails on the latest main CI run (23017465466). Unrelated to this PR.

…n, system context

Rebased onto current main after upstream moved agent-command from
src/commands/agent.ts to src/agents/agent-command.ts.

- Cross-provider image privacy: strip images when fallback targets a
  different provider; suppress prompt-based image detection on
  cross-provider retry.
- Partial execution detection: capture tool execution metadata from the
  embedded runner, attach sanitized tool names to FailoverError, and
  thread through the fallback loop.
- System context injection: partial execution warnings injected via
  extraSystemPrompt; suppressed on cross-provider retries (CWE-200).
- Prompt preservation: always re-send original prompt on fallback retry
  instead of generic "Continue where you left off" message.

Closes openclaw#43481, closes openclaw#43482, closes openclaw#43492

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mitchmcalister mitchmcalister force-pushed the fix/unified-fallback-retry-safety branch from 350de99 to 0cff9a4 Compare March 24, 2026 23:24
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Mar 24, 2026

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Unbounded tool name accumulation in fallback retries leaks internal tool names and bloats prompts
2 🔵 Low Image sanitization bypass when suppressPromptImageDetection is enabled
Vulnerabilities

1. 🔵 Unbounded tool name accumulation in fallback retries leaks internal tool names and bloats prompts

Property Value
Severity Low
CWE CWE-200
Location src/agents/model-fallback.ts:735-752

Description

In runWithModelFallback, partial execution state is merged across attempts by unioning tool names, but the merged list is not re-sanitized/limited after the union.

  • Each failed attempt can contribute up to 20 sanitized tool names (FailoverError.sanitizeToolNames).
  • On subsequent failures, lastPartialExecution.toolNames is unioned with the new attempt’s described.partialExecution.toolNames using a Set.
  • The result can exceed the intended limit (20 names, 100 chars each) across multiple retries.
  • That merged list is later embedded into the system prompt via buildPartialExecutionSystemContext (same-provider retries), causing:
    • Information disclosure of more internal tool names than intended.
    • Prompt bloat / DoS risk (larger prompts, more tokens, potential context overflow).

Vulnerable code:

lastPartialExecution = lastPartialExecution
  ? {
      ...lastPartialExecution,
      toolNames: [
        ...new Set([
          ...lastPartialExecution.toolNames,
          ...described.partialExecution.toolNames,
        ]),
      ],
      didSendViaMessagingTool:
        lastPartialExecution.didSendViaMessagingTool ||
        described.partialExecution.didSendViaMessagingTool,
    }
  : described.partialExecution;

Recommendation

Re-apply sanitization/limits after merging tool names, so the cross-attempt accumulated state cannot grow beyond the intended cap.

Example fix:

import { FailoverError } from "./failover-error.js";

const merged = [
  ...new Set([
    ...lastPartialExecution.toolNames,
    ...described.partialExecution.toolNames,
  ]),
];

lastPartialExecution = {
  ...lastPartialExecution,
  toolNames: FailoverError.sanitizeToolNames(merged),
  didSendViaMessagingTool:
    lastPartialExecution.didSendViaMessagingTool ||
    described.partialExecution.didSendViaMessagingTool,
};

Optionally, consider also enforcing a total character budget for buildPartialExecutionSystemContext to prevent prompt bloat even if tool names are long or numerous.


2. 🔵 Image sanitization bypass when suppressPromptImageDetection is enabled

Property Value
Severity Low
CWE CWE-400
Location src/agents/pi-embedded-runner/run/attempt.ts:2812-2827

Description

In runEmbeddedAttempt, when params.suppressPromptImageDetection is true, the code bypasses detectAndLoadPromptImages() and directly forwards params.images to the model without applying the usual sanitization/limits.

  • detectAndLoadPromptImages() normally sanitizes both prompt-detected images and existingImages via sanitizeImagesWithLog()sanitizeImageBlocks(), enforcing maxBytes and maxDimensionPx.
  • The new suppression path returns { images: params.images ?? [] } and those images are later passed to activeSession.prompt(...) unchanged.
  • If an attacker can supply params.images (e.g., via API integrations, plugins, or other callers), they may provide oversized or pathological image payloads that would otherwise be resized/dropped, potentially causing excessive memory/CPU usage during downstream processing or provider request construction (resource exhaustion).

Vulnerable code:

const imageResult = params.suppressPromptImageDetection
  ? { images: params.images ?? [], detectedRefs: [], loadedCount: 0, skippedCount: 0 }
  : await detectAndLoadPromptImages({ ... maxBytes, maxDimensionPx ... });

if (imageResult.images.length > 0) {
  await abortable(activeSession.prompt(effectivePrompt, { images: imageResult.images }));
}

Recommendation

Ensure params.images are sanitized even when prompt-based detection is suppressed.

For example, keep suppression limited to detection/loading of prompt references, but still run sanitization on existingImages:

import { sanitizeImageBlocks } from "../../tool-images.js";

const imageSanitization = resolveImageSanitizationLimits(params.config);
const sanitizedExisting = params.images
  ? (await sanitizeImageBlocks(params.images, "prompt:images", {
      maxBytes: MAX_IMAGE_BYTES,
      maxDimensionPx: imageSanitization.maxDimensionPx,
    })).images
  : [];

const imageResult = params.suppressPromptImageDetection
  ? { images: sanitizedExisting, detectedRefs: [], loadedCount: 0, skippedCount: 0 }
  : await detectAndLoadPromptImages({
      ...,
      existingImages: params.images,
      maxBytes: MAX_IMAGE_BYTES,
      maxDimensionPx: imageSanitization.maxDimensionPx,
    });

Also consider adding an explicit invariant/guard that params.images must already be sanitized when suppression is enabled, or enforce a hard cap on decoded bytes/count regardless of mode.


Analyzed PR: #44188 at commit 4cf0a10

Last updated on: 2026-03-25T01:57:39Z

@mitchmcalister
Copy link
Copy Markdown
Contributor Author

Rebased onto current main — the upstream file move (src/commands/agent.ts → src/agents/agent-command.ts) is resolved and CI should be clean. No upstream changes have addressed the root causes (prompt/image loss on fallback retry, cross-provider image disclosure, or partial execution blindness).

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: 0cff9a4ddc

ℹ️ 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/agents/model-fallback.ts Outdated
lastError = isKnownFailover ? normalized : err;
const described = describeFailoverError(normalized);
previousFailureReason = described.reason ?? "unknown";
lastPartialExecution = described.partialExecution ?? lastPartialExecution;
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 Merge partial-execution state across fallback failures

When multiple fallback attempts fail with partialExecution, runWithModelFallback overwrites earlier state with the latest one via lastPartialExecution = described.partialExecution ?? lastPartialExecution. This drops side-effect history from earlier failed attempts (for example, first attempt sent a message, second attempt only read a file), so later retries can lose the didSendViaMessagingTool warning and repeat user-visible actions. This should merge partial-execution state across attempts (union tool names and OR the messaging flag) rather than replace it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed in 4cf0a10. lastPartialExecution now merges across attempts: union of tool names and OR of didSendViaMessagingTool, so side-effect history from earlier failed attempts carries forward. Added a test covering the multi-attempt merge case.

… partial execution state

Address reviewer feedback on openclaw#44188:

- Suppress detectAndLoadPromptImages on same-provider format-error
  retries, not just cross-provider retries. When images are intentionally
  stripped for a format failure, prompt-based image detection should also
  be suppressed to prevent re-loading local images (Aisle openclaw#2, CWE-200).
- Merge partialExecution state across fallback attempts instead of
  replacing: union tool names and OR the didSendViaMessagingTool flag so
  side-effect history from earlier failed attempts carries forward
  (Codex review feedback).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mitchmcalister
Copy link
Copy Markdown
Contributor Author

Addressed in 4cf0a10:

#1 (High): Acknowledged — other runWithModelFallback call sites (e.g. followup-runner.ts) don't apply cross-provider guards. This is intentionally out of scope for this PR (see Non-goals), but worth a follow-up issue.

#2 (Medium): Fixed — suppressPromptImageDetection now also triggers on same-provider format retries, matching the resolveRetryImages strip behavior.

#3 (Low): Intentional trade-off. Same-provider = same trust boundary; tool names are already sanitized via FailoverError.sanitizeToolNames().

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: 4cf0a10dd5

ℹ️ 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 on lines +417 to +420
// Fallback retries only fire on thrown errors (not partial streaming results),
// so the original prompt is always safe to re-send. The orphaned-user-message
// repair in attempt.ts handles any residual duplicate user turns.
const effectivePrompt = 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 Keep retry prompts generic for resumed CLI sessions

This change makes runAgentAttempt always resend params.body on fallback retries, but the “orphaned-user-message repair” only exists in the embedded runner path, not the CLI runner path. When a CLI provider retries against an existing cliSessionId (resume mode) after a timeout/rate-limit failure, the same user prompt can be appended again to that backend session, which can produce duplicated or inconsistent follow-up behavior. This regression is specific to resumed CLI sessions and was previously avoided by using the generic continuation retry prompt.

Useful? React with 👍 / 👎.

@mitchmcalister
Copy link
Copy Markdown
Contributor Author

Re: Codex review on agent-command.ts:420 (CLI session duplicate turns) — pre-existing behavior. The old generic continuation message had the same duplicate-turn issue on CLI session retries. Orphan repair for the CLI runner path is a separate concern tracked in #54192.

@mitchmcalister
Copy link
Copy Markdown
Contributor Author

Superseded by #57959 — narrower scope after #55632 landed prompt preservation for new sessions. The new PR focuses on the remaining image safety and partial execution detection layers. See #57959 for the full design discussion from #44188 carried forward.

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

Labels

agents Agent runtime and tooling commands Command implementations size: L

Projects

None yet

2 participants