feat(session): engine-agnostic sessions with per-turn engine switching#5
Merged
feat(session): engine-agnostic sessions with per-turn engine switching#5
Conversation
… switching When the user changes the default AI engine in Settings, existing sessions now seamlessly switch to the new engine on the next message — same session, continuous conversation. The engine is resolved dynamically per-turn via drift detection in the sendMessage/resumeSession chokepoint, reusing the existing provider-mode-drift pattern. Key changes: - ManagedSession.switchEngine() — updates dual engineKind storage, injects conversation summary into context, emits engine_switch system event - detectAndApplyEngineDrift() — DRY helper in SessionOrchestrator called from both sendMessage() and resumeSessionInternal() - buildConversationSummary() — pure function extracting text turns with budget-capped head+tail truncation strategy - EngineSwitchEvent type + SystemEventView rendering with ArrowRightLeft icon - Debug logs at settings change, message entry, and drift detection points Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…umeSessionInternal When the SDK lifecycle was still alive (multi-turn wait mode), the fast path at L1127 pushed the message directly into the existing queue and returned before engine drift detection at L1150 ever ran. This caused engine switches to be silently ignored for active sessions. Move detectAndApplyEngineDrift() before the fast path check. If drift is detected, set forceRestart=true so the fast path is skipped and the session gets a full lifecycle restart with the new engine. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add comprehensive logging to close observability gaps in the request lifecycle, modeled on the Codex logging standard: - QueryLifecycle: start (prompt preview, option keys, model), pushMessage (turn seq + content preview), stop (turns completed) - SessionOrchestrator: pre-flight summary before lifecycle.start() with prompt layer sizes, message count, MCP server count, model; completion summary with duration and final state - ManagedSession: state machine transitions (event type + from state), addMessage (role + block count), switchEngine (from/to + summary length) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ifecycle Add comprehensive logging to close observability gaps in the memory extraction and injection pipeline: - MemoryService: DataBus event arrival, content skip reasons, extraction start/end with content length, context injection entry/result - MemoryExtractor: LLM call success path with response length, candidate count, and duration - MemoryQualityGate: per-candidate routing decisions (accept/merge/reject with reason), FTS search failure warning - MemoryRetriever: retrieval entry with projectId, fetch counts for project/user memories - MemoryDebounceQueue: enqueue with queue depth, flush with batch size, queue overflow warning - SessionOrchestrator: memory context injection failure now logs warning instead of silent catch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion When the LLM returns a response but 0 candidates are parsed, there was no visibility into why. Add logging for all silent failure paths: - No memories array in response (wrong JSON structure) — logs keys + preview - All candidates filtered out — logs per-reason counters (empty, too long, low confidence) Prompted by real debugging: LLM returned 1065 chars but candidateCount=0 with no explanation in logs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sized content Root cause: LLM extracted a rich project memory (tech stack + architecture + conventions) exceeding the 500-char maxContentLength limit, which was silently discarded. User saw nothing — worst outcome for valuable content. Three-layer fix following "it just works" UX principle: 1. Raise maxContentLength 500 → 1000 — accommodates real-world structured knowledge that is naturally longer than simple preferences 2. Add length constraint to extraction prompt — LLM now knows the limit and is instructed to split rich content into multiple atomic memories 3. Graceful degradation — if content still exceeds the limit, truncate with ellipsis instead of discarding. Partial value > zero value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace truncation with pass-through: the extraction prompt already specifies the length constraint. If the LLM still exceeds it, the extra content is necessary for completeness — store as-is rather than destroying information integrity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Project-scoped memories now display the project name alongside the scope label (e.g., "Project · OpenCow") instead of just "Project". Resolves the name from appStore.projects using the memory's projectId. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The filter toggle showed "全局/Global" while memory cards showed "用户/User" for the same user-scope concept. Unified to "全局/Global" everywhere since user-scope memories apply across all projects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d MemorySection Replace inline switch implementations with the shared Switch from @/components/ui/switch, consistent with ConnectionCard usage. Eliminates duplicated toggle markup and ensures visual consistency. Co-Authored-By: Claude <noreply@anthropic.com>
Tests used 600-char content to exceed the old 500-char limit, but the limit was raised to 1000. Bump test content to 1100 chars so the "too long" rejection paths are still exercised. Co-Authored-By: Claude <noreply@anthropic.com>
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
Existing sessions now seamlessly switch to a new AI engine when the user changes the default in Settings — same session, continuous conversation, no restart needed. The engine is resolved dynamically per-turn via drift detection at the sendMessage/resumeSession chokepoint. This PR also hardens the memory extraction pipeline, adds structured dev-tracing logs across session and memory lifecycles, and unifies UI switch components.
Changes
Engine-agnostic sessions
ManagedSession.switchEngine()with conversation summary injection for context continuitydetectAndApplyEngineDrift()DRY helper called from bothsendMessage()andresumeSessionInternal()EngineSwitchEventtype andSystemEventViewrendering with ArrowRightLeft iconbuildConversationSummary()pure function with budget-capped head+tail truncationMemory system hardening
Structured dev-tracing logs
UI consistency
Switchcomponent fromui/switchTest Plan