@exreve/exk 1.0.61 → 1.0.63

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,28 @@ 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
+ let piBackend;
277
+ try {
278
+ piBackend = require('./piBackend.js').piBackend;
279
+ }
280
+ catch (err) {
281
+ throw new Error(`Pi backend requested but module failed to load: ${err.message}. Install with: cd $(npm root -g)/@exreve/exk && npm install @mariozechner/pi-coding-agent`);
282
+ }
283
+ if (!piBackend.isAvailable()) {
284
+ throw new Error('Pi backend requested but SDK (@mariozechner/pi-coding-agent) is not installed. Install with: cd $(npm root -g)/@exreve/exk && npm install @mariozechner/pi-coding-agent');
285
+ }
286
+ return piBackend;
287
+ }
288
+ return claudeBackend;
289
+ }
397
290
  /** Set the socket reference for backend communication (called from app-child.ts) */
398
291
  setSocketRef(socket) {
399
292
  this.socketRef = socket;
@@ -500,26 +393,28 @@ export class AgentSessionManager {
500
393
  // Store the handler for this session
501
394
  this.sessionHandlers.set(sessionId, handler);
502
395
  const abortController = new AbortController();
503
- // Restore claudeSessionId from disk (survives CLI restart)
396
+ // Restore sdkSessionId from disk (survives CLI restart)
504
397
  const persistedState = loadSessionState(sessionId);
505
- const restoredClaudeSessionId = persistedState?.claudeSessionId;
506
- if (restoredClaudeSessionId) {
507
- console.log(`[AgentSessionManager] Restored claudeSessionId for session ${sessionId}: ${restoredClaudeSessionId}`);
398
+ const restoredSdkSessionId = persistedState?.claudeSessionId;
399
+ if (restoredSdkSessionId) {
400
+ console.log(`[AgentSessionManager] Restored sdkSessionId for session ${sessionId}: ${restoredSdkSessionId}`);
508
401
  }
402
+ // Select backend for this session
403
+ const backend = this.selectBackend(handler.agentBackend);
509
404
  this.sessions.set(sessionId, {
510
405
  abortController,
511
406
  messages: [],
512
- toolNameMap: new Map(),
513
407
  totalInputTokens: 0,
514
408
  totalOutputTokens: 0,
515
409
  totalCostUsd: 0,
516
410
  promptQueue: [],
517
411
  isProcessingQueue: false,
518
- claudeSessionId: restoredClaudeSessionId, // Restored from disk or undefined
412
+ sdkSessionId: restoredSdkSessionId, // Restored from disk or undefined
519
413
  childProcesses: new Set(),
520
414
  claudeProcessGroupId: undefined,
521
415
  currentPromptId: undefined,
522
416
  model: sessionModel,
417
+ backend,
523
418
  });
524
419
  // Auto-regenerate CLAUDE.md for fresh project context
525
420
  await this.regenerateClaudeMd(projectPath);
@@ -564,7 +459,7 @@ export class AgentSessionManager {
564
459
  if (!session.isProcessingQueue) {
565
460
  this.processPromptQueue(sessionId);
566
461
  }
567
- else if (session.isProcessingQueue && !session.activeQueryStream && !this.emergencyStopInProgress.has(sessionId)) {
462
+ else if (session.isProcessingQueue && !session.activeBackendStream && !this.emergencyStopInProgress.has(sessionId)) {
568
463
  // Safety: isProcessingQueue is true but there's no active stream and no emergency stop
569
464
  // This means the queue got stuck (e.g. from a previous abort return that bypassed cleanup)
570
465
  console.warn(`[agentSession] Queue stuck detected for session ${sessionId}, resetting isProcessingQueue`);
@@ -627,16 +522,16 @@ export class AgentSessionManager {
627
522
  // This ensures real-time status updates before any async operations
628
523
  onStatusUpdate?.('running');
629
524
  // Wait for current query to finish before starting next prompt
630
- if (session.activeQueryStream !== undefined) {
525
+ if (session.activeBackendStream !== undefined) {
631
526
  try {
632
- for await (const _ of session.activeQueryStream) { }
527
+ for await (const _ of session.activeBackendStream) { }
633
528
  }
634
529
  catch (err) {
635
- console.error(`[AgentSession] Error draining active query stream:`, err);
530
+ console.error(`[AgentSession] Error draining active backend stream:`, err);
636
531
  }
637
- session.activeQueryStream = undefined;
532
+ session.activeBackendStream = undefined;
638
533
  }
639
- session.activeQueryStream = undefined;
534
+ session.activeBackendStream = undefined;
640
535
  // Build final prompt with enhancers
641
536
  let finalPrompt = effectivePrompt;
642
537
  if (enhancers && enhancers.length > 0) {
@@ -691,155 +586,14 @@ export class AgentSessionManager {
691
586
  totalTokens: session.totalInputTokens + session.totalOutputTokens
692
587
  }
693
588
  });
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
589
+ // Resolve provider and model configuration
837
590
  const sessionModel = session.model || CLAUDE_CONFIG.model;
838
- // Resolve local model adapter overrides (if using OpenAI-compatible endpoint)
839
591
  let effectiveModel = sessionModel;
840
- let effectiveApiKey = queryOptions.apiKey;
841
- let effectiveEnv = queryOptions.env;
592
+ let effectiveApiKey = CLAUDE_CONFIG.apiKey;
593
+ let effectiveEnv = envForClaudeCodeChild();
842
594
  let effectiveSettings;
595
+ let effectiveProvider;
596
+ // Resolve local model adapter overrides (if using OpenAI-compatible endpoint)
843
597
  const localOverrides = await getLocalModelEnvOverrides(sessionModel);
844
598
  if (localOverrides) {
845
599
  effectiveModel = unwrapModelName(sessionModel);
@@ -849,12 +603,8 @@ export class AgentSessionManager {
849
603
  ANTHROPIC_API_KEY: localOverrides.apiKey,
850
604
  ANTHROPIC_BASE_URL: localOverrides.baseUrl,
851
605
  };
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
606
  effectiveSettings = { env: { ANTHROPIC_API_KEY: localOverrides.apiKey, ANTHROPIC_BASE_URL: localOverrides.baseUrl } };
856
607
  console.log(`[agentSession] Using local model adapter: ${sessionModel} -> ${localOverrides.baseUrl}`);
857
- console.log(`[agentSession] effectiveSettings for local model:`, JSON.stringify(effectiveSettings));
858
608
  }
859
609
  else {
860
610
  // Resolve provider for multi-provider switching (Z.ai / MiniMax)
@@ -862,6 +612,7 @@ export class AgentSessionManager {
862
612
  console.log(`[agentSession] Resolved provider: ${resolved.provider} for model: ${sessionModel}`);
863
613
  effectiveApiKey = resolved.apiKey;
864
614
  effectiveEnv = envForClaudeCodeChild(undefined, resolved);
615
+ effectiveProvider = resolved.provider;
865
616
  // Build settings env to prevent ~/.claude/settings.json from overriding our credentials
866
617
  const settingsEnv = {
867
618
  ANTHROPIC_API_KEY: resolved.apiKey,
@@ -890,7 +641,7 @@ export class AgentSessionManager {
890
641
  apiKey: resolved.apiKey,
891
642
  model: resolved.model,
892
643
  });
893
- effectiveApiKey = 'cerebras-via-proxy'; // Proxy doesn't validate the key
644
+ effectiveApiKey = 'cerebras-via-proxy';
894
645
  effectiveEnv = envForClaudeCodeChild(undefined, { ...resolved, baseUrl: proxyUrl });
895
646
  settingsEnv.ANTHROPIC_API_KEY = effectiveApiKey;
896
647
  settingsEnv.ANTHROPIC_BASE_URL = proxyUrl;
@@ -903,39 +654,44 @@ export class AgentSessionManager {
903
654
  effectiveSettings = { env: settingsEnv };
904
655
  console.log(`[agentSession] Provider: ${resolved.provider}, baseUrl: ${resolved.baseUrl}, model: ${resolved.model}`);
905
656
  }
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)
657
+ // Determine if we should resume (only for Claude backend, matching provider)
658
+ let resumeSessionId;
659
+ if (session.sdkSessionId && !localOverrides) {
660
+ const persisted = loadSessionState(sessionId);
661
+ const currentProvider = resolveProvider(sessionModel).provider;
662
+ if (persisted?.provider === currentProvider) {
663
+ resumeSessionId = session.sdkSessionId;
924
664
  }
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
665
+ }
666
+ // Build backend config
667
+ const backendConfig = {
668
+ cwd: projectPath,
669
+ apiKey: effectiveApiKey,
670
+ model: effectiveModel,
671
+ baseUrl: effectiveEnv.ANTHROPIC_BASE_URL,
672
+ provider: effectiveProvider,
673
+ resumeSessionId,
674
+ env: effectiveEnv,
675
+ settings: effectiveSettings,
676
+ attachmentDir,
677
+ routingSessionId: sessionId,
678
+ routingPromptId: promptId,
679
+ signal: abortController.signal,
680
+ };
681
+ // Execute prompt via the selected backend
682
+ const backendStream = session.backend.executePrompt(finalPrompt, backendConfig);
683
+ session.activeBackendStream = backendStream;
684
+ // Process backend events and map to SessionOutput
685
+ const abortCheckInterval = 200;
930
686
  let lastAbortCheck = Date.now();
931
687
  try {
932
- for await (const message of queryStream) {
933
- // Check abort on each message (existing behavior)
688
+ for await (const event of backendStream) {
689
+ // Check abort on each event
934
690
  if (session.abortController.signal.aborted || this.emergencyStopInProgress.has(sessionId)) {
935
691
  console.log(`[agentSession] Aborting prompt ${effectivePromptId} - abort signal received`);
936
692
  break;
937
693
  }
938
- // Periodic check for long-running operations
694
+ // Periodic abort check
939
695
  const now = Date.now();
940
696
  if (now - lastAbortCheck > abortCheckInterval) {
941
697
  lastAbortCheck = now;
@@ -944,376 +700,218 @@ export class AgentSessionManager {
944
700
  break;
945
701
  }
946
702
  }
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) {
703
+ // Map BackendEvent SessionOutput
704
+ switch (event.type) {
705
+ case 'init': {
706
+ // SDK session ID received
707
+ const newSessionId = event.sessionId;
708
+ if (session.sdkSessionId && session.sdkSessionId !== newSessionId) {
953
709
  session.contextLost = true;
954
- console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.claudeSessionId} → ${systemMsg.session_id}`);
710
+ console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.sdkSessionId} → ${newSessionId}`);
955
711
  }
956
- session.claudeSessionId = systemMsg.session_id;
957
- saveSessionState(sessionId, { claudeSessionId: systemMsg.session_id, model: session.model, provider: resolveProvider(session.model).provider, updatedAt: Date.now() });
712
+ session.sdkSessionId = newSessionId;
713
+ saveSessionState(sessionId, {
714
+ claudeSessionId: newSessionId,
715
+ model: session.model,
716
+ provider: resolveProvider(session.model).provider,
717
+ updatedAt: Date.now(),
718
+ });
719
+ break;
958
720
  }
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) {
721
+ case 'assistant_message': {
722
+ // Update SDK session ID if provided
723
+ if (event.sdkSessionId) {
724
+ if (session.sdkSessionId && session.sdkSessionId !== event.sdkSessionId) {
725
+ session.contextLost = true;
726
+ console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.sdkSessionId} ${event.sdkSessionId}`);
727
+ }
728
+ session.sdkSessionId = event.sdkSessionId;
729
+ saveSessionState(sessionId, {
730
+ claudeSessionId: event.sdkSessionId,
731
+ model: session.model,
732
+ provider: resolveProvider(session.model).provider,
733
+ updatedAt: Date.now(),
734
+ });
735
+ }
736
+ // Handle context window error
737
+ if (event.isContextWindowError) {
738
+ console.warn(`[AgentSessionManager] Context window limit reached for session ${sessionId}. Clearing context.`);
739
+ session.sdkSessionId = undefined;
965
740
  session.contextLost = true;
966
- console.warn(`[AgentSessionManager] Context lost! Session ID changed in assistant msg: ${session.claudeSessionId} → ${msg.session_id}`);
741
+ deleteSessionState(sessionId);
742
+ onOutput({
743
+ type: 'system',
744
+ data: {
745
+ message: 'Context window limit reached. Session context has been cleared. The next prompt will start fresh with a summary of previous conversation.',
746
+ subtype: 'context_window_reset',
747
+ },
748
+ timestamp: Date.now(),
749
+ metadata: {
750
+ subtype: 'context_window_reset',
751
+ contextInfo: {
752
+ messageCount: session.messages.length,
753
+ totalInputTokens: session.totalInputTokens,
754
+ totalOutputTokens: session.totalOutputTokens,
755
+ totalTokens: session.totalInputTokens + session.totalOutputTokens,
756
+ }
757
+ }
758
+ });
967
759
  }
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
760
+ session.messages.push({
761
+ role: 'assistant',
762
+ content: event.raw,
763
+ timestamp: Date.now()
764
+ });
983
765
  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
- },
766
+ type: 'assistant',
767
+ data: event.raw,
989
768
  timestamp: Date.now(),
990
769
  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
- }
770
+ error: event.error,
771
+ contextSize: session.messages.length,
998
772
  }
999
773
  });
774
+ break;
1000
775
  }
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);
776
+ case 'tool_result': {
777
+ onOutput({
778
+ type: 'tool_result',
779
+ data: event.result,
780
+ timestamp: Date.now(),
781
+ metadata: {
782
+ toolName: event.toolName ?? undefined,
783
+ toolResult: event.result,
784
+ toolUseId: event.toolUseId || undefined,
785
+ parentToolUseId: event.parentToolUseId ?? undefined,
786
+ isSynthetic: event.isSynthetic,
1011
787
  }
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
- };
788
+ });
789
+ break;
1042
790
  }
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
791
+ case 'tool_progress': {
792
+ onOutput({
793
+ type: 'tool_progress',
794
+ data: event.raw,
795
+ timestamp: Date.now(),
796
+ metadata: {
797
+ toolName: event.toolName,
798
+ toolUseId: event.toolUseId,
799
+ elapsedTimeSeconds: event.elapsedTimeSeconds,
800
+ parentToolUseId: event.parentToolUseId,
1065
801
  }
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;
802
+ });
803
+ break;
1102
804
  }
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' };
805
+ case 'system': {
806
+ onOutput({
807
+ type: 'system',
808
+ data: event.raw,
809
+ timestamp: Date.now(),
810
+ metadata: {
811
+ subtype: event.subtype,
812
+ messageType: event.subtype || 'system',
1132
813
  }
1133
- }
814
+ });
815
+ break;
1134
816
  }
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' };
817
+ case 'result': {
818
+ // Update usage tracking
819
+ if (event.usage) {
820
+ session.totalInputTokens += event.usage.inputTokens;
821
+ session.totalOutputTokens += event.usage.outputTokens;
822
+ session.totalCostUsd += event.usage.totalCostUsd || 0;
823
+ session.lastUsage = {
824
+ inputTokens: event.usage.inputTokens,
825
+ outputTokens: event.usage.outputTokens,
826
+ totalTokens: event.usage.inputTokens + event.usage.outputTokens,
827
+ };
828
+ }
829
+ const exitCode = event.isError ? 1 : 0;
830
+ onOutput({
831
+ type: 'result',
832
+ data: event.raw,
833
+ timestamp: Date.now(),
834
+ metadata: {
835
+ isError: event.isError,
836
+ exitCode,
837
+ durationMs: event.usage?.durationMs,
838
+ totalCostUsd: event.usage?.totalCostUsd,
839
+ numTurns: event.usage?.numTurns,
840
+ contextInfo: {
841
+ messageCount: session.messages.length,
842
+ totalInputTokens: session.totalInputTokens,
843
+ totalOutputTokens: session.totalOutputTokens,
844
+ totalTokens: session.totalInputTokens + session.totalOutputTokens,
845
+ totalCostUsd: session.totalCostUsd,
846
+ lastUsage: session.lastUsage,
1159
847
  }
1160
848
  }
1161
- else {
1162
- toolResult = toolResultBlock;
1163
- }
1164
- }
849
+ });
850
+ this.promptAbortControllers.delete(promptId);
851
+ onStatusUpdate?.(exitCode === 0 ? 'completed' : 'error');
852
+ onComplete(exitCode);
853
+ session.activeBackendStream = undefined;
854
+ break; // Prompt complete, continue to next in queue
1165
855
  }
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;
856
+ case 'progress': {
1172
857
  onOutput({
1173
- type: 'tool_result',
1174
- data: toolResult,
858
+ type: 'progress',
859
+ data: event.raw || { message: event.message },
1175
860
  timestamp: Date.now(),
1176
861
  metadata: {
1177
- toolName: resolvedName,
1178
- toolResult: toolResult,
1179
- toolUseId: toolUseId || undefined,
1180
- parentToolUseId: msg.parent_tool_use_id,
1181
- isSynthetic: msg.isSynthetic
862
+ progress: { message: event.message },
1182
863
  }
1183
864
  });
865
+ break;
1184
866
  }
1185
- else {
867
+ case 'stream_event': {
1186
868
  onOutput({
1187
- type: 'user',
1188
- data: msg.message,
869
+ type: 'stream_event',
870
+ data: event.raw,
1189
871
  timestamp: Date.now(),
1190
872
  metadata: {
1191
- parentToolUseId: null,
1192
- isSynthetic: msg.isSynthetic
873
+ parentToolUseId: event.parentToolUseId,
1193
874
  }
1194
875
  });
876
+ break;
877
+ }
878
+ case 'rate_limit': {
879
+ onOutput({
880
+ type: 'rate_limit_event',
881
+ data: event.raw,
882
+ timestamp: Date.now(),
883
+ });
884
+ break;
885
+ }
886
+ case 'prompt_suggestion': {
887
+ onOutput({
888
+ type: 'prompt_suggestion',
889
+ data: event.suggestion,
890
+ timestamp: Date.now(),
891
+ });
892
+ break;
1195
893
  }
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
894
  }
1292
895
  }
1293
896
  }
1294
897
  catch (streamError) {
1295
- // Check if this was an abort-related error
1296
898
  if (session.abortController.signal.aborted || this.emergencyStopInProgress.has(sessionId)) {
1297
899
  console.log(`[agentSession] Stream aborted for prompt ${effectivePromptId}`);
1298
- // Handle abort gracefully
1299
900
  onStatusUpdate?.('cancelled');
1300
901
  onComplete(null);
1301
- session.activeQueryStream = undefined;
902
+ session.activeBackendStream = undefined;
1302
903
  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
904
  break;
1306
905
  }
1307
- // Re-throw non-abort errors
1308
906
  throw streamError;
1309
907
  }
1310
- session.activeQueryStream = undefined;
1311
- session.currentPromptId = undefined; // Clear current prompt when done
908
+ session.activeBackendStream = undefined;
909
+ session.currentPromptId = undefined;
1312
910
  }
1313
911
  catch (error) {
1314
912
  const currentSession = this.sessions.get(sessionId);
1315
913
  if (currentSession) {
1316
- currentSession.activeQueryStream = undefined;
914
+ currentSession.activeBackendStream = undefined;
1317
915
  }
1318
916
  // Clean up abort controller (promptId is guaranteed to exist here due to check at line 204)
1319
917
  if (promptId) {
@@ -1335,7 +933,7 @@ export class AgentSessionManager {
1335
933
  const session = this.sessions.get(sessionId);
1336
934
  if (session) {
1337
935
  session.abortController.abort();
1338
- session.activeQueryStream = undefined;
936
+ session.activeBackendStream = undefined;
1339
937
  this.sessions.delete(sessionId);
1340
938
  }
1341
939
  // Clean up persisted state
@@ -1386,59 +984,42 @@ export class AgentSessionManager {
1386
984
  const session = this.sessions.get(sessionId);
1387
985
  if (!session)
1388
986
  return;
987
+ // Delegate to backend if it supports process killing (ClaudeBackend)
988
+ if (session.backend instanceof claudeBackend.constructor) {
989
+ ;
990
+ session.backend.killProcesses();
991
+ return;
992
+ }
993
+ // Fallback: generic process cleanup
1389
994
  const isWin = process.platform === 'win32';
1390
- // 1. Kill all tracked child processes
1391
995
  if (session.childProcesses && session.childProcesses.size > 0) {
1392
996
  console.log(`[agentSession] Killing ${session.childProcesses.size} tracked child processes`);
1393
997
  for (const child of session.childProcesses) {
1394
998
  if (!child.killed) {
1395
999
  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
1000
+ if (!isWin)
1407
1001
  child.kill('SIGTERM');
1408
- }
1409
- }
1410
- catch (e) {
1411
- // Process may already be dead
1412
1002
  }
1003
+ catch { /* already dead */ }
1413
1004
  }
1414
1005
  }
1415
- // Wait a bit for graceful shutdown, then force kill
1416
1006
  await new Promise(resolve => setTimeout(resolve, 500));
1417
1007
  for (const child of session.childProcesses) {
1418
1008
  if (!child.killed) {
1419
1009
  try {
1420
- if (!isWin && child.pid) {
1421
- // Unix: force kill with SIGKILL
1010
+ if (!isWin && child.pid)
1422
1011
  child.kill('SIGKILL');
1423
- }
1424
- }
1425
- catch (e) {
1426
- // Already dead
1427
1012
  }
1013
+ catch { /* already dead */ }
1428
1014
  }
1429
1015
  }
1430
1016
  session.childProcesses.clear();
1431
1017
  }
1432
- // 2. Kill the entire process group on Unix-like systems
1433
1018
  if (!isWin && session.claudeProcessGroupId) {
1434
1019
  try {
1435
- console.log(`[agentSession] Killing process group ${session.claudeProcessGroupId}`);
1436
- // Kill entire process group using negative PID
1437
1020
  process.kill(-session.claudeProcessGroupId, 'SIGKILL');
1438
1021
  }
1439
- catch (e) {
1440
- // Process group may already be dead
1441
- }
1022
+ catch { /* dead */ }
1442
1023
  session.claudeProcessGroupId = undefined;
1443
1024
  }
1444
1025
  }
@@ -1487,7 +1068,7 @@ export class AgentSessionManager {
1487
1068
  // 4. Clear the prompt queue
1488
1069
  session.promptQueue = [];
1489
1070
  // 5. Clear active stream
1490
- session.activeQueryStream = undefined;
1071
+ session.activeBackendStream = undefined;
1491
1072
  // 6. Reset processing state
1492
1073
  session.isProcessingQueue = false;
1493
1074
  // 7. Clean up abort controllers map (only for this session's prompts, not ALL sessions)
@@ -1512,18 +1093,5 @@ export class AgentSessionManager {
1512
1093
  return { success: false, message: error.message || 'Emergency stop failed' };
1513
1094
  }
1514
1095
  }
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
1096
  }
1529
1097
  export const agentSessionManager = new AgentSessionManager();