@exreve/exk 1.0.60 → 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,371 +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 = msg.parent_tool_use_id;
1090
- // STEP 1: Always 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
- }
801
+ });
802
+ break;
1097
803
  }
1098
- // STEP 2: Use tool_use_result as the primary data source.
1099
- // For built-in tools (Bash, Read, Edit, Write, Glob, Grep) it's a structured
1100
- // object like {stdout, stderr} or {type:'text', file:{...}} — use directly.
1101
- // For MCP tools it's a content-block array [{type:'text', text:'...'}] — parse text.
1102
- if (msg.tool_use_result) {
1103
- const raw = msg.tool_use_result;
1104
- if (Array.isArray(raw)) {
1105
- // MCP tool result: [{type:'text', text:'...'}] — extract and parse
1106
- const textParts = raw
1107
- .filter((c) => c.type === 'text')
1108
- .map((c) => c.text);
1109
- const rawContent = textParts.join('\n');
1110
- try {
1111
- toolResult = JSON.parse(rawContent);
1112
- }
1113
- catch {
1114
- toolResult = { content: rawContent, type: 'text' };
1115
- }
1116
- }
1117
- else if (typeof raw === 'object' && raw !== null) {
1118
- // Built-in tool result: {stdout, stderr, ...} or {type, file, ...} — use directly
1119
- toolResult = raw;
1120
- }
1121
- else if (typeof raw === 'string') {
1122
- try {
1123
- toolResult = JSON.parse(raw);
1124
- }
1125
- catch {
1126
- 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',
1127
812
  }
1128
- }
813
+ });
814
+ break;
1129
815
  }
1130
- // STEP 3: Fallback to parsing message.content if tool_use_result wasn't available
1131
- // (e.g. subagent calls where tool_use_result may be absent)
1132
- if (!toolResult && Array.isArray(msg.message?.content)) {
1133
- const contentBlocks = msg.message.content;
1134
- const toolResultBlock = contentBlocks.find((c) => c.type === 'tool_result');
1135
- if (toolResultBlock) {
1136
- if (typeof toolResultBlock.content === 'string') {
1137
- try {
1138
- toolResult = JSON.parse(toolResultBlock.content);
1139
- }
1140
- catch {
1141
- toolResult = { content: toolResultBlock.content, type: 'text' };
1142
- }
1143
- }
1144
- else if (Array.isArray(toolResultBlock.content)) {
1145
- const textParts = toolResultBlock.content
1146
- .filter((c) => c.type === 'text')
1147
- .map((c) => c.text);
1148
- const rawContent = textParts.join('\n');
1149
- try {
1150
- toolResult = JSON.parse(rawContent);
1151
- }
1152
- catch {
1153
- 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,
1154
846
  }
1155
847
  }
1156
- else {
1157
- toolResult = toolResultBlock;
1158
- }
1159
- }
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
1160
854
  }
1161
- if (toolResult) {
1162
- // Map lookup is O(1), falls back to O(n) history scan, then heuristic
1163
- const mappedName = toolUseId ? session.toolNameMap.get(toolUseId) ?? null : null;
1164
- const historyName = mappedName || lookupToolNameFromHistory(session.messages, toolUseId);
1165
- const detectedName = historyName || extractToolName(toolResult);
1166
- const resolvedName = detectedName;
855
+ case 'progress': {
1167
856
  onOutput({
1168
- type: 'tool_result',
1169
- data: toolResult,
857
+ type: 'progress',
858
+ data: event.raw || { message: event.message },
1170
859
  timestamp: Date.now(),
1171
860
  metadata: {
1172
- toolName: resolvedName,
1173
- toolResult: toolResult,
1174
- toolUseId: toolUseId || undefined,
1175
- parentToolUseId: msg.parent_tool_use_id,
1176
- isSynthetic: msg.isSynthetic
861
+ progress: { message: event.message },
1177
862
  }
1178
863
  });
864
+ break;
1179
865
  }
1180
- else {
866
+ case 'stream_event': {
1181
867
  onOutput({
1182
- type: 'user',
1183
- data: msg.message,
868
+ type: 'stream_event',
869
+ data: event.raw,
1184
870
  timestamp: Date.now(),
1185
871
  metadata: {
1186
- parentToolUseId: null,
1187
- isSynthetic: msg.isSynthetic
872
+ parentToolUseId: event.parentToolUseId,
1188
873
  }
1189
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;
1190
892
  }
1191
- }
1192
- else if (message.type === 'system') {
1193
- const sysMsg = message;
1194
- onOutput({
1195
- type: 'system',
1196
- data: sysMsg,
1197
- timestamp: Date.now(),
1198
- metadata: {
1199
- subtype: sysMsg.subtype,
1200
- messageType: sysMsg.subtype || 'system'
1201
- }
1202
- });
1203
- }
1204
- else if (message.type === 'tool_progress') {
1205
- const msg = message;
1206
- onOutput({
1207
- type: 'tool_progress',
1208
- data: msg,
1209
- timestamp: Date.now(),
1210
- metadata: {
1211
- toolName: msg.tool_name,
1212
- toolUseId: msg.tool_use_id,
1213
- elapsedTimeSeconds: msg.elapsed_time_seconds,
1214
- parentToolUseId: msg.parent_tool_use_id
1215
- }
1216
- });
1217
- }
1218
- else if (message.type === 'auth_status') {
1219
- onOutput({
1220
- type: 'auth_status',
1221
- data: message,
1222
- timestamp: Date.now(),
1223
- metadata: {
1224
- isAuthenticating: message.isAuthenticating,
1225
- error: message.error
1226
- }
1227
- });
1228
- }
1229
- else if (message.type === 'stream_event') {
1230
- onOutput({
1231
- type: 'stream_event',
1232
- data: message.event || message,
1233
- timestamp: Date.now(),
1234
- metadata: {
1235
- parentToolUseId: message.parent_tool_use_id
1236
- }
1237
- });
1238
- }
1239
- else if (message.type === 'tool_use_summary') {
1240
- const msg = message;
1241
- onOutput({
1242
- type: 'tool_use_summary',
1243
- data: msg.summary || '',
1244
- timestamp: Date.now(),
1245
- metadata: {
1246
- precedingToolUseIds: msg.preceding_tool_use_ids,
1247
- uuid: msg.uuid,
1248
- sessionId: msg.session_id
1249
- }
1250
- });
1251
- }
1252
- else if (message.type === 'rate_limit_event') {
1253
- const msg = message;
1254
- onOutput({
1255
- type: 'rate_limit_event',
1256
- data: msg,
1257
- timestamp: Date.now(),
1258
- metadata: {
1259
- rateLimitInfo: msg.rate_limit_info,
1260
- uuid: msg.uuid,
1261
- sessionId: msg.session_id
1262
- }
1263
- });
1264
- }
1265
- else if (message.type === 'prompt_suggestion') {
1266
- const msg = message;
1267
- onOutput({
1268
- type: 'prompt_suggestion',
1269
- data: msg.suggestion || '',
1270
- timestamp: Date.now(),
1271
- metadata: {
1272
- uuid: msg.uuid,
1273
- sessionId: msg.session_id
1274
- }
1275
- });
1276
- }
1277
- else if (message.type === 'keep_alive') {
1278
- // Internal keepalive - silently ignore
1279
- }
1280
- else {
1281
- onOutput({
1282
- type: 'stdout',
1283
- data: JSON.stringify(message, null, 2),
1284
- timestamp: Date.now()
1285
- });
1286
893
  }
1287
894
  }
1288
895
  }
1289
896
  catch (streamError) {
1290
- // Check if this was an abort-related error
1291
897
  if (session.abortController.signal.aborted || this.emergencyStopInProgress.has(sessionId)) {
1292
898
  console.log(`[agentSession] Stream aborted for prompt ${effectivePromptId}`);
1293
- // Handle abort gracefully
1294
899
  onStatusUpdate?.('cancelled');
1295
900
  onComplete(null);
1296
- session.activeQueryStream = undefined;
901
+ session.activeBackendStream = undefined;
1297
902
  session.currentPromptId = undefined;
1298
- // Use break instead of return to ensure isProcessingQueue gets reset
1299
- // after the while loop at the end of processPromptQueue
1300
903
  break;
1301
904
  }
1302
- // Re-throw non-abort errors
1303
905
  throw streamError;
1304
906
  }
1305
- session.activeQueryStream = undefined;
1306
- session.currentPromptId = undefined; // Clear current prompt when done
907
+ session.activeBackendStream = undefined;
908
+ session.currentPromptId = undefined;
1307
909
  }
1308
910
  catch (error) {
1309
911
  const currentSession = this.sessions.get(sessionId);
1310
912
  if (currentSession) {
1311
- currentSession.activeQueryStream = undefined;
913
+ currentSession.activeBackendStream = undefined;
1312
914
  }
1313
915
  // Clean up abort controller (promptId is guaranteed to exist here due to check at line 204)
1314
916
  if (promptId) {
@@ -1330,7 +932,7 @@ export class AgentSessionManager {
1330
932
  const session = this.sessions.get(sessionId);
1331
933
  if (session) {
1332
934
  session.abortController.abort();
1333
- session.activeQueryStream = undefined;
935
+ session.activeBackendStream = undefined;
1334
936
  this.sessions.delete(sessionId);
1335
937
  }
1336
938
  // Clean up persisted state
@@ -1381,59 +983,42 @@ export class AgentSessionManager {
1381
983
  const session = this.sessions.get(sessionId);
1382
984
  if (!session)
1383
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
1384
993
  const isWin = process.platform === 'win32';
1385
- // 1. Kill all tracked child processes
1386
994
  if (session.childProcesses && session.childProcesses.size > 0) {
1387
995
  console.log(`[agentSession] Killing ${session.childProcesses.size} tracked child processes`);
1388
996
  for (const child of session.childProcesses) {
1389
997
  if (!child.killed) {
1390
998
  try {
1391
- if (isWin) {
1392
- // Windows: use taskkill to force kill
1393
- if (child.pid) {
1394
- spawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t'], {
1395
- stdio: 'ignore',
1396
- windowsHide: true
1397
- });
1398
- }
1399
- }
1400
- else {
1401
- // Unix: try graceful SIGTERM first, then SIGKILL
999
+ if (!isWin)
1402
1000
  child.kill('SIGTERM');
1403
- }
1404
- }
1405
- catch (e) {
1406
- // Process may already be dead
1407
1001
  }
1002
+ catch { /* already dead */ }
1408
1003
  }
1409
1004
  }
1410
- // Wait a bit for graceful shutdown, then force kill
1411
1005
  await new Promise(resolve => setTimeout(resolve, 500));
1412
1006
  for (const child of session.childProcesses) {
1413
1007
  if (!child.killed) {
1414
1008
  try {
1415
- if (!isWin && child.pid) {
1416
- // Unix: force kill with SIGKILL
1009
+ if (!isWin && child.pid)
1417
1010
  child.kill('SIGKILL');
1418
- }
1419
- }
1420
- catch (e) {
1421
- // Already dead
1422
1011
  }
1012
+ catch { /* already dead */ }
1423
1013
  }
1424
1014
  }
1425
1015
  session.childProcesses.clear();
1426
1016
  }
1427
- // 2. Kill the entire process group on Unix-like systems
1428
1017
  if (!isWin && session.claudeProcessGroupId) {
1429
1018
  try {
1430
- console.log(`[agentSession] Killing process group ${session.claudeProcessGroupId}`);
1431
- // Kill entire process group using negative PID
1432
1019
  process.kill(-session.claudeProcessGroupId, 'SIGKILL');
1433
1020
  }
1434
- catch (e) {
1435
- // Process group may already be dead
1436
- }
1021
+ catch { /* dead */ }
1437
1022
  session.claudeProcessGroupId = undefined;
1438
1023
  }
1439
1024
  }
@@ -1482,7 +1067,7 @@ export class AgentSessionManager {
1482
1067
  // 4. Clear the prompt queue
1483
1068
  session.promptQueue = [];
1484
1069
  // 5. Clear active stream
1485
- session.activeQueryStream = undefined;
1070
+ session.activeBackendStream = undefined;
1486
1071
  // 6. Reset processing state
1487
1072
  session.isProcessingQueue = false;
1488
1073
  // 7. Clean up abort controllers map (only for this session's prompts, not ALL sessions)
@@ -1507,18 +1092,5 @@ export class AgentSessionManager {
1507
1092
  return { success: false, message: error.message || 'Emergency stop failed' };
1508
1093
  }
1509
1094
  }
1510
- /**
1511
- * Build a fresh MCP server for a query call.
1512
- * The SDK's query() connects the MCP server's internal transport, so we cannot
1513
- * reuse a single instance across multiple queries. This must be called fresh each time.
1514
- */
1515
- buildMcpServer(sessionId, attachmentDir, promptId) {
1516
- console.log(`[buildMcpServer] Session ${sessionId}: attachmentDir=${attachmentDir || 'none'}, promptId=${promptId || 'none'}`);
1517
- return createModuleMcpServer({
1518
- attachmentDir,
1519
- sessionId,
1520
- promptId,
1521
- });
1522
- }
1523
1095
  }
1524
1096
  export const agentSessionManager = new AgentSessionManager();