Skip to content

fix(cli-runner): drop stale claude-cli sessionId when transcript missing#77030

Merged
steipete merged 1 commit into
openclaw:mainfrom
openperf:fix/77011-claude-cli-missing-transcript-resume-loop
May 4, 2026
Merged

fix(cli-runner): drop stale claude-cli sessionId when transcript missing#77030
steipete merged 1 commit into
openclaw:mainfrom
openperf:fix/77011-claude-cli-missing-transcript-resume-loop

Conversation

@openperf

@openperf openperf commented May 4, 2026

Copy link
Copy Markdown
Member

Summary

  • Problem: After a half-installed update.run (Gateway update.run can leave half-installed package, killing live session transcripts #77011), live Telegram-direct sessions stop responding indefinitely. The Claude CLI live session repeatedly invokes claude --resume <stale-sid> against a transcript that no longer exists at ~/.claude/projects/<project>/<sessionId>.jsonl. The first attempt hangs to the 5-minute hard timeout (durationMs=300011 error=FailoverError); every following turn fast-fails (durationMs=384 error=FailoverError) without ever clearing the dead binding. Concretely, src/agents/cli-runner/prepare.ts:259-271 only validates auth-profile / auth-epoch / system-prompt / mcp hashes via resolveCliSessionReuse (src/agents/cli-session.ts:127); it never checks that the transcript file the persisted cliSessionBindings.claude-cli.sessionId points at is still on disk.

  • Root Cause: A transcript-existence probe (claudeCliSessionTranscriptHasContent in src/agents/command/attempt-execution.helpers.ts:80) is already wired into the user-driven path at src/agents/command/attempt-execution.ts:443-467 to clear stale bindings before invoking runCliAgent. The auto-reply / followup / Telegram-direct path takes a different entrypoint — src/auto-reply/reply/agent-runner-execution.ts:1305 calls runCliAgent directly and does not go through runAgentAttempt, so the existing probe is not consulted on this code path. The session_expired retry inside src/agents/cli-runner.ts:331 does not compensate either, because claude --resume against a missing transcript surfaces as reason=timeout, not reason=session_expired; the retry branch never fires and the persisted cliSessionBinding is never overwritten with a fresh sessionId. Every subsequent turn re-reads the same dead sessionId until the operator explicitly runs openclaw sessions cleanup --enforce --fix-missing. The cron isolated-agent path (src/cron/isolated-agent/run-execution.runtime.ts) is in the same category — also enters runCliAgent directly.

  • Fix: Push the transcript-existence pre-flight down into prepareCliRunContext, the single common entry point for all CLI-run callers (attempt-execution.ts, agent-runner-execution.ts, cron/isolated-agent/run-execution.runtime.ts). When the candidate sessionId is non-empty, the provider is claude-cli, and the on-disk Claude CLI transcript has no assistant message (i.e. resume target is dead), drop the binding for this turn and return a CliReusableSession with invalidatedReason: "missing-transcript". The current turn then runs as a fresh claude session (no --resume), and the existing post-run flow in src/agents/command/session-store.ts:163-168 writes the brand-new cliSessionBinding back to the session store, replacing the dead one. The loop is broken at the per-turn level — no manual cleanup needed, no schema migration, no protocol bump. The check is gated on isClaudeCliProvider(params.provider) so non-claude providers pay zero cost. The helper is exposed via the existing prepareDeps injection seam so tests stay hermetic and don't touch real ~/.claude/projects/.

  • What changed:

    • src/agents/cli-runner/prepare.ts — import isClaudeCliProvider and claudeCliSessionTranscriptHasContent; expose the latter through prepareDeps for test injection; before computing reusableCliSession, run the claude-cli transcript probe and short-circuit to { invalidatedReason: "missing-transcript" } when the resume target is dead.
    • src/agents/cli-runner/types.ts — extend CliReusableSession.invalidatedReason union with the new "missing-transcript" case.
    • src/agents/cli-runner/prepare.test.ts — add a vi.mock for the plugin-sdk/anthropic-cli.js facade (so the test runs without bundled-plugin runtime), plus three behavior tests: drop on missing transcript, keep on present transcript, no probe for non-claude providers.
    • CHANGELOG.md — single Fixes line under Unreleased referencing the issue.
  • What did NOT change (scope boundary):

    • No protocol changes; update.run orchestration / atomic-swap logic in src/infra/update-runner.ts and src/infra/package-update-steps.ts is untouched (the npm path already stages and atomically swaps).
    • No changes to non-claude-cli backends (codex, gemini, etc.) — provider gate prevents cross-provider impact.
    • No session-store schema changes; no migration; no doctor changes.
    • The existing user-driven check in attempt-execution.ts:443-467 is intentionally left in place. It still has the value of clearing the persisted binding synchronously for that path (so a follow-up read inside the same request sees the cleared entry); this PR's pre-flight is a cross-caller safety net, not a replacement. Both call the same helper, so the two layers cannot diverge.
    • The session_expired retry path at cli-runner.ts:331-358 is unchanged. The two failure modes are complementary: session_expired is the clean-error case (Claude returns "Conversation not found"), already handled in-process by the retry; timeout is the silent-hang case (Claude blocks until the per-turn deadline), addressed here at the preparation layer above.

Reproduction

  1. Start the gateway with an active Claude-CLI agent on a Telegram-direct session and let it persist a cliSessionBindings.claude-cli.sessionId (any successful turn does this).
  2. Simulate the half-installed update.run outcome by removing the corresponding ~/.claude/projects/<project>/<sessionId>.jsonl file (or replace its contents with an empty file). The persisted session entry still references the now-missing sessionId.
  3. Send a follow-up message to the Telegram bot. Without this fix, observe claude live session turn failed: ... durationMs=300011 error=FailoverError (5-minute resume hang) and cli session reset is not logged. Every subsequent turn fails immediately with the same FailoverError; the binding never refreshes; user is stuck.
  4. With this fix, observe cli session reset: provider=claude-cli reason=missing-transcript ... once at the start of the next turn, the run completes against a fresh Claude session, and the new sessionId is persisted by the existing post-run flow. Subsequent turns succeed.

Risk / Mitigation

  • Risk 1 — false positives: Could a healthy binding be dropped? claudeCliSessionTranscriptHasContent reads up to SESSION_FILE_MAX_RECORDS=500 lines and returns true once it sees an assistant message. A binding is only persisted by session-store.ts:setCliSessionBinding after a successful run, which by definition has flushed at least one assistant turn — so a freshly-written binding always passes the probe. The probe walks ~/.claude/projects/* (already done in attempt-execution.helpers.ts), so home-dir / project-prefix mismatches behave the same as in the existing user-driven check.
    Mitigation: covered by the new "keeps the claude-cli sessionId when the on-disk transcript is present" test, plus the existing claudeCliSessionTranscriptHasContent suite in attempt-execution.test.ts:366-432 (already validates symlink rejection, path-traversal rejection, and assistant-message detection).
  • Risk 2 — non-claude regressions: Could other providers' resume paths break? The probe is gated on isClaudeCliProvider(params.provider), so any non-claude provider follows the unmodified existing branch.
    Mitigation: new "does not probe the transcript for non-claude-cli providers" test asserts transcriptCheck is never called and the existing { sessionId } flow is preserved.
  • Risk 3 — extra I/O per turn: The probe adds one fs.readdir plus a small bounded fs.open+readline per claude-cli turn. The same helper is already on the user-driven hot path via attempt-execution.ts:447; we are not introducing a new I/O class, only widening its coverage. Worst-case is bounded by SESSION_FILE_MAX_RECORDS=500 lines and short-circuits on the first assistant message (typically line 1-2).
    Mitigation: the helper itself is unchanged and already production-tested.
  • Risk 4 — cross-package import: The new import of claudeCliSessionTranscriptHasContent crosses from agents/cli-runner/ into agents/command/, and isClaudeCliProvider is loaded through the plugin-sdk/anthropic-cli.js facade.
    Mitigation: both directions are already used in core (prepare.ts already imports ../command/types.js; attempt-execution.ts already loads the same facade). No new architectural seam, no cycle (helpers.ts does not import back into cli-runner). Test file mocks the facade locally so the unit suite stays hermetic.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Agents (cli-runner / claude-cli resume path)
  • Auto-reply / followup runs (indirectly: now goes through the new pre-flight)
  • Tests (prepare.test.ts unit coverage)
  • Changelog (Unreleased Fixes entry)

Linked Issue/PR

Refs #77011 — addresses the missing-transcript auto-recovery scenario described in the issue. The other two items in the same issue (update.run atomicity and shell-only cron systemEvent run session rows) are independent failure modes; they remain open and out of scope here so they can be tracked and shipped separately.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S maintainer Maintainer-authored PR labels May 4, 2026
@clawsweeper

clawsweeper Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

Summary
Adds a Claude CLI transcript-content preflight in prepareCliRunContext, extends the reusable-session invalidation reason, adds prepare-layer tests for missing/present/non-Claude cases, and records an Unreleased changelog fix.

Reproducibility: yes. at source level. Current main checks missing Claude CLI transcripts only in runAgentAttempt, while auto-reply and cron direct callers pass stored session ids into runCliAgent without that guard.

Next step before merge
No repair-lane candidate: the latest diff has no concrete branch defect for automation; remaining action is normal maintainer, CI, and mergeability handling.

Security
Cleared: The diff reuses an existing bounded local transcript probe and adds tests/changelog only; it does not add dependencies, CI permissions, secret handling, downloads, or package execution paths.

Review details

Best possible solution:

Land the shared prepare-layer missing-transcript guard after normal maintainer, CI, and mergeability checks, while leaving the broader update.run and shell-only cron work tracked in #77011.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level. Current main checks missing Claude CLI transcripts only in runAgentAttempt, while auto-reply and cron direct callers pass stored session ids into runCliAgent without that guard.

Is this the best way to solve the issue?

Yes. Moving the provider-gated transcript probe into prepareCliRunContext is the narrow shared fix for all direct runCliAgent callers and avoids schema, protocol, or non-Claude provider changes.

What I checked:

  • Protected PR handling: The provided GitHub context lists authorAssociation: MEMBER and the protected maintainer label, so this cleanup workflow should not close the PR.
  • Current main only has the transcript guard in the user-driven path: runAgentAttempt checks claudeCliSessionTranscriptHasContent and clears the stored Claude CLI binding before calling runCliAgent, but this guard is scoped to that entrypoint. (src/agents/command/attempt-execution.ts:441, 111df161df38)
  • Existing transcript helper is bounded and path-safe: The helper normalizes the session id, walks ~/.claude/projects, rejects missing/non-file/symlink/path-like targets, and returns true only after finding an assistant message. (src/agents/command/attempt-execution.helpers.ts:80, 111df161df38)
  • Auto-reply caller bypasses the user-driven guard: The auto-reply/follow-up path reads cliSessionBinding and calls runCliAgent directly with cliSessionId and cliSessionBinding. (src/auto-reply/reply/agent-runner-execution.ts:1287, 111df161df38)
  • Cron caller bypasses the user-driven guard: The isolated cron path passes the stored CLI session id straight into runCliAgent, making the prepare layer the shared boundary for direct callers. (src/cron/isolated-agent/run-executor.ts:147, 111df161df38)
  • PR implements shared prepare-layer preflight: The branch derives a Claude CLI candidate session id from binding or legacy id, probes the transcript only for claude-cli, and returns invalidatedReason: "missing-transcript" so the turn starts without --resume. (src/agents/cli-runner/prepare.ts:271, dd7b934ffd41)

Likely related people:

  • Peter Steinberger: Current-main blame points the CLI session reuse block, transcript helper, user-driven guard, cron direct CLI call, and session-store persistence to commit 0fa70f5a47 with Peter as author/committer metadata. (role: recent maintainer; confidence: medium; commits: 0fa70f5a47f8; files: src/agents/cli-runner/prepare.ts, src/agents/command/attempt-execution.helpers.ts, src/agents/command/attempt-execution.ts)
  • Kelaw - Keshav's Agent: The prior ClawSweeper review comment for this PR attributes the current CLI session reuse, missing-transcript helper, and direct caller wiring to commit 12d90a26f7; local shallow history limited direct follow-up, so confidence is lower. (role: introduced adjacent behavior; confidence: low; commits: 12d90a26f7b8; files: src/agents/cli-runner/prepare.ts, src/agents/command/attempt-execution.helpers.ts, src/agents/command/attempt-execution.ts)

Remaining risk / open question:

  • The provided PR metadata reports mergeable=false, so a maintainer may still need to handle rebase or conflict state before landing.
  • No live Telegram/Claude CLI gateway run was executed in this read-only review; confidence comes from source inspection and the PR's unit coverage.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 111df161df38.

Re-review progress:

@openperf
openperf force-pushed the fix/77011-claude-cli-missing-transcript-resume-loop branch 2 times, most recently from ab863a2 to dd7b934 Compare May 4, 2026 01:58
…ing (openclaw#77011)

Probe ~/.claude/projects/.../<sid>.jsonl in prepareCliRunContext before
emitting `claude --resume <sid>`. When the on-disk transcript no longer
exists (e.g. after a half-installed update.run, manual prune, or Claude
CLI reinstall), drop the saved cliSessionBinding so this turn starts a
fresh session instead of timing out on a dead resume target. The post-run
session-store flow then writes the new sessionId back, ending the loop.
@steipete
steipete force-pushed the fix/77011-claude-cli-missing-transcript-resume-loop branch from dd7b934 to e9919ad Compare May 4, 2026 02:08
@steipete
steipete merged commit 7e296ae into openclaw:main May 4, 2026
21 checks passed
@steipete

steipete commented May 4, 2026

Copy link
Copy Markdown
Contributor

Landed via temp rebase onto main.

  • Local proof: git diff --check; pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/agents/cli-runner/prepare.ts src/agents/cli-runner/types.ts src/agents/cli-runner/prepare.test.ts; pnpm test src/agents/cli-runner/prepare.test.ts
  • Testbox proof: pnpm check:changed on tbx_01kqrbt8zaapa69ccf7fz46sy9 (exit 0)
  • Rebased PR SHA: e9919ad
  • Land commit: 7e296ae

Thanks @openperf!

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

Labels

agents Agent runtime and tooling maintainer Maintainer-authored PR size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants