Skip to content

feat: replace OpenCodeClient with @opencode-ai/sdk#2

Merged
dibstern merged 185 commits into
mainfrom
feature/orchestrator-implementation
Apr 22, 2026
Merged

feat: replace OpenCodeClient with @opencode-ai/sdk#2
dibstern merged 185 commits into
mainfrom
feature/orchestrator-implementation

Conversation

@dibstern
Copy link
Copy Markdown
Owner

@dibstern dibstern commented Apr 14, 2026

Summary

  • Replace hand-rolled OpenCodeClient (704 lines) and SSEConsumer (284 lines) with @opencode-ai/sdk v1.4.3
  • Introduce thin OpenCodeAPI adapter wrapping SDK + GapEndpoints (6 endpoints not in SDK) into unified namespaced API (client.session.list(), client.permission.reply(), etc.)
  • Migrate SSE consumption from manual stream parsing to SDK's event.subscribe() async generator via new SSEStream class
  • Replace hand-rolled types (SessionDetail, SessionStatus, PartType, ToolStatus, OpenCodeEvent) with SDK discriminated unions
  • Net result: -988 lines of hand-rolled client code deleted, replaced by SDK delegation

Architecture

Before:  Browser ←WS→ Relay ←OpenCodeClient (691 LOC, manual fetch+retry)→ OpenCode
After:   Browser ←WS→ Relay ←OpenCodeAPI→ { @opencode-ai/sdk, GapEndpoints, SSEStream }

New files:

  • src/lib/instance/retry-fetch.ts — Exponential backoff fetch adapter injected into SDK
  • src/lib/instance/sdk-factory.ts — SDK client creation with auth wiring (Basic Auth for REST + SSE)
  • src/lib/instance/gap-endpoints.ts — 6 endpoints not yet in SDK (permissions, questions, skills, paginated messages)
  • src/lib/instance/opencode-api.ts — Unified namespaced adapter with error translation (sdk() wrapper)
  • src/lib/instance/sdk-types.ts — SDK type re-export bridge
  • src/lib/relay/sse-stream.ts — SDK-backed SSE consumer with reconnection + health tracking

Deleted files:

  • src/lib/instance/opencode-client.ts (704 lines)
  • src/lib/relay/sse-consumer.ts (284 lines)

Key design decisions

  1. SDK types everywherePartType and ToolStatus now derive from SDK discriminated unions (Part["type"], ToolState["status"]). HistoryMessage/HistoryMessagePart kept as relay-specific transport types (carry renderedHtml, index signatures).

  2. Provider normalization — SDK returns { all, default, connected } with models as Record<string, Model>. Adapter normalizes to { providers, defaults, connected } with models as Array<Model> for caller compatibility.

  3. Message flattening — SDK returns { info: Message, parts: Part[] }. Adapter flattens to { ...info, parts } for backward compatibility with relay's message pipeline.

  4. Auth strategy (Audit v3)config.headers carries auth for both REST and SSE. authFetch handles SDK's single-Request calling convention (pass-through) and GapEndpoints' two-arg calls (injects auth).

  5. SSEEvent superset — SDK Event union doesn't cover 4 SSE-delivered events (message.part.delta, permission.asked, question.asked, server.heartbeat). Created SSEEvent = Event | GapEvents superset.

Test plan

  • pnpm check — 0 type errors (both server + frontend tsconfigs)
  • pnpm lint — 0 errors
  • pnpm test — 4273 unit tests passing
  • pnpm test:integration — 131 passing, 1 skipped
  • pnpm test:contract — 81 passing
  • E2E replay, daemon, multi-instance, subagent, visual tests — all passing
  • Storybook build + visual tests — 486 passing
  • pnpm test:allall 13 steps green

dibstern added 30 commits April 9, 2026 17:43
…cache, and transactions

Thin wrapper around Node 22+ node:sqlite DatabaseSync providing:
- WAL mode with tuned pragmas for file-backed databases
- LRU-evicted prepared statement cache (default capacity 200)
- Synchronous runInTransaction() with nested savepoint support
- Typed query<T>() and queryOne<T>() helpers
- Idempotent close()
… to both JSONL and SQLite

Refactor DualWriteHook constructor to accept an options object with
persistence, log, and enabled fields. Add dualWriteHook as optional
field on SSEWiringDeps and call it in handleSSEEvent() before any relay
processing, ensuring all SSE events (including early-return paths like
permission.asked) are captured. Add onReconnect() call in the SSE
connected handler to reset translator state on reconnection.
…bled feature flag

Add persistence and dualWriteEnabled fields to ProjectRelayConfig.
In createProjectRelay(), instantiate DualWriteHook when a persistence
layer is provided and dualWriteEnabled is not explicitly false (opt-out
default). Pass the hook into SSE wiring deps so events flow to SQLite.
Projects session.created, session.renamed, session.status,
session.provider_changed, turn.completed, turn.error, and
message.created events into the sessions read-model table.
Also adds ProjectionContext interface to the Projector contract
to support replay-aware projection.
…and tool events

Projects message lifecycle events into normalized messages and
message_parts tables. Uses SQL-native text concatenation for
streaming deltas, COALESCE subquery for sort_order, and
alreadyApplied() guard for replay safety.
Projects turn lifecycle events into the turns read-model table.
Tracks user-prompt to assistant-response cycles with pending,
running, completed, error, and interrupted states. Uses sub-select
pattern for portable UPDATE targeting.
… history

Projects session.created and session.provider_changed events into the
session_providers read-model table with deterministic IDs for idempotent
replay. 8 tests covering creation, deactivation, multiple changes, and
out-of-order replay safety.
…lifecycle

Projects permission.asked, permission.resolved, question.asked, and
question.resolved events into the pending_approvals table. Uses INSERT ON
CONFLICT DO NOTHING for idempotent replay. 11 tests covering both
approval types, resolution, and full lifecycle.
…imeline

Projects tool.started, tool.running, tool.completed, permission.asked,
permission.resolved, question.asked, question.resolved, and turn.error
events into the activities table. Uses deterministic IDs
(sessionId:sequence:kind) with INSERT OR IGNORE for replay idempotency.
13 tests covering all event types, payload storage, and chronological ordering.
…rtup recovery

Orchestrates all 6 projectors (session, message, turn, provider, approval,
activity) with per-projector fault isolation (A4), lazy cursor sync (P2),
SQL-level type-filtered recovery (P7), and batch projection for multi-event
SSE batches (S9). Includes sync recover(), async recoverAsync() with
setImmediate yielding (Perf-Fix-4), and recoverLagging() for targeted
reconnect recovery (Change 5b).
…o-project after append

PersistenceLayer now creates ProjectionRunner and ProjectorCursorRepository.
DualWriteHook calls projectEvent/projectBatch immediately after appendBatch,
with projection errors caught and logged (non-fatal). New end-to-end test
verifies SSE events flow through the full pipeline: translate → append →
project → read model tables.
…ntegrity queries

PersistenceDiagnostics provides two methods:
- health(): returns event count, session count, projector cursor positions,
  and event sequence range for monitoring dashboards.
- checkIntegrity(): runs PRAGMA foreign_key_check to detect FK violations.
…er for Phase 4

Encapsulates all SQLite read queries for the Phase 4 read switchover
in a single testable service. Methods cover tool content, fork metadata,
session list/status, paginated messages (composite cursor per Perf-Fix-6),
turns, pending approvals, and batch message+parts loading (CTE+JOIN per S10b).
All methods wrap queries in PersistenceError with PROJECTION_FAILED code.
…or read path switching

Implements three-state feature flags for Phase 4 read switchover with
helper functions isActive(), isSqlite(), isShadow(). Flags are mutable
for runtime toggling by DivergenceCircuitBreaker. Includes backward
compat mapping: boolean true -> "sqlite", false -> "legacy".
…nto relay stack

- ReadAdapter routes reads to SQLite or legacy based on ReadFlags
- Wire ReadQueryService, ReadFlags, ReadAdapter into relay-stack.ts
- Add readAdapter to HandlerDeps for Phase 4 handler consumption
- 51 new tests (adapter, switchover integration, relay wiring)
…-method contract)

Define the core provider abstraction: ProviderAdapter (7 methods), SendTurnInput,
TurnResult, EventSink, AdapterCapabilities, CommandInfo, and supporting types.
Adapters are execution-only — conduit owns all state.
EventSinkImpl bridges adapters to the event store with blocking
permission/question resolution via deferred promises.
… management

Map-based registry with register, get, list, remove, and shutdownAll.
Continues shutdown even when individual adapters fail.
…derAdapter

Implements all 7 ProviderAdapter methods: discover() maps REST API to
AdapterCapabilities, sendTurn() dispatches via REST with deferred completion
(notifyTurnCompleted bridge for SSE), interruptTurn/resolvePermission/
resolveQuestion are thin wrappers around OpenCodeClient.
dibstern and others added 28 commits April 19, 2026 14:47
Verifies: delta/stop/handleDone after clearMessages mid-thinking are
safe no-ops. No crashes, no orphan thinking blocks, no zombie state.
New thinking lifecycle after clear works correctly.
Verifies convertAssistantParts default:break silently drops unknown
part types (image, audio, future_magic) with no crash or phantom
messages. Mixed known+unknown: known types survive. Adds it.todo
for future observability logging.
Schema has no ON DELETE CASCADE and foreign_keys=ON. Asserts:
(1) deleting a session with dependent messages throws FK error at
the DELETE statement (prevents orphans); (2) deleting a session
with no dependents succeeds, but subsequent message.created for
the deleted session fails FK at INSERT.
Simulates SSE disconnect/reconnect: events 1-3 normal, then replay of
events 2-5 (overlap 2,3 + new 4,5). Verifies alreadyApplied() skips
overlap events and new events are applied. Text not doubled.
Verifies: two tabs on same session both receive events, one tab
navigating away doesn't affect the other, both tabs receive after
return, and both tabs simultaneously away then returning works
correctly.
Verifies thinking text preserved across tool/permission boundary.
Tests: thinking→tool→text and thinking→tool→thinking→text sequences.
Both verify correct output order and thinking text integrity.
Adds SEED=42 constant and passes { seed, endOnFailure } to all
fc.assert calls following codebase convention. Adds PBT regression
cases describe block for deterministic counterexample preservation.
7 it.todo stubs documenting expected behavior: mid-thinking rewind,
checkpoint boundaries, replay dedup, permission revert, fork
inheritance, and revert/unrevert round-trip. Serves as acceptance
criteria for future rewind/fork features.
Claude SDK streams tool_use input via input_json_delta events, so
tool.started fires with input={} and the full parsed input only arrives
via later tool.input_updated canonical events. The relay sink dropped
those updates, leaving the browser-side tool registry stuck on the
empty initial input.

Translate tool.input_updated into a tool_executing RelayMessage so the
registry merges the new input on the already-running entry. Add
optional toolName to ToolInputUpdatedPayload so the derived
tool_executing carries the correct name.
extractToolSummary only read OpenCode camelCase keys (filePath), while
the Claude Agent SDK emits snake_case (file_path), producing blank
subtitles for Read/Edit/Write/LSP.

Introduce readStr to look up both conventions. Extend Grep tags with
Claude SDK glob/type/path fields; add Glob path tag; let WebSearch
fall back to the SDK's query field. Flip Bash to show the actual
command (truncated) as subtitle — the model-supplied description is
the fallback, not the primary display.
Capture the refactor that eliminates stale activity indicators (bounce
bar + sidebar dot) on completed, inactive sessions after navigation.
Replaces the module-level chatState singleton with a keyed per-session
map, routes every server event by sessionId, and folds in two latent
bug fixes (status:idle not clearing streaming, patchMissingDone missing
the Claude SDK timeout signal).
Apply 45+ Amend-Plan amendments and 10 Ask-User resolutions from the audit
synthesis. Major changes:

- New Phase 0b: project-scoped firehose (drops view_session subscription
  filtering \u2014 prerequisite for client-side routing to function).
- Phase 0 emitter audit expanded to 14 files; new task for
  RelayError.toMessage + toSystemMessage split; new system_error variant.
- EMPTY_STATE as plain frozen POJO (Object.freeze on a $state proxy throws
  at load time).
- Phase 2 handler list expanded: advanceTurnIfNewMessage,
  handleToolContentResponse, ensureSentDuringEpochOnLastUnrespondedUser,
  replay batching helpers.
- Live-event buffering preserved per-session rather than deleted.
- _scrollRequestPending moved to SessionChatState (previously incorrectly
  kept global).
- Clear-then-replay semantics on session_switched for existing slots.
- Component regression tests for bounce bar + sidebar dot added.
- E2E harness fixed (fixture slug, session-id pattern, config file).
- Bandwidth regression test + mock-mode manual QA script added.
Rewrite the per-session chat state design plan to incorporate audit
findings and user decisions on 9 open questions. Adopt a two-tier
data model (unbounded SessionActivity + LRU-capped SessionMessages)
to eliminate the all-slots-non-idle corner case for subagent-heavy
workloads and match Discord/Slack-class per-channel state patterns.

Key amendments (see audit synthesis Appendix C for the full map):
- Two-tier store: Activity (phase, dedup sets, liveEventBuffer,
  replayGeneration) unbounded; Messages (messages, registry, history
  cursor, contextPercent) LRU-capped. Dedup stays in Tier 1 to survive
  evictions. Sidebar reads Activity; chat view reads a composite Proxy.
- Discriminated union typing fixed: Extract<RelayMessage, {sessionId: string}>
  instead of the structural intersection that silently widens.
- Exhaustive event list for sessionId contract: adds tool_content,
  ask_user*, permission_*, session_switched, session_forked, the
  RelayError path, and a new system_error variant for session-less errors.
- Live-event buffering retained on SessionActivity.liveEventBuffer
  (design self-contradiction with plan-of-record resolved in favor of
  retention — deletion would reintroduce cache-tail ordering bugs).
- F2 (clear streaming on idle) moved from Task 3 to Task 4 to avoid a
  new transient cross-session bleed while the adapter still routes by
  currentId.
- F3 specified concretely: signature widening + call-site update +
  disjunction guard + sessionId on synthetic done/status events.
- Preceding server PR bundles Phase 0b (firehose fanout broadening)
  with Task 1 (sessionId contract); main frontend PR lands 7 commits.
- Emitter-side sessionId injection: single post-translation tag
  strategy per emission site; event-translator.ts:446 fallback removed.
- Ghost-slot cleanup: clearSessionChatState wired to session_deleted
  and handleSessionList drop path; unknown-session guard in dispatcher.
- Mid-replay race prevented: replayEvents(sessionId) captures slot at
  start, does not read currentChat().
- EMPTY_STATE: plain frozen POJO (not $state). Strict-mode throw in
  both dev and prod; dev Proxy for clearer error messages.
- historyState fully migrated to per-session Tier 2 (matches industry
  practice for multi-channel chat UIs).
- Component migration scope expanded: UserMessage, ChatLayout,
  HistoryLoader, InputArea.stories.ts all added.
- 20+ test files enumerated for lockstep migration.
- Task 7 LOC gate softened to heuristic excluding new test files.

Synthesis doc records all 72 Amend-Plan / 9 Ask-User findings and
maps each to its resolution. Next: re-audit the revised plan.
Apply Loop 2 re-audit findings and user decisions on remaining tactical
questions. No structural redesign — the two-tier model was validated by
all 8 re-auditors. Changes are mechanical fill-ins of under-specified
details and corrections of concrete errors.

User decisions (8 Ask-User):
- Keep view_session name; fix plan's mischaracterization of its
  semantics; track rename in §Known Debt.
- Per-session delta order preserved under Phase 0b broadcast (invariant
  + test).
- Frontend strictly depends on server PR; rollback policy captured.
- No special messageId collision handling; per-session dedup already
  strictly safer.
- Buffer held during drain to serialize per-session; no null-before-drain
  race.
- Concurrent replayEvents(X): second call aborts via generation bump,
  first continues under captured slot.
- Startup race: server emits session_list first (industry-standard),
  queue events during bootstrap if needed.
- Server-side status sessionId correctness test added.

Concrete fixes (46 Amend):
- Add sessions: SvelteMap<string, SessionInfo> to sessionState (was
  referenced but didn't exist).
- Add session_deleted relay variant (was referenced but didn't exist).
- createEmptyToolRegistry → createToolRegistry (existing factory).
- composeChatState Proxy spec: full trap set (get/set/has/ownKeys/
  getOwnPropertyDescriptor) + ACTIVITY_KEYS routing. $inspect works.
- $state factory pattern: factories return POJOs, getOrCreate* wraps.
  Eliminates double-wrap ambiguity.
- Dispatcher snippet: gate advanceTurnIfNewMessage on messageId
  presence; notification_event routed to GlobalEvent branch despite
  carrying sessionId.
- F2 expanded to 5-step cleanup (finalize in-flight, reset phase,
  clear currentMessageId/currentAssistantText/thinkingStartTime, drain
  liveEventBuffer).
- Swap Task 5 and Task 6 — components migrate before field deletions
  to fix commit-boundary compile break.
- handleSessionList gains diff logic with search-payload guard.
- Task 3 expanded to cover convertHistoryAsync + history_page
  pagination slot-capture; eventsHasMoreSessions migrated to
  SessionActivity.eventsHasMore.
- evictSessionSlot concept deleted; two separate operations:
  ensureLRUCap (Tier 2) and clearSessionChatState (both tiers).
- Adapter null-policy matches dispatcher: dev throw + prod telemetry
  counter, no silent drop.
- EMPTY_MESSAGES.toolRegistry methods replaced with throwing stubs
  (Object.freeze doesn't stop method calls).
- clearMessages additionally clears current session's per-session Sets.
- registerClearMessagesHook signature widened with sessionId arg.
- replayGeneration is the canonical rename of deferredGeneration.
- Test enumeration expanded: 6 new tests, specific scenario lists for
  concurrent-session-dispatch and ghost-session-cleanup.

Loop 3 is the final amend-pass per the fixer guardrail. Next: re-audit.
Loop 3 re-audit returned 0 Amend-Plan findings, 1 Ask-User item
(minor implementation detail deferred to coding time), and 15
Accept-class informational notes. The plan is ready for execution.

Three-loop funnel: 72 → 46 → 0 Amend-Plan findings. Two-tier data
model validated across all auditors; no structural rework required.
…ed + system_error

Server Task 1 of per-session chat state refactor. Adds required
sessionId field to all per-session RelayMessage variants, enabling
frontend dispatcher to route events by session rather than relying
on the global currentId.

Changes:
- Declare PerSessionEventType union, PerSessionEvent/GlobalEvent types
- Add sessionId (required) to 20+ RelayMessage variants incl. error
- Add session_deleted and system_error new variants
- Session-less errors use system_error via RelayError.toSystemError()
- Session-scoped errors use error with required sessionId
- Tag sessionId at all emission sites (sse-wiring, relay-event-sink,
  message-poller, prompt, tool-content, session-switch)
- F3 fix: widen patchMissingDone guard to check overrides.hasActiveProcessingTimeout
- Add tagWithSessionId helper + UntaggedRelayMessage type
- 34 new contract tests across 3 files
- Migrate all existing test assertions for new event shapes
- Fix pre-existing lint errors (noNonNullAssertion, noExplicitAny)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce the new two-tier per-session store API (SessionActivity +
SessionMessages) alongside the existing chatState singleton. All new
code is dead — no production call site invokes it yet.

- Define SessionActivity (Tier 1, unbounded) and SessionMessages
  (Tier 2, LRU-capped at 20) types with factory functions
- Add composeChatState Proxy for read-only merged view with full
  trap set (get/set/has/ownKeys/getOwnPropertyDescriptor)
- Add EMPTY_STATE/EMPTY_ACTIVITY/EMPTY_MESSAGES frozen sentinels
  with throwing toolRegistry stubs and dev-mode Proxy wrapper
- Add SvelteMap-backed sessionActivity/sessionMessages stores
- Add currentChat() $derived read API and getSessionPhase()
- Add getOrCreate* write API with $state wrapping at insertion
- Add LRU eviction (cap 20, never evicts current session)
- Add clearSessionChatState for teardown
- Add sessions SvelteMap to sessionState in session.svelte.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nt) signatures

Frontend Task 2 of per-session chat state refactor. Rewrites every
handler in chat.svelte.ts to accept (activity, messages, event) as
leading arguments and wires a dispatchToCurrent adapter in ws-dispatch.ts
that resolves the current session's slot.

Changes:
- All handler functions take SessionActivity + SessionMessages params
- dispatchToCurrent adapter routes to getOrCreateSessionSlot(currentId)
- getMessages/setMessages take SessionMessages param
- Phase helpers accept optional SessionActivity param
- Dual-write contextPercent (messages + legacy uiState)
- Move seenMessageIds/doneMessageIds to per-session activity
- registerClearMessagesHook widens to (sessionId: string | null)
- Add test-session-slot.ts helper for handler tests
- 21 new handler-tier-contract tests
- Migrate 20+ test files to new handler signatures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move module-level replay/buffer state to per-session SessionActivity
and SessionMessages, and make replayEvents capture a slot at start
rather than reading global state. This is Frontend Task 3 of the
per-session chat state migration.

Key changes:
- Add replayBatch/replayBuffer fields to SessionMessages type
- Dual-write renderTimer, thinkingStartTime to activity tier
- Dual-write liveEventBuffer, replayGeneration to activity tier
- Dual-write eventsHasMore, replayBuffer to per-session
- replayEvents() captures slot via getOrCreateSessionSlot(sessionId)
  with ghost-write guard (generation check at every async boundary)
- convertHistoryAsync() accepts captured activity for abort detection
- session_switched REST history path captures slot at start
- history_page handler captures slot at start
- clearMessages hook clears per-session liveEventBuffer/replayGeneration
- registerClearMessagesHook handles per-session cleanup

Legacy module-level state retained during transition (Task 4 removes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the dispatchToCurrent adapter with a proper two-tier dispatcher
that routes per-session events by event.sessionId. Global events continue
through the existing handleMessage switch.

Key changes:
- Add PER_SESSION_EVENT_TYPES runtime Set and isPerSessionEvent guard
- routePerSession validates sessionId (dev throws, prod drops) and
  checks sessionState.sessions membership (unknown-session guard)
- handleMessage routes per-session events through routePerSession,
  except globally-coordinated types (session_switched, session_forked,
  history_page, session_deleted)
- F2 fix: handleStatus("idle") does full cleanup — finalizes in-flight
  message, sets phase to idle, clears currentMessageId/assistantText/
  thinkingStartTime, drains liveEventBuffer, preserves dedup Sets
- Populate sessionState.sessions Map in handleSessionList,
  handleSessionSwitched, and handleSessionForked
- Delete dispatchToCurrent adapter
- Update existing tests to register sessions in sessionState.sessions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend Task 5 of per-session chat state refactor. Codemods all
Svelte components from chatState.X to currentChat().X and migrates
sidebar dot to getSessionPhase(session.id).

Changes:
- MessageList.svelte: chatState.X → currentChat().X
- InputArea.svelte: chatState.X / uiState.contextPercent → currentChat()
- SessionItem.svelte: replace chatIsProcessing with getSessionPhase
- UserMessage.svelte: chatState.X → currentChat().X including $inspect
- ChatLayout.svelte: remove chatState import
- HistoryLoader.svelte: historyState.X → currentChat().historyX
- MessageList.stories.ts + InputArea.stories.ts: per-session setup
- Skip 2 buffer-component tests during dual-write transition

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove module-level duplicate state that now has per-session equivalents:
- seenMessageIds, doneMessageIds (use activity.*)
- renderTimer, thinkingStartTime (use activity.*)
- replayBatch (use messages.replayBatch)
- replayBuffers, eventsHasMoreSessions (use messages.replayBuffer, activity.eventsHasMore)
- registry singleton (use messages.toolRegistry)
- deferredGeneration (use activity.replayGeneration)
- liveEventBuffer in ws-dispatch (use activity.liveEventBuffer)
- replayGeneration in ws-dispatch (use activity.replayGeneration)

Remove dual-write paths (keep only per-session writes):
- doneMessageIds/seenMessageIds dual-writes in advanceTurnIfNewMessage, handleDone
- renderTimer dual-writes in handleDelta, flushAndFinalizeAssistant, flushPendingRender
- thinkingStartTime dual-writes in handleThinkingStart/Stop
- replayBatch dual-writes in beginReplayBatch, setMessages, commitReplayFinal
- replayBuffer dual-writes in commitReplayFinal, consumeReplayBuffer
- uiState.contextPercent dual-write in updateContextFromTokens

Delete stash/restore session cache (replaced by two-tier per-session store):
- stashSessionMessages, restoreCachedMessages, evictCachedMessages
- sessionMessageCache Map and CachedSession interface
- Update session.svelte.ts switchToSession to remove stash/restore

Wire session teardown:
- session_deleted handler in ws-dispatch.ts calls clearSessionChatState
- handleSessionList diff logic detects removed sessions and cleans up
- Search-payload guard prevents cleanup on filtered results
- Bump outgoing session's replayGeneration on session_switched

New tests:
- session-slot-eviction.test.ts: LRU cap, never-evict-current, lazy reconstruct
- ghost-session-cleanup.test.ts: session_deleted wiring, handleSessionList diff,
  search-payload guard, active-session teardown

Test migration:
- turn-epoch-queued-pipeline: remove stash/restore imports and cache round-trip test
- replay-batch: pass per-session args to discardReplayBatch
- replay-paging: use real session slot for clearMessages buffer test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove deprecated shims, orphaned comments, and stale dual-write
patterns left over from the per-session state migration (Tasks 1-6).

- session-overrides.ts: remove GLOBAL sentinel, deprecated single-arg
  overloads, and backward-compatible property getters (model, agent,
  modelUserSelected, variant) — all callers now use per-session API
- event-pipeline.ts: remove CACHEABLE_EVENT_TYPES / CacheableEventType
  deprecated aliases (no importers remain)
- ws-dispatch.ts: migrate handleToolContentResponse from chatState to
  per-session setMessages/getMessages; remove dispatchToCurrent comments
- chat.svelte.ts: export setMessages; remove Task-reference comments,
  dual-write annotations, stash/restore cache comment
- Clean stale chatState references in comments across HistoryLoader,
  session.svelte.ts, shared-types.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dibstern dibstern merged commit f780c0b into main Apr 22, 2026
@dibstern dibstern deleted the feature/orchestrator-implementation branch April 22, 2026 00:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant