fix(agents): preserve original task prompt on model fallback for new sessions#55632
Conversation
Greptile SummaryThis PR fixes a real bug where subagent (new-session) fallback retries would receive only a generic recovery prompt — "Continue where you left off. The previous model attempt failed or timed out." — instead of the original task, causing the subagent to silently lose its entire task context. The root cause is that Key changes:
The logic is clean and the tests are thorough. One minor edge case worth keeping in mind: Confidence Score: 4/5Safe to merge; the fix is correct for all described failure modes and unit-tested thoroughly, with a narrow theoretical edge case noted. The change is minimal, targeted, and backed by comprehensive unit tests. The single P2 concern (static No files require special attention; the edge-case note on
|
| Filename | Overview |
|---|---|
| src/agents/agent-command.ts | Adds sessionHasHistory: !isNewSession when invoking runAgentAttempt inside the fallback loop. Minimal, correct for described early-failure scenarios, with a narrow edge case for new sessions where the primary model completes at least one response turn. |
| src/agents/command/attempt-execution.ts | Adds optional sessionHasHistory param to resolveFallbackRetryPrompt and runAgentAttempt; returns original body when history is absent on fallback retry. Logic is clean and well-commented. |
| src/agents/command/attempt-execution.test.ts | New test file with comprehensive unit coverage for resolveFallbackRetryPrompt across all relevant combinations of isFallbackRetry and sessionHasHistory. |
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/agent-command.ts
Line: 788
Comment:
**`isNewSession` doesn't reflect whether the primary attempt wrote history**
`isNewSession` is resolved once before the fallback loop starts and never updated. The fix correctly handles the case where the primary model fails immediately (auth error, prompt rejection) before any history is flushed. However, if the primary model completes at least one full assistant-response turn — which, per the PR description, is when `SessionManager` flushes the initial user message to disk — the session will actually have history in the transcript even though `isNewSession` is still `true`.
In that edge case, passing `sessionHasHistory: false` causes the fallback to re-send the original body, producing a duplicated user message in the transcript: `[user: task] [assistant: partial work] [user: task again]`. The recovery prompt (`"Continue where you left off…"`) would be more appropriate there.
For the primary target scenarios (auth errors, prompt rejections) this is fine since both fail before any output is written. But if the scope of fallback retries ever widens to include mid-turn failures on new sessions, a more precise check — e.g. inspecting the on-disk transcript after the primary attempt — would be safer than relying on the pre-run `isNewSession` flag.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(agents): preserve original task prom..." | Re-trigger Greptile
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
Vulnerabilities1. 🟡 Unbounded JSONL line parsing can cause memory/CPU DoS in sessionFileHasContent()
Description
Impacts:
Vulnerable code: const rl = readline.createInterface({ input: fh.createReadStream({ encoding: "utf-8" }) });
for await (const line of rl) {
...
obj = JSON.parse(line);
}RecommendationAdd hard limits on data processed by bytes, not just record count. Suggested mitigations (combine as appropriate):
const stat = await fs.stat(sessionFile);
if (stat.size > 2 * 1024 * 1024) return false; // choose an appropriate cap
const MAX_LINE_CHARS = 64 * 1024;
for await (const line of rl) {
if (line.length > MAX_LINE_CHARS) return false; // or break/continue
...
const obj = JSON.parse(line);
}
These guards prevent a single malicious/accidental huge JSONL record from consuming excessive memory/CPU during fallback checks. 2. 🟡 Unclosed readline/stream in sessionFileHasContent() can leak resources on early return
DescriptionThe Because
Vulnerable code: const rl = readline.createInterface({ input: fh.createReadStream({ encoding: "utf-8" }) });
...
if ( ... assistant ... ) {
return true;
}
...
} finally {
await fh.close();
}RecommendationEnsure the One safe pattern is to keep references to the stream and close/destroy them in a const fh = await fs.open(sessionFile, "r");
let rl: readline.Interface | undefined;
let stream: NodeJS.ReadableStream | undefined;
try {
stream = fh.createReadStream({ encoding: "utf-8" });
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
for await (const line of rl) {
...
if (isAssistant) {
return true;
}
}
return false;
} finally {
rl?.close();
// @ts-expect-error: destroy exists on ReadStream
(stream as any)?.destroy?.();
await fh.close();
}This prevents leftover active handles and avoids closing the file handle while the stream is still in use. Analyzed PR: #55632 at commit Last updated on: 2026-03-27T13:35:04Z |
2759bad to
e730597
Compare
Fixes three medium-severity security issues identified by Aisle Security Analysis on PR openclaw#55632: - CWE-400: Unbounded session transcript read in sessionFileHasContent() - CWE-400: Symlink-following in sessionFileHasContent() - CWE-201: Sensitive prompt replay to a different fallback provider
|
All four security issues flagged by Aisle have been addressed across the last two commits:
The original fix for #55581 is untouched — same-provider fallback still works exactly as before. Tests covering all cases have been added, including one with a 300KB+ user message to verify the JSONL parser finds the assistant record regardless of content size. |
e772995 to
0229484
Compare
|
Pushed a small follow-up to the PR branch. In simple terms: the real rule here is whether the session already has saved history. If it does not, the fallback still needs the original task prompt. That should stay true even when the fallback switches providers. So the follow-up removes the cross-provider special case and keeps the fix focused on transcript history only. Verified with the targeted test, |
…llback retry Address Greptile review feedback: replace the static !isNewSession flag with a dynamic sessionFileHasContent() check that reads the on-disk transcript before each fallback retry. This correctly handles the edge case where the primary model completes at least one assistant-response turn (flushing the user message to disk) before failing - the fallback now sends the recovery prompt instead of duplicating the original body. The !isNewSession short-circuit is kept as a fast path so existing sessions skip the file read entirely.
Fixes three medium-severity security issues identified by Aisle Security Analysis on PR openclaw#55632: - CWE-400: Unbounded session transcript read in sessionFileHasContent() - CWE-400: Symlink-following in sessionFileHasContent() - CWE-201: Sensitive prompt replay to a different fallback provider
Replace bounded byte-prefix substring matching in sessionFileHasContent() with line-by-line JSONL record parsing. The previous approach could miss an assistant message when the preceding user content exceeded the 256KB read limit, causing a false negative that blocks cross-provider fallback entirely.
6b0f325 to
f62b574
Compare
obviyus
left a comment
There was a problem hiding this comment.
Reviewed latest changes; landing now.
…nclaw#55632) (thanks @openperf) * fix(agents): preserve original task prompt on model fallback for new sessions * fix(agents): use dynamic transcript check for sessionHasHistory on fallback retry Address Greptile review feedback: replace the static !isNewSession flag with a dynamic sessionFileHasContent() check that reads the on-disk transcript before each fallback retry. This correctly handles the edge case where the primary model completes at least one assistant-response turn (flushing the user message to disk) before failing - the fallback now sends the recovery prompt instead of duplicating the original body. The !isNewSession short-circuit is kept as a fast path so existing sessions skip the file read entirely. * fix(agents): address security vulnerabilities in session fallback logic Fixes three medium-severity security issues identified by Aisle Security Analysis on PR openclaw#55632: - CWE-400: Unbounded session transcript read in sessionFileHasContent() - CWE-400: Symlink-following in sessionFileHasContent() - CWE-201: Sensitive prompt replay to a different fallback provider * fix(agents): use JSONL parsing for session history detection (CWE-703) Replace bounded byte-prefix substring matching in sessionFileHasContent() with line-by-line JSONL record parsing. The previous approach could miss an assistant message when the preceding user content exceeded the 256KB read limit, causing a false negative that blocks cross-provider fallback entirely. * fix(agents): preserve fallback prompt across providers --------- Co-authored-by: Ayaan Zaidi <[email protected]>
…nclaw#55632) (thanks @openperf) * fix(agents): preserve original task prompt on model fallback for new sessions * fix(agents): use dynamic transcript check for sessionHasHistory on fallback retry Address Greptile review feedback: replace the static !isNewSession flag with a dynamic sessionFileHasContent() check that reads the on-disk transcript before each fallback retry. This correctly handles the edge case where the primary model completes at least one assistant-response turn (flushing the user message to disk) before failing - the fallback now sends the recovery prompt instead of duplicating the original body. The !isNewSession short-circuit is kept as a fast path so existing sessions skip the file read entirely. * fix(agents): address security vulnerabilities in session fallback logic Fixes three medium-severity security issues identified by Aisle Security Analysis on PR openclaw#55632: - CWE-400: Unbounded session transcript read in sessionFileHasContent() - CWE-400: Symlink-following in sessionFileHasContent() - CWE-201: Sensitive prompt replay to a different fallback provider * fix(agents): use JSONL parsing for session history detection (CWE-703) Replace bounded byte-prefix substring matching in sessionFileHasContent() with line-by-line JSONL record parsing. The previous approach could miss an assistant message when the preceding user content exceeded the 256KB read limit, causing a false negative that blocks cross-provider fallback entirely. * fix(agents): preserve fallback prompt across providers --------- Co-authored-by: Ayaan Zaidi <[email protected]>
…nclaw#55632) (thanks @openperf) * fix(agents): preserve original task prompt on model fallback for new sessions * fix(agents): use dynamic transcript check for sessionHasHistory on fallback retry Address Greptile review feedback: replace the static !isNewSession flag with a dynamic sessionFileHasContent() check that reads the on-disk transcript before each fallback retry. This correctly handles the edge case where the primary model completes at least one assistant-response turn (flushing the user message to disk) before failing - the fallback now sends the recovery prompt instead of duplicating the original body. The !isNewSession short-circuit is kept as a fast path so existing sessions skip the file read entirely. * fix(agents): address security vulnerabilities in session fallback logic Fixes three medium-severity security issues identified by Aisle Security Analysis on PR openclaw#55632: - CWE-400: Unbounded session transcript read in sessionFileHasContent() - CWE-400: Symlink-following in sessionFileHasContent() - CWE-201: Sensitive prompt replay to a different fallback provider * fix(agents): use JSONL parsing for session history detection (CWE-703) Replace bounded byte-prefix substring matching in sessionFileHasContent() with line-by-line JSONL record parsing. The previous approach could miss an assistant message when the preceding user content exceeded the 256KB read limit, causing a false negative that blocks cross-provider fallback entirely. * fix(agents): preserve fallback prompt across providers --------- Co-authored-by: Ayaan Zaidi <[email protected]>
Summary
Fixes #55581 — Preserves the original task prompt during model fallback for new sessions (like subagent spawns) instead of replacing it with a generic recovery message.
Root Cause
resolveFallbackRetryPromptreplaces the original task prompt with"Continue where you left off. The previous model attempt failed or timed out."to prevent duplicate user messages in the session history.sessions_spawnfor subagents), theSessionManageronly flushes the initial user message to the transcript file after the first assistant response arrives.Changes
src/agents/command/attempt-execution.tssessionHasHistoryparameter toresolveFallbackRetryPromptandrunAgentAttempt.resolveFallbackRetryPromptto return the originalbodyinstead of the recovery message whensessionHasHistoryis false, ensuring the fallback model receives the actual task.src/agents/agent-command.ts!isNewSessionas thesessionHasHistoryargument when callingrunAgentAttemptwithin the fallback loop.src/agents/command/attempt-execution.test.tsresolveFallbackRetryPromptto verify prompt preservation logic for both new and existing sessions across initial attempts and fallback retries.Test Results
resolveFallbackRetryPrompt.