feat: replace OpenCodeClient with @opencode-ai/sdk#2
Merged
Conversation
…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()
…ession stream versioning
…abase initialization
…g, and event store writes
… 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.
…or replay progress
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.
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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
OpenCodeClient(704 lines) andSSEConsumer(284 lines) with@opencode-ai/sdkv1.4.3OpenCodeAPIadapter wrapping SDK +GapEndpoints(6 endpoints not in SDK) into unified namespaced API (client.session.list(),client.permission.reply(), etc.)event.subscribe()async generator via newSSEStreamclassSessionDetail,SessionStatus,PartType,ToolStatus,OpenCodeEvent) with SDK discriminated unionsArchitecture
New files:
src/lib/instance/retry-fetch.ts— Exponential backoff fetch adapter injected into SDKsrc/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 bridgesrc/lib/relay/sse-stream.ts— SDK-backed SSE consumer with reconnection + health trackingDeleted files:
src/lib/instance/opencode-client.ts(704 lines)src/lib/relay/sse-consumer.ts(284 lines)Key design decisions
SDK types everywhere —
PartTypeandToolStatusnow derive from SDK discriminated unions (Part["type"],ToolState["status"]).HistoryMessage/HistoryMessagePartkept as relay-specific transport types (carryrenderedHtml, index signatures).Provider normalization — SDK returns
{ all, default, connected }with models asRecord<string, Model>. Adapter normalizes to{ providers, defaults, connected }with models asArray<Model>for caller compatibility.Message flattening — SDK returns
{ info: Message, parts: Part[] }. Adapter flattens to{ ...info, parts }for backward compatibility with relay's message pipeline.Auth strategy (Audit v3) —
config.headerscarries auth for both REST and SSE.authFetchhandles SDK's single-Request calling convention (pass-through) and GapEndpoints' two-arg calls (injects auth).SSEEvent superset — SDK
Eventunion doesn't cover 4 SSE-delivered events (message.part.delta,permission.asked,question.asked,server.heartbeat). CreatedSSEEvent = Event | GapEventssuperset.Test plan
pnpm check— 0 type errors (both server + frontend tsconfigs)pnpm lint— 0 errorspnpm test— 4273 unit tests passingpnpm test:integration— 131 passing, 1 skippedpnpm test:contract— 81 passingpnpm test:all— all 13 steps green