Skip to content

fix: retain initial state for resumable streams - #1

Open
Kinfe123 wants to merge 2 commits into
mainfrom
fix/resume-initial-state
Open

fix: retain initial state for resumable streams#1
Kinfe123 wants to merge 2 commits into
mainfrom
fix/resume-initial-state

Conversation

@Kinfe123

@Kinfe123 Kinfe123 commented Aug 3, 2026

Copy link
Copy Markdown

Summary

  • retain the state snapshot and a generated run ID for each active thread run
  • expose the retained snapshot through POST /api/initial-state
  • reject /api/resume when its run ID no longer matches the retained snapshot
  • cover the new contract through the scaler integration suite

Why

While testing resume after a refresh or local history mutation, I found that the sync server replayed the correct delta bytes but did not retain the state those deltas were generated against. A resuming client could therefore apply the run onto a different local base and silently reconstruct incorrect conversation state.

This is the server half of assistant-ui/assistant-ui#5562. The companion client change is assistant-ui/assistant-ui#5564, which fetches this snapshot before attaching to the unchanged delta stream. The server change should merge and deploy first.

Compatibility

Existing clients can continue calling /api/resume without a runId. The replay stream format is unchanged. Clients using the new snapshot preflight include its runId, which makes a replaced-run race fail with 409 and X-Stream-Status: run_mismatch instead of applying incompatible deltas.

Verification

  • pnpm build
  • ran Redis, test backend, sync server, and scaler locally
  • all five end-to-end scenarios passed, including retained-state retrieval, matching resume, and mismatched-run rejection

Track in Rupic

@rupic-app rupic-app Bot added stage/draft Author still working; not ready for review pkg/scaler Changes a package under packages/ pkg/sync-server Changes a package under packages/ pkg/test-client Changes a package under packages/ R3 Draft; not ready for review type/bugfix Bug fix size/M 50–199 lines changed labels Aug 3, 2026
@Kinfe123
Kinfe123 marked this pull request as ready for review August 3, 2026 20:59
@rupic-app

rupic-app Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review · round 2 · comment · d39a38d

Merge as is — resumable state lifecycle is hardened; test-count documentation is stale

This PR should merge as written: it addresses a real missing resumable-stream invariant, commits the run identity and snapshot only after a usable backend body exists, clears replaced-run state on setup failure, and accounts for snapshot bytes during eviction. The new endpoint and run-ID guard are routed through both layers, and the added scenarios cover successful, failed, replacement, and missing-thread paths. README.md:141-147 still says the client runs five tests while packages/test-client/src/index.ts:384-393 invokes eight, but that documentation mismatch does not affect merge safety.

Worth noting, not blocking

  • README.md:141 — The README says the test client runs five integration tests and lists only the first five (README.md:141-147), but main() now invokes eight scenarios, including failed-start, failed-replacement, and missing-thread checks (packages/test-client/src/index.ts:384-393). The documentation should match the current suite.

Prior findings

Finding Status
runId and initialState were published before fetch confirmed a usable backend body Resolved — Commit d39a38d resets prior state before replacement and commits the new identity only after result.ok, result.body, and tee setup succeed (packages/sync-server/src/ThreadSync.ts:103-183).
retained state was absent from the memory-eviction estimate Resolved — Commit d39a38d tracks initialStateBytes in getBufferedBytesEstimate() and clears it during eviction (packages/sync-server/src/ThreadSync.ts:315-327).
the integration test lacked failed-start, failed-replacement, and missing-thread cases Resolved — The head adds those cases in packages/test-client/src/index.ts:288-381 and adds the failing backend endpoint in packages/test-server/src/index.ts:26-28.

Review threads

  • fixed: review-threads.md:1 by @@rupic-app: The current head clears the old body and retained state before replacement, publishes the new identity after a usable response body, and clears all retained fields on failed setup (packages/sync-server/src/ThreadSync.ts:103-180).
  • fixed: review-threads.md:4 by @@rupic-app: The current estimate adds initialStateBytes, and eviction clears the snapshot and its byte estimate (packages/sync-server/src/ThreadSync.ts:315-327).
  • fixed: @rupic-app · 2026-08-03T20:59:37Z by @@rupic-app: The issue-level review’s lifecycle, memory-accounting, and regression-test concerns are addressed by d39a38d in ThreadSync.ts:103-183, :315-327, and test-client/src/index.ts:288-381.

Merge state: The PR is open, non-draft, and reported mergeable with an unstable merge state. CI is queued. Two review threads are marked unresolved and outdated; human review-request state is unknown.; blockers: Two review threads from @rupic-app are marked unresolved and outdated in the current review state.

Evidence · 3 verified · 2 not verified

Verified

  • POST /api/initial-state exposes the retained snapshot
    Both routing layers contain the requested endpoint and preserve the upstream status and JSON body.
  • A mismatched run ID is rejected with 409 and X-Stream-Status: run_mismatch
    The response contract is directly implemented in the head.
  • Existing clients can continue calling /api/resume without a runId
    The compatibility path remains explicit in the route and ThreadSync API.

Not verified

  • The server retains the initial state and a generated run ID for each active thread run
    The lifecycle is statically present, but successful, failed, and replacement-run behavior requires runtime execution to settle completely.
  • All five end-to-end scenarios passed
    The reported runtime result is not established by the diff, and the current entrypoint has eight scenarios rather than five.

Read the full diff, context pack, all six changed files, surrounding routes, package scripts, base history, prior reviews, threads, issue comments, and PR status. No project tests or PR code were executed. CI is queued; three prior findings and three prior comments were re-verified against the current head.

Round Commit Verdict When
1 a5a1f2e comment 2026-08-03 21:06 UTC
2 d39a38d comment 2026-08-05 00:46 UTC

Comment @rupic-app stop reviewing to stop reviewing this PR.

Review by Rupic.

@rupic-app rupic-app Bot added stage/needs-review No reviews yet; waiting for a maintainer R1 Actionable, normal priority and removed stage/draft Author still working; not ready for review R3 Draft; not ready for review labels Aug 3, 2026

@rupic-app rupic-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 inline comments below, on packages/sync-server/src/ThreadSync.ts (2).

Full review: #1 (comment)

Comment thread packages/sync-server/src/ThreadSync.ts Outdated
): Promise<Response> {
this.generation++;
const currentGeneration = this.generation;
this.runId = randomUUID();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runId and initialState are published before fetch confirms a usable backend body. On a connection error or non-OK/no-body response, getInitialState at :193-199 still returns the attempted run; on a failed replacement, the old body is not cleared before the new attempt, so a matching resume can replay old bytes under the new snapshot identity. Please commit the new identity only after stream setup succeeds, or clear all replaced-run state on every failure path.

Comment thread packages/sync-server/src/ThreadSync.ts Outdated
this.generation++;
const currentGeneration = this.generation;
this.runId = randomUUID();
this.initialState = requestBody.state ?? null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new retained object is not represented in the memory-eviction estimate. With the existing 50 MB JSON limit, multiple active requests can retain large snapshots while getBufferedBytesEstimate returns zero when no stream bytes are buffered, so the eviction selector cannot relieve this new memory pressure. Please bound or account for snapshot memory in the same policy.

@rupic-app rupic-app Bot added stage/awaiting-reviewer Has reviews but not approved; reviewer follow-up needed and removed stage/needs-review No reviews yet; waiting for a maintainer labels Aug 3, 2026
@rupic-app rupic-app Bot added pkg/test-server Changes a package under packages/ size/L 200–599 lines changed and removed size/M 50–199 lines changed labels Aug 5, 2026

@rupic-app rupic-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 inline comment below, on README.md.

Full review: #1 (comment)

Comment thread README.md
```

This starts redis, 2 sync servers, a scaler, and a fake AI backend (test-server). The test client runs 4 integration tests:
This starts redis, 2 sync servers, a scaler, and a fake AI backend (test-server). The test client runs 5 integration tests:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README says the test client runs five integration tests and lists only the first five (README.md:141-147), but main() now invokes eight scenarios, including failed-start, failed-replacement, and missing-thread checks (packages/test-client/src/index.ts:384-393). The documentation should match the current suite.

@okisdev

okisdev commented Aug 5, 2026

Copy link
Copy Markdown

the client half merged as assistant-ui/assistant-ui#5564 with the contract settled on "the server is authoritative": a resume request now carries runId and no state, and the preflight expects 204 No Content when no run is active (the client then skips the resume silently). two alignment points for this PR before the feature works end to end: return 204 instead of 404 from the initial-state route when there is no retained run, and do not require a state field on /api/resume. runId validation as implemented here already matches.

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

Labels

pkg/scaler Changes a package under packages/ pkg/sync-server Changes a package under packages/ pkg/test-client Changes a package under packages/ pkg/test-server Changes a package under packages/ R1 Actionable, normal priority size/L 200–599 lines changed stage/awaiting-reviewer Has reviews but not approved; reviewer follow-up needed type/bugfix Bug fix

Development

Successfully merging this pull request may close these issues.

2 participants