Skip to content

Fix blank-session restarts after interrupt or failed run - #1135

Closed
bunnysayzz wants to merge 9049 commits into
CodebuffAI:mainfrom
bunnysayzz:fix/preserve-session-state-1054
Closed

Fix blank-session restarts after interrupt or failed run#1135
bunnysayzz wants to merge 9049 commits into
CodebuffAI:mainfrom
bunnysayzz:fix/preserve-session-state-1054

Conversation

@bunnysayzz

Copy link
Copy Markdown

Fixes #1054.

Root cause

The CLI only synced previousRunStateRef (the continuation state passed to the SDK as previousRun) when client.run() settled. But Esc releases the input lock immediately, inside the abort listener. So a follow-up message sent the moment the user hits Esc could be built from a stale (or null) ref. With a null ref the SDK builds a fresh session state and the chat comes back as an empty, brand-new conversation, which reads exactly like a hard reset.

Two more holes in the same family:

  1. The thrown-error catch path (failed/expired runs) never synced the ref, only disk, so the next prompt in the same process also lost history.
  2. A sessionState-less run state could be persisted and then adopted on restart (loadMostRecentChatState fabricated { output } when run-state.json had no session state), poisoning every resumed chat.

Fix

  • setupStreamingContext accepts an onAbort callback, invoked synchronously at the top of the abort listener, before the input lock is released. useSendMessage passes syncRunState(latestRunStateSnapshot) so an interrupt checkpoints the latest SDK snapshot immediately.
  • useSendMessage now owns a generation token per admitted run. A superseded run settling late can never adopt state, persist a checkpoint, or touch shared queue state over the run that replaced it (also gated handleRunCompletion/handleRunError/finally with it).
  • The catch path syncs the ref too, then persists, so a failed turn resumes from the last snapshot.
  • loadMostRecentChatState treats a run-state without sessionState as unrestorable; the transcript still restores, the agent context does not.

Tests

  • New hook-level suite cli/src/hooks/__tests__/use-send-message.test.tsx drives the real wiring (createRunConfig + a controllable client via a small DI seam, matching the repo documented DI-over-module-mocking approach): abort-then-follow-up carries full history, a superseded run settling late cannot clobber the newer run state, rejected runs resume from the last snapshot, and sessionState-less states are never adopted.
  • Helper tests cover the onAbort callback contract (sync ordering before lock release, throwing callback still cleans up).
  • Storage tests cover the load-side poison guard.

All targeted suites pass (92 tests). Typecheck, prettier, and git diff --check are clean; the app bundle builds.

codebuff public sync bot added 30 commits August 25, 2026 05:09
Source: CodebuffAI/freebuff-private@ddfa6c89a27176e4e1afef0bd597077698e3d9e3
Source: CodebuffAI/freebuff-private@9b8fb92689a45c3d52b65f6d4177bb8b0e4a29db
Source: CodebuffAI/freebuff-private@e20dcb5811c89bc349a283c8fb8b474f03857eaa
Source: CodebuffAI/freebuff-private@1a9bb08ef45331c94cfa616a7e5f91dbc3375e7b
Source: CodebuffAI/freebuff-private@2204d7f013215d4a9746f87ccadb88422bdbf0ce
Source: CodebuffAI/freebuff-private@d85f7624f6df913150afd97028ce8034caf9401c
Source: CodebuffAI/freebuff-private@496a274da0db56d0465478154dbd368ead06dc33
Source: CodebuffAI/freebuff-private@2479764c0c4dc98e75a5c5778d29d10938b5997e
Source: CodebuffAI/freebuff-private@72cb80a710ca0b69c0ea1c92caa09449ae14ae23
Source: CodebuffAI/freebuff-private@00c76d2a5513197c7f5963463a1511ce8a4086d6
Source: CodebuffAI/freebuff-private@ae3b428c053013f6c40d893c785f6f383a1eefb7
Source: CodebuffAI/freebuff-private@1f928dbc1d0b1c4ae199f3b12111897300092931
Source: CodebuffAI/freebuff-private@23d94d0a7272b2c1460bb5576c0359cf0fe2765a
Source: CodebuffAI/freebuff-private@56076c9423fb9d6b185fb66d4ec7d7818c544aa4
Source: CodebuffAI/freebuff-private@7ac0799f05337a0df3eafe682e030544ce191d2f
Source: CodebuffAI/freebuff-private@2464fed8ca7d9730dfa2226e1c4267353c2b1e85
Source: CodebuffAI/freebuff-private@d6c79c09d66c9187499f37445a7f82a93ae674a1
Source: CodebuffAI/freebuff-private@db635212c3c95b5545a46afd2fffa3e0d345dd48
Source: CodebuffAI/freebuff-private@8602eeac5f86c63dcdf7051675bda726cc8e0b71
Source: CodebuffAI/freebuff-private@03bff2f48d6c40320acc1cb41ca389d222ac2baa
Source: CodebuffAI/freebuff-private@54215700121b04bef2a7857489eb2017d24a113f
Source: CodebuffAI/freebuff-private@4f4f9a6cd4e9c7aa793a32fcdc8438ced2a0f5a2
Source: CodebuffAI/freebuff-private@5f1f82c2fb2bcf95a01ffb6fc28ccfc25619a83e
Source: CodebuffAI/freebuff-private@9d8b240c6862ab9c5c720af7a1facbf85a48a877
Source: CodebuffAI/freebuff-private@1b960fe83deb21c87e4cb615d7789123361ac407
Source: CodebuffAI/freebuff-private@e8b8ef0c39f6e5fd0f1da7fe1d2704027dcbfb16
Source: CodebuffAI/freebuff-private@407268a57ca56f64b87013573c29bc72058fff28
Source: CodebuffAI/freebuff-private@a138234f7d9310b8d2a89a7f9e478badc2f1b3f3
Source: CodebuffAI/freebuff-private@f0d724e3249e82a81fe934e51aa36f8d20d211e2
Source: CodebuffAI/freebuff-private@344142df6218b6602860cfc57dad3cca639ab8fd
codebuff public sync bot and others added 24 commits August 31, 2026 02:28
Source: CodebuffAI/freebuff-private@2f358ebf68b79b05acb81f18ee42ef4295edee17
Source: CodebuffAI/freebuff-private@e411009a76fbd9f689550bd51bc3b938209ec6f8
Source: CodebuffAI/freebuff-private@70a710b1d0941b7bd3a742dbc8e18f276b602850
Source: CodebuffAI/freebuff-private@3535770948dfde40d9b53936ad8487a3dbb90893
Source: CodebuffAI/freebuff-private@bba508a9f359d5e49230c932ef03fdb733c18bff
Source: CodebuffAI/freebuff-private@741dcfc4f0fc1e7c33d4544fc5ce7259e851c920
Source: CodebuffAI/freebuff-private@d9a3b75bd429ea44aca483addec9b5a3e3bcc79e
Source: CodebuffAI/freebuff-private@b76209dd2ec3f278ef28660c239c4e484572d7b4
Source: CodebuffAI/freebuff-private@ff62cf3506968799272123e6f637cdee6935e48f
Source: CodebuffAI/freebuff-private@d6591b0c4986740a517395811b4fe9d6e5db47b2
Source: CodebuffAI/freebuff-private@87a991308dfe74c4e68cf07c4206d0fc63db1c8a
Source: CodebuffAI/freebuff-private@5c9444e7ef7993a29e064220ef46c3a58af12b7d
Source: CodebuffAI/freebuff-private@e40b501c923f0bd2f33749cf6f081d9e74b42282
Source: CodebuffAI/freebuff-private@a82256a7abc33584a5bb1be29fbbd9c67c2dd8d7
Source: CodebuffAI/freebuff-private@42d9d1db9a7b2895732d49ab7d6ce84162fc81d4
Source: CodebuffAI/freebuff-private@61c0ffffb152a9b77e2ecfcd3d8adb0e4e4cfb87
Source: CodebuffAI/freebuff-private@a40168234a220d145b5ff7cf702b6364184d7296
Source: CodebuffAI/freebuff-private@91527d25716d4282b0e454e92ecf8b2fc8c1c9f1
Source: CodebuffAI/freebuff-private@09f98d52aaf28097b9509b34acdcd987ade63586
Source: CodebuffAI/freebuff-private@041104880304c80b39253ac43d4b6ffda95d0d65
Source: CodebuffAI/freebuff-private@59224af313776f1447bb0cd9908143c7c8c42014
Source: CodebuffAI/freebuff-private@e9ae9c9973b2a4201c679519104edbac24e18084
Source: CodebuffAI/freebuff-private@add9a3e8c175b5346ef9c0e1a210da2ce04c4847
Fixes CodebuffAI#1054. Esc-aborting a session and sending a
follow-up could start a brand new, blank conversation because the
continuation state (previousRunStateRef) was only synced when run()
settled, while the abort listener released the input lock immediately.

- SetupStreamingContext gains an onAbort callback invoked synchronously
  at the top of the abort listener, before the lock is released, so the
  run owner can checkpoint its latest SDK snapshot.
- useSendMessage passes syncRunState(latestRunStateSnapshot) as onAbort,
  and syncRunState now guards with a generation token so a superseded
  run settling late can never adopt state, persist a checkpoint, or
  touch shared queue state over the run that replaced it.
- The catch path (failed/expired runs) now also syncs the ref, not just
  disk, so the next prompt resumes from the last snapshot instead of
  stale or null history.
- loadMostRecentChatState stops adopting/persisting sessionState-less
  run states, which made the SDK build a blank session on restart.

Adds hook-level regression tests through the real createRunConfig /
client.run wiring: abort then follow-up carries full history, late
superseded runs cannot clobber, the rejected-run path resumes from the
last snapshot, and sessionState-less states are never adopted. Helper
and storage suites updated.
@bunnysayzz
bunnysayzz force-pushed the fix/preserve-session-state-1054 branch from 6072b48 to 1ab202d Compare August 31, 2026 19:38
@bunnysayzz

ghost commented Aug 31, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review. Good catch on the nullability audit.

I grepped for all callers of loadMostRecentChatState across the codebase (excluding test files). There is exactly one production caller: use-send-message.ts line 177, inside the continueChat useEffect. Here is how it handles the null case:

const loadedState = loadMostRecentChatState(continueChatId ?? undefined)
if (loadedState) {
  previousRunStateRef.current = loadedState.runState
  setRunState(loadedState.runState)
  setMessages(sanitizeRestoredMessages(loadedState.messages))
  if (loadedState.chatId) {
    setCurrentChatId(loadedState.chatId)
  }
}
  • previousRunStateRef is typed RunState | null, so assigning loadedState.runState (now RunState | null) is safe.
  • setRunState accepts RunState | null (it comes from the chat store which already supports both).
  • The outer if (loadedState) guard means we never reach these lines when the whole state is null.
  • The nullability change in SavedChatState.runState is specifically documented in the type comment at line 27: "Null when nothing usable was persisted (a run-state without sessionState is never adopted)."

The other references to loadMostRecentChatState are in run-state-storage.test.ts (unit tests that already exercise the null path) and in eval fixture diffs (not real code).

Also rebased the branch on latest upstream (170+ new commits) and resolved the 3 merge conflicts (use-send-message.ts, helpers/send-message.ts, use-send-message.test.tsx). All clean.

@victorxheng

ghost commented Aug 31, 2026

Copy link
Copy Markdown

Apologies — this PR was auto-closed by GitHub when we force-pushed a history rewrite of this repository (repository maintenance; every commit SHA changed). That was not a judgment on this PR, and GitHub does not allow us to reopen it because the commits it was based on no longer exist in the new history.

If you'd like to continue with this change: rebase your branch onto the new main (or recreate it from a fresh clone) and open a new PR — feel free to link back to this one for context, and we'll pick up the review there.

Sorry for the churn, and thanks for contributing.

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

Labels

bot:triaged Classified by the community triage bot pr:port-candidate Worth porting into the private source tree

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session state is lost after free session expires or run is interrupted

3 participants