feat(bridge): SDK capabilities — LiveSession, TodoWrite, cost tracking, AskUserQuestion#33
Merged
Conversation
…ed feature gating Define ProviderCapabilities in base.ts with boolean flags for each SDK feature (slashCommands, askUserQuestion, streamingInput, todoTracking, costInUsd, skills, sessionResume). Both ClaudeSDKProvider (all true) and CodexProvider (mostly false, sessionResume true) implement capabilities(). No runtime behavior change — foundation for subsequent capability-gated features.
Extract askSingleQuestion() method and loop over all questions sequentially. Previously only questions[0] was processed — answers for questions 1-3 were silently discarded. Each question now gets its own IM card, sent one at a time after the previous is answered.
Unrecognized /commands (e.g. /compact, /clear) now fall through to the SDK provider when capabilities().slashCommands is true. When the provider doesn't support slash commands (e.g. Codex), a warning message is sent instead of silently forwarding unsupported input.
…king Add MessageInjector queue class in base.ts. When provider supports streamingInput capability, SDKEngine creates an injector per active query. Claude SDK provider uses AsyncGenerator prompt to yield injected messages mid-query. BridgeManager routes mid-processing messages to the injector instead of rejecting with "please wait".
Add todo_update canonical event. ClaudeAdapter extracts todo data from TodoWrite tool_use blocks (still hidden from normal tool display) and emits todo_update events. MessageRenderer shows progress inline: 📋 Progress (2/5) with status icons per task. Gated by capabilities().todoTracking — skipped for Codex provider.
CostTracker is now per-session instead of per-query, accumulating sessionTotalUsd and queryCount across queries. Footer shows cumulative cost after the first query (e.g. "$0.04 (Σ $0.12)"). Cost display is suppressed when costUsd is 0 (e.g. Codex provider).
- Fix multi-select answer format: join with ", " (comma-space) per SDK spec - Add preview field support to option types throughout the chain - Enable toolConfig.askUserQuestion.previewFormat: 'markdown' in SDK query - Display option previews as indented blocks in IM question cards
…anUseTool - Extract modelUsage per-model cost breakdown from SDKResultMessage - Show per-model cost when multiple models used (e.g. "sonnet-4 $0.02 + opus-4 $0.08") - Add modelUsage schema to query_result canonical event - Pass toolUseID, blockedPath, agentID in canUseTool options per SDK spec - Include blockedPath in permission reason for better file operation display - Return toolUseID in PermissionResult for proper SDK matching
… send Reply-to-message (quoting the bot's card) = inject into active turn via streaming input. Direct send = show "please wait" for next turn. This matches natural IM semantics: reply = add context, new message = new instruction.
- Reply to bot's working card → inject into active turn (streaming input) - Direct send while processing → queue for next turn, auto-process after - Track current working card messageId to distinguish reply targets - Queued messages show "📥 Queued — will process after current task" - SDKEngine auto-processes queue after each turn completes
Replace streamingInput/MessageInjector with LiveSession abstraction aligned with Claude SDK's AsyncGenerator and Codex's Thread/Turn model. Add TurnParams, LiveSession interface with startTurn/steerTurn/ interruptTurn/close. Add liveSession to ProviderCapabilities. Extract AskUserQuestionHandler type to base.ts for reuse.
ClaudeLiveSession wraps a long-lived SDK query() with AsyncGenerator prompt. Background consumer routes SDK events to per-turn streams, split at result boundaries. Per-turn permission/AskUserQuestion handlers support different callbacks per turn. API: startTurn() → per-turn stream, steerTurn() → mid-turn inject, interruptTurn() → q.interrupt(), close() → teardown.
SDKEngine now manages LiveSessions via a registry keyed by channelType:chatId:workdir. For providers with liveSession capability, startTurn() returns per-turn streams; for others, falls back to streamChat(). ConversationEngine accepts pre-built streamResult. BridgeManager uses canSteer/steer for reply-to working card injection, queueMessage for direct sends. Session cleanup on /new and expiry.
…eamChat streamChat() no longer handles streaming input — that's now managed by ClaudeLiveSession. Remove the dead messageInjector/AsyncGenerator code from streamChat(). All streaming input goes through LiveSession.steerTurn().
- Fix modelUsage type in ConversationEngine onQueryResult callback - Fix costTracker.start() not called for LiveSession turns - Guard startTurn() against overlapping turns (close previous if active) - Remove dead turnCompleteResolve field from ClaudeLiveSession - Extract shared code to claude-shared.ts (buildSubprocessEnv, preparePromptWithImages, SAFE_PERMISSIONS, classifyAuthError)
/new command now calls sdkEngine.closeSession() before creating a new session. Previously, the old LiveSession remained alive in the registry with its background consumer running, leaking a Claude Code process per /new invocation.
- Reset ClaudeAdapter between turns (clear hiddenToolUseIds Set) - Cap message queue at 10 per chat, reject with warning when full - Add idle session pruning: every 60s check for sessions idle >30min - Track lastActiveAt per ManagedSession for pruning decisions - Wire pruning start/stop into BridgeManager lifecycle
…r splits When Telegram splits a long message at 4096 chars, multiple messages arrive in quick succession. BridgeManager now waits up to 500ms to collect follow-up parts from the same user/chat and merges them into a single message before processing. Non-text messages and commands are not coalesced.
[claude-sdk] → [tlive:sdk], [claude-live] → [tlive:session], [codex] → [tlive:codex], [bridge] → [tlive:engine]
…on, error recovery Critical: - Pass effort/model at LiveSession creation and per-turn via setModel() - Add effort/model to createSession params and ClaudeLiveSessionOptions Important: - Replace recursive queue processing with iterative drainQueue in BridgeManager - Skip 500ms coalesce wait for messages shorter than ~3900 chars - Add try/catch around createSession to fall back to streamChat on failure - Call adapter.reset() on error path in consumeInBackground - Make dequeueMessage public for BridgeManager access
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
Align IM bridge with official Claude Agent SDK capabilities. Major architectural change: replace per-message
query()calls with long-livedLiveSessionabstraction following the SDK's recommended streaming input pattern.New Architecture: LiveSession (Thread/Turn Model)
LiveSessioninterface — aligned with both Claude SDK AsyncGenerator and Codex Thread/Turn/Steer modelClaudeLiveSession— long-livedquery()with background consumer, per-turn stream splittingSessionRegistry— perchannelType:chatId:workdir, supports multi-workspaceSDK Capabilities
ProviderCapabilitiesinterface for provider-based feature gatingAskUserQuestionfix (was only processingquestions[0])TodoWriteprogress cards in IM (📋 Progress 2/5)modelUsage)AskUserQuestionaligned with SDK docs: preview field,previewFormat: 'markdown', multi-select", "formatcanUseToolaligned with SDK reference:toolUseID,blockedPath,agentIDclaude-shared.ts[tlive:module]log prefixesCodex Compatibility
All gating via
provider.capabilities(). Codex returnsliveSession: false→ falls back to per-messagestreamChat().Test plan