@exreve/exk 1.0.61 → 1.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,12 @@
1
- import { query } from '@anthropic-ai/claude-agent-sdk';
2
- import { execSync, spawn } from 'child_process';
1
+ import { execSync } from 'child_process';
3
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
4
3
  import { symlink as fsSymlink } from 'fs';
5
4
  import { getSkillContent } from './skills/index.js';
6
5
  import { isLocalModel, unwrapModelName, startOpenAIAdapter, getAdapterConfig } from './openaiAdapter.js';
7
6
  import { ensureProxy } from './proxyManager.js';
8
- import { createModuleMcpServer } from './moduleMcpServer.js';
7
+ import { claudeBackend } from './claudeBackend.js';
9
8
  import path from 'path';
10
9
  import os from 'os';
11
- import { createRequire } from 'module';
12
10
  import { promisify } from 'util';
13
11
  // ============ Session State Persistence ============
14
12
  // Persists claudeSessionId to disk so context survives CLI restarts.
@@ -53,135 +51,8 @@ function deleteSessionState(sessionId) {
53
51
  // Ignore cleanup errors
54
52
  }
55
53
  }
56
- /**
57
- * Resolve path to the SDK's bundled cli.js.
58
- * We resolve this ourselves so it works reliably on Windows when running from
59
- * the PS1 install (ttc.cmd -> node ttc.js); the SDK's internal resolution via
60
- * import.meta.url can fail or produce wrong paths in that context.
61
- * CACHED: Path is resolved once at module load time for performance.
62
- */
63
- function resolveSdkCliPath() {
64
- try {
65
- const req = typeof globalThis.require === 'function'
66
- ? globalThis.require
67
- : createRequire(import.meta.url);
68
- const pkgPath = req.resolve('@anthropic-ai/claude-agent-sdk/package.json');
69
- const cliPath = path.join(path.dirname(pkgPath), 'cli.js');
70
- return existsSync(cliPath) ? cliPath : undefined;
71
- }
72
- catch {
73
- return undefined;
74
- }
75
- }
76
- // Cache the resolved Claude executable path at module load time
77
- const CACHED_CLAUDE_PATH = (() => {
78
- const envPath = process.env.TTC_CLAUDE_PATH;
79
- if (envPath)
80
- return envPath;
81
- const sdkPath = resolveSdkCliPath();
82
- if (sdkPath)
83
- return sdkPath;
84
- const localPath = path.join(os.homedir(), '.local', 'bin', 'claude');
85
- if (existsSync(localPath))
86
- return localPath;
87
- return undefined;
88
- })();
89
54
  // Promisify symlink for async use
90
55
  const symlinkAsync = promisify(fsSymlink);
91
- // Helper function to extract tool name from result structure
92
- /**
93
- * Detect tool name from the shape of tool_use_result.
94
- *
95
- * Uses discriminating keys from the SDK's own type definitions
96
- * (sdk-tools.d.ts: BashOutput, GrepOutput, GlobOutput, FileReadOutput,
97
- * FileEditOutput, FileWriteOutput, TodoWriteOutput, etc.)
98
- */
99
- function extractToolName(toolResult) {
100
- if (!toolResult || typeof toolResult !== 'object')
101
- return 'unknown';
102
- const r = toolResult;
103
- // ── Read (FileReadOutput): {type: 'text'|'image'|'notebook'|'pdf'|'parts'|'file_unchanged', file: {...}}
104
- if (r.file && typeof r.file === 'object'
105
- && ['text', 'image', 'notebook', 'pdf', 'parts', 'file_unchanged'].includes(r.type)) {
106
- return 'Read';
107
- }
108
- // ── Edit (FileEditOutput): {filePath, oldString, newString, structuredPatch}
109
- if (r.filePath && r.oldString !== undefined && r.newString !== undefined
110
- && Array.isArray(r.structuredPatch)) {
111
- return 'Edit';
112
- }
113
- // ── Write (FileWriteOutput): {type: 'create'|'update', filePath, content, structuredPatch}
114
- if (r.filePath && (r.type === 'create' || r.type === 'update')
115
- && r.content !== undefined) {
116
- return 'Write';
117
- }
118
- // ── Grep (GrepOutput): {mode, numFiles, filenames, content?, numLines?}
119
- if (typeof r.numFiles === 'number' && Array.isArray(r.filenames)
120
- && r.mode !== undefined) {
121
- return 'Grep';
122
- }
123
- // ── Glob (GlobOutput): {durationMs, numFiles, filenames, truncated}
124
- if (typeof r.numFiles === 'number' && Array.isArray(r.filenames)
125
- && typeof r.durationMs === 'number' && r.truncated !== undefined) {
126
- return 'Glob';
127
- }
128
- // ── TodoWrite (TodoWriteOutput): {oldTodos, newTodos}
129
- if (Array.isArray(r.oldTodos) && Array.isArray(r.newTodos)) {
130
- return 'TodoWrite';
131
- }
132
- // ── Bash (BashOutput): {stdout, stderr, interrupted, ...}
133
- if (r.stdout !== undefined || r.stderr !== undefined) {
134
- return 'Bash';
135
- }
136
- // ── WebSearch (WebSearchOutput): {query, results}
137
- if (typeof r.query === 'string' && Array.isArray(r.results)) {
138
- return 'WebSearch';
139
- }
140
- // ── WebFetch (WebFetchOutput): {url, result, code, bytes}
141
- if (typeof r.url === 'string' && typeof r.result === 'string'
142
- && typeof r.code === 'number') {
143
- return 'WebFetch';
144
- }
145
- // ── send_file (custom MCP tool): content is JSON with _type marker
146
- if (r.content && typeof r.content === 'string' && r.type === 'text') {
147
- try {
148
- const parsed = JSON.parse(r.content);
149
- if (parsed._type === 'send_file')
150
- return 'send_file';
151
- }
152
- catch { /* not JSON, fall through */ }
153
- // SDK 0.2.x: content-only results from nested tool calls (no stdout/stderr wrapper)
154
- // Don't default to Bash — the history lookup is authoritative
155
- return 'unknown';
156
- }
157
- // ── Agent/Task output: {agentId, content, status}
158
- if (r.agentId && Array.isArray(r.content) && r.status) {
159
- return 'Task';
160
- }
161
- return 'unknown';
162
- }
163
- // Look up tool name from the most recent assistant message's tool_use blocks by tool_use_id
164
- function lookupToolNameFromHistory(messages, toolUseId) {
165
- if (!toolUseId)
166
- return null;
167
- for (let i = messages.length - 1; i >= 0; i--) {
168
- const msg = messages[i];
169
- if (msg.role !== 'assistant')
170
- continue;
171
- // msg.content is the SDK message object: {role: 'assistant', content: [{type: 'text',...}, {type: 'tool_use',...}]}
172
- let content = typeof msg.content === 'string' ? null : msg.content;
173
- // Unwrap nested content: {content: [...]} → [...]
174
- if (content && !Array.isArray(content) && Array.isArray(content.content)) {
175
- content = content.content;
176
- }
177
- if (!Array.isArray(content))
178
- continue;
179
- const toolUse = content.find((c) => c.type === 'tool_use' && c.id === toolUseId);
180
- if (toolUse?.name)
181
- return toolUse.name;
182
- }
183
- return null;
184
- }
185
56
  // AI config - loaded from server after registration, stored in ~/.talk-to-code/ai-config.json
186
57
  // (Do not read ANTHROPIC_* / CLAUDE_MODEL from the host environment — only this file + code default model.)
187
58
  const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json');
@@ -394,6 +265,27 @@ export class AgentSessionManager {
394
265
  emergencyStopInProgress = new Set(); // Track sessions being emergency stopped
395
266
  sessionHandlers = new Map(); // Track handlers for each session
396
267
  socketRef = null; // Socket.IO reference for fetching session history from backend
268
+ /**
269
+ * Select the appropriate agent backend based on model/provider configuration.
270
+ * Can be overridden per-session via handler.agentBackend or env var TTC_AGENT_BACKEND.
271
+ */
272
+ selectBackend(agentBackend) {
273
+ // Priority: explicit parameter > env var
274
+ const requested = agentBackend || process.env.TTC_AGENT_BACKEND;
275
+ if (requested === 'pi') {
276
+ try {
277
+ const { piBackend } = require('./piBackend.js');
278
+ if (piBackend.isAvailable()) {
279
+ return piBackend;
280
+ }
281
+ console.warn('[AgentSessionManager] Pi backend requested but SDK not installed, falling back to Claude');
282
+ }
283
+ catch {
284
+ console.warn('[AgentSessionManager] Pi backend not available, falling back to Claude');
285
+ }
286
+ }
287
+ return claudeBackend;
288
+ }
397
289
  /** Set the socket reference for backend communication (called from app-child.ts) */
398
290
  setSocketRef(socket) {
399
291
  this.socketRef = socket;
@@ -500,26 +392,28 @@ export class AgentSessionManager {
500
392
  // Store the handler for this session
501
393
  this.sessionHandlers.set(sessionId, handler);
502
394
  const abortController = new AbortController();
503
- // Restore claudeSessionId from disk (survives CLI restart)
395
+ // Restore sdkSessionId from disk (survives CLI restart)
504
396
  const persistedState = loadSessionState(sessionId);
505
- const restoredClaudeSessionId = persistedState?.claudeSessionId;
506
- if (restoredClaudeSessionId) {
507
- console.log(`[AgentSessionManager] Restored claudeSessionId for session ${sessionId}: ${restoredClaudeSessionId}`);
397
+ const restoredSdkSessionId = persistedState?.claudeSessionId;
398
+ if (restoredSdkSessionId) {
399
+ console.log(`[AgentSessionManager] Restored sdkSessionId for session ${sessionId}: ${restoredSdkSessionId}`);
508
400
  }
401
+ // Select backend for this session
402
+ const backend = this.selectBackend(handler.agentBackend);
509
403
  this.sessions.set(sessionId, {
510
404
  abortController,
511
405
  messages: [],
512
- toolNameMap: new Map(),
513
406
  totalInputTokens: 0,
514
407
  totalOutputTokens: 0,
515
408
  totalCostUsd: 0,
516
409
  promptQueue: [],
517
410
  isProcessingQueue: false,
518
- claudeSessionId: restoredClaudeSessionId, // Restored from disk or undefined
411
+ sdkSessionId: restoredSdkSessionId, // Restored from disk or undefined
519
412
  childProcesses: new Set(),
520
413
  claudeProcessGroupId: undefined,
521
414
  currentPromptId: undefined,
522
415
  model: sessionModel,
416
+ backend,
523
417
  });
524
418
  // Auto-regenerate CLAUDE.md for fresh project context
525
419
  await this.regenerateClaudeMd(projectPath);
@@ -564,7 +458,7 @@ export class AgentSessionManager {
564
458
  if (!session.isProcessingQueue) {
565
459
  this.processPromptQueue(sessionId);
566
460
  }
567
- else if (session.isProcessingQueue && !session.activeQueryStream && !this.emergencyStopInProgress.has(sessionId)) {
461
+ else if (session.isProcessingQueue && !session.activeBackendStream && !this.emergencyStopInProgress.has(sessionId)) {
568
462
  // Safety: isProcessingQueue is true but there's no active stream and no emergency stop
569
463
  // This means the queue got stuck (e.g. from a previous abort return that bypassed cleanup)
570
464
  console.warn(`[agentSession] Queue stuck detected for session ${sessionId}, resetting isProcessingQueue`);
@@ -627,16 +521,16 @@ export class AgentSessionManager {
627
521
  // This ensures real-time status updates before any async operations
628
522
  onStatusUpdate?.('running');
629
523
  // Wait for current query to finish before starting next prompt
630
- if (session.activeQueryStream !== undefined) {
524
+ if (session.activeBackendStream !== undefined) {
631
525
  try {
632
- for await (const _ of session.activeQueryStream) { }
526
+ for await (const _ of session.activeBackendStream) { }
633
527
  }
634
528
  catch (err) {
635
- console.error(`[AgentSession] Error draining active query stream:`, err);
529
+ console.error(`[AgentSession] Error draining active backend stream:`, err);
636
530
  }
637
- session.activeQueryStream = undefined;
531
+ session.activeBackendStream = undefined;
638
532
  }
639
- session.activeQueryStream = undefined;
533
+ session.activeBackendStream = undefined;
640
534
  // Build final prompt with enhancers
641
535
  let finalPrompt = effectivePrompt;
642
536
  if (enhancers && enhancers.length > 0) {
@@ -691,155 +585,14 @@ export class AgentSessionManager {
691
585
  totalTokens: session.totalInputTokens + session.totalOutputTokens
692
586
  }
693
587
  });
694
- // Use cached Claude executable path (resolved at module load time for performance)
695
- const pathToClaudeCodeExecutable = CACHED_CLAUDE_PATH;
696
- // Build query options - include abort signal for cancellation
697
- const queryOptions = {
698
- signal: abortController.signal, // Pass abort signal to SDK for interruption
699
- cwd: projectPath,
700
- apiKey: CLAUDE_CONFIG.apiKey,
701
- model: CLAUDE_CONFIG.model,
702
- tools: { type: 'preset', preset: 'claude_code' },
703
- disallowedTools: ['AskUserQuestion', 'analyze_image'], // Disable built-in analyze_image (we provide our own via MCP)
704
- settingSources: ['project'], // Enable CLAUDE.md loading
705
- permissionMode: 'bypassPermissions',
706
- allowDangerouslySkipPermissions: true,
707
- // Create a fresh MCP server for each query call (SDK connects transport internally, cannot reuse)
708
- ...(() => {
709
- const mcpServer = this.buildMcpServer(sessionId, attachmentDir, promptId);
710
- return { mcpServers: { 'claude-voice-modules': mcpServer } };
711
- })(),
712
- ...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
713
- spawnClaudeCodeProcess: (spawnOptions) => {
714
- const { command, args, cwd: cwd2, env, signal } = spawnOptions;
715
- // Debug: log what env/args are being passed to Claude process
716
- console.log(`[agentSession] Spawn ANTHROPIC_BASE_URL:`, env?.ANTHROPIC_BASE_URL || '(not set)');
717
- console.log(`[agentSession] Spawn ANTHROPIC_API_KEY:`, env?.ANTHROPIC_API_KEY ? '(set)' : '(not set)');
718
- console.log(`[agentSession] Spawn args:`, args?.join(' '));
719
- // Only check file existence when command is a path (not a bare name like "claude" from PATH)
720
- const hasPathSep = command.includes(path.sep) || command.includes('/') || command.includes('\\');
721
- if (hasPathSep && !existsSync(command)) {
722
- throw new Error(`Executable not found at ${command}. Set path with: ttc config --claude-path "<path>" (or TTC_CLAUDE_PATH)`);
723
- }
724
- try {
725
- if (cwd2 && !existsSync(cwd2)) {
726
- mkdirSync(cwd2, { recursive: true });
727
- }
728
- }
729
- catch (err) {
730
- console.error(`[AgentSession] Failed to create working directory ${cwd2}:`, err);
731
- }
732
- const isWin = process.platform === 'win32';
733
- // Ensure PATH includes common node locations, especially in containers
734
- const defaultPath = isWin
735
- ? (process.env.Path || process.env.PATH || '')
736
- : '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
737
- const spawnEnv = {
738
- ...env,
739
- IS_SANDBOX: '1', // Tell Claude Code it's in a sandbox — skip root-user checks
740
- PATH: env.PATH || process.env.PATH || defaultPath,
741
- ...(isWin
742
- ? {
743
- USERPROFILE: env.USERPROFILE || process.env.USERPROFILE || os.homedir(),
744
- USERNAME: env.USERNAME || process.env.USERNAME || 'user',
745
- HOME: env.USERPROFILE || process.env.USERPROFILE || os.homedir(), // Windows: use Windows home, not /home/user
746
- }
747
- : { HOME: env.HOME || process.env.HOME || os.homedir(), USER: env.USER || process.env.USER || 'user' }),
748
- };
749
- // If command is 'node' and not found, try to resolve it
750
- if (command === 'node' && !hasPathSep) {
751
- try {
752
- const nodePath = execSync('which node', { encoding: 'utf-8', env: spawnEnv }).trim();
753
- if (nodePath) {
754
- const child = spawn(nodePath, args, {
755
- cwd: cwd2 || process.cwd(),
756
- stdio: ["pipe", "pipe", env.DEBUG_CLAUDE_AGENT_SDK ? "pipe" : "ignore"],
757
- signal,
758
- env: spawnEnv,
759
- windowsHide: true,
760
- detached: !isWin // Create process group on Unix for tree-killing
761
- });
762
- // Track child process for force-kill
763
- if (!session.childProcesses)
764
- session.childProcesses = new Set();
765
- session.childProcesses.add(child);
766
- // Store process group ID for Unix (negative PID kills entire group)
767
- if (!isWin && child.pid) {
768
- session.claudeProcessGroupId = child.pid;
769
- }
770
- // Clean up when process exits
771
- child.on('exit', () => {
772
- session.childProcesses.delete(child);
773
- });
774
- child.on('error', () => {
775
- session.childProcesses.delete(child);
776
- });
777
- return child;
778
- }
779
- }
780
- catch {
781
- // Fall through to original spawn
782
- }
783
- }
784
- const child = spawn(command, args, {
785
- cwd: cwd2 || process.cwd(),
786
- stdio: ["pipe", "pipe", env.DEBUG_CLAUDE_AGENT_SDK ? "pipe" : "ignore"],
787
- signal,
788
- env: spawnEnv,
789
- windowsHide: true,
790
- detached: !isWin // Create process group on Unix for tree-killing
791
- });
792
- // Track child process for force-kill
793
- if (!session.childProcesses)
794
- session.childProcesses = new Set();
795
- session.childProcesses.add(child);
796
- // Store process group ID for Unix (negative PID kills entire group)
797
- if (!isWin && child.pid) {
798
- session.claudeProcessGroupId = child.pid;
799
- }
800
- // Clean up when process exits
801
- child.on('exit', () => {
802
- session.childProcesses.delete(child);
803
- });
804
- child.on('error', () => {
805
- session.childProcesses.delete(child);
806
- });
807
- return child;
808
- },
809
- env: envForClaudeCodeChild(),
810
- hooks: {
811
- // HookCallbackMatcher format: each entry must be { hooks: [callback] }
812
- // NOT a raw callback array — wrong format silently breaks MCP server registration.
813
- PostToolUse: [{
814
- hooks: [(_toolResult) => {
815
- // Tool result is handled by the user message handler below
816
- return { continue: true };
817
- }]
818
- }],
819
- Notification: [{
820
- hooks: [(notification) => {
821
- onOutput({
822
- type: 'progress',
823
- data: notification,
824
- timestamp: Date.now(),
825
- metadata: {
826
- progress: {
827
- message: typeof notification === 'string' ? notification : JSON.stringify(notification)
828
- }
829
- }
830
- });
831
- return { continue: true };
832
- }]
833
- }],
834
- }
835
- };
836
- // Log model being used for debugging
588
+ // Resolve provider and model configuration
837
589
  const sessionModel = session.model || CLAUDE_CONFIG.model;
838
- // Resolve local model adapter overrides (if using OpenAI-compatible endpoint)
839
590
  let effectiveModel = sessionModel;
840
- let effectiveApiKey = queryOptions.apiKey;
841
- let effectiveEnv = queryOptions.env;
591
+ let effectiveApiKey = CLAUDE_CONFIG.apiKey;
592
+ let effectiveEnv = envForClaudeCodeChild();
842
593
  let effectiveSettings;
594
+ let effectiveProvider;
595
+ // Resolve local model adapter overrides (if using OpenAI-compatible endpoint)
843
596
  const localOverrides = await getLocalModelEnvOverrides(sessionModel);
844
597
  if (localOverrides) {
845
598
  effectiveModel = unwrapModelName(sessionModel);
@@ -849,12 +602,8 @@ export class AgentSessionManager {
849
602
  ANTHROPIC_API_KEY: localOverrides.apiKey,
850
603
  ANTHROPIC_BASE_URL: localOverrides.baseUrl,
851
604
  };
852
- // Override settings to prevent ~/.claude/settings.json env from overriding our proxy URL.
853
- // Claude Code CLI reads settings.json → env section and applies those on top of spawn env,
854
- // which would replace our ANTHROPIC_BASE_URL with the z.ai URL.
855
605
  effectiveSettings = { env: { ANTHROPIC_API_KEY: localOverrides.apiKey, ANTHROPIC_BASE_URL: localOverrides.baseUrl } };
856
606
  console.log(`[agentSession] Using local model adapter: ${sessionModel} -> ${localOverrides.baseUrl}`);
857
- console.log(`[agentSession] effectiveSettings for local model:`, JSON.stringify(effectiveSettings));
858
607
  }
859
608
  else {
860
609
  // Resolve provider for multi-provider switching (Z.ai / MiniMax)
@@ -862,6 +611,7 @@ export class AgentSessionManager {
862
611
  console.log(`[agentSession] Resolved provider: ${resolved.provider} for model: ${sessionModel}`);
863
612
  effectiveApiKey = resolved.apiKey;
864
613
  effectiveEnv = envForClaudeCodeChild(undefined, resolved);
614
+ effectiveProvider = resolved.provider;
865
615
  // Build settings env to prevent ~/.claude/settings.json from overriding our credentials
866
616
  const settingsEnv = {
867
617
  ANTHROPIC_API_KEY: resolved.apiKey,
@@ -890,7 +640,7 @@ export class AgentSessionManager {
890
640
  apiKey: resolved.apiKey,
891
641
  model: resolved.model,
892
642
  });
893
- effectiveApiKey = 'cerebras-via-proxy'; // Proxy doesn't validate the key
643
+ effectiveApiKey = 'cerebras-via-proxy';
894
644
  effectiveEnv = envForClaudeCodeChild(undefined, { ...resolved, baseUrl: proxyUrl });
895
645
  settingsEnv.ANTHROPIC_API_KEY = effectiveApiKey;
896
646
  settingsEnv.ANTHROPIC_BASE_URL = proxyUrl;
@@ -903,39 +653,44 @@ export class AgentSessionManager {
903
653
  effectiveSettings = { env: settingsEnv };
904
654
  console.log(`[agentSession] Provider: ${resolved.provider}, baseUrl: ${resolved.baseUrl}, model: ${resolved.model}`);
905
655
  }
906
- // Create query stream - resume session if we have a Claude session ID
907
- // Always explicitly set model even when resuming to ensure we use the session's model
908
- const queryStream = query({
909
- prompt: finalPrompt,
910
- options: {
911
- ...queryOptions,
912
- apiKey: effectiveApiKey,
913
- model: effectiveModel,
914
- env: effectiveEnv,
915
- ...(effectiveSettings ? { settings: effectiveSettings } : {}),
916
- ...(session.claudeSessionId && !localOverrides && (() => {
917
- // Don't resume if provider changed since last session (context format may differ)
918
- const persisted = loadSessionState(sessionId);
919
- const currentProvider = resolveProvider(sessionModel).provider;
920
- return persisted?.provider === currentProvider;
921
- })() ? { resume: session.claudeSessionId } : {})
922
- // Note: don't resume session for local models - context format differs
923
- // Note: also don't resume if provider differs from persisted session provider (context format may differ)
656
+ // Determine if we should resume (only for Claude backend, matching provider)
657
+ let resumeSessionId;
658
+ if (session.sdkSessionId && !localOverrides) {
659
+ const persisted = loadSessionState(sessionId);
660
+ const currentProvider = resolveProvider(sessionModel).provider;
661
+ if (persisted?.provider === currentProvider) {
662
+ resumeSessionId = session.sdkSessionId;
924
663
  }
925
- });
926
- session.activeQueryStream = queryStream;
927
- // Process messages with enhanced abort checking
928
- // Create a wrapped stream that checks abort status more frequently
929
- const abortCheckInterval = 200; // Check every 200ms
664
+ }
665
+ // Build backend config
666
+ const backendConfig = {
667
+ cwd: projectPath,
668
+ apiKey: effectiveApiKey,
669
+ model: effectiveModel,
670
+ baseUrl: effectiveEnv.ANTHROPIC_BASE_URL,
671
+ provider: effectiveProvider,
672
+ resumeSessionId,
673
+ env: effectiveEnv,
674
+ settings: effectiveSettings,
675
+ attachmentDir,
676
+ routingSessionId: sessionId,
677
+ routingPromptId: promptId,
678
+ signal: abortController.signal,
679
+ };
680
+ // Execute prompt via the selected backend
681
+ const backendStream = session.backend.executePrompt(finalPrompt, backendConfig);
682
+ session.activeBackendStream = backendStream;
683
+ // Process backend events and map to SessionOutput
684
+ const abortCheckInterval = 200;
930
685
  let lastAbortCheck = Date.now();
931
686
  try {
932
- for await (const message of queryStream) {
933
- // Check abort on each message (existing behavior)
687
+ for await (const event of backendStream) {
688
+ // Check abort on each event
934
689
  if (session.abortController.signal.aborted || this.emergencyStopInProgress.has(sessionId)) {
935
690
  console.log(`[agentSession] Aborting prompt ${effectivePromptId} - abort signal received`);
936
691
  break;
937
692
  }
938
- // Periodic check for long-running operations
693
+ // Periodic abort check
939
694
  const now = Date.now();
940
695
  if (now - lastAbortCheck > abortCheckInterval) {
941
696
  lastAbortCheck = now;
@@ -944,376 +699,218 @@ export class AgentSessionManager {
944
699
  break;
945
700
  }
946
701
  }
947
- // Capture Claude SDK session ID from system init message
948
- if (message.type === 'system' && message.subtype === 'init') {
949
- const systemMsg = message;
950
- if (systemMsg.session_id) {
951
- // Detect context loss: session_id changed unexpectedly (resume failed)
952
- if (session.claudeSessionId && session.claudeSessionId !== systemMsg.session_id) {
702
+ // Map BackendEvent SessionOutput
703
+ switch (event.type) {
704
+ case 'init': {
705
+ // SDK session ID received
706
+ const newSessionId = event.sessionId;
707
+ if (session.sdkSessionId && session.sdkSessionId !== newSessionId) {
953
708
  session.contextLost = true;
954
- console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.claudeSessionId} → ${systemMsg.session_id}`);
709
+ console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.sdkSessionId} → ${newSessionId}`);
955
710
  }
956
- session.claudeSessionId = systemMsg.session_id;
957
- saveSessionState(sessionId, { claudeSessionId: systemMsg.session_id, model: session.model, provider: resolveProvider(session.model).provider, updatedAt: Date.now() });
711
+ session.sdkSessionId = newSessionId;
712
+ saveSessionState(sessionId, {
713
+ claudeSessionId: newSessionId,
714
+ model: session.model,
715
+ provider: resolveProvider(session.model).provider,
716
+ updatedAt: Date.now(),
717
+ });
718
+ break;
958
719
  }
959
- }
960
- if (message.type === 'assistant') {
961
- const msg = message;
962
- // Capture Claude session ID from assistant message (always update to track session changes)
963
- if (msg.session_id) {
964
- if (session.claudeSessionId && session.claudeSessionId !== msg.session_id) {
720
+ case 'assistant_message': {
721
+ // Update SDK session ID if provided
722
+ if (event.sdkSessionId) {
723
+ if (session.sdkSessionId && session.sdkSessionId !== event.sdkSessionId) {
724
+ session.contextLost = true;
725
+ console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.sdkSessionId} ${event.sdkSessionId}`);
726
+ }
727
+ session.sdkSessionId = event.sdkSessionId;
728
+ saveSessionState(sessionId, {
729
+ claudeSessionId: event.sdkSessionId,
730
+ model: session.model,
731
+ provider: resolveProvider(session.model).provider,
732
+ updatedAt: Date.now(),
733
+ });
734
+ }
735
+ // Handle context window error
736
+ if (event.isContextWindowError) {
737
+ console.warn(`[AgentSessionManager] Context window limit reached for session ${sessionId}. Clearing context.`);
738
+ session.sdkSessionId = undefined;
965
739
  session.contextLost = true;
966
- console.warn(`[AgentSessionManager] Context lost! Session ID changed in assistant msg: ${session.claudeSessionId} → ${msg.session_id}`);
740
+ deleteSessionState(sessionId);
741
+ onOutput({
742
+ type: 'system',
743
+ data: {
744
+ message: 'Context window limit reached. Session context has been cleared. The next prompt will start fresh with a summary of previous conversation.',
745
+ subtype: 'context_window_reset',
746
+ },
747
+ timestamp: Date.now(),
748
+ metadata: {
749
+ subtype: 'context_window_reset',
750
+ contextInfo: {
751
+ messageCount: session.messages.length,
752
+ totalInputTokens: session.totalInputTokens,
753
+ totalOutputTokens: session.totalOutputTokens,
754
+ totalTokens: session.totalInputTokens + session.totalOutputTokens,
755
+ }
756
+ }
757
+ });
967
758
  }
968
- session.claudeSessionId = msg.session_id;
969
- saveSessionState(sessionId, { claudeSessionId: msg.session_id, model: session.model, provider: resolveProvider(session.model).provider, updatedAt: Date.now() });
970
- }
971
- // Detect context window limit error from the API
972
- // The SDK sends this as an assistant message with error: 'max_output_tokens'
973
- // when the provider returns 'model_context_window_exceeded'
974
- const isContextWindowError = msg.error === 'max_output_tokens' && ((typeof msg.message === 'string' && msg.message.includes('context window limit')) ||
975
- (Array.isArray(msg.message?.content) && msg.message.content.some((c) => typeof c?.text === 'string' && c.text.includes('context window limit'))));
976
- if (isContextWindowError) {
977
- console.warn(`[AgentSessionManager] Context window limit reached for session ${sessionId}. Clearing context for fresh start.`);
978
- // Clear the session ID so next prompt starts fresh (no resume)
979
- session.claudeSessionId = undefined;
980
- session.contextLost = true;
981
- deleteSessionState(sessionId);
982
- // Emit a user-friendly system message
759
+ session.messages.push({
760
+ role: 'assistant',
761
+ content: event.raw,
762
+ timestamp: Date.now()
763
+ });
983
764
  onOutput({
984
- type: 'system',
985
- data: {
986
- message: 'Context window limit reached. Session context has been cleared. The next prompt will start fresh with a summary of previous conversation.',
987
- subtype: 'context_window_reset',
988
- },
765
+ type: 'assistant',
766
+ data: event.raw,
989
767
  timestamp: Date.now(),
990
768
  metadata: {
991
- subtype: 'context_window_reset',
992
- contextInfo: {
993
- messageCount: session.messages.length,
994
- totalInputTokens: session.totalInputTokens,
995
- totalOutputTokens: session.totalOutputTokens,
996
- totalTokens: session.totalInputTokens + session.totalOutputTokens,
997
- }
769
+ error: event.error,
770
+ contextSize: session.messages.length,
998
771
  }
999
772
  });
773
+ break;
1000
774
  }
1001
- session.messages.push({
1002
- role: 'assistant',
1003
- content: msg.message,
1004
- timestamp: Date.now()
1005
- });
1006
- // Populate toolNameMap from this assistant message's tool_use blocks
1007
- if (Array.isArray(msg.message?.content)) {
1008
- for (const block of msg.message.content) {
1009
- if (block.type === 'tool_use' && block.id && block.name) {
1010
- session.toolNameMap.set(block.id, block.name);
775
+ case 'tool_result': {
776
+ onOutput({
777
+ type: 'tool_result',
778
+ data: event.result,
779
+ timestamp: Date.now(),
780
+ metadata: {
781
+ toolName: event.toolName ?? undefined,
782
+ toolResult: event.result,
783
+ toolUseId: event.toolUseId || undefined,
784
+ parentToolUseId: event.parentToolUseId ?? undefined,
785
+ isSynthetic: event.isSynthetic,
1011
786
  }
1012
- }
1013
- }
1014
- onOutput({
1015
- type: 'assistant',
1016
- data: msg.message,
1017
- timestamp: Date.now(),
1018
- metadata: {
1019
- parentToolUseId: msg.parent_tool_use_id,
1020
- uuid: msg.uuid,
1021
- sessionId: msg.session_id,
1022
- error: msg.error,
1023
- contextSize: session.messages.length
1024
- }
1025
- });
1026
- }
1027
- else if (message.type === 'result') {
1028
- const msg = message;
1029
- // Update usage tracking
1030
- if (msg.usage) {
1031
- const usage = msg.usage;
1032
- const inputTokens = usage.input_tokens || usage.inputTokens || 0;
1033
- const outputTokens = usage.output_tokens || usage.outputTokens || 0;
1034
- session.totalInputTokens += inputTokens;
1035
- session.totalOutputTokens += outputTokens;
1036
- session.totalCostUsd += msg.total_cost_usd || 0;
1037
- session.lastUsage = {
1038
- inputTokens,
1039
- outputTokens,
1040
- totalTokens: inputTokens + outputTokens
1041
- };
787
+ });
788
+ break;
1042
789
  }
1043
- onOutput({
1044
- type: 'result',
1045
- data: msg,
1046
- timestamp: Date.now(),
1047
- metadata: {
1048
- subtype: msg.subtype,
1049
- isError: msg.is_error,
1050
- exitCode: msg.is_error ? 1 : 0,
1051
- durationMs: msg.duration_ms,
1052
- durationApiMs: msg.duration_api_ms,
1053
- totalCostUsd: msg.total_cost_usd,
1054
- usage: msg.usage,
1055
- modelUsage: msg.modelUsage,
1056
- structuredOutput: msg.structured_output,
1057
- numTurns: msg.num_turns,
1058
- contextInfo: {
1059
- messageCount: session.messages.length,
1060
- totalInputTokens: session.totalInputTokens,
1061
- totalOutputTokens: session.totalOutputTokens,
1062
- totalTokens: session.totalInputTokens + session.totalOutputTokens,
1063
- totalCostUsd: session.totalCostUsd,
1064
- lastUsage: session.lastUsage
790
+ case 'tool_progress': {
791
+ onOutput({
792
+ type: 'tool_progress',
793
+ data: event.raw,
794
+ timestamp: Date.now(),
795
+ metadata: {
796
+ toolName: event.toolName,
797
+ toolUseId: event.toolUseId,
798
+ elapsedTimeSeconds: event.elapsedTimeSeconds,
799
+ parentToolUseId: event.parentToolUseId,
1065
800
  }
1066
- }
1067
- });
1068
- // Emit status update: CLI finished processing (real-time)
1069
- const exitCode = msg.is_error ? 1 : 0;
1070
- // Clean up abort controller
1071
- this.promptAbortControllers.delete(promptId);
1072
- // Update status immediately based on exit code
1073
- onStatusUpdate?.(exitCode === 0 ? 'completed' : 'error');
1074
- onComplete(exitCode);
1075
- session.activeQueryStream = undefined;
1076
- break; // Prompt complete, continue to next in queue
1077
- }
1078
- else if (message.type === 'user') {
1079
- const msg = message;
1080
- // SDK sends tool results with TWO data sources:
1081
- // message.content[0] — {type:'tool_result', tool_use_id:'...', content: <raw text>}
1082
- // msg.tool_use_result — structured object with {stdout,stderr} for Bash,
1083
- // {type,file} for Read, or [{type:'text',text:'...'}] for MCP tools
1084
- //
1085
- // The tool_use_id lives ONLY in message.content[].tool_use_id.
1086
- // The structured result lives in tool_use_result for built-in tools.
1087
- // For MCP tools, tool_use_result is a content-block array we need to parse.
1088
- let toolResult = null;
1089
- let toolUseId = null;
1090
- // STEP 1: Extract tool_use_id from message.content (authoritative source)
1091
- if (Array.isArray(msg.message?.content)) {
1092
- const contentBlocks = msg.message.content;
1093
- const toolResultBlock = contentBlocks.find((c) => c.type === 'tool_result');
1094
- if (toolResultBlock?.tool_use_id) {
1095
- toolUseId = toolResultBlock.tool_use_id;
1096
- }
1097
- }
1098
- // STEP 1b: Fallback — use parent_tool_use_id if STEP 1 didn't find it
1099
- // (subagent results where the message.content may not contain tool_result block)
1100
- if (!toolUseId && msg.parent_tool_use_id) {
1101
- toolUseId = msg.parent_tool_use_id;
801
+ });
802
+ break;
1102
803
  }
1103
- // STEP 2: Use tool_use_result as the primary data source.
1104
- // For built-in tools (Bash, Read, Edit, Write, Glob, Grep) it's a structured
1105
- // object like {stdout, stderr} or {type:'text', file:{...}} — use directly.
1106
- // For MCP tools it's a content-block array [{type:'text', text:'...'}] — parse text.
1107
- if (msg.tool_use_result) {
1108
- const raw = msg.tool_use_result;
1109
- if (Array.isArray(raw)) {
1110
- // MCP tool result: [{type:'text', text:'...'}] — extract and parse
1111
- const textParts = raw
1112
- .filter((c) => c.type === 'text')
1113
- .map((c) => c.text);
1114
- const rawContent = textParts.join('\n');
1115
- try {
1116
- toolResult = JSON.parse(rawContent);
1117
- }
1118
- catch {
1119
- toolResult = { content: rawContent, type: 'text' };
1120
- }
1121
- }
1122
- else if (typeof raw === 'object' && raw !== null) {
1123
- // Built-in tool result: {stdout, stderr, ...} or {type, file, ...} — use directly
1124
- toolResult = raw;
1125
- }
1126
- else if (typeof raw === 'string') {
1127
- try {
1128
- toolResult = JSON.parse(raw);
1129
- }
1130
- catch {
1131
- toolResult = { content: raw, type: 'text' };
804
+ case 'system': {
805
+ onOutput({
806
+ type: 'system',
807
+ data: event.raw,
808
+ timestamp: Date.now(),
809
+ metadata: {
810
+ subtype: event.subtype,
811
+ messageType: event.subtype || 'system',
1132
812
  }
1133
- }
813
+ });
814
+ break;
1134
815
  }
1135
- // STEP 3: Fallback to parsing message.content if tool_use_result wasn't available
1136
- // (e.g. subagent calls where tool_use_result may be absent)
1137
- if (!toolResult && Array.isArray(msg.message?.content)) {
1138
- const contentBlocks = msg.message.content;
1139
- const toolResultBlock = contentBlocks.find((c) => c.type === 'tool_result');
1140
- if (toolResultBlock) {
1141
- if (typeof toolResultBlock.content === 'string') {
1142
- try {
1143
- toolResult = JSON.parse(toolResultBlock.content);
1144
- }
1145
- catch {
1146
- toolResult = { content: toolResultBlock.content, type: 'text' };
1147
- }
1148
- }
1149
- else if (Array.isArray(toolResultBlock.content)) {
1150
- const textParts = toolResultBlock.content
1151
- .filter((c) => c.type === 'text')
1152
- .map((c) => c.text);
1153
- const rawContent = textParts.join('\n');
1154
- try {
1155
- toolResult = JSON.parse(rawContent);
1156
- }
1157
- catch {
1158
- toolResult = { content: rawContent, type: 'text' };
816
+ case 'result': {
817
+ // Update usage tracking
818
+ if (event.usage) {
819
+ session.totalInputTokens += event.usage.inputTokens;
820
+ session.totalOutputTokens += event.usage.outputTokens;
821
+ session.totalCostUsd += event.usage.totalCostUsd || 0;
822
+ session.lastUsage = {
823
+ inputTokens: event.usage.inputTokens,
824
+ outputTokens: event.usage.outputTokens,
825
+ totalTokens: event.usage.inputTokens + event.usage.outputTokens,
826
+ };
827
+ }
828
+ const exitCode = event.isError ? 1 : 0;
829
+ onOutput({
830
+ type: 'result',
831
+ data: event.raw,
832
+ timestamp: Date.now(),
833
+ metadata: {
834
+ isError: event.isError,
835
+ exitCode,
836
+ durationMs: event.usage?.durationMs,
837
+ totalCostUsd: event.usage?.totalCostUsd,
838
+ numTurns: event.usage?.numTurns,
839
+ contextInfo: {
840
+ messageCount: session.messages.length,
841
+ totalInputTokens: session.totalInputTokens,
842
+ totalOutputTokens: session.totalOutputTokens,
843
+ totalTokens: session.totalInputTokens + session.totalOutputTokens,
844
+ totalCostUsd: session.totalCostUsd,
845
+ lastUsage: session.lastUsage,
1159
846
  }
1160
847
  }
1161
- else {
1162
- toolResult = toolResultBlock;
1163
- }
1164
- }
848
+ });
849
+ this.promptAbortControllers.delete(promptId);
850
+ onStatusUpdate?.(exitCode === 0 ? 'completed' : 'error');
851
+ onComplete(exitCode);
852
+ session.activeBackendStream = undefined;
853
+ break; // Prompt complete, continue to next in queue
1165
854
  }
1166
- if (toolResult) {
1167
- // Map lookup is O(1), falls back to O(n) history scan, then heuristic
1168
- const mappedName = toolUseId ? session.toolNameMap.get(toolUseId) ?? null : null;
1169
- const historyName = mappedName || lookupToolNameFromHistory(session.messages, toolUseId);
1170
- const detectedName = historyName || extractToolName(toolResult);
1171
- const resolvedName = detectedName;
855
+ case 'progress': {
1172
856
  onOutput({
1173
- type: 'tool_result',
1174
- data: toolResult,
857
+ type: 'progress',
858
+ data: event.raw || { message: event.message },
1175
859
  timestamp: Date.now(),
1176
860
  metadata: {
1177
- toolName: resolvedName,
1178
- toolResult: toolResult,
1179
- toolUseId: toolUseId || undefined,
1180
- parentToolUseId: msg.parent_tool_use_id,
1181
- isSynthetic: msg.isSynthetic
861
+ progress: { message: event.message },
1182
862
  }
1183
863
  });
864
+ break;
1184
865
  }
1185
- else {
866
+ case 'stream_event': {
1186
867
  onOutput({
1187
- type: 'user',
1188
- data: msg.message,
868
+ type: 'stream_event',
869
+ data: event.raw,
1189
870
  timestamp: Date.now(),
1190
871
  metadata: {
1191
- parentToolUseId: null,
1192
- isSynthetic: msg.isSynthetic
872
+ parentToolUseId: event.parentToolUseId,
1193
873
  }
1194
874
  });
875
+ break;
876
+ }
877
+ case 'rate_limit': {
878
+ onOutput({
879
+ type: 'rate_limit_event',
880
+ data: event.raw,
881
+ timestamp: Date.now(),
882
+ });
883
+ break;
884
+ }
885
+ case 'prompt_suggestion': {
886
+ onOutput({
887
+ type: 'prompt_suggestion',
888
+ data: event.suggestion,
889
+ timestamp: Date.now(),
890
+ });
891
+ break;
1195
892
  }
1196
- }
1197
- else if (message.type === 'system') {
1198
- const sysMsg = message;
1199
- onOutput({
1200
- type: 'system',
1201
- data: sysMsg,
1202
- timestamp: Date.now(),
1203
- metadata: {
1204
- subtype: sysMsg.subtype,
1205
- messageType: sysMsg.subtype || 'system'
1206
- }
1207
- });
1208
- }
1209
- else if (message.type === 'tool_progress') {
1210
- const msg = message;
1211
- onOutput({
1212
- type: 'tool_progress',
1213
- data: msg,
1214
- timestamp: Date.now(),
1215
- metadata: {
1216
- toolName: msg.tool_name,
1217
- toolUseId: msg.tool_use_id,
1218
- elapsedTimeSeconds: msg.elapsed_time_seconds,
1219
- parentToolUseId: msg.parent_tool_use_id
1220
- }
1221
- });
1222
- }
1223
- else if (message.type === 'auth_status') {
1224
- onOutput({
1225
- type: 'auth_status',
1226
- data: message,
1227
- timestamp: Date.now(),
1228
- metadata: {
1229
- isAuthenticating: message.isAuthenticating,
1230
- error: message.error
1231
- }
1232
- });
1233
- }
1234
- else if (message.type === 'stream_event') {
1235
- onOutput({
1236
- type: 'stream_event',
1237
- data: message.event || message,
1238
- timestamp: Date.now(),
1239
- metadata: {
1240
- parentToolUseId: message.parent_tool_use_id
1241
- }
1242
- });
1243
- }
1244
- else if (message.type === 'tool_use_summary') {
1245
- const msg = message;
1246
- onOutput({
1247
- type: 'tool_use_summary',
1248
- data: msg.summary || '',
1249
- timestamp: Date.now(),
1250
- metadata: {
1251
- precedingToolUseIds: msg.preceding_tool_use_ids,
1252
- uuid: msg.uuid,
1253
- sessionId: msg.session_id
1254
- }
1255
- });
1256
- }
1257
- else if (message.type === 'rate_limit_event') {
1258
- const msg = message;
1259
- onOutput({
1260
- type: 'rate_limit_event',
1261
- data: msg,
1262
- timestamp: Date.now(),
1263
- metadata: {
1264
- rateLimitInfo: msg.rate_limit_info,
1265
- uuid: msg.uuid,
1266
- sessionId: msg.session_id
1267
- }
1268
- });
1269
- }
1270
- else if (message.type === 'prompt_suggestion') {
1271
- const msg = message;
1272
- onOutput({
1273
- type: 'prompt_suggestion',
1274
- data: msg.suggestion || '',
1275
- timestamp: Date.now(),
1276
- metadata: {
1277
- uuid: msg.uuid,
1278
- sessionId: msg.session_id
1279
- }
1280
- });
1281
- }
1282
- else if (message.type === 'keep_alive') {
1283
- // Internal keepalive - silently ignore
1284
- }
1285
- else {
1286
- onOutput({
1287
- type: 'stdout',
1288
- data: JSON.stringify(message, null, 2),
1289
- timestamp: Date.now()
1290
- });
1291
893
  }
1292
894
  }
1293
895
  }
1294
896
  catch (streamError) {
1295
- // Check if this was an abort-related error
1296
897
  if (session.abortController.signal.aborted || this.emergencyStopInProgress.has(sessionId)) {
1297
898
  console.log(`[agentSession] Stream aborted for prompt ${effectivePromptId}`);
1298
- // Handle abort gracefully
1299
899
  onStatusUpdate?.('cancelled');
1300
900
  onComplete(null);
1301
- session.activeQueryStream = undefined;
901
+ session.activeBackendStream = undefined;
1302
902
  session.currentPromptId = undefined;
1303
- // Use break instead of return to ensure isProcessingQueue gets reset
1304
- // after the while loop at the end of processPromptQueue
1305
903
  break;
1306
904
  }
1307
- // Re-throw non-abort errors
1308
905
  throw streamError;
1309
906
  }
1310
- session.activeQueryStream = undefined;
1311
- session.currentPromptId = undefined; // Clear current prompt when done
907
+ session.activeBackendStream = undefined;
908
+ session.currentPromptId = undefined;
1312
909
  }
1313
910
  catch (error) {
1314
911
  const currentSession = this.sessions.get(sessionId);
1315
912
  if (currentSession) {
1316
- currentSession.activeQueryStream = undefined;
913
+ currentSession.activeBackendStream = undefined;
1317
914
  }
1318
915
  // Clean up abort controller (promptId is guaranteed to exist here due to check at line 204)
1319
916
  if (promptId) {
@@ -1335,7 +932,7 @@ export class AgentSessionManager {
1335
932
  const session = this.sessions.get(sessionId);
1336
933
  if (session) {
1337
934
  session.abortController.abort();
1338
- session.activeQueryStream = undefined;
935
+ session.activeBackendStream = undefined;
1339
936
  this.sessions.delete(sessionId);
1340
937
  }
1341
938
  // Clean up persisted state
@@ -1386,59 +983,42 @@ export class AgentSessionManager {
1386
983
  const session = this.sessions.get(sessionId);
1387
984
  if (!session)
1388
985
  return;
986
+ // Delegate to backend if it supports process killing (ClaudeBackend)
987
+ if (session.backend instanceof claudeBackend.constructor) {
988
+ ;
989
+ session.backend.killProcesses();
990
+ return;
991
+ }
992
+ // Fallback: generic process cleanup
1389
993
  const isWin = process.platform === 'win32';
1390
- // 1. Kill all tracked child processes
1391
994
  if (session.childProcesses && session.childProcesses.size > 0) {
1392
995
  console.log(`[agentSession] Killing ${session.childProcesses.size} tracked child processes`);
1393
996
  for (const child of session.childProcesses) {
1394
997
  if (!child.killed) {
1395
998
  try {
1396
- if (isWin) {
1397
- // Windows: use taskkill to force kill
1398
- if (child.pid) {
1399
- spawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t'], {
1400
- stdio: 'ignore',
1401
- windowsHide: true
1402
- });
1403
- }
1404
- }
1405
- else {
1406
- // Unix: try graceful SIGTERM first, then SIGKILL
999
+ if (!isWin)
1407
1000
  child.kill('SIGTERM');
1408
- }
1409
- }
1410
- catch (e) {
1411
- // Process may already be dead
1412
1001
  }
1002
+ catch { /* already dead */ }
1413
1003
  }
1414
1004
  }
1415
- // Wait a bit for graceful shutdown, then force kill
1416
1005
  await new Promise(resolve => setTimeout(resolve, 500));
1417
1006
  for (const child of session.childProcesses) {
1418
1007
  if (!child.killed) {
1419
1008
  try {
1420
- if (!isWin && child.pid) {
1421
- // Unix: force kill with SIGKILL
1009
+ if (!isWin && child.pid)
1422
1010
  child.kill('SIGKILL');
1423
- }
1424
- }
1425
- catch (e) {
1426
- // Already dead
1427
1011
  }
1012
+ catch { /* already dead */ }
1428
1013
  }
1429
1014
  }
1430
1015
  session.childProcesses.clear();
1431
1016
  }
1432
- // 2. Kill the entire process group on Unix-like systems
1433
1017
  if (!isWin && session.claudeProcessGroupId) {
1434
1018
  try {
1435
- console.log(`[agentSession] Killing process group ${session.claudeProcessGroupId}`);
1436
- // Kill entire process group using negative PID
1437
1019
  process.kill(-session.claudeProcessGroupId, 'SIGKILL');
1438
1020
  }
1439
- catch (e) {
1440
- // Process group may already be dead
1441
- }
1021
+ catch { /* dead */ }
1442
1022
  session.claudeProcessGroupId = undefined;
1443
1023
  }
1444
1024
  }
@@ -1487,7 +1067,7 @@ export class AgentSessionManager {
1487
1067
  // 4. Clear the prompt queue
1488
1068
  session.promptQueue = [];
1489
1069
  // 5. Clear active stream
1490
- session.activeQueryStream = undefined;
1070
+ session.activeBackendStream = undefined;
1491
1071
  // 6. Reset processing state
1492
1072
  session.isProcessingQueue = false;
1493
1073
  // 7. Clean up abort controllers map (only for this session's prompts, not ALL sessions)
@@ -1512,18 +1092,5 @@ export class AgentSessionManager {
1512
1092
  return { success: false, message: error.message || 'Emergency stop failed' };
1513
1093
  }
1514
1094
  }
1515
- /**
1516
- * Build a fresh MCP server for a query call.
1517
- * The SDK's query() connects the MCP server's internal transport, so we cannot
1518
- * reuse a single instance across multiple queries. This must be called fresh each time.
1519
- */
1520
- buildMcpServer(sessionId, attachmentDir, promptId) {
1521
- console.log(`[buildMcpServer] Session ${sessionId}: attachmentDir=${attachmentDir || 'none'}, promptId=${promptId || 'none'}`);
1522
- return createModuleMcpServer({
1523
- attachmentDir,
1524
- sessionId,
1525
- promptId,
1526
- });
1527
- }
1528
1095
  }
1529
1096
  export const agentSessionManager = new AgentSessionManager();