docs/plans/2026-08-08-selective-session-restore.md
main containing #8933 merge commit 962dc8edocs/design/2026-08-08-selective-session-restore.mdThe delivery order is merged #8691, #8833, and #8882; exact-shape restore coalescing in #8933; this selective-restore implementation; and then the durable checkpoint. #8824 was superseded by this split series. #8883's legacy watchdog retry fix and the later PR3c/PR3d resync/repair and branch-adoption slices are not prerequisites for this bounded-hydration implementation.
Create a separate Draft branch from fresh main, confirm its history contains
#8882 and #8933, and rerun their transactional and request-shape regressions
before adding projection code. Do not add selective commits to #8743, #8882, or
#8933.
Implement selective restore as one end-to-end daemon fix. Reviewable commits may
follow the phases below, but do not merge an intermediate PR that only removes a
pre-lease load or moves historyPageSize: the post-lease read remains
authoritative until the selective projection replaces it, and early I/O bounding
is incomplete until every runtime consumer uses that projection. Do not merge an
unused projection API, change TUI/export/fork loading, or add checkpoint
persistence in this PR. Keep daemon live-task read/wait/startup lookup and
realtime startup-context full-content reads outside this slice as well: they do
not consume the ACP restore result and need a separate bounded-content contract
before migration.
The implementation is complete only when the cold ACP daemon restore path no
longer calls SessionService.loadSession(), constructs one fresh transcript
index in the correct startup-frozen writer mode, restores every named runtime
consumer, and returns the requested replay semantics.
This is a feature spanning core, CLI, ACP bridge, and daemon consumers. Before
implementation, report its production-logic line count and cross-package/core
ownership to maintainers and obtain an explicit scope review. Do not disguise a
large refactor as this feature: if the implementation becomes a 500+
production-line core refactor, the repository's maintainer-only gate applies.
SessionTranscriptReader index with separate runtime and
replay UUID chains plus the minimum projection hints named in the design.estimateIndexCacheBytes() for all newly retained index metadata,
including container, key, value, and base-object overhead. Add hint-heavy
cache-budget tests that exercise every new category and prove that an index
whose own estimate exceeds the entire cache budget may serve requests sharing
its in-flight build, but its completed value is not cached and its byte-budget
admission does not evict already-cached values. Retain existing pending
coalescing and entry-count or aggregate LRU behavior.goal_status candidates needed by the
existing reducers, including malformed candidates that affect precedence;
discard unrelated slash-command output.readActiveTranscriptChain() or the old loader.setImmediate and reset both. Do not add a setting or protocol field. Preserve
one-scan semantics and document that one large synchronous JSON parse remains
indivisible.newSessionConfig() and one final named loadCliConfig() host-options object;
do not add another positional parameter, and keep ordinary CLI callers
unchanged.Config.activateChatRecording() and
create the projection only after acquisition. Otherwise preload one fresh
frozen projection before Config construction so the default daemon path is
also fixed.ChatRecordingService from reduced recorder state. In leased mode,
skip constructor restore and initialize or replace Goal runtime after recorder
activation. In preloaded mode, construct the legacy active recorder and Goal
runtime directly from the ready projection.ResumedSessionData semantics.startNewSession(). Add memoized
prepareRestore(records, checkpointWindow?) and
activateRestoredWork(): preparation restores state and performs legacy
migration without starting autonomous work; activation latches idempotently,
waits for preparation, and then starts pending checkpoint/continuation work.
Daemon Session creation does not await preparation merely for migration, while
getGoalRuntimeReady() waits for both phases. Retain restore() as the
non-daemon wrapper that awaits both, and make disposal prevent unfinished
preparation or activation from committing runtime state or broadcasting.
Reject activation before preparation has started, and make disposal settle
any readiness waiter that would otherwise remain blocked only on activation.GeminiClient.initialize() in createAndStoreSession(), then add one narrow
synchronous preparation slot after Gemini initialization, the second managed
admission check, and the active-id conflict check but before new Session(...)
and sessions.set(). Build modes, models, config options, artifact/replay
metadata, and the complete ACP success value in that slot so active-runtime
model selection matches current behavior and a builder failure leaves no
Session. Only then synchronously restore file history exactly once in the
existing creation sequence; do not defer it until /rewind or the first file
operation. Start its best-effort missing-backup validation once only from
successful restore finalization, because that validation may append a
transcript record. When file checkpointing is disabled, neither hydrate nor
validate the reduced snapshots and release the unused projection field.
Restore turn parents, initial turn, background notification ids, goal
runtime/hooks, and artifact state from their explicit projection fields; feed
the normalized minimal Goal records through the existing recovery and
legacy-card helpers. With no projection, construct an empty requested runtime
whose recorder parent is null; a non-empty system/metadata-only chain keeps
its real final record UUID.historyPageSize clients.resumeSession.anchorRecordId?: string, validate/strip it in the bridge, and use it only as
the last fallback for the existing public history anchor.all, recent(limit), or none replay plus
action, response/stream mode, and inherited-history policy; only identical
shapes coalesce, while omitted versus explicit pages and unequal limits return
restore_in_progress.load/all, load/recent(limit), or
resume/none participates in its normalized key, and a non-identical shape
permanently fences the obsolete raw result while retaining same-shape timeout
retry within the same lifecycle. Explicit lifecycle cancellation also fences
an old raw result when a later intent returns to the same shape. Selective
implementation must not add another coordinator.historyPageSize uses the REST/ACP integer
range; streamed load and resume ignore the unused field for warm and cold
Sessions. The bridge request type correctly documents omitted historyReplay
as streamed load. Selective code adds the projection-mode mapping and replay
limits behind this established normalized shape.errorKind: transcript_page_too_large, which REST maps to
413 transcript_page_too_large; preserve the typed limit error past the
collector's ordinary partial/replayError downgrade and do not add
transformed-update trimming.historyPageSize, but recovery requires
the resulting aligned selection to fit. A single source record or minimum
aligned group that remains oversized keeps the typed failure. Omitted
historyPageSize retains its legacy compatibility semantics.startingSessionIds/reserveStartingSessionId() reservation;
do not create a parallel preparation set. Hold the existing reservation from
before settings/existence I/O through the existing Session creation attempt or
failure. Keep the current handler finally release and conflict checks; do not
add reservation-to-map conversion, a provisional unregistered Session, or a
second publication protocol.createAndStoreSession()'s current early map insertion, reporter
notification, fallible replay/worktree/Goal/rewriter setup, and
discardStoredSessionIfCurrent()/removeStoredSessionEntry() rollback. New
projection and envelope failures happen before the call; response-builder
failures happen in its post-Gemini/pre-construction slot. Both leave no map
entry. Failures at the existing guarded setup points use their current
stored-session cleanup. Do not add map-independent teardown, gate every Session
constructor callback, or claim to repair unrelated pre-existing cleanup edges.session.installRewriter() and before session.startCronScheduler() and the
available-command timer. It is called exactly once, is synchronous, and does
not throw, with independent error boundaries around best-effort attribution
application, scheduling GoalRuntime.activateRestoredWork(), and starting
idempotent FileHistory missing-backup validation. Attach rejection handlers
immediately to both async actions and independently contain synchronous
invocation failures, so one action cannot skip another or produce an
unhandled rejection. Do not await async completion or change
existing background/worktree, callback, reporter, cron, command, publication,
or rollback timing. Keep every fallible/awaited setup step before this
finalizer; the existing cron start and command timer remain internally
best-effort after it.transcript_too_large, and recent envelope
overflow becomes ACP -32012/REST 413 transcript_page_too_large. Preserve
typed data for coalesced waiters and do not expand the public success schema.errorKind: transcript_too_large, map to REST 413 transcript_too_large,
never call the old loader, and do not affect a sibling session.qwen.session.loadReplay value at or below
32 MiB and at the first byte above it. Cover one individually oversized source
record and collectively oversized individually valid updates, including
object, array, comma, bootstrap, synthetic, and finalization overhead.getGoalRuntimeReady() waits for
both, disposal suppresses unfinished state/broadcast/work, and non-daemon
restore() retains its current awaited semantics. Also verify activation
before preparation starts rejects and disposal does not leave readiness
pending while it waits for finalization that will never occur.qwen CLI and record an E2E
plan/result under .qwen/e2e-tests/.packages/core.main containing the
final #8882 and #8933 code, review the selective-only diff, and run their
integration coverage with selective-restore 409, 413, timeout/504,
cancellation, and staging failures on the modern client_identity path.
Assert the committed session-id and workspace-cwd source tuple remains
attached and usable, and successful adoption changes transcript, connection,
metadata, and ownership atomically. Preserve #8882's legacy detach-first
behavior when that capability is explicitly absent.npm run build && npm run typecheck from the repository root.SessionService.loadSession()./review workflow when available; do not invoke Qwen Review
unless explicitly requested.962dc8e; fresh main contains both
#8882 and #8933.main.Config construction otherwise; no projection path performs a
second scan through paging/cache helpers.session/load/session/resume path
calls the old full loader, including under a small-transcript threshold;
benchmark-only comparisons are the only exception.partial replay response.historyPageSize still returns full visible replay.recoverGoalFromRecords(): newer malformed v2
records do not hide an earlier valid v2, but unsupported-only v2 history
blocks legacy fallback.loadCliConfig() callers cannot accidentally populate the projection.startNewSession() release all pending
projection payloads; Config does not become a second lifetime history
cache.createAndStoreSession() or in its
post-Gemini/pre-construction response slot leave no map entry; failures at
its currently guarded setup points use the existing stored-session rollback
and leave no stale Session/Goal hook, observer, Config, or map entry.restore() preserves existing awaited behavior.client_identity path,
selective-restore 409, 413, timeout/504, cancellation, and staging failures
preserve the committed session-id and workspace-cwd source tuple, while a
successful switch commits transcript, connection, metadata, and ownership
atomically. Explicitly unsupported-capability fallback retains legacy
detach-first behavior.historyReplay as streamed load.historyPageSize
compatibility remains unchanged.