Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/design/2026-08-11-transactional-same-session-refresh.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Restore identity includes the normalized session and workspace, the effective re

A same-session request waits for the source runner to be ready and free of local, restored, or observed work before it starts. This wait does not consume the restore budget. The budget starts with the raw RPC, and signal, lifecycle, navigation, resync, or environment changes can still cancel the intent. Resync remains authoritative and continues through its existing destructive recovery path for this change.

Source-bound branch, create, attach, and legacy restore operations exclude ordinary restores. The exclusion follows the raw operation rather than an outer action timeout: a timed-out create keeps restores blocked until its raw request settles, and a late successful create is detached once. A blocked controlled target is still routed through the coordinator so it publishes a terminal failed transition and does not leave the host waiting indefinitely.
Source-bound branch, create, attach, and legacy restore operations exclude ordinary restores. The exclusion follows the raw operation rather than an outer action timeout: a timed-out create keeps restores blocked until its raw request settles, and a late successful create is detached once. A controlled target discovered during a source-bound operation remains pending and is retried once the final source-bound operation settles, so a transient interlock cannot permanently drop the host's desired target.

## Cursor capture and integrity

Expand All @@ -40,6 +40,12 @@ Raw `clientId` props are desired input rather than committed owner state. A mode

The commit CAS includes the source object's current client ID. If SDK prompt-admission self-heal updates that ID in place, the prepared candidate is discarded and the healed source remains active. Failures publish one recoverable transition failure while leaving source connection, transcript, prompt, metadata, and controls usable; they never rewrite the source as missing or disconnected.

The committed client ID is also the recovery identity. Once a modern rebind commits, later renders cannot restore the initial prop into the committed client ref; subsequent ring or epoch recovery therefore requests the attachment that actually owns the current runner. Legacy daemons still mirror the prop because they do not support transactional client ownership.

All terminal intent paths retire a prepared candidate and release its source-tail capture. A raw restore timeout may retain the capture only until that raw request settles, allowing an exact-shape retry to adopt its result without leaving event capture enabled after the intent has otherwise failed.

Bounded load responses carry the same event epoch as full load responses. The bridge snapshots the replay watermark and epoch together and returns them only if both remain unchanged through the persisted-page read, preserving the provider's same-epoch commit check.

## Verification and risks

Unit coverage checks delayed success and failure, local and observer prompt gating, response completeness, partial or degraded replay, epoch and tail gaps, cursor catch-up, client-ID rebind, in-place self-heal, late cleanup, and cross-session arbitration. SDK tests cover epoch and replay-integrity propagation. A real-daemon JSDOM test withholds an already-completed same-session load response, sends live source work during the hold, and verifies atomic replay-plus-tail commit without loss or duplication; structured timeout and client-ID rebind paths verify source preservation and transcript continuity.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ describe('qwen serve WebUI transactional same-session refresh', () => {
token: activeDaemon!.token,
sessionId: source.sessionId,
workspaceCwd: resolvedWorkspace,
historyPageSize: 100,
...(clientId ? { clientId } : {}),
},
createElement(Harness),
Expand Down
1 change: 1 addition & 0 deletions packages/acp-bridge/src/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4364,6 +4364,7 @@ describe('createAcpSessionBridge', () => {
attached: true,
historyHasMore: true,
lastEventId: loaded.lastEventId,
eventEpoch: loaded.eventEpoch,
compactedReplay: [
{
type: 'session_update',
Expand Down
3 changes: 3 additions & 0 deletions packages/acp-bridge/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5399,6 +5399,7 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
for (let attempt = 0; attempt < 2; attempt++) {
try {
const lastEventId = entry.events.lastEventId;
const eventEpoch = entry.events.epoch;
const seenCursors = new Set<string>();
let emptyPageCount = 0;
let cursor: string | undefined;
Expand Down Expand Up @@ -5436,12 +5437,14 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
if (
byId.get(entry.sessionId) === entry &&
!entry.promptActive &&
entry.events.epoch === eventEpoch &&
entry.events.lastEventId === lastEventId
) {
return {
compactedReplay: page.events,
liveJournal: [],
lastEventId,
eventEpoch,
...(page.partial === true ? { partial: true as const } : {}),
...(page.replayError !== undefined
? { replayError: page.replayError }
Expand Down
Loading
Loading