@enderfga/claw-orchestrator 3.0.0

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