Skip to content

fix(agents): stop gateway crash from wedged claude-cli turns and persist heartbeat session bindings#98933

Merged
obviyus merged 2 commits into
mainfrom
codex/fix-cli-live-crash-heartbeat-binding
Jul 2, 2026
Merged

fix(agents): stop gateway crash from wedged claude-cli turns and persist heartbeat session bindings#98933
obviyus merged 2 commits into
mainfrom
codex/fix-cli-live-crash-heartbeat-binding

Conversation

@obviyus

@obviyus obviyus commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #98894
Fixes #98895

What Problem This Solves

Two P1 defects in the claude-cli backend found by a reliability audit:

  1. A wedged Claude CLI child could crash the entire gateway. In the live session, the turn's outputPromise was created with no rejection handler attached until after await writeTurnInput(...). If the child was alive but not reading stdin (hung startup, or a reseed history prompt larger than the 64 KiB pipe buffer), the stdin write callback never fired — and when the turn's no-output timer rejected the turn, the rejection had zero handlers. Node's unhandledRejection handler does not treat FailoverError as suppressible and calls process.exit(1): one stuck CLI turn took down every agent and channel on the gateway.
  2. Heartbeat turns silently dropped claude-cli session-binding updates (regression from 0314819, Jun 15). persistSessionUsageUpdate skipped applyCliSessionIdToSessionPatch for heartbeat and preserveRuntimeModel runs. Claude print-mode --resume mints a new session id every turn, so each heartbeat advanced the CLI conversation into a new session while the stored binding stayed on the old one — the next user turn resumed the pre-heartbeat session, silently losing everything the agent did during heartbeats and orphaning a forked session under ~/.claude/projects per heartbeat.

Why This Change Was Made

  1. The rejection now always has a handler while the prompt write is in flight: a no-op catch sentinel is attached before the write, and the write is raced against the turn promise so a timeout/abort during a backpressured write settles the race instead of leaking; the canonical result still propagates through the final await outputPromise. A late rejection of the losing writeTurnInput promise is absorbed by the settled race.
  2. The binding-persistence gate is restored to preserveUserFacingRunState (internal handoffs only), exactly the semantics f55e986 established and 0eca3a9 intended: heartbeats keep preserving user-facing model-display state, but the CLI session binding (and binding clears) from the run persist again.

User Impact

Gateways no longer hard-exit when a Claude CLI child wedges during turn startup — the turn fails through the normal failover path instead. Agents with heartbeats stop losing CLI-side conversation context after every heartbeat and stop accumulating orphaned forked sessions on disk.

Evidence

  • New regression test where mock stdin never invokes its write callback (the existing mocks call it synchronously, which is why this class was invisible): the no-output timer fires mid-write, the turn fails with the FailoverError, and a process.on("unhandledRejection") probe asserts nothing escapes.
  • New tests proving a heartbeat run's cliSessionBinding is persisted, a heartbeat clearCliSessionBinding is honored, and model-display fields remain preserved.
  • Validation: node scripts/run-vitest.mjs src/agents/cli-runner.spawn.test.ts src/auto-reply/reply/session.test.ts (187 tests pass), pnpm tsgo pass, node scripts/run-oxlint.mjs on touched files clean, repo autoreview (branch mode) clean.

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

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper status: review started.

I am starting a fresh review of this pull request: fix(agents): stop gateway crash from wedged claude-cli turns and persist heartbeat session bindings This is item 1/1 in the current shard. Shard 0/1.

This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking.

Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted.

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

1 participant