Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
When a Claude CLI session is invalidated (e.g. reason=missing-transcript), the historyPrompt fallback is unreachable for any session that has never been compacted, because loadCliSessionReseedMessages returns [] in that case. Observed in production: 96/96 invalidations over 14 days produced historyPrompt=none and the agent restarted with no prior context.
Steps to reproduce
- Start an OpenClaw conversational session (e.g. Discord DM,
dmScope: per-channel-peer) on claude-cli runtime.
- Have a short exchange (1–3 turns), well below any compaction threshold.
- Wait for the Claude CLI live session to close on idle (
CLAUDE_LIVE_IDLE_TIMEOUT_MS = 600 * 1e3, i.e. 10 min) OR cause an interruption mid-stream.
- Send a new message.
- Observe in
~/.openclaw/logs/gateway.log:
cli session reset: provider=claude-cli reason=missing-transcript
cli exec: provider=claude-cli ... useResume=false ... reuse=invalidated:missing-transcript historyPrompt=none
- The agent has no memory of the prior turns.
Expected behavior
When reuse=invalidated:* fires, the openClawHistoryPrompt fallback path is intended to inject prior conversation context (per the existing code in prepare.runtime.../execute.runtime). The agent should restart the CLI session with a "Continue this conversation using the OpenClaw transcript below" preamble (the literal string built by buildCliSessionHistoryPrompt).
Actual behavior
historyPrompt=none is logged on every invalidation event. The fresh CLI session receives only the current user message with no prior context. From the user's perspective, the agent has lost memory.
OpenClaw version
2026.5.6 (c97b9f7)
Operating system
macOS (Darwin 25.3.0, arm64)
Install method
npm install -g openclaw
Model
claude-cli/claude-opus-4-7 (also reproduced with claude-cli/claude-sonnet-4-6 and claude-cli/claude-haiku-4-5)
Provider / routing chain
openclaw → claude-cli → anthropic
Logs, screenshots, and evidence
Code reference — src/agents/cli-runner/session-history.ts (bundled at dist/session-history-DD0rk4PH.js in 2026.5.6):
async function loadCliSessionReseedMessages(params) {
const entries = await loadCliSessionEntries(params);
const latestCompactionIndex = entries.findLastIndex(
(entry) => entry.type === "compaction" && typeof entry.summary === "string"
);
if (latestCompactionIndex < 0) return []; // ← returns empty if never compacted
// ... compaction-summary + tail-message path
}
Consumed at dist/prepare.runtime-BtdJ-JrR.js ~line 823:
const openClawHistoryPrompt = reusableCliSession.sessionId
? void 0
: buildCliSessionHistoryPrompt({
messages: await loadCliSessionReseedMessages({...}),
prompt: preparedPrompt
});
Since loadCliSessionReseedMessages returns [] for un-compacted sessions, buildCliSessionHistoryPrompt finds no rendered history and returns undefined. The fallback path is silently inert.
Production gateway log breakdown over 14 days:
- 77 events with
reason=missing-transcript
- 19 events with
reason=system-prompt
- 96/96 logged
historyPrompt=none — fallback never produced any prelude.
Impact and severity
- Affected: any user on a conversational channel (Discord, Telegram, etc.) using the
claude-cli provider with sessions short enough to never trigger compaction (in practice, the vast majority of DM-style usage).
- Severity: Behavior bug, user-visible. The agent loses memory mid-conversation when sessions are invalidated, which happens on idle close (every 10 min of inactivity) and on any system-prompt change (e.g. workspace bootstrap drift, gitStatus updates).
- Frequency: 77 missing-transcript + 19 system-prompt invalidations in 14 days on a single instance with moderate Discord traffic.
- Consequence: degraded user experience perceived as "the bot lost context"; manual re-priming needed each time.
Additional information
Suggested fix (untested) — when no compaction is present, fall back to a tail of raw messages:
async function loadCliSessionReseedMessages(params) {
const entries = await loadCliSessionEntries(params);
const latestCompactionIndex = entries.findLastIndex(
(entry) => entry.type === "compaction" && typeof entry.summary === "string"
);
- if (latestCompactionIndex < 0) return [];
+ if (latestCompactionIndex < 0) {
+ // No compaction yet — fall back to last N raw messages so the
+ // historyPrompt path isn't silently inert on short sessions.
+ const tailMessages = entries.flatMap((entry) =>
+ entry.type === "message" ? [entry.message] : []
+ );
+ return tailMessages.slice(-RESEED_TAIL_FALLBACK_MESSAGES);
+ }
// ... existing compaction path unchanged
}
A new constant like RESEED_TAIL_FALLBACK_MESSAGES = 30 would keep the prelude bounded; each message is then capped further by maxHistoryChars = 12288 in buildCliSessionHistoryPrompt.
Architectural note (broader gap) — this bug is symptomatic of a wider design gap: backend invalidations (cli session reset reason=missing-transcript, reason=system-prompt) emit only a cliBackendLog.info(...) line, with no plugin hook fired. The before_reset plugin hook is only emitted by performGatewaySessionReset (i.e. user-initiated /new and /reset), not by these backend resets. As a result, plugins cannot observe or react to these silent invalidations.
This gives maintainers two possible fix paths:
- Local fix (this issue): make
loadCliSessionReseedMessages produce a usable prelude even without prior compaction (the diff above).
- Architectural fix (broader): fire a
before_reset (or new typed) plugin hook on backend invalidations too, so plugins can implement their own context-recovery strategy. This would also enable observability for operators tracking session health.
The two are independent — fix (1) is enough to resolve the symptom; fix (2) would unlock more patterns for power users without requiring upstream patching.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
When a Claude CLI session is invalidated (e.g.
reason=missing-transcript), thehistoryPromptfallback is unreachable for any session that has never been compacted, becauseloadCliSessionReseedMessagesreturns[]in that case. Observed in production: 96/96 invalidations over 14 days producedhistoryPrompt=noneand the agent restarted with no prior context.Steps to reproduce
dmScope: per-channel-peer) onclaude-cliruntime.CLAUDE_LIVE_IDLE_TIMEOUT_MS = 600 * 1e3, i.e. 10 min) OR cause an interruption mid-stream.~/.openclaw/logs/gateway.log:Expected behavior
When
reuse=invalidated:*fires, theopenClawHistoryPromptfallback path is intended to inject prior conversation context (per the existing code inprepare.runtime.../execute.runtime). The agent should restart the CLI session with a "Continue this conversation using the OpenClaw transcript below" preamble (the literal string built bybuildCliSessionHistoryPrompt).Actual behavior
historyPrompt=noneis logged on every invalidation event. The fresh CLI session receives only the current user message with no prior context. From the user's perspective, the agent has lost memory.OpenClaw version
2026.5.6 (c97b9f7)
Operating system
macOS (Darwin 25.3.0, arm64)
Install method
npm install -g openclaw
Model
claude-cli/claude-opus-4-7 (also reproduced with claude-cli/claude-sonnet-4-6 and claude-cli/claude-haiku-4-5)
Provider / routing chain
openclaw → claude-cli → anthropic
Logs, screenshots, and evidence
Code reference —
src/agents/cli-runner/session-history.ts(bundled atdist/session-history-DD0rk4PH.jsin 2026.5.6):Consumed at
dist/prepare.runtime-BtdJ-JrR.js~line 823:Since
loadCliSessionReseedMessagesreturns[]for un-compacted sessions,buildCliSessionHistoryPromptfinds no rendered history and returnsundefined. The fallback path is silently inert.Production gateway log breakdown over 14 days:
reason=missing-transcriptreason=system-prompthistoryPrompt=none— fallback never produced any prelude.Impact and severity
claude-cliprovider with sessions short enough to never trigger compaction (in practice, the vast majority of DM-style usage).Additional information
Suggested fix (untested) — when no compaction is present, fall back to a tail of raw messages:
async function loadCliSessionReseedMessages(params) { const entries = await loadCliSessionEntries(params); const latestCompactionIndex = entries.findLastIndex( (entry) => entry.type === "compaction" && typeof entry.summary === "string" ); - if (latestCompactionIndex < 0) return []; + if (latestCompactionIndex < 0) { + // No compaction yet — fall back to last N raw messages so the + // historyPrompt path isn't silently inert on short sessions. + const tailMessages = entries.flatMap((entry) => + entry.type === "message" ? [entry.message] : [] + ); + return tailMessages.slice(-RESEED_TAIL_FALLBACK_MESSAGES); + } // ... existing compaction path unchanged }A new constant like
RESEED_TAIL_FALLBACK_MESSAGES = 30would keep the prelude bounded; each message is then capped further bymaxHistoryChars = 12288inbuildCliSessionHistoryPrompt.Architectural note (broader gap) — this bug is symptomatic of a wider design gap: backend invalidations (
cli session reset reason=missing-transcript,reason=system-prompt) emit only acliBackendLog.info(...)line, with no plugin hook fired. Thebefore_resetplugin hook is only emitted byperformGatewaySessionReset(i.e. user-initiated/newand/reset), not by these backend resets. As a result, plugins cannot observe or react to these silent invalidations.This gives maintainers two possible fix paths:
loadCliSessionReseedMessagesproduce a usable prelude even without prior compaction (the diff above).before_reset(or new typed) plugin hook on backend invalidations too, so plugins can implement their own context-recovery strategy. This would also enable observability for operators tracking session health.The two are independent — fix (1) is enough to resolve the symptom; fix (2) would unlock more patterns for power users without requiring upstream patching.