Skip to content

fix: preserve session state on session expiration, abort, and compaction (Fixes #1054) - #1055

Closed
Utkarsh-X wants to merge 8544 commits into
CodebuffAI:mainfrom
Utkarsh-X:fix/preserve-session-state-on-abort-compaction
Closed

fix: preserve session state on session expiration, abort, and compaction (Fixes #1054)#1055
Utkarsh-X wants to merge 8544 commits into
CodebuffAI:mainfrom
Utkarsh-X:fix/preserve-session-state-on-abort-compaction

Conversation

@Utkarsh-X

Copy link
Copy Markdown

Fixes #1054

Summary

Fixes an issue where sending a message (such as "continue") after a free session expires or after interrupting with Esc causes the assistant to lose all conversation memory and restart exploration from scratch.

Root Cause

  1. Missing in-memory state update on errors / session expiration (use-send-message.ts):
    When a free session ends or fails with a gate error, the catch (error) block in use-send-message.ts calls saveChatState to persist the snapshot to disk, but does not update previousRunStateRef.current in React memory. When the user sends a follow-up prompt in the same running session, sendMessage reads the un-synced previousRunStateRef.current, causing sdk/src/run.ts to instantiate a blank initialSessionState (messageHistory = []).

  2. Abort race condition (use-send-message.ts):
    When a user interrupts with Esc, the UI input lock was released before client.run() finished resolving without synchronously assigning previousRunStateRef.current = latestRunStateSnapshot.

  3. Compaction continuation guidance (compact-history.ts, context-pruner.ts):
    When deterministic compaction runs, the history is collapsed into a synthetic summary. Because the system prompt retains # Initial Git Changes, models seeing 0 assistant turns occasionally re-ran exploratory git diff and tree commands.

Changes

  • cli/src/hooks/use-send-message.ts:
    • Assign previousRunStateRef.current = latestRunStateSnapshot and setRunState(latestRunStateSnapshot) inside registerActiveRun upon abort.
    • Assign previousRunStateRef.current = latestRunStateSnapshot and setRunState(latestRunStateSnapshot) in the catch (error) block alongside saveChatState.
  • cli/src/hooks/helpers/send-message.ts:
    • Updated the abort listener comment to reflect synchronous state preservation.
  • packages/agent-runtime/src/compact-history.ts & agents/context-pruner.ts:
    • Clarified CONTINUATION_TEXT (in 100% parity) to instruct models that initial repository exploration is already recorded in <historical_memory>.

Verification

  • bun test sdk/src/__tests__/run-cancellation.test.ts (16 passed)
  • bun test cli/src/utils/__tests__/run-state-storage.test.ts (37 passed)
  • bun test packages/agent-runtime/src/__tests__/compact-history.test.ts (31 passed)
  • packages/agent-runtime/src/__tests__/context-pruner-parity.test.ts (13 passed)
  • agents/__tests__/context-pruner.test.ts (69 passed)
  • bun run build:sdk and bun run build:freebuff both compile with exit code 0.

codebuff public sync bot added 30 commits August 11, 2026 23:08
Source: CodebuffAI/freebuff-private@989bb8c61e4762a9d79cc0ff479c7922f201e63a
Source: CodebuffAI/freebuff-private@63ec05d92ac9c392dc386a22eb415415115c7523
Source: CodebuffAI/freebuff-private@b3a9d1faf569e471f946c6d3d3c5fbeb61900182
Source: CodebuffAI/freebuff-private@17cbf15bd63c38af9318457f92640995c6540607
Source: CodebuffAI/freebuff-private@0c7276367068f246ec9c523b5e2078b46ade5f7c
Source: CodebuffAI/freebuff-private@36ee91ab8154de8f1bf5c99ecd121f78adcf7eff
Source: CodebuffAI/freebuff-private@6a8f6dec4683ca397f3e6b3a4cc4502ec6d1a709
Source: CodebuffAI/freebuff-private@1cd1d7c913acc0036c73eae46c733a874d2c16e6
Source: CodebuffAI/freebuff-private@f0ace74a1e76fd25a593cb8be1763076918edb0a
Source: CodebuffAI/freebuff-private@c6b14363aebdde92e722125f4a67094815962e4e
Source: CodebuffAI/freebuff-private@e2c89fa660e311bbf04e8d4215304f3717d75444
Source: CodebuffAI/freebuff-private@7865740bee9db5ad19358cfde5b69657dc23d93a
Source: CodebuffAI/freebuff-private@15d63fa71aa935991bffec728f2829dcf5e66514
Source: CodebuffAI/freebuff-private@c7fff22d8116c60d5f5f2e4b67b9b2732d941b1c
Source: CodebuffAI/freebuff-private@0121bc8926be2779e448dd9cd1ac2adf43402866
Source: CodebuffAI/freebuff-private@90b77e567506e3d506c157a1736eae8609ff07eb
Source: CodebuffAI/freebuff-private@ad4a927add796f3de33d06e69c877d813604192f
Source: CodebuffAI/freebuff-private@c86ed3de2607d872443c970f34d2665d25f06904
Source: CodebuffAI/freebuff-private@6100e67ec7952c8f1a23291a40032a4eebcc7085
Source: CodebuffAI/freebuff-private@88eab4c713bce9528c86315b03d48e23e35509d0
Source: CodebuffAI/freebuff-private@072e1253d03e770b05549dc3360f1c205090c22b
Source: CodebuffAI/freebuff-private@3ff9c8cad639f2611ba78220b0229f19bbcc92e1
Source: CodebuffAI/freebuff-private@df1b05adc1ac24a18ad779c2ad264923ed6331ed
Source: CodebuffAI/freebuff-private@886af9a3b902d4e8ee119f0870dd728f0f296e28
Source: CodebuffAI/freebuff-private@eb8dd7db9bb51d72728cdf36d4a4f1dea35e1633
Source: CodebuffAI/freebuff-private@05bcfb1c5002c6330b0211443d27b3da3a4e5f63
Source: CodebuffAI/freebuff-private@510fbcde62289fefa59ba69bf145d3894fef3a25
Source: CodebuffAI/freebuff-private@6ae5ea4dced5f97abe7c1b8e8da29e9c32546687
Source: CodebuffAI/freebuff-private@a360b968dd40291bc6ca99a3c216b81efee226d2
Source: CodebuffAI/freebuff-private@de457c329b234bf693e8c6fbf8fc1a77e5d632fc
codebuff public sync bot and others added 18 commits August 18, 2026 12:45
Source: CodebuffAI/freebuff-private@1a266d49e262bd59bb4dacb53dfbe7756008f381
Source: CodebuffAI/freebuff-private@b7acf4133e761c2f3cfc6e03a4e3d3dcb4d8518d
Source: CodebuffAI/freebuff-private@27d24194cacc5484ea3ca93302584c8b9cd23161
Source: CodebuffAI/freebuff-private@fc9fbf9b7c7dfb6caa89772c0a587663327f02cd
Co-authored-by: Owen <168240387+obro79@users.noreply.github.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Source: CodebuffAI/freebuff-private@2de38ff1e34b4fc3b3f4648cb4929a765636faba
Source: CodebuffAI/freebuff-private@b83949ed90043c17b7162794ed78fd05c329abcf
Source: CodebuffAI/freebuff-private@96ec987dc725bb92d1f4a843ed6f0ba6b2ba59b7
Source: CodebuffAI/freebuff-private@27707ed1435077accc80843917082a84246d2196
Source: CodebuffAI/freebuff-private@a4ae011cc542b68a22c8d5b418760b3a00a92f12
Source: CodebuffAI/freebuff-private@c07593091b09c25797036e321629a0a87460035a
Source: CodebuffAI/freebuff-private@6ae785db56a65a9693fb0eab58d6898dc8b951f8
Source: CodebuffAI/freebuff-private@0b4e303584d2819ade9c4b0ee28503c64d56d15c
Source: CodebuffAI/freebuff-private@0f1f33ab276f526c006e3d8470d79f43fe593d2c
Source: CodebuffAI/freebuff-private@9f2d977b7dbade9227abcde49fd6dab2b74cd128
Source: CodebuffAI/freebuff-private@861e5f893a5de981e2a7f47c644a586c589c66b7
Source: CodebuffAI/freebuff-private@fe36f824b3f8e48f973eedc009d038b799a05fbf
Source: CodebuffAI/freebuff-private@53019e92b0877513c60f2c82af4bb350669152a0
@codebuff-team

ghost commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Thanks for the detailed root-cause writeup — this is a well-documented PR and the instinct (sync previousRunStateRef before disk save resolves) is reasonable. A few things to strengthen before this is portable:

  1. No new test covers the actual bug. You cite passing existing suites (run-cancellation.test.ts, run-state-storage.test.ts, etc.), but none of those appear to exercise the specific regression: send a follow-up message immediately after abort/expiration and assert messageHistory is preserved rather than reset to []. Without a repro test, this fix could regress silently later, and a maintainer can't confirm the root cause is actually what you describe versus a coincidental fix.

  2. Duplicate state assignment logic. The same previousRunStateRef.current = latestRunStateSnapshot; setRunState(latestRunStateSnapshot) pair is now inlined in two places (registerActiveRun's abort handler and the catch block). Worth factoring into a small helper to avoid drift if one branch is updated later without the other.

  3. Prompt text changes (compact-history.ts, context-pruner.ts) are the riskier part. These affect model behavior broadly, not just the abort/expire path this PR is nominally about — bundling a system-prompt tweak with a state-sync bug fix makes it harder to isolate cause/effect if either regresses. These two concerns would be easier to review (and port) as separate PRs.

  4. Confirm setRunState doesn't trigger an unwanted re-render/flash given it's now called synchronously in the abort listener as well as the catch block — worth a sentence on why that's safe.

Overall the direction is right but this needs a regression test demonstrating the fixed behavior, and ideally splitting the prompt-text change into its own PR.

@codebuff-team codebuff-team added bot:triaged Classified by the community triage bot pr:needs-work Right idea, not mergeable as written labels Aug 20, 2026
@Utkarsh-X
Utkarsh-X force-pushed the fix/preserve-session-state-on-abort-compaction branch from 87d92b8 to f1418c3 Compare August 20, 2026 08:36
@Utkarsh-X

ghost commented Aug 20, 2026

Copy link
Copy Markdown
Author

Thanks for the thoughtful review and constructive feedback. I've addressed all four points in the latest commit (f1418c3):

1. Regression Tests (cli/src/hooks/helpers/__tests__/send-message.test.ts)

Added a dedicated test suite verifying the exact regression path:

  • User Abort (Esc): Simulates an active turn receiving user input, assistant thought, and a completed read_files tool call. Verifies that upon abort, previousRunStateRef.current is immediately populated in memory, so createRunConfig produces a runConfig.previousRun containing the full 3-item messageHistory rather than resetting to undefined/[].
  • Session Expiration / Error: Tests the catch (error) branch (e.g. session ended banner) to ensure sending "continue" preserves context from the expired session.
  • Cross-Chat Isolation: Tests that syncRunState is a no-op if the user switches chats mid-flight (runChatIsCurrent() === false), ensuring interrupted turns never bleed into unrelated conversations.

2. Factored Duplicate State Sync Logic (cli/src/hooks/use-send-message.ts)

Consolidated the state assignment into a single closure in sendMessage scope:

const syncRunState = (state: RunState) => {
  if (!runChatIsCurrent()) return
  previousRunStateRef.current = state
  setRunState(state)
}

@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.

@Utkarsh-X

ghost commented Sep 1, 2026

Copy link
Copy Markdown
Author

No worries at all, totally understand the history rewrite!

I've rebased cleanly onto the new main and opened #1181 with all the review feedback and regression tests addressed. We can continue the review there!

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:needs-work Right idea, not mergeable as written

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