@a1hvdy/cc-openclaw 0.6.0 → 0.7.1
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/src/engines/persistent-session.js +13 -0
- package/dist/src/engines/persistent-session.js.map +1 -1
- package/dist/src/lib/config.d.ts +2 -0
- package/dist/src/lib/config.js +19 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/trajectory.d.ts +1 -1
- package/dist/src/lib/trajectory.js.map +1 -1
- package/dist/src/lib/vendor-paths.d.ts +6 -4
- package/dist/src/lib/vendor-paths.js +21 -14
- package/dist/src/lib/vendor-paths.js.map +1 -1
- package/dist/src/openai-compat/openai-compat.d.ts +7 -1
- package/dist/src/openai-compat/openai-compat.js +35 -4
- package/dist/src/openai-compat/openai-compat.js.map +1 -1
- package/dist/src/openai-compat/sse-translator.d.ts +23 -3
- package/dist/src/openai-compat/sse-translator.js +45 -6
- package/dist/src/openai-compat/sse-translator.js.map +1 -1
- package/dist/src/types.d.ts +9 -0
- package/package.json +2 -3
- package/vendor/base-oneshot-session.d.ts +0 -87
- package/vendor/base-oneshot-session.js +0 -227
- package/vendor/base-oneshot-session.js.map +0 -1
- package/vendor/circuit-breaker.d.ts +0 -21
- package/vendor/circuit-breaker.js +0 -47
- package/vendor/circuit-breaker.js.map +0 -1
- package/vendor/consensus.d.ts +0 -20
- package/vendor/consensus.js +0 -52
- package/vendor/consensus.js.map +0 -1
- package/vendor/constants.d.ts +0 -130
- package/vendor/constants.js +0 -139
- package/vendor/constants.js.map +0 -1
- package/vendor/council.d.ts +0 -67
- package/vendor/council.js +0 -913
- package/vendor/council.js.map +0 -1
- package/vendor/embedded-server.d.ts +0 -25
- package/vendor/embedded-server.js +0 -373
- package/vendor/embedded-server.js.map +0 -1
- package/vendor/inbox-manager.d.ts +0 -38
- package/vendor/inbox-manager.js +0 -111
- package/vendor/inbox-manager.js.map +0 -1
- package/vendor/index.d.ts +0 -63
- package/vendor/index.js +0 -705
- package/vendor/index.js.map +0 -1
- package/vendor/logger.d.ts +0 -16
- package/vendor/logger.js +0 -44
- package/vendor/logger.js.map +0 -1
- package/vendor/models.d.ts +0 -69
- package/vendor/models.js +0 -289
- package/vendor/models.js.map +0 -1
- package/vendor/openai-compat.d.ts +0 -197
- package/vendor/openai-compat.js +0 -765
- package/vendor/openai-compat.js.map +0 -1
- package/vendor/persistent-codex-session.d.ts +0 -16
- package/vendor/persistent-codex-session.js +0 -105
- package/vendor/persistent-codex-session.js.map +0 -1
- package/vendor/persistent-cursor-session.d.ts +0 -21
- package/vendor/persistent-cursor-session.js +0 -241
- package/vendor/persistent-cursor-session.js.map +0 -1
- package/vendor/persistent-custom-session.d.ts +0 -78
- package/vendor/persistent-custom-session.js +0 -937
- package/vendor/persistent-custom-session.js.map +0 -1
- package/vendor/persistent-gemini-session.d.ts +0 -21
- package/vendor/persistent-gemini-session.js +0 -216
- package/vendor/persistent-gemini-session.js.map +0 -1
- package/vendor/persistent-session.d.ts +0 -74
- package/vendor/persistent-session.js +0 -698
- package/vendor/persistent-session.js.map +0 -1
- package/vendor/proxy/anthropic-adapter.d.ts +0 -136
- package/vendor/proxy/anthropic-adapter.js +0 -392
- package/vendor/proxy/anthropic-adapter.js.map +0 -1
- package/vendor/proxy/handler.d.ts +0 -39
- package/vendor/proxy/handler.js +0 -323
- package/vendor/proxy/handler.js.map +0 -1
- package/vendor/proxy/schema-cleaner.d.ts +0 -11
- package/vendor/proxy/schema-cleaner.js +0 -34
- package/vendor/proxy/schema-cleaner.js.map +0 -1
- package/vendor/proxy/thought-cache.d.ts +0 -19
- package/vendor/proxy/thought-cache.js +0 -53
- package/vendor/proxy/thought-cache.js.map +0 -1
- package/vendor/session-manager.d.ts +0 -211
- package/vendor/session-manager.js +0 -1345
- package/vendor/session-manager.js.map +0 -1
- package/vendor/skill-resolver.js +0 -107
- package/vendor/types.d.ts +0 -466
- package/vendor/types.js +0 -8
- package/vendor/types.js.map +0 -1
- package/vendor/validation.d.ts +0 -31
- package/vendor/validation.js +0 -104
- package/vendor/validation.js.map +0 -1
|
@@ -1,698 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistent Claude Code Session — wraps `claude` CLI via child_process.spawn
|
|
3
|
-
*
|
|
4
|
-
* Maintains a long-running Claude Code process with streaming JSON I/O.
|
|
5
|
-
* Enables multi-turn agent loops, continuous conversation, and real-time streaming.
|
|
6
|
-
*/
|
|
7
|
-
import { spawn } from 'node:child_process';
|
|
8
|
-
import { EventEmitter } from 'node:events';
|
|
9
|
-
import * as readline from 'node:readline';
|
|
10
|
-
import * as fs from 'node:fs';
|
|
11
|
-
import * as path from 'node:path';
|
|
12
|
-
import { getModelPricing, } from './types.js';
|
|
13
|
-
import { resolveAlias, getContextWindow, isClaudeModel } from './models.js';
|
|
14
|
-
import { CONTEXT_HIGH_THRESHOLD, MAX_HISTORY_ITEMS, DEFAULT_HISTORY_LIMIT, SESSION_READY_TIMEOUT_MS, SESSION_READY_FALLBACK_MS, TURN_TIMEOUT_MS, COMPACT_TIMEOUT_MS, STOP_SIGKILL_DELAY_MS, SESSION_EVENT, } from './constants.js';
|
|
15
|
-
// ─── PersistentClaudeSession ─────────────────────────────────────────────────
|
|
16
|
-
export class PersistentClaudeSession extends EventEmitter {
|
|
17
|
-
options;
|
|
18
|
-
claudeBin;
|
|
19
|
-
proc = null;
|
|
20
|
-
_rl = null;
|
|
21
|
-
_isReady = false;
|
|
22
|
-
_isPaused = false;
|
|
23
|
-
_isBusy = false;
|
|
24
|
-
currentRequestId = 0;
|
|
25
|
-
_streamCallbacks = null;
|
|
26
|
-
_contextHighFired = false; // v0.6.0: deprecated, kept for compat
|
|
27
|
-
_contextHighLastFiredAt = 0; // v0.6.0: timestamp-based cooldown gate
|
|
28
|
-
_realModel = null;
|
|
29
|
-
sessionId;
|
|
30
|
-
stats;
|
|
31
|
-
constructor(config, claudeBin) {
|
|
32
|
-
super();
|
|
33
|
-
this.claudeBin = claudeBin || process.env.CLAUDE_BIN || 'claude';
|
|
34
|
-
this.options = {
|
|
35
|
-
...config,
|
|
36
|
-
permissionMode: config.permissionMode || 'acceptEdits',
|
|
37
|
-
hooks: {},
|
|
38
|
-
modelOverrides: config.modelOverrides || {},
|
|
39
|
-
};
|
|
40
|
-
this.stats = {
|
|
41
|
-
turns: 0,
|
|
42
|
-
toolCalls: 0,
|
|
43
|
-
toolErrors: 0,
|
|
44
|
-
tokensIn: 0,
|
|
45
|
-
tokensOut: 0,
|
|
46
|
-
cachedTokens: 0,
|
|
47
|
-
costUsd: 0,
|
|
48
|
-
startTime: null,
|
|
49
|
-
lastActivity: null,
|
|
50
|
-
history: [],
|
|
51
|
-
lastTurnContextTokens: 0, // v0.6.0
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
get pid() {
|
|
55
|
-
return this.proc?.pid ?? undefined;
|
|
56
|
-
}
|
|
57
|
-
get isReady() {
|
|
58
|
-
return this._isReady;
|
|
59
|
-
}
|
|
60
|
-
get isPaused() {
|
|
61
|
-
return this._isPaused;
|
|
62
|
-
}
|
|
63
|
-
get isBusy() {
|
|
64
|
-
return this._isBusy;
|
|
65
|
-
}
|
|
66
|
-
// ─── Start ───────────────────────────────────────────────────────────────
|
|
67
|
-
async start() {
|
|
68
|
-
const resolvedBin = this.claudeBin;
|
|
69
|
-
// v0.6.0: --include-partial-messages opt-in (was hardcoded ON, 10-100×
|
|
70
|
-
// JSON parse overhead per turn for events the live-card discarded anyway).
|
|
71
|
-
const args = [
|
|
72
|
-
'-p',
|
|
73
|
-
'--input-format',
|
|
74
|
-
'stream-json',
|
|
75
|
-
'--output-format',
|
|
76
|
-
'stream-json',
|
|
77
|
-
'--replay-user-messages',
|
|
78
|
-
'--verbose',
|
|
79
|
-
'--permission-mode',
|
|
80
|
-
this.options.permissionMode || 'acceptEdits',
|
|
81
|
-
];
|
|
82
|
-
if (this.options.includePartialMessages) {
|
|
83
|
-
args.splice(args.indexOf('--verbose') + 1, 0, '--include-partial-messages');
|
|
84
|
-
}
|
|
85
|
-
// Model alias resolution
|
|
86
|
-
if (this.options.model) {
|
|
87
|
-
const resolved = this.resolveModel(this.options.model);
|
|
88
|
-
if (resolved !== this.options.model)
|
|
89
|
-
this.options.model = resolved;
|
|
90
|
-
}
|
|
91
|
-
// Resume / fork
|
|
92
|
-
const resumeId = this.options.claudeResumeId || this.options.resumeSessionId;
|
|
93
|
-
if (resumeId) {
|
|
94
|
-
args.push('--resume', resumeId);
|
|
95
|
-
if (this.options.forkSession)
|
|
96
|
-
args.push('--fork-session');
|
|
97
|
-
}
|
|
98
|
-
if (this.options.customSessionId)
|
|
99
|
-
args.push('--session-id', this.options.customSessionId);
|
|
100
|
-
// Model — proxy mode mapping
|
|
101
|
-
if (this.options.model) {
|
|
102
|
-
if (!isClaudeModel(this.options.model) && this.options.baseUrl) {
|
|
103
|
-
this._realModel = this.options.model;
|
|
104
|
-
args.push('--model', 'opus');
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
const cliModel = this.options.model.includes('/') ? this.options.model.split('/').pop() : this.options.model;
|
|
108
|
-
args.push('--model', cliModel);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// Tool control
|
|
112
|
-
if (this.options.allowedTools?.length)
|
|
113
|
-
args.push('--allowed-tools', this.options.allowedTools.join(','));
|
|
114
|
-
if (this.options.disallowedTools?.length)
|
|
115
|
-
args.push('--disallowed-tools', this.options.disallowedTools.join(','));
|
|
116
|
-
if (this.options.tools !== undefined && this.options.tools !== null) {
|
|
117
|
-
const t = Array.isArray(this.options.tools) ? this.options.tools.join(',') : this.options.tools;
|
|
118
|
-
args.push('--tools', t);
|
|
119
|
-
}
|
|
120
|
-
// System prompts
|
|
121
|
-
if (this.options.systemPrompt)
|
|
122
|
-
args.push('--system-prompt', this.options.systemPrompt);
|
|
123
|
-
if (this.options.appendSystemPrompt)
|
|
124
|
-
args.push('--append-system-prompt', this.options.appendSystemPrompt);
|
|
125
|
-
// Limits
|
|
126
|
-
if (this.options.maxTurns)
|
|
127
|
-
args.push('--max-turns', String(this.options.maxTurns));
|
|
128
|
-
if (this.options.maxBudgetUsd)
|
|
129
|
-
args.push('--max-budget-usd', String(this.options.maxBudgetUsd));
|
|
130
|
-
// Permissions
|
|
131
|
-
if (this.options.dangerouslySkipPermissions)
|
|
132
|
-
args.push('--dangerously-skip-permissions');
|
|
133
|
-
// Agents
|
|
134
|
-
if (this.options.agents) {
|
|
135
|
-
const json = typeof this.options.agents === 'string' ? this.options.agents : JSON.stringify(this.options.agents);
|
|
136
|
-
args.push('--agents', json);
|
|
137
|
-
}
|
|
138
|
-
if (this.options.agent)
|
|
139
|
-
args.push('--agent', this.options.agent);
|
|
140
|
-
// Directories
|
|
141
|
-
if (this.options.addDir?.length) {
|
|
142
|
-
for (const dir of this.options.addDir)
|
|
143
|
-
args.push('--add-dir', dir);
|
|
144
|
-
}
|
|
145
|
-
// Effort
|
|
146
|
-
if (this.options.effort && this.options.effort !== 'auto')
|
|
147
|
-
args.push('--effort', this.options.effort);
|
|
148
|
-
// Auto mode
|
|
149
|
-
if (this.options.enableAutoMode || this.options.permissionMode === 'auto')
|
|
150
|
-
args.push('--enable-auto-mode');
|
|
151
|
-
// Session name
|
|
152
|
-
if (this.options.sessionName)
|
|
153
|
-
args.push('-n', this.options.sessionName);
|
|
154
|
-
// New CLI flags
|
|
155
|
-
if (this.options.bare)
|
|
156
|
-
args.push('--bare');
|
|
157
|
-
if (this.options.worktree) {
|
|
158
|
-
args.push('--worktree');
|
|
159
|
-
if (typeof this.options.worktree === 'string' && this.options.worktree !== 'true')
|
|
160
|
-
args.push(this.options.worktree);
|
|
161
|
-
}
|
|
162
|
-
if (this.options.fallbackModel)
|
|
163
|
-
args.push('--fallback-model', this.options.fallbackModel);
|
|
164
|
-
if (this.options.jsonSchema)
|
|
165
|
-
args.push('--json-schema', this.options.jsonSchema);
|
|
166
|
-
if (this.options.mcpConfig) {
|
|
167
|
-
const configs = Array.isArray(this.options.mcpConfig) ? this.options.mcpConfig : [this.options.mcpConfig];
|
|
168
|
-
for (const c of configs)
|
|
169
|
-
args.push('--mcp-config', c);
|
|
170
|
-
}
|
|
171
|
-
if (this.options.settings)
|
|
172
|
-
args.push('--settings', this.options.settings);
|
|
173
|
-
if (this.options.noSessionPersistence)
|
|
174
|
-
args.push('--no-session-persistence');
|
|
175
|
-
if (this.options.betas) {
|
|
176
|
-
const bl = Array.isArray(this.options.betas) ? this.options.betas : this.options.betas.split(',');
|
|
177
|
-
for (const b of bl)
|
|
178
|
-
args.push('--betas', b.trim());
|
|
179
|
-
}
|
|
180
|
-
// Ensure CWD exists (normalize to prevent path traversal)
|
|
181
|
-
if (this.options.cwd) {
|
|
182
|
-
this.options.cwd = path.resolve(this.options.cwd);
|
|
183
|
-
if (!fs.existsSync(this.options.cwd)) {
|
|
184
|
-
fs.mkdirSync(this.options.cwd, { recursive: true });
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// Build spawn environment
|
|
188
|
-
// Preserve the parent process PATH so the resolved binary and any PATH-relative
|
|
189
|
-
// tools (git, node, npm, etc.) remain accessible on all platforms and distros.
|
|
190
|
-
const spawnEnv = {
|
|
191
|
-
...process.env,
|
|
192
|
-
PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin',
|
|
193
|
-
};
|
|
194
|
-
if (this.options.baseUrl)
|
|
195
|
-
spawnEnv.ANTHROPIC_BASE_URL = this.options.baseUrl;
|
|
196
|
-
if (this.options.enableAgentTeams)
|
|
197
|
-
spawnEnv.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = 'true';
|
|
198
|
-
if (this._realModel && this.options.baseUrl) {
|
|
199
|
-
const base = this.options.baseUrl.replace(/\/$/, '');
|
|
200
|
-
spawnEnv.ANTHROPIC_BASE_URL = `${base}/real/${this._realModel}`;
|
|
201
|
-
}
|
|
202
|
-
// Spawn
|
|
203
|
-
this.proc = spawn(resolvedBin, args, {
|
|
204
|
-
cwd: this.options.cwd,
|
|
205
|
-
env: spawnEnv,
|
|
206
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
207
|
-
detached: true,
|
|
208
|
-
});
|
|
209
|
-
// Unref so the parent process can exit independently of the child.
|
|
210
|
-
this.proc.unref();
|
|
211
|
-
// Parse stdout line-by-line
|
|
212
|
-
this._rl = readline.createInterface({ input: this.proc.stdout, crlfDelay: Infinity });
|
|
213
|
-
this._rl.on('line', (line) => {
|
|
214
|
-
if (!line.trim())
|
|
215
|
-
return;
|
|
216
|
-
try {
|
|
217
|
-
const event = JSON.parse(line);
|
|
218
|
-
this._handleEvent(event);
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
this.emit(SESSION_EVENT.LOG, `[stdout] ${line}`);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
this.proc.stderr?.on('data', (data) => {
|
|
225
|
-
const sanitized = data
|
|
226
|
-
.toString()
|
|
227
|
-
.replace(/sk-[a-zA-Z0-9_-]{10,}/g, 'sk-***')
|
|
228
|
-
.replace(/ANTHROPIC_API_KEY=[^\s]+/g, 'ANTHROPIC_API_KEY=***')
|
|
229
|
-
.replace(/OPENAI_API_KEY=[^\s]+/g, 'OPENAI_API_KEY=***')
|
|
230
|
-
.replace(/GEMINI_API_KEY=[^\s]+/g, 'GEMINI_API_KEY=***')
|
|
231
|
-
.replace(/Bearer [a-zA-Z0-9_-]+/g, 'Bearer ***');
|
|
232
|
-
this.emit(SESSION_EVENT.LOG, `[stderr] ${sanitized}`);
|
|
233
|
-
});
|
|
234
|
-
this.proc.on('close', (code) => {
|
|
235
|
-
this._isReady = false;
|
|
236
|
-
this.emit(SESSION_EVENT.CLOSE, code);
|
|
237
|
-
});
|
|
238
|
-
this.proc.on('error', (err) => {
|
|
239
|
-
this.emit(SESSION_EVENT.ERROR, err);
|
|
240
|
-
});
|
|
241
|
-
// Wait for ready
|
|
242
|
-
return new Promise((resolve, reject) => {
|
|
243
|
-
const timeout = setTimeout(() => reject(new Error('Timeout waiting for session ready')), SESSION_READY_TIMEOUT_MS);
|
|
244
|
-
this.once(SESSION_EVENT.READY, () => {
|
|
245
|
-
clearTimeout(timeout);
|
|
246
|
-
resolve(this);
|
|
247
|
-
});
|
|
248
|
-
this.once(SESSION_EVENT.ERROR, (err) => {
|
|
249
|
-
clearTimeout(timeout);
|
|
250
|
-
reject(err);
|
|
251
|
-
});
|
|
252
|
-
// Detect premature CLI exit to avoid hanging or marking a dead process as "ready".
|
|
253
|
-
const onCloseBeforeReady = (code) => {
|
|
254
|
-
if (!this._isReady) {
|
|
255
|
-
clearTimeout(timeout);
|
|
256
|
-
reject(new Error(`Claude process exited prematurely with code ${code}. Session failed to start.`));
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
this.once(SESSION_EVENT.CLOSE, onCloseBeforeReady);
|
|
260
|
-
// Emit ready on the first `system` init event from the CLI.
|
|
261
|
-
// Fall back to a 2 s timer in case the CLI version doesn't emit one.
|
|
262
|
-
const onInit = () => {
|
|
263
|
-
if (!this._isReady) {
|
|
264
|
-
this._isReady = true;
|
|
265
|
-
// Cleanup the early-close listener since initialization succeeded
|
|
266
|
-
this.removeListener(SESSION_EVENT.CLOSE, onCloseBeforeReady);
|
|
267
|
-
this.emit(SESSION_EVENT.READY);
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
this.once(SESSION_EVENT.INIT, onInit);
|
|
271
|
-
setTimeout(() => {
|
|
272
|
-
this.removeListener(SESSION_EVENT.INIT, onInit);
|
|
273
|
-
// If process already exited, reject instead of falsely marking ready
|
|
274
|
-
if (this.proc?.killed || this.proc?.exitCode !== null) {
|
|
275
|
-
clearTimeout(timeout);
|
|
276
|
-
this.removeListener(SESSION_EVENT.CLOSE, onCloseBeforeReady);
|
|
277
|
-
reject(new Error('Claude CLI process crashed immediately upon startup. Fallback timer aborted.'));
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (!this._isReady) {
|
|
281
|
-
this._isReady = true;
|
|
282
|
-
this.removeListener(SESSION_EVENT.CLOSE, onCloseBeforeReady);
|
|
283
|
-
this.emit(SESSION_EVENT.READY);
|
|
284
|
-
}
|
|
285
|
-
}, SESSION_READY_FALLBACK_MS);
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
// ─── Event Handling ──────────────────────────────────────────────────────
|
|
289
|
-
_handleEvent(event) {
|
|
290
|
-
const type = event.type;
|
|
291
|
-
this.stats.lastActivity = new Date().toISOString();
|
|
292
|
-
// Track history (keep last 100)
|
|
293
|
-
this.stats.history.push({ time: this.stats.lastActivity, type, event });
|
|
294
|
-
if (this.stats.history.length > MAX_HISTORY_ITEMS)
|
|
295
|
-
this.stats.history.shift();
|
|
296
|
-
switch (type) {
|
|
297
|
-
case 'system':
|
|
298
|
-
if (event.subtype === 'init') {
|
|
299
|
-
this.sessionId = event.session_id;
|
|
300
|
-
this.stats.startTime = new Date().toISOString();
|
|
301
|
-
this.emit(SESSION_EVENT.INIT, event);
|
|
302
|
-
}
|
|
303
|
-
this.emit(SESSION_EVENT.SYSTEM, event);
|
|
304
|
-
break;
|
|
305
|
-
case 'stream_event': {
|
|
306
|
-
const inner = event.event;
|
|
307
|
-
if (!inner)
|
|
308
|
-
break;
|
|
309
|
-
const innerType = inner.type;
|
|
310
|
-
if (innerType === 'content_block_start') {
|
|
311
|
-
const block = inner.content_block;
|
|
312
|
-
if (block?.type === 'tool_use') {
|
|
313
|
-
this.stats.toolCalls++;
|
|
314
|
-
const toolEvent = { tool: { name: block.name, input: {} } };
|
|
315
|
-
try {
|
|
316
|
-
this._streamCallbacks?.onToolUse?.(toolEvent);
|
|
317
|
-
}
|
|
318
|
-
catch (err) {
|
|
319
|
-
this.emit(SESSION_EVENT.LOG, `[stream callback error] onToolUse: ${err.message}`);
|
|
320
|
-
}
|
|
321
|
-
this.emit(SESSION_EVENT.TOOL_USE, toolEvent);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
else if (innerType === 'content_block_delta') {
|
|
325
|
-
const delta = inner.delta;
|
|
326
|
-
if (delta?.type === 'text_delta' && delta.text) {
|
|
327
|
-
try {
|
|
328
|
-
this._streamCallbacks?.onText?.(delta.text);
|
|
329
|
-
}
|
|
330
|
-
catch (err) {
|
|
331
|
-
this.emit(SESSION_EVENT.LOG, `[stream callback error] onText: ${err.message}`);
|
|
332
|
-
}
|
|
333
|
-
this.emit(SESSION_EVENT.TEXT, delta.text);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
else if (innerType === 'message_delta') {
|
|
337
|
-
const usage = inner.usage;
|
|
338
|
-
if (usage) {
|
|
339
|
-
this.stats.tokensIn += usage.input_tokens || 0;
|
|
340
|
-
this.stats.tokensOut += usage.output_tokens || 0;
|
|
341
|
-
this.stats.cachedTokens += usage.cache_read_input_tokens || 0;
|
|
342
|
-
this._updateCost();
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
this.emit(SESSION_EVENT.STREAM_EVENT, event);
|
|
346
|
-
break;
|
|
347
|
-
}
|
|
348
|
-
case 'user':
|
|
349
|
-
this.stats.turns++;
|
|
350
|
-
this.emit(SESSION_EVENT.USER_ECHO, event);
|
|
351
|
-
break;
|
|
352
|
-
case 'assistant':
|
|
353
|
-
this.emit(SESSION_EVENT.ASSISTANT, event);
|
|
354
|
-
if (event.message?.content && Array.isArray(event.message.content)) {
|
|
355
|
-
for (const block of event.message.content) {
|
|
356
|
-
if (block.type === 'tool_use') {
|
|
357
|
-
this.stats.toolCalls++;
|
|
358
|
-
const toolEvent = {
|
|
359
|
-
tool: {
|
|
360
|
-
name: block.name,
|
|
361
|
-
input: block.input || {},
|
|
362
|
-
},
|
|
363
|
-
};
|
|
364
|
-
try {
|
|
365
|
-
this._streamCallbacks?.onToolUse?.(toolEvent);
|
|
366
|
-
}
|
|
367
|
-
catch (err) {
|
|
368
|
-
this.emit(SESSION_EVENT.LOG, `[stream callback error] onToolUse: ${err.message}`);
|
|
369
|
-
}
|
|
370
|
-
this.emit(SESSION_EVENT.TOOL_USE, toolEvent);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
break;
|
|
375
|
-
case 'tool_use':
|
|
376
|
-
this.stats.toolCalls++;
|
|
377
|
-
try {
|
|
378
|
-
this._streamCallbacks?.onToolUse?.(event);
|
|
379
|
-
}
|
|
380
|
-
catch (err) {
|
|
381
|
-
this.emit(SESSION_EVENT.LOG, `[stream callback error] onToolUse: ${err.message}`);
|
|
382
|
-
}
|
|
383
|
-
this.emit(SESSION_EVENT.TOOL_USE, event);
|
|
384
|
-
break;
|
|
385
|
-
case 'tool_result':
|
|
386
|
-
try {
|
|
387
|
-
this._streamCallbacks?.onToolResult?.(event);
|
|
388
|
-
}
|
|
389
|
-
catch (err) {
|
|
390
|
-
this.emit(SESSION_EVENT.LOG, `[stream callback error] onToolResult: ${err.message}`);
|
|
391
|
-
}
|
|
392
|
-
if (event.is_error || event.error) {
|
|
393
|
-
this.stats.toolErrors++;
|
|
394
|
-
this._fireHook('onToolError', {
|
|
395
|
-
tool: event.tool_use_id,
|
|
396
|
-
error: event.error,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
this.emit(SESSION_EVENT.TOOL_RESULT, event);
|
|
400
|
-
break;
|
|
401
|
-
case 'error':
|
|
402
|
-
this.emit(SESSION_EVENT.ERROR, new Error(String(event.error) || JSON.stringify(event)));
|
|
403
|
-
break;
|
|
404
|
-
case 'result': {
|
|
405
|
-
const usage = event.usage;
|
|
406
|
-
if (usage) {
|
|
407
|
-
this.stats.tokensIn += usage.input_tokens || 0;
|
|
408
|
-
this.stats.tokensOut += usage.output_tokens || 0;
|
|
409
|
-
this.stats.cachedTokens += usage.cache_read_input_tokens || 0;
|
|
410
|
-
// v0.6.0: track real per-turn context occupancy.
|
|
411
|
-
this.stats.lastTurnContextTokens =
|
|
412
|
-
(usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
413
|
-
this._updateCost();
|
|
414
|
-
}
|
|
415
|
-
this.emit(SESSION_EVENT.RESULT, event);
|
|
416
|
-
this.emit(SESSION_EVENT.TURN_COMPLETE, event);
|
|
417
|
-
this._fireHook('onTurnComplete', {
|
|
418
|
-
text: event.result,
|
|
419
|
-
usage,
|
|
420
|
-
stopReason: event.stop_reason,
|
|
421
|
-
});
|
|
422
|
-
// v0.6.0: onContextHigh now keys off lastTurnContextTokens with
|
|
423
|
-
// 60s cooldown (was: cumulative tokens with one-shot gate).
|
|
424
|
-
if (this.stats.lastTurnContextTokens > CONTEXT_HIGH_THRESHOLD) {
|
|
425
|
-
const now = Date.now();
|
|
426
|
-
if (now - (this._contextHighLastFiredAt || 0) > 60000) {
|
|
427
|
-
this._contextHighLastFiredAt = now;
|
|
428
|
-
this._fireHook('onContextHigh', { tokensUsed: this.stats.lastTurnContextTokens, threshold: CONTEXT_HIGH_THRESHOLD });
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const stopReason = event.stop_reason;
|
|
432
|
-
if (stopReason === 'error' || stopReason === 'rate_limit') {
|
|
433
|
-
this._fireHook('onStopFailure', { reason: stopReason, error: event.error });
|
|
434
|
-
}
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
default:
|
|
438
|
-
this.emit(SESSION_EVENT.EVENT, event);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
// ─── Send ────────────────────────────────────────────────────────────────
|
|
442
|
-
async send(message, options = {}) {
|
|
443
|
-
if (!this._isReady || !this.proc)
|
|
444
|
-
throw new Error('Session not ready. Call start() first.');
|
|
445
|
-
const requestId = ++this.currentRequestId;
|
|
446
|
-
let finalMessage = typeof message === 'string' ? message : message;
|
|
447
|
-
if (typeof finalMessage === 'string') {
|
|
448
|
-
if (options.effort === 'high' || options.effort === 'max') {
|
|
449
|
-
finalMessage = `ultrathink\n\n${finalMessage}`;
|
|
450
|
-
}
|
|
451
|
-
if (options.plan) {
|
|
452
|
-
finalMessage = `/plan ${finalMessage}`;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
const payload = {
|
|
456
|
-
type: 'user',
|
|
457
|
-
message: {
|
|
458
|
-
role: 'user',
|
|
459
|
-
content: typeof finalMessage === 'string' ? [{ type: 'text', text: finalMessage }] : finalMessage,
|
|
460
|
-
},
|
|
461
|
-
};
|
|
462
|
-
this.proc.stdin.write(JSON.stringify(payload) + '\n');
|
|
463
|
-
if (options.callbacks)
|
|
464
|
-
this._streamCallbacks = options.callbacks;
|
|
465
|
-
if (options.waitForComplete) {
|
|
466
|
-
this._isBusy = true;
|
|
467
|
-
try {
|
|
468
|
-
return await this._waitForTurnComplete(options.timeout || TURN_TIMEOUT_MS);
|
|
469
|
-
}
|
|
470
|
-
finally {
|
|
471
|
-
this._isBusy = false;
|
|
472
|
-
if (options.callbacks)
|
|
473
|
-
this._streamCallbacks = null;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return { requestId, sent: true };
|
|
477
|
-
}
|
|
478
|
-
// ─── Wait for Turn Complete ──────────────────────────────────────────────
|
|
479
|
-
_waitForTurnComplete(timeout) {
|
|
480
|
-
return new Promise((resolve, reject) => {
|
|
481
|
-
let settled = false;
|
|
482
|
-
let streamedText = '';
|
|
483
|
-
let allAssistantText = '';
|
|
484
|
-
const toolNames = [];
|
|
485
|
-
const onText = (chunk) => {
|
|
486
|
-
streamedText += chunk;
|
|
487
|
-
};
|
|
488
|
-
this.on(SESSION_EVENT.TEXT, onText);
|
|
489
|
-
const onAssistant = (event) => {
|
|
490
|
-
if (event.message?.content && Array.isArray(event.message.content)) {
|
|
491
|
-
for (const block of event.message.content) {
|
|
492
|
-
if (block.type === 'text' && block.text)
|
|
493
|
-
allAssistantText += block.text + '\n';
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
this.on(SESSION_EVENT.ASSISTANT, onAssistant);
|
|
498
|
-
const onToolUse = (event) => {
|
|
499
|
-
const tool = event.tool;
|
|
500
|
-
toolNames.push(tool?.name || event.name || 'unknown');
|
|
501
|
-
};
|
|
502
|
-
this.on(SESSION_EVENT.TOOL_USE, onToolUse);
|
|
503
|
-
const cleanup = () => {
|
|
504
|
-
clearTimeout(timer);
|
|
505
|
-
this.removeListener(SESSION_EVENT.TEXT, onText);
|
|
506
|
-
this.removeListener(SESSION_EVENT.ASSISTANT, onAssistant);
|
|
507
|
-
this.removeListener(SESSION_EVENT.TOOL_USE, onToolUse);
|
|
508
|
-
this.removeListener(SESSION_EVENT.TURN_COMPLETE, onTurnComplete);
|
|
509
|
-
this.removeListener(SESSION_EVENT.ERROR, onError);
|
|
510
|
-
this.removeListener(SESSION_EVENT.CLOSE, onClose);
|
|
511
|
-
};
|
|
512
|
-
const timer = setTimeout(() => {
|
|
513
|
-
if (settled)
|
|
514
|
-
return;
|
|
515
|
-
settled = true;
|
|
516
|
-
cleanup();
|
|
517
|
-
reject(new Error('Timeout waiting for response'));
|
|
518
|
-
}, timeout);
|
|
519
|
-
const onTurnComplete = (event) => {
|
|
520
|
-
if (settled)
|
|
521
|
-
return;
|
|
522
|
-
settled = true;
|
|
523
|
-
cleanup();
|
|
524
|
-
let text = event.result || streamedText || allAssistantText.trim() || '';
|
|
525
|
-
if (!text && toolNames.length > 0) {
|
|
526
|
-
const unique = [...new Set(toolNames)];
|
|
527
|
-
text = `[Agent completed ${toolNames.length} tool calls: ${unique.join(', ')}]`;
|
|
528
|
-
}
|
|
529
|
-
resolve({ text, event });
|
|
530
|
-
};
|
|
531
|
-
const onError = (err) => {
|
|
532
|
-
if (settled)
|
|
533
|
-
return;
|
|
534
|
-
settled = true;
|
|
535
|
-
cleanup();
|
|
536
|
-
reject(err);
|
|
537
|
-
};
|
|
538
|
-
const onClose = (code) => {
|
|
539
|
-
if (settled)
|
|
540
|
-
return;
|
|
541
|
-
settled = true;
|
|
542
|
-
cleanup();
|
|
543
|
-
const text = streamedText || allAssistantText.trim() || '';
|
|
544
|
-
resolve({
|
|
545
|
-
text,
|
|
546
|
-
event: {
|
|
547
|
-
type: 'result',
|
|
548
|
-
result: text,
|
|
549
|
-
stop_reason: 'process_exit',
|
|
550
|
-
exit_code: code,
|
|
551
|
-
},
|
|
552
|
-
});
|
|
553
|
-
};
|
|
554
|
-
this.once(SESSION_EVENT.TURN_COMPLETE, onTurnComplete);
|
|
555
|
-
this.once(SESSION_EVENT.ERROR, onError);
|
|
556
|
-
this.once(SESSION_EVENT.CLOSE, onClose);
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
// ─── Utilities ───────────────────────────────────────────────────────────
|
|
560
|
-
getStats() {
|
|
561
|
-
return {
|
|
562
|
-
turns: this.stats.turns,
|
|
563
|
-
toolCalls: this.stats.toolCalls,
|
|
564
|
-
toolErrors: this.stats.toolErrors,
|
|
565
|
-
tokensIn: this.stats.tokensIn,
|
|
566
|
-
tokensOut: this.stats.tokensOut,
|
|
567
|
-
cachedTokens: this.stats.cachedTokens,
|
|
568
|
-
costUsd: Math.round(this.stats.costUsd * 10000) / 10000,
|
|
569
|
-
isReady: this._isReady,
|
|
570
|
-
startTime: this.stats.startTime,
|
|
571
|
-
lastActivity: this.stats.lastActivity,
|
|
572
|
-
// Approximate context window utilization based on model's known window size.
|
|
573
|
-
// Claude Code doesn't expose exact context usage via the JSON protocol,
|
|
574
|
-
// so this is a best-effort heuristic. May overcount because cumulative
|
|
575
|
-
// token counts include the full conversation history replayed each turn.
|
|
576
|
-
// v0.6.0: contextPercent uses last-turn occupancy, not lifetime cumulative.
|
|
577
|
-
contextPercent: Math.min(100, Math.round((this.stats.lastTurnContextTokens /
|
|
578
|
-
getContextWindow(this.options.resolvedModel || this.options.model || 'claude-sonnet-4-6')) *
|
|
579
|
-
100)),
|
|
580
|
-
sessionId: this.sessionId,
|
|
581
|
-
uptime: this.stats.startTime ? Math.round((Date.now() - new Date(this.stats.startTime).getTime()) / 1000) : 0,
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
getHistory(limit = DEFAULT_HISTORY_LIMIT) {
|
|
585
|
-
return this.stats.history.slice(-limit);
|
|
586
|
-
}
|
|
587
|
-
async compact(summary) {
|
|
588
|
-
const msg = summary ? `/compact ${summary}` : '/compact';
|
|
589
|
-
return this.send(msg, { waitForComplete: true, timeout: COMPACT_TIMEOUT_MS });
|
|
590
|
-
}
|
|
591
|
-
getEffort() {
|
|
592
|
-
return this.options.effort || 'auto';
|
|
593
|
-
}
|
|
594
|
-
setEffort(level) {
|
|
595
|
-
this.options.effort = level;
|
|
596
|
-
}
|
|
597
|
-
getCost() {
|
|
598
|
-
const pricing = getModelPricing(this.options.model);
|
|
599
|
-
const nonCachedIn = Math.max(0, this.stats.tokensIn - this.stats.cachedTokens);
|
|
600
|
-
return {
|
|
601
|
-
model: this.options.model || 'default',
|
|
602
|
-
tokensIn: this.stats.tokensIn,
|
|
603
|
-
tokensOut: this.stats.tokensOut,
|
|
604
|
-
cachedTokens: this.stats.cachedTokens,
|
|
605
|
-
pricing: { inputPer1M: pricing.input, outputPer1M: pricing.output, cachedPer1M: pricing.cached },
|
|
606
|
-
breakdown: {
|
|
607
|
-
inputCost: (nonCachedIn / 1_000_000) * pricing.input,
|
|
608
|
-
cachedCost: (this.stats.cachedTokens / 1_000_000) * (pricing.cached ?? 0),
|
|
609
|
-
outputCost: (this.stats.tokensOut / 1_000_000) * pricing.output,
|
|
610
|
-
},
|
|
611
|
-
totalUsd: this.stats.costUsd,
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
resolveModel(alias) {
|
|
615
|
-
if (this.options.modelOverrides?.[alias])
|
|
616
|
-
return this.options.modelOverrides[alias];
|
|
617
|
-
return resolveAlias(alias);
|
|
618
|
-
}
|
|
619
|
-
pause() {
|
|
620
|
-
this._isPaused = true;
|
|
621
|
-
this.emit(SESSION_EVENT.PAUSED, { sessionId: this.sessionId });
|
|
622
|
-
}
|
|
623
|
-
resume() {
|
|
624
|
-
this._isPaused = false;
|
|
625
|
-
this.emit(SESSION_EVENT.RESUMED, { sessionId: this.sessionId });
|
|
626
|
-
}
|
|
627
|
-
stop() {
|
|
628
|
-
this._fireHook('onStop', { cost: this.getCost(), stats: this.getStats() });
|
|
629
|
-
if (this._rl) {
|
|
630
|
-
this._rl.close();
|
|
631
|
-
this._rl = null;
|
|
632
|
-
}
|
|
633
|
-
if (this.proc) {
|
|
634
|
-
const pid = this.proc.pid;
|
|
635
|
-
this.proc.stdin?.end();
|
|
636
|
-
this.proc.stdout?.destroy();
|
|
637
|
-
this.proc.stderr?.destroy();
|
|
638
|
-
try {
|
|
639
|
-
process.kill(-pid, 'SIGTERM');
|
|
640
|
-
}
|
|
641
|
-
catch (err) {
|
|
642
|
-
if (err.code !== 'ESRCH') {
|
|
643
|
-
this.emit(SESSION_EVENT.LOG, `[stop] kill(-${pid}, SIGTERM) failed: ${err.message}`);
|
|
644
|
-
}
|
|
645
|
-
try {
|
|
646
|
-
this.proc.kill('SIGTERM');
|
|
647
|
-
}
|
|
648
|
-
catch (innerErr) {
|
|
649
|
-
if (innerErr.code !== 'ESRCH') {
|
|
650
|
-
this.emit(SESSION_EVENT.LOG, `[stop] proc.kill(SIGTERM) failed: ${innerErr.message}`);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
const p = this.proc;
|
|
655
|
-
setTimeout(() => {
|
|
656
|
-
try {
|
|
657
|
-
process.kill(-pid, 'SIGKILL');
|
|
658
|
-
}
|
|
659
|
-
catch {
|
|
660
|
-
/* ESRCH expected — process already gone */
|
|
661
|
-
}
|
|
662
|
-
try {
|
|
663
|
-
p.kill('SIGKILL');
|
|
664
|
-
}
|
|
665
|
-
catch {
|
|
666
|
-
/* ESRCH expected */
|
|
667
|
-
}
|
|
668
|
-
}, STOP_SIGKILL_DELAY_MS);
|
|
669
|
-
this.proc = null;
|
|
670
|
-
}
|
|
671
|
-
this._isReady = false;
|
|
672
|
-
this._isPaused = false;
|
|
673
|
-
this.emit(SESSION_EVENT.CLOSE, 143);
|
|
674
|
-
}
|
|
675
|
-
// ─── Private ─────────────────────────────────────────────────────────────
|
|
676
|
-
_updateCost() {
|
|
677
|
-
const pricing = getModelPricing(this.options.model);
|
|
678
|
-
const nonCachedIn = Math.max(0, this.stats.tokensIn - this.stats.cachedTokens);
|
|
679
|
-
this.stats.costUsd =
|
|
680
|
-
(nonCachedIn / 1_000_000) * pricing.input +
|
|
681
|
-
(this.stats.cachedTokens / 1_000_000) * (pricing.cached ?? 0) +
|
|
682
|
-
(this.stats.tokensOut / 1_000_000) * pricing.output;
|
|
683
|
-
}
|
|
684
|
-
_fireHook(hookName, data) {
|
|
685
|
-
const hooks = this.options.hooks;
|
|
686
|
-
const hook = hooks?.[hookName];
|
|
687
|
-
if (typeof hook === 'function') {
|
|
688
|
-
try {
|
|
689
|
-
hook(data);
|
|
690
|
-
}
|
|
691
|
-
catch (err) {
|
|
692
|
-
this.emit(SESSION_EVENT.LOG, `[hook error] ${hookName}: ${err.message}`);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
this.emit(`hook:${hookName}`, data);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
//# sourceMappingURL=persistent-session.js.map
|