@exreve/exk 1.0.61 → 1.0.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/agentBackend.js +12 -0
- package/dist/cli/agentSession.js +252 -685
- package/dist/cli/claudeBackend.js +535 -0
- package/dist/cli/moduleMcpServer.js +50 -311
- package/dist/cli/piBackend.js +391 -0
- package/dist/cli/sessionHandlers.js +3 -2
- package/dist/cli/sharedTools.js +259 -0
- package/dist/ttc-cli.tar.gz +0 -0
- package/package.json +1 -1
package/dist/cli/agentSession.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
395
|
+
// Restore sdkSessionId from disk (survives CLI restart)
|
|
504
396
|
const persistedState = loadSessionState(sessionId);
|
|
505
|
-
const
|
|
506
|
-
if (
|
|
507
|
-
console.log(`[AgentSessionManager] Restored
|
|
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
|
-
|
|
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.
|
|
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.
|
|
524
|
+
if (session.activeBackendStream !== undefined) {
|
|
631
525
|
try {
|
|
632
|
-
for await (const _ of session.
|
|
526
|
+
for await (const _ of session.activeBackendStream) { }
|
|
633
527
|
}
|
|
634
528
|
catch (err) {
|
|
635
|
-
console.error(`[AgentSession] Error draining active
|
|
529
|
+
console.error(`[AgentSession] Error draining active backend stream:`, err);
|
|
636
530
|
}
|
|
637
|
-
session.
|
|
531
|
+
session.activeBackendStream = undefined;
|
|
638
532
|
}
|
|
639
|
-
session.
|
|
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
|
-
//
|
|
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 =
|
|
841
|
-
let effectiveEnv =
|
|
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';
|
|
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
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
|
933
|
-
// Check abort on each
|
|
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
|
|
693
|
+
// Periodic abort check
|
|
939
694
|
const now = Date.now();
|
|
940
695
|
if (now - lastAbortCheck > abortCheckInterval) {
|
|
941
696
|
lastAbortCheck = now;
|
|
@@ -944,376 +699,218 @@ export class AgentSessionManager {
|
|
|
944
699
|
break;
|
|
945
700
|
}
|
|
946
701
|
}
|
|
947
|
-
//
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
if (session.
|
|
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.
|
|
709
|
+
console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.sdkSessionId} → ${newSessionId}`);
|
|
955
710
|
}
|
|
956
|
-
session.
|
|
957
|
-
saveSessionState(sessionId, {
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
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.
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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: '
|
|
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
|
-
|
|
992
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
totalCostUsd: msg.total_cost_usd,
|
|
1054
|
-
usage: msg.usage,
|
|
1055
|
-
modelUsage: msg.modelUsage,
|
|
1056
|
-
structuredOutput: msg.structured_output,
|
|
1057
|
-
numTurns: msg.num_turns,
|
|
1058
|
-
contextInfo: {
|
|
1059
|
-
messageCount: session.messages.length,
|
|
1060
|
-
totalInputTokens: session.totalInputTokens,
|
|
1061
|
-
totalOutputTokens: session.totalOutputTokens,
|
|
1062
|
-
totalTokens: session.totalInputTokens + session.totalOutputTokens,
|
|
1063
|
-
totalCostUsd: session.totalCostUsd,
|
|
1064
|
-
lastUsage: session.lastUsage
|
|
790
|
+
case 'tool_progress': {
|
|
791
|
+
onOutput({
|
|
792
|
+
type: 'tool_progress',
|
|
793
|
+
data: event.raw,
|
|
794
|
+
timestamp: Date.now(),
|
|
795
|
+
metadata: {
|
|
796
|
+
toolName: event.toolName,
|
|
797
|
+
toolUseId: event.toolUseId,
|
|
798
|
+
elapsedTimeSeconds: event.elapsedTimeSeconds,
|
|
799
|
+
parentToolUseId: event.parentToolUseId,
|
|
1065
800
|
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// Emit status update: CLI finished processing (real-time)
|
|
1069
|
-
const exitCode = msg.is_error ? 1 : 0;
|
|
1070
|
-
// Clean up abort controller
|
|
1071
|
-
this.promptAbortControllers.delete(promptId);
|
|
1072
|
-
// Update status immediately based on exit code
|
|
1073
|
-
onStatusUpdate?.(exitCode === 0 ? 'completed' : 'error');
|
|
1074
|
-
onComplete(exitCode);
|
|
1075
|
-
session.activeQueryStream = undefined;
|
|
1076
|
-
break; // Prompt complete, continue to next in queue
|
|
1077
|
-
}
|
|
1078
|
-
else if (message.type === 'user') {
|
|
1079
|
-
const msg = message;
|
|
1080
|
-
// SDK sends tool results with TWO data sources:
|
|
1081
|
-
// message.content[0] — {type:'tool_result', tool_use_id:'...', content: <raw text>}
|
|
1082
|
-
// msg.tool_use_result — structured object with {stdout,stderr} for Bash,
|
|
1083
|
-
// {type,file} for Read, or [{type:'text',text:'...'}] for MCP tools
|
|
1084
|
-
//
|
|
1085
|
-
// The tool_use_id lives ONLY in message.content[].tool_use_id.
|
|
1086
|
-
// The structured result lives in tool_use_result for built-in tools.
|
|
1087
|
-
// For MCP tools, tool_use_result is a content-block array we need to parse.
|
|
1088
|
-
let toolResult = null;
|
|
1089
|
-
let toolUseId = null;
|
|
1090
|
-
// STEP 1: Extract tool_use_id from message.content (authoritative source)
|
|
1091
|
-
if (Array.isArray(msg.message?.content)) {
|
|
1092
|
-
const contentBlocks = msg.message.content;
|
|
1093
|
-
const toolResultBlock = contentBlocks.find((c) => c.type === 'tool_result');
|
|
1094
|
-
if (toolResultBlock?.tool_use_id) {
|
|
1095
|
-
toolUseId = toolResultBlock.tool_use_id;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
// STEP 1b: Fallback — use parent_tool_use_id if STEP 1 didn't find it
|
|
1099
|
-
// (subagent results where the message.content may not contain tool_result block)
|
|
1100
|
-
if (!toolUseId && msg.parent_tool_use_id) {
|
|
1101
|
-
toolUseId = msg.parent_tool_use_id;
|
|
801
|
+
});
|
|
802
|
+
break;
|
|
1102
803
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const textParts = raw
|
|
1112
|
-
.filter((c) => c.type === 'text')
|
|
1113
|
-
.map((c) => c.text);
|
|
1114
|
-
const rawContent = textParts.join('\n');
|
|
1115
|
-
try {
|
|
1116
|
-
toolResult = JSON.parse(rawContent);
|
|
1117
|
-
}
|
|
1118
|
-
catch {
|
|
1119
|
-
toolResult = { content: rawContent, type: 'text' };
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
else if (typeof raw === 'object' && raw !== null) {
|
|
1123
|
-
// Built-in tool result: {stdout, stderr, ...} or {type, file, ...} — use directly
|
|
1124
|
-
toolResult = raw;
|
|
1125
|
-
}
|
|
1126
|
-
else if (typeof raw === 'string') {
|
|
1127
|
-
try {
|
|
1128
|
-
toolResult = JSON.parse(raw);
|
|
1129
|
-
}
|
|
1130
|
-
catch {
|
|
1131
|
-
toolResult = { content: raw, type: 'text' };
|
|
804
|
+
case 'system': {
|
|
805
|
+
onOutput({
|
|
806
|
+
type: 'system',
|
|
807
|
+
data: event.raw,
|
|
808
|
+
timestamp: Date.now(),
|
|
809
|
+
metadata: {
|
|
810
|
+
subtype: event.subtype,
|
|
811
|
+
messageType: event.subtype || 'system',
|
|
1132
812
|
}
|
|
1133
|
-
}
|
|
813
|
+
});
|
|
814
|
+
break;
|
|
1134
815
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
816
|
+
case 'result': {
|
|
817
|
+
// Update usage tracking
|
|
818
|
+
if (event.usage) {
|
|
819
|
+
session.totalInputTokens += event.usage.inputTokens;
|
|
820
|
+
session.totalOutputTokens += event.usage.outputTokens;
|
|
821
|
+
session.totalCostUsd += event.usage.totalCostUsd || 0;
|
|
822
|
+
session.lastUsage = {
|
|
823
|
+
inputTokens: event.usage.inputTokens,
|
|
824
|
+
outputTokens: event.usage.outputTokens,
|
|
825
|
+
totalTokens: event.usage.inputTokens + event.usage.outputTokens,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
const exitCode = event.isError ? 1 : 0;
|
|
829
|
+
onOutput({
|
|
830
|
+
type: 'result',
|
|
831
|
+
data: event.raw,
|
|
832
|
+
timestamp: Date.now(),
|
|
833
|
+
metadata: {
|
|
834
|
+
isError: event.isError,
|
|
835
|
+
exitCode,
|
|
836
|
+
durationMs: event.usage?.durationMs,
|
|
837
|
+
totalCostUsd: event.usage?.totalCostUsd,
|
|
838
|
+
numTurns: event.usage?.numTurns,
|
|
839
|
+
contextInfo: {
|
|
840
|
+
messageCount: session.messages.length,
|
|
841
|
+
totalInputTokens: session.totalInputTokens,
|
|
842
|
+
totalOutputTokens: session.totalOutputTokens,
|
|
843
|
+
totalTokens: session.totalInputTokens + session.totalOutputTokens,
|
|
844
|
+
totalCostUsd: session.totalCostUsd,
|
|
845
|
+
lastUsage: session.lastUsage,
|
|
1159
846
|
}
|
|
1160
847
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
848
|
+
});
|
|
849
|
+
this.promptAbortControllers.delete(promptId);
|
|
850
|
+
onStatusUpdate?.(exitCode === 0 ? 'completed' : 'error');
|
|
851
|
+
onComplete(exitCode);
|
|
852
|
+
session.activeBackendStream = undefined;
|
|
853
|
+
break; // Prompt complete, continue to next in queue
|
|
1165
854
|
}
|
|
1166
|
-
|
|
1167
|
-
// Map lookup is O(1), falls back to O(n) history scan, then heuristic
|
|
1168
|
-
const mappedName = toolUseId ? session.toolNameMap.get(toolUseId) ?? null : null;
|
|
1169
|
-
const historyName = mappedName || lookupToolNameFromHistory(session.messages, toolUseId);
|
|
1170
|
-
const detectedName = historyName || extractToolName(toolResult);
|
|
1171
|
-
const resolvedName = detectedName;
|
|
855
|
+
case 'progress': {
|
|
1172
856
|
onOutput({
|
|
1173
|
-
type: '
|
|
1174
|
-
data:
|
|
857
|
+
type: 'progress',
|
|
858
|
+
data: event.raw || { message: event.message },
|
|
1175
859
|
timestamp: Date.now(),
|
|
1176
860
|
metadata: {
|
|
1177
|
-
|
|
1178
|
-
toolResult: toolResult,
|
|
1179
|
-
toolUseId: toolUseId || undefined,
|
|
1180
|
-
parentToolUseId: msg.parent_tool_use_id,
|
|
1181
|
-
isSynthetic: msg.isSynthetic
|
|
861
|
+
progress: { message: event.message },
|
|
1182
862
|
}
|
|
1183
863
|
});
|
|
864
|
+
break;
|
|
1184
865
|
}
|
|
1185
|
-
|
|
866
|
+
case 'stream_event': {
|
|
1186
867
|
onOutput({
|
|
1187
|
-
type: '
|
|
1188
|
-
data:
|
|
868
|
+
type: 'stream_event',
|
|
869
|
+
data: event.raw,
|
|
1189
870
|
timestamp: Date.now(),
|
|
1190
871
|
metadata: {
|
|
1191
|
-
parentToolUseId:
|
|
1192
|
-
isSynthetic: msg.isSynthetic
|
|
872
|
+
parentToolUseId: event.parentToolUseId,
|
|
1193
873
|
}
|
|
1194
874
|
});
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
case 'rate_limit': {
|
|
878
|
+
onOutput({
|
|
879
|
+
type: 'rate_limit_event',
|
|
880
|
+
data: event.raw,
|
|
881
|
+
timestamp: Date.now(),
|
|
882
|
+
});
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
case 'prompt_suggestion': {
|
|
886
|
+
onOutput({
|
|
887
|
+
type: 'prompt_suggestion',
|
|
888
|
+
data: event.suggestion,
|
|
889
|
+
timestamp: Date.now(),
|
|
890
|
+
});
|
|
891
|
+
break;
|
|
1195
892
|
}
|
|
1196
|
-
}
|
|
1197
|
-
else if (message.type === 'system') {
|
|
1198
|
-
const sysMsg = message;
|
|
1199
|
-
onOutput({
|
|
1200
|
-
type: 'system',
|
|
1201
|
-
data: sysMsg,
|
|
1202
|
-
timestamp: Date.now(),
|
|
1203
|
-
metadata: {
|
|
1204
|
-
subtype: sysMsg.subtype,
|
|
1205
|
-
messageType: sysMsg.subtype || 'system'
|
|
1206
|
-
}
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
else if (message.type === 'tool_progress') {
|
|
1210
|
-
const msg = message;
|
|
1211
|
-
onOutput({
|
|
1212
|
-
type: 'tool_progress',
|
|
1213
|
-
data: msg,
|
|
1214
|
-
timestamp: Date.now(),
|
|
1215
|
-
metadata: {
|
|
1216
|
-
toolName: msg.tool_name,
|
|
1217
|
-
toolUseId: msg.tool_use_id,
|
|
1218
|
-
elapsedTimeSeconds: msg.elapsed_time_seconds,
|
|
1219
|
-
parentToolUseId: msg.parent_tool_use_id
|
|
1220
|
-
}
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
else if (message.type === 'auth_status') {
|
|
1224
|
-
onOutput({
|
|
1225
|
-
type: 'auth_status',
|
|
1226
|
-
data: message,
|
|
1227
|
-
timestamp: Date.now(),
|
|
1228
|
-
metadata: {
|
|
1229
|
-
isAuthenticating: message.isAuthenticating,
|
|
1230
|
-
error: message.error
|
|
1231
|
-
}
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
else if (message.type === 'stream_event') {
|
|
1235
|
-
onOutput({
|
|
1236
|
-
type: 'stream_event',
|
|
1237
|
-
data: message.event || message,
|
|
1238
|
-
timestamp: Date.now(),
|
|
1239
|
-
metadata: {
|
|
1240
|
-
parentToolUseId: message.parent_tool_use_id
|
|
1241
|
-
}
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
else if (message.type === 'tool_use_summary') {
|
|
1245
|
-
const msg = message;
|
|
1246
|
-
onOutput({
|
|
1247
|
-
type: 'tool_use_summary',
|
|
1248
|
-
data: msg.summary || '',
|
|
1249
|
-
timestamp: Date.now(),
|
|
1250
|
-
metadata: {
|
|
1251
|
-
precedingToolUseIds: msg.preceding_tool_use_ids,
|
|
1252
|
-
uuid: msg.uuid,
|
|
1253
|
-
sessionId: msg.session_id
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1257
|
-
else if (message.type === 'rate_limit_event') {
|
|
1258
|
-
const msg = message;
|
|
1259
|
-
onOutput({
|
|
1260
|
-
type: 'rate_limit_event',
|
|
1261
|
-
data: msg,
|
|
1262
|
-
timestamp: Date.now(),
|
|
1263
|
-
metadata: {
|
|
1264
|
-
rateLimitInfo: msg.rate_limit_info,
|
|
1265
|
-
uuid: msg.uuid,
|
|
1266
|
-
sessionId: msg.session_id
|
|
1267
|
-
}
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1270
|
-
else if (message.type === 'prompt_suggestion') {
|
|
1271
|
-
const msg = message;
|
|
1272
|
-
onOutput({
|
|
1273
|
-
type: 'prompt_suggestion',
|
|
1274
|
-
data: msg.suggestion || '',
|
|
1275
|
-
timestamp: Date.now(),
|
|
1276
|
-
metadata: {
|
|
1277
|
-
uuid: msg.uuid,
|
|
1278
|
-
sessionId: msg.session_id
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
else if (message.type === 'keep_alive') {
|
|
1283
|
-
// Internal keepalive - silently ignore
|
|
1284
|
-
}
|
|
1285
|
-
else {
|
|
1286
|
-
onOutput({
|
|
1287
|
-
type: 'stdout',
|
|
1288
|
-
data: JSON.stringify(message, null, 2),
|
|
1289
|
-
timestamp: Date.now()
|
|
1290
|
-
});
|
|
1291
893
|
}
|
|
1292
894
|
}
|
|
1293
895
|
}
|
|
1294
896
|
catch (streamError) {
|
|
1295
|
-
// Check if this was an abort-related error
|
|
1296
897
|
if (session.abortController.signal.aborted || this.emergencyStopInProgress.has(sessionId)) {
|
|
1297
898
|
console.log(`[agentSession] Stream aborted for prompt ${effectivePromptId}`);
|
|
1298
|
-
// Handle abort gracefully
|
|
1299
899
|
onStatusUpdate?.('cancelled');
|
|
1300
900
|
onComplete(null);
|
|
1301
|
-
session.
|
|
901
|
+
session.activeBackendStream = undefined;
|
|
1302
902
|
session.currentPromptId = undefined;
|
|
1303
|
-
// Use break instead of return to ensure isProcessingQueue gets reset
|
|
1304
|
-
// after the while loop at the end of processPromptQueue
|
|
1305
903
|
break;
|
|
1306
904
|
}
|
|
1307
|
-
// Re-throw non-abort errors
|
|
1308
905
|
throw streamError;
|
|
1309
906
|
}
|
|
1310
|
-
session.
|
|
1311
|
-
session.currentPromptId = undefined;
|
|
907
|
+
session.activeBackendStream = undefined;
|
|
908
|
+
session.currentPromptId = undefined;
|
|
1312
909
|
}
|
|
1313
910
|
catch (error) {
|
|
1314
911
|
const currentSession = this.sessions.get(sessionId);
|
|
1315
912
|
if (currentSession) {
|
|
1316
|
-
currentSession.
|
|
913
|
+
currentSession.activeBackendStream = undefined;
|
|
1317
914
|
}
|
|
1318
915
|
// Clean up abort controller (promptId is guaranteed to exist here due to check at line 204)
|
|
1319
916
|
if (promptId) {
|
|
@@ -1335,7 +932,7 @@ export class AgentSessionManager {
|
|
|
1335
932
|
const session = this.sessions.get(sessionId);
|
|
1336
933
|
if (session) {
|
|
1337
934
|
session.abortController.abort();
|
|
1338
|
-
session.
|
|
935
|
+
session.activeBackendStream = undefined;
|
|
1339
936
|
this.sessions.delete(sessionId);
|
|
1340
937
|
}
|
|
1341
938
|
// Clean up persisted state
|
|
@@ -1386,59 +983,42 @@ export class AgentSessionManager {
|
|
|
1386
983
|
const session = this.sessions.get(sessionId);
|
|
1387
984
|
if (!session)
|
|
1388
985
|
return;
|
|
986
|
+
// Delegate to backend if it supports process killing (ClaudeBackend)
|
|
987
|
+
if (session.backend instanceof claudeBackend.constructor) {
|
|
988
|
+
;
|
|
989
|
+
session.backend.killProcesses();
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
// Fallback: generic process cleanup
|
|
1389
993
|
const isWin = process.platform === 'win32';
|
|
1390
|
-
// 1. Kill all tracked child processes
|
|
1391
994
|
if (session.childProcesses && session.childProcesses.size > 0) {
|
|
1392
995
|
console.log(`[agentSession] Killing ${session.childProcesses.size} tracked child processes`);
|
|
1393
996
|
for (const child of session.childProcesses) {
|
|
1394
997
|
if (!child.killed) {
|
|
1395
998
|
try {
|
|
1396
|
-
if (isWin)
|
|
1397
|
-
// Windows: use taskkill to force kill
|
|
1398
|
-
if (child.pid) {
|
|
1399
|
-
spawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t'], {
|
|
1400
|
-
stdio: 'ignore',
|
|
1401
|
-
windowsHide: true
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
else {
|
|
1406
|
-
// Unix: try graceful SIGTERM first, then SIGKILL
|
|
999
|
+
if (!isWin)
|
|
1407
1000
|
child.kill('SIGTERM');
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
catch (e) {
|
|
1411
|
-
// Process may already be dead
|
|
1412
1001
|
}
|
|
1002
|
+
catch { /* already dead */ }
|
|
1413
1003
|
}
|
|
1414
1004
|
}
|
|
1415
|
-
// Wait a bit for graceful shutdown, then force kill
|
|
1416
1005
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1417
1006
|
for (const child of session.childProcesses) {
|
|
1418
1007
|
if (!child.killed) {
|
|
1419
1008
|
try {
|
|
1420
|
-
if (!isWin && child.pid)
|
|
1421
|
-
// Unix: force kill with SIGKILL
|
|
1009
|
+
if (!isWin && child.pid)
|
|
1422
1010
|
child.kill('SIGKILL');
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
catch (e) {
|
|
1426
|
-
// Already dead
|
|
1427
1011
|
}
|
|
1012
|
+
catch { /* already dead */ }
|
|
1428
1013
|
}
|
|
1429
1014
|
}
|
|
1430
1015
|
session.childProcesses.clear();
|
|
1431
1016
|
}
|
|
1432
|
-
// 2. Kill the entire process group on Unix-like systems
|
|
1433
1017
|
if (!isWin && session.claudeProcessGroupId) {
|
|
1434
1018
|
try {
|
|
1435
|
-
console.log(`[agentSession] Killing process group ${session.claudeProcessGroupId}`);
|
|
1436
|
-
// Kill entire process group using negative PID
|
|
1437
1019
|
process.kill(-session.claudeProcessGroupId, 'SIGKILL');
|
|
1438
1020
|
}
|
|
1439
|
-
catch
|
|
1440
|
-
// Process group may already be dead
|
|
1441
|
-
}
|
|
1021
|
+
catch { /* dead */ }
|
|
1442
1022
|
session.claudeProcessGroupId = undefined;
|
|
1443
1023
|
}
|
|
1444
1024
|
}
|
|
@@ -1487,7 +1067,7 @@ export class AgentSessionManager {
|
|
|
1487
1067
|
// 4. Clear the prompt queue
|
|
1488
1068
|
session.promptQueue = [];
|
|
1489
1069
|
// 5. Clear active stream
|
|
1490
|
-
session.
|
|
1070
|
+
session.activeBackendStream = undefined;
|
|
1491
1071
|
// 6. Reset processing state
|
|
1492
1072
|
session.isProcessingQueue = false;
|
|
1493
1073
|
// 7. Clean up abort controllers map (only for this session's prompts, not ALL sessions)
|
|
@@ -1512,18 +1092,5 @@ export class AgentSessionManager {
|
|
|
1512
1092
|
return { success: false, message: error.message || 'Emergency stop failed' };
|
|
1513
1093
|
}
|
|
1514
1094
|
}
|
|
1515
|
-
/**
|
|
1516
|
-
* Build a fresh MCP server for a query call.
|
|
1517
|
-
* The SDK's query() connects the MCP server's internal transport, so we cannot
|
|
1518
|
-
* reuse a single instance across multiple queries. This must be called fresh each time.
|
|
1519
|
-
*/
|
|
1520
|
-
buildMcpServer(sessionId, attachmentDir, promptId) {
|
|
1521
|
-
console.log(`[buildMcpServer] Session ${sessionId}: attachmentDir=${attachmentDir || 'none'}, promptId=${promptId || 'none'}`);
|
|
1522
|
-
return createModuleMcpServer({
|
|
1523
|
-
attachmentDir,
|
|
1524
|
-
sessionId,
|
|
1525
|
-
promptId,
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
1095
|
}
|
|
1529
1096
|
export const agentSessionManager = new AgentSessionManager();
|