@getpaseo/server 0.1.14 → 0.1.16
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/server/client/daemon-client.d.ts +41 -4
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +355 -84
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +10 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +261 -18
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-projections.d.ts +5 -0
- package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
- package/dist/server/server/agent/agent-projections.js +24 -0
- package/dist/server/server/agent/agent-projections.js.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +11 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +15 -5
- package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.js +2 -0
- package/dist/server/server/agent/agent-storage.js.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +2 -0
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js +2 -0
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts +7 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.js +1470 -237
- package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js +19 -4
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +40 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js +1 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
- package/dist/server/server/client-message-id.d.ts +3 -0
- package/dist/server/server/client-message-id.d.ts.map +1 -0
- package/dist/server/server/client-message-id.js +12 -0
- package/dist/server/server/client-message-id.js.map +1 -0
- package/dist/server/server/persisted-config.d.ts +8 -8
- package/dist/server/server/persistence-hooks.js +1 -1
- package/dist/server/server/persistence-hooks.js.map +1 -1
- package/dist/server/server/relay-transport.d.ts.map +1 -1
- package/dist/server/server/relay-transport.js +27 -28
- package/dist/server/server/relay-transport.js.map +1 -1
- package/dist/server/server/session.d.ts +4 -2
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +122 -31
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/websocket-server.d.ts +8 -4
- package/dist/server/server/websocket-server.d.ts.map +1 -1
- package/dist/server/server/websocket-server.js +272 -75
- package/dist/server/server/websocket-server.js.map +1 -1
- package/dist/server/shared/daemon-endpoints.d.ts +9 -1
- package/dist/server/shared/daemon-endpoints.d.ts.map +1 -1
- package/dist/server/shared/daemon-endpoints.js +18 -3
- package/dist/server/shared/daemon-endpoints.js.map +1 -1
- package/dist/server/shared/messages.d.ts +2065 -313
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +40 -1
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/shared/tool-call-display.d.ts.map +1 -1
- package/dist/server/shared/tool-call-display.js +4 -0
- package/dist/server/shared/tool-call-display.js.map +1 -1
- package/package.json +3 -3
|
@@ -6,6 +6,7 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
8
8
|
import { mapClaudeCanceledToolCall, mapClaudeCompletedToolCall, mapClaudeFailedToolCall, mapClaudeRunningToolCall, } from "./claude/tool-call-mapper.js";
|
|
9
|
+
import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
|
|
9
10
|
import { applyProviderEnv, isProviderCommandAvailable, } from "../provider-launch-config.js";
|
|
10
11
|
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
|
11
12
|
const fsPromises = promises;
|
|
@@ -118,6 +119,7 @@ const REWIND_COMMAND = {
|
|
|
118
119
|
description: "Rewind tracked files to a previous user message",
|
|
119
120
|
argumentHint: "[user_message_uuid]",
|
|
120
121
|
};
|
|
122
|
+
const INTERRUPT_TOOL_USE_PLACEHOLDER = "[Request interrupted by user for tool use]";
|
|
121
123
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
122
124
|
function resolveClaudeBinary() {
|
|
123
125
|
try {
|
|
@@ -150,6 +152,148 @@ function resolveClaudeSpawnCommand(spawnOptions, runtimeSettings) {
|
|
|
150
152
|
args: [...commandConfig.argv.slice(1), ...spawnOptions.args],
|
|
151
153
|
};
|
|
152
154
|
}
|
|
155
|
+
function applyRuntimeSettingsToClaudeOptions(options, runtimeSettings) {
|
|
156
|
+
const hasEnvOverrides = Object.keys(runtimeSettings?.env ?? {}).length > 0;
|
|
157
|
+
const commandMode = runtimeSettings?.command?.mode;
|
|
158
|
+
const needsCustomSpawn = hasEnvOverrides || commandMode === "append" || commandMode === "replace";
|
|
159
|
+
if (!needsCustomSpawn) {
|
|
160
|
+
return options;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
...options,
|
|
164
|
+
spawnClaudeCodeProcess: (spawnOptions) => {
|
|
165
|
+
const resolved = resolveClaudeSpawnCommand(spawnOptions, runtimeSettings);
|
|
166
|
+
return spawn(resolved.command, resolved.args, {
|
|
167
|
+
cwd: spawnOptions.cwd,
|
|
168
|
+
env: applyProviderEnv(spawnOptions.env, runtimeSettings),
|
|
169
|
+
signal: spawnOptions.signal,
|
|
170
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function summarizeClaudeOptionsForLog(options) {
|
|
176
|
+
const systemPromptRaw = options.systemPrompt;
|
|
177
|
+
const systemPromptSummary = (() => {
|
|
178
|
+
if (!systemPromptRaw) {
|
|
179
|
+
return { mode: "none", preset: null };
|
|
180
|
+
}
|
|
181
|
+
if (typeof systemPromptRaw === "string") {
|
|
182
|
+
return { mode: "string", preset: null };
|
|
183
|
+
}
|
|
184
|
+
const prompt = systemPromptRaw;
|
|
185
|
+
const promptType = typeof prompt.type === "string" ? prompt.type : "custom";
|
|
186
|
+
return {
|
|
187
|
+
mode: promptType === "preset"
|
|
188
|
+
? "preset"
|
|
189
|
+
: "custom",
|
|
190
|
+
preset: typeof prompt.preset === "string" && prompt.preset.length > 0
|
|
191
|
+
? prompt.preset
|
|
192
|
+
: null,
|
|
193
|
+
};
|
|
194
|
+
})();
|
|
195
|
+
const mcpServerNames = options.mcpServers
|
|
196
|
+
? Object.keys(options.mcpServers).sort()
|
|
197
|
+
: [];
|
|
198
|
+
return {
|
|
199
|
+
cwd: typeof options.cwd === "string" ? options.cwd : null,
|
|
200
|
+
permissionMode: typeof options.permissionMode === "string"
|
|
201
|
+
? options.permissionMode
|
|
202
|
+
: null,
|
|
203
|
+
model: typeof options.model === "string" ? options.model : null,
|
|
204
|
+
includePartialMessages: options.includePartialMessages === true,
|
|
205
|
+
settingSources: Array.isArray(options.settingSources)
|
|
206
|
+
? options.settingSources
|
|
207
|
+
: [],
|
|
208
|
+
enableFileCheckpointing: options.enableFileCheckpointing === true,
|
|
209
|
+
hasResume: typeof options.resume === "string" && options.resume.length > 0,
|
|
210
|
+
maxThinkingTokens: typeof options.maxThinkingTokens === "number"
|
|
211
|
+
? options.maxThinkingTokens
|
|
212
|
+
: null,
|
|
213
|
+
hasEnv: !!options.env,
|
|
214
|
+
envKeyCount: Object.keys(options.env ?? {}).length,
|
|
215
|
+
hasMcpServers: mcpServerNames.length > 0,
|
|
216
|
+
mcpServerNames,
|
|
217
|
+
systemPromptMode: systemPromptSummary.mode,
|
|
218
|
+
systemPromptPreset: systemPromptSummary.preset,
|
|
219
|
+
hasCanUseTool: typeof options.canUseTool === "function",
|
|
220
|
+
hasSpawnOverride: typeof options.spawnClaudeCodeProcess === "function",
|
|
221
|
+
hasStderrHandler: typeof options.stderr === "function",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function isToolResultTextBlock(value) {
|
|
225
|
+
return (!!value &&
|
|
226
|
+
typeof value === "object" &&
|
|
227
|
+
value.type === "text" &&
|
|
228
|
+
typeof value.text === "string");
|
|
229
|
+
}
|
|
230
|
+
function normalizeForDeterministicString(value, seen) {
|
|
231
|
+
if (value === null ||
|
|
232
|
+
typeof value === "string" ||
|
|
233
|
+
typeof value === "number" ||
|
|
234
|
+
typeof value === "boolean") {
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
if (typeof value === "bigint") {
|
|
238
|
+
return value.toString();
|
|
239
|
+
}
|
|
240
|
+
if (typeof value === "function") {
|
|
241
|
+
return "[function]";
|
|
242
|
+
}
|
|
243
|
+
if (typeof value === "symbol") {
|
|
244
|
+
return value.toString();
|
|
245
|
+
}
|
|
246
|
+
if (typeof value === "undefined") {
|
|
247
|
+
return "[undefined]";
|
|
248
|
+
}
|
|
249
|
+
if (Array.isArray(value)) {
|
|
250
|
+
return value.map((entry) => normalizeForDeterministicString(entry, seen));
|
|
251
|
+
}
|
|
252
|
+
if (typeof value === "object") {
|
|
253
|
+
const objectValue = value;
|
|
254
|
+
if (seen.has(objectValue)) {
|
|
255
|
+
return "[circular]";
|
|
256
|
+
}
|
|
257
|
+
seen.add(objectValue);
|
|
258
|
+
const record = value;
|
|
259
|
+
const normalized = {};
|
|
260
|
+
for (const key of Object.keys(record).sort()) {
|
|
261
|
+
normalized[key] = normalizeForDeterministicString(record[key], seen);
|
|
262
|
+
}
|
|
263
|
+
seen.delete(objectValue);
|
|
264
|
+
return normalized;
|
|
265
|
+
}
|
|
266
|
+
return String(value);
|
|
267
|
+
}
|
|
268
|
+
function deterministicStringify(value) {
|
|
269
|
+
if (typeof value === "undefined") {
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const normalized = normalizeForDeterministicString(value, new WeakSet());
|
|
274
|
+
if (typeof normalized === "string") {
|
|
275
|
+
return normalized;
|
|
276
|
+
}
|
|
277
|
+
return JSON.stringify(normalized);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
try {
|
|
281
|
+
return String(value);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return "[unserializable]";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function coerceToolResultContentToString(content) {
|
|
289
|
+
if (typeof content === "string") {
|
|
290
|
+
return content;
|
|
291
|
+
}
|
|
292
|
+
if (Array.isArray(content) && content.every((block) => isToolResultTextBlock(block))) {
|
|
293
|
+
return content.map((block) => block.text).join("");
|
|
294
|
+
}
|
|
295
|
+
return deterministicStringify(content);
|
|
296
|
+
}
|
|
153
297
|
export function extractUserMessageText(content) {
|
|
154
298
|
if (typeof content === "string") {
|
|
155
299
|
const normalized = content.trim();
|
|
@@ -179,10 +323,18 @@ export function extractUserMessageText(content) {
|
|
|
179
323
|
const combined = parts.join("\n\n").trim();
|
|
180
324
|
return combined.length > 0 ? combined : null;
|
|
181
325
|
}
|
|
182
|
-
const
|
|
326
|
+
const MAX_SUB_AGENT_LOG_ENTRIES = 200;
|
|
327
|
+
const MAX_SUB_AGENT_SUMMARY_CHARS = 160;
|
|
183
328
|
function isMetadata(value) {
|
|
184
329
|
return typeof value === "object" && value !== null;
|
|
185
330
|
}
|
|
331
|
+
function readTrimmedString(value) {
|
|
332
|
+
if (typeof value !== "string") {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
const trimmed = value.trim();
|
|
336
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
337
|
+
}
|
|
186
338
|
function isMcpServerConfig(value) {
|
|
187
339
|
if (!isMetadata(value)) {
|
|
188
340
|
return false;
|
|
@@ -312,6 +464,448 @@ function resolvePermissionKind(toolName, input) {
|
|
|
312
464
|
}
|
|
313
465
|
return "tool";
|
|
314
466
|
}
|
|
467
|
+
const ACTIVE_RUN_STATES = new Set([
|
|
468
|
+
"queued",
|
|
469
|
+
"awaiting_response",
|
|
470
|
+
"streaming",
|
|
471
|
+
"finalizing",
|
|
472
|
+
]);
|
|
473
|
+
class RunTracker {
|
|
474
|
+
constructor() {
|
|
475
|
+
this.runs = new Map();
|
|
476
|
+
this.runByTaskId = new Map();
|
|
477
|
+
this.runByParentMessageId = new Map();
|
|
478
|
+
this.runByMessageId = new Map();
|
|
479
|
+
}
|
|
480
|
+
createRun(input) {
|
|
481
|
+
const run = {
|
|
482
|
+
id: input.id,
|
|
483
|
+
owner: input.owner,
|
|
484
|
+
queue: input.queue,
|
|
485
|
+
state: "queued",
|
|
486
|
+
promptReplaySeen: input.promptReplaySeen ?? true,
|
|
487
|
+
taskIds: new Set(),
|
|
488
|
+
parentMessageIds: new Set(),
|
|
489
|
+
messageIds: new Set(),
|
|
490
|
+
};
|
|
491
|
+
this.runs.set(run.id, run);
|
|
492
|
+
return run;
|
|
493
|
+
}
|
|
494
|
+
getRun(runId) {
|
|
495
|
+
return this.runs.get(runId) ?? null;
|
|
496
|
+
}
|
|
497
|
+
getForegroundRun() {
|
|
498
|
+
for (const run of this.runs.values()) {
|
|
499
|
+
if (run.owner === "foreground" && this.isActive(run.state)) {
|
|
500
|
+
return run;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
listActiveRuns(owner) {
|
|
506
|
+
const runs = [];
|
|
507
|
+
for (const run of this.runs.values()) {
|
|
508
|
+
if (!this.isActive(run.state)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (owner && run.owner !== owner) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
runs.push(run);
|
|
515
|
+
}
|
|
516
|
+
return runs;
|
|
517
|
+
}
|
|
518
|
+
hasActiveRuns(owner) {
|
|
519
|
+
for (const run of this.runs.values()) {
|
|
520
|
+
if (!this.isActive(run.state)) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (owner && run.owner !== owner) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
getLatestActiveRun(owner) {
|
|
531
|
+
let latest = null;
|
|
532
|
+
for (const run of this.runs.values()) {
|
|
533
|
+
if (!this.isActive(run.state)) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (owner && run.owner !== owner) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
latest = run;
|
|
540
|
+
}
|
|
541
|
+
return latest;
|
|
542
|
+
}
|
|
543
|
+
isRunActive(run) {
|
|
544
|
+
if (!run) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
return this.isActive(run.state);
|
|
548
|
+
}
|
|
549
|
+
resolveByIdentifiers(identifiers) {
|
|
550
|
+
if (identifiers.taskId) {
|
|
551
|
+
const run = this.resolveMappedRun(this.runByTaskId, identifiers.taskId);
|
|
552
|
+
if (run) {
|
|
553
|
+
return { run, reason: "task_id" };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (identifiers.parentMessageId) {
|
|
557
|
+
const run = this.resolveMappedRun(this.runByParentMessageId, identifiers.parentMessageId);
|
|
558
|
+
if (run) {
|
|
559
|
+
return { run, reason: "parent_message_id" };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (identifiers.messageId) {
|
|
563
|
+
const run = this.resolveMappedRun(this.runByMessageId, identifiers.messageId);
|
|
564
|
+
if (run) {
|
|
565
|
+
return { run, reason: "message_id" };
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return { run: null, reason: "metadata" };
|
|
569
|
+
}
|
|
570
|
+
bindIdentifiers(run, identifiers) {
|
|
571
|
+
if (identifiers.taskId) {
|
|
572
|
+
run.taskIds.add(identifiers.taskId);
|
|
573
|
+
this.runByTaskId.set(identifiers.taskId, run.id);
|
|
574
|
+
}
|
|
575
|
+
if (identifiers.parentMessageId) {
|
|
576
|
+
run.parentMessageIds.add(identifiers.parentMessageId);
|
|
577
|
+
this.runByParentMessageId.set(identifiers.parentMessageId, run.id);
|
|
578
|
+
}
|
|
579
|
+
if (identifiers.messageId) {
|
|
580
|
+
run.messageIds.add(identifiers.messageId);
|
|
581
|
+
this.runByMessageId.set(identifiers.messageId, run.id);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
transition(run, nextState) {
|
|
585
|
+
run.state = nextState;
|
|
586
|
+
}
|
|
587
|
+
complete(run, terminalState) {
|
|
588
|
+
run.state = terminalState;
|
|
589
|
+
this.clearRunIndex(run);
|
|
590
|
+
}
|
|
591
|
+
deriveLifecycle(pendingPermissionCount) {
|
|
592
|
+
for (const run of this.runs.values()) {
|
|
593
|
+
if (this.isActive(run.state)) {
|
|
594
|
+
return "running";
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (pendingPermissionCount > 0) {
|
|
598
|
+
return "permission";
|
|
599
|
+
}
|
|
600
|
+
for (const run of this.runs.values()) {
|
|
601
|
+
if (run.state === "error") {
|
|
602
|
+
return "error";
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return "idle";
|
|
606
|
+
}
|
|
607
|
+
resolveMappedRun(mapping, identifier) {
|
|
608
|
+
const runId = mapping.get(identifier);
|
|
609
|
+
if (!runId) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
const run = this.runs.get(runId);
|
|
613
|
+
if (!run || !this.isActive(run.state)) {
|
|
614
|
+
mapping.delete(identifier);
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
return run;
|
|
618
|
+
}
|
|
619
|
+
clearRunIndex(run) {
|
|
620
|
+
for (const taskId of run.taskIds) {
|
|
621
|
+
this.runByTaskId.delete(taskId);
|
|
622
|
+
}
|
|
623
|
+
for (const parentMessageId of run.parentMessageIds) {
|
|
624
|
+
this.runByParentMessageId.delete(parentMessageId);
|
|
625
|
+
}
|
|
626
|
+
for (const messageId of run.messageIds) {
|
|
627
|
+
this.runByMessageId.delete(messageId);
|
|
628
|
+
}
|
|
629
|
+
run.taskIds.clear();
|
|
630
|
+
run.parentMessageIds.clear();
|
|
631
|
+
run.messageIds.clear();
|
|
632
|
+
}
|
|
633
|
+
isActive(state) {
|
|
634
|
+
return ACTIVE_RUN_STATES.has(state);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
class TimelineAssembler {
|
|
638
|
+
constructor() {
|
|
639
|
+
this.messages = new Map();
|
|
640
|
+
this.activeMessageByRun = new Map();
|
|
641
|
+
this.syntheticMessageCounter = 0;
|
|
642
|
+
}
|
|
643
|
+
consume(input) {
|
|
644
|
+
if (input.message.type === "assistant") {
|
|
645
|
+
return this.consumeAssistantMessage(input.message, input.runId, input.messageIdHint ?? null);
|
|
646
|
+
}
|
|
647
|
+
if (input.message.type === "stream_event") {
|
|
648
|
+
return this.consumeStreamEvent(input.message, input.runId, input.messageIdHint ?? null);
|
|
649
|
+
}
|
|
650
|
+
return [];
|
|
651
|
+
}
|
|
652
|
+
consumeAssistantMessage(message, runId, messageIdHint) {
|
|
653
|
+
const messageId = this.readMessageIdFromAssistantMessage(message) ??
|
|
654
|
+
messageIdHint ??
|
|
655
|
+
this.resolveMessageId({ runId, createIfMissing: true, messageId: null });
|
|
656
|
+
if (!messageId) {
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
const state = this.ensureMessageState(messageId, runId);
|
|
660
|
+
const fragments = this.extractFragments(message.message?.content);
|
|
661
|
+
return this.applyAbsoluteFragments(state, fragments);
|
|
662
|
+
}
|
|
663
|
+
consumeStreamEvent(message, runId, messageIdHint) {
|
|
664
|
+
const event = message.event;
|
|
665
|
+
const eventType = readTrimmedString(event.type);
|
|
666
|
+
const streamEventMessageId = this.readMessageIdFromStreamEvent(event) ?? messageIdHint;
|
|
667
|
+
if (eventType === "message_start") {
|
|
668
|
+
const messageId = this.resolveMessageId({
|
|
669
|
+
runId,
|
|
670
|
+
createIfMissing: true,
|
|
671
|
+
messageId: streamEventMessageId,
|
|
672
|
+
});
|
|
673
|
+
if (!messageId) {
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
this.ensureMessageState(messageId, runId);
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
if (eventType === "message_stop") {
|
|
680
|
+
const messageId = this.resolveMessageId({
|
|
681
|
+
runId,
|
|
682
|
+
createIfMissing: false,
|
|
683
|
+
messageId: streamEventMessageId,
|
|
684
|
+
});
|
|
685
|
+
if (!messageId) {
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
return this.finalizeMessage(messageId, runId);
|
|
689
|
+
}
|
|
690
|
+
if (eventType === "content_block_start") {
|
|
691
|
+
return this.consumeDeltaContent(event.content_block, runId, streamEventMessageId);
|
|
692
|
+
}
|
|
693
|
+
if (eventType === "content_block_delta") {
|
|
694
|
+
return this.consumeDeltaContent(event.delta, runId, streamEventMessageId);
|
|
695
|
+
}
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
consumeDeltaContent(content, runId, messageIdHint) {
|
|
699
|
+
const fragments = this.extractFragments(content);
|
|
700
|
+
if (fragments.length === 0) {
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
const messageId = this.resolveMessageId({
|
|
704
|
+
runId,
|
|
705
|
+
createIfMissing: true,
|
|
706
|
+
messageId: messageIdHint,
|
|
707
|
+
});
|
|
708
|
+
if (!messageId) {
|
|
709
|
+
return [];
|
|
710
|
+
}
|
|
711
|
+
const state = this.ensureMessageState(messageId, runId);
|
|
712
|
+
return this.appendFragments(state, fragments);
|
|
713
|
+
}
|
|
714
|
+
appendFragments(state, fragments) {
|
|
715
|
+
for (const fragment of fragments) {
|
|
716
|
+
if (fragment.kind === "assistant") {
|
|
717
|
+
state.assistantText += fragment.text;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
state.reasoningText += fragment.text;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return this.emitNewContent(state);
|
|
724
|
+
}
|
|
725
|
+
applyAbsoluteFragments(state, fragments) {
|
|
726
|
+
const assistantText = fragments
|
|
727
|
+
.filter((fragment) => fragment.kind === "assistant")
|
|
728
|
+
.map((fragment) => fragment.text)
|
|
729
|
+
.join("");
|
|
730
|
+
const reasoningText = fragments
|
|
731
|
+
.filter((fragment) => fragment.kind === "reasoning")
|
|
732
|
+
.map((fragment) => fragment.text)
|
|
733
|
+
.join("");
|
|
734
|
+
if (assistantText.length > 0) {
|
|
735
|
+
if (!assistantText.startsWith(state.assistantText)) {
|
|
736
|
+
state.emittedAssistantLength = 0;
|
|
737
|
+
}
|
|
738
|
+
state.assistantText = assistantText;
|
|
739
|
+
}
|
|
740
|
+
if (reasoningText.length > 0) {
|
|
741
|
+
if (!reasoningText.startsWith(state.reasoningText)) {
|
|
742
|
+
state.emittedReasoningLength = 0;
|
|
743
|
+
}
|
|
744
|
+
state.reasoningText = reasoningText;
|
|
745
|
+
}
|
|
746
|
+
return this.emitNewContent(state);
|
|
747
|
+
}
|
|
748
|
+
finalizeMessage(messageId, runId) {
|
|
749
|
+
const state = this.messages.get(messageId);
|
|
750
|
+
if (!state) {
|
|
751
|
+
return [];
|
|
752
|
+
}
|
|
753
|
+
state.stopped = true;
|
|
754
|
+
const items = this.emitNewContent(state);
|
|
755
|
+
if (runId && this.activeMessageByRun.get(runId) === messageId) {
|
|
756
|
+
this.activeMessageByRun.delete(runId);
|
|
757
|
+
}
|
|
758
|
+
return items;
|
|
759
|
+
}
|
|
760
|
+
emitNewContent(state) {
|
|
761
|
+
const items = [];
|
|
762
|
+
const nextAssistantText = state.assistantText.slice(state.emittedAssistantLength);
|
|
763
|
+
if (nextAssistantText.length > 0 &&
|
|
764
|
+
nextAssistantText !== INTERRUPT_TOOL_USE_PLACEHOLDER) {
|
|
765
|
+
state.emittedAssistantLength = state.assistantText.length;
|
|
766
|
+
items.push({ type: "assistant_message", text: nextAssistantText });
|
|
767
|
+
}
|
|
768
|
+
const nextReasoningText = state.reasoningText.slice(state.emittedReasoningLength);
|
|
769
|
+
if (nextReasoningText.length > 0) {
|
|
770
|
+
state.emittedReasoningLength = state.reasoningText.length;
|
|
771
|
+
items.push({ type: "reasoning", text: nextReasoningText });
|
|
772
|
+
}
|
|
773
|
+
return items;
|
|
774
|
+
}
|
|
775
|
+
ensureMessageState(messageId, runId) {
|
|
776
|
+
const existing = this.messages.get(messageId);
|
|
777
|
+
if (existing) {
|
|
778
|
+
existing.stopped = false;
|
|
779
|
+
if (runId) {
|
|
780
|
+
this.activeMessageByRun.set(runId, messageId);
|
|
781
|
+
}
|
|
782
|
+
return existing;
|
|
783
|
+
}
|
|
784
|
+
const created = {
|
|
785
|
+
id: messageId,
|
|
786
|
+
assistantText: "",
|
|
787
|
+
reasoningText: "",
|
|
788
|
+
emittedAssistantLength: 0,
|
|
789
|
+
emittedReasoningLength: 0,
|
|
790
|
+
stopped: false,
|
|
791
|
+
};
|
|
792
|
+
this.messages.set(messageId, created);
|
|
793
|
+
if (runId) {
|
|
794
|
+
this.activeMessageByRun.set(runId, messageId);
|
|
795
|
+
}
|
|
796
|
+
return created;
|
|
797
|
+
}
|
|
798
|
+
resolveMessageId(input) {
|
|
799
|
+
if (input.messageId) {
|
|
800
|
+
return input.messageId;
|
|
801
|
+
}
|
|
802
|
+
if (input.runId) {
|
|
803
|
+
const active = this.activeMessageByRun.get(input.runId);
|
|
804
|
+
if (active) {
|
|
805
|
+
return active;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (!input.createIfMissing) {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
const synthetic = `synthetic-message-${++this.syntheticMessageCounter}`;
|
|
812
|
+
if (input.runId) {
|
|
813
|
+
this.activeMessageByRun.set(input.runId, synthetic);
|
|
814
|
+
}
|
|
815
|
+
return synthetic;
|
|
816
|
+
}
|
|
817
|
+
extractFragments(content) {
|
|
818
|
+
if (typeof content === "string") {
|
|
819
|
+
if (content.length === 0) {
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
return [{ kind: "assistant", text: content }];
|
|
823
|
+
}
|
|
824
|
+
const blocks = Array.isArray(content) ? content : [content];
|
|
825
|
+
const fragments = [];
|
|
826
|
+
for (const rawBlock of blocks) {
|
|
827
|
+
if (!isClaudeContentChunk(rawBlock)) {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if ((rawBlock.type === "text" || rawBlock.type === "text_delta") &&
|
|
831
|
+
typeof rawBlock.text === "string" &&
|
|
832
|
+
rawBlock.text.length > 0) {
|
|
833
|
+
fragments.push({ kind: "assistant", text: rawBlock.text });
|
|
834
|
+
}
|
|
835
|
+
if ((rawBlock.type === "thinking" || rawBlock.type === "thinking_delta") &&
|
|
836
|
+
typeof rawBlock.thinking === "string" &&
|
|
837
|
+
rawBlock.thinking.length > 0) {
|
|
838
|
+
fragments.push({ kind: "reasoning", text: rawBlock.thinking });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return fragments;
|
|
842
|
+
}
|
|
843
|
+
readMessageIdFromAssistantMessage(message) {
|
|
844
|
+
const candidate = message;
|
|
845
|
+
return readTrimmedString(candidate.message_id) ??
|
|
846
|
+
readTrimmedString(candidate.message?.id) ??
|
|
847
|
+
null;
|
|
848
|
+
}
|
|
849
|
+
readMessageIdFromStreamEvent(event) {
|
|
850
|
+
const message = event.message;
|
|
851
|
+
return (readTrimmedString(event.message_id) ??
|
|
852
|
+
readTrimmedString(message?.id) ??
|
|
853
|
+
null);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function isMetadataOnlySdkMessage(message) {
|
|
857
|
+
if (message.type === "system") {
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
if (message.type !== "user") {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
return isTaskNotificationUserContent(message.message?.content);
|
|
864
|
+
}
|
|
865
|
+
export function readEventIdentifiers(message) {
|
|
866
|
+
const root = message;
|
|
867
|
+
const messageType = readTrimmedString(root.type);
|
|
868
|
+
const streamEvent = root.event;
|
|
869
|
+
const streamEventMessage = streamEvent?.message;
|
|
870
|
+
const messageContainer = root.message;
|
|
871
|
+
return {
|
|
872
|
+
taskId: readTrimmedString(root.task_id) ??
|
|
873
|
+
readTrimmedString(streamEvent?.task_id) ??
|
|
874
|
+
readTrimmedString(streamEventMessage?.task_id) ??
|
|
875
|
+
readTrimmedString(messageContainer?.task_id) ??
|
|
876
|
+
null,
|
|
877
|
+
parentMessageId: readTrimmedString(root.parent_message_id) ??
|
|
878
|
+
readTrimmedString(streamEvent?.parent_message_id) ??
|
|
879
|
+
readTrimmedString(streamEventMessage?.parent_message_id) ??
|
|
880
|
+
readTrimmedString(messageContainer?.parent_message_id) ??
|
|
881
|
+
null,
|
|
882
|
+
messageId: readTrimmedString(root.message_id) ??
|
|
883
|
+
readTrimmedString(streamEvent?.message_id) ??
|
|
884
|
+
readTrimmedString(streamEventMessage?.id) ??
|
|
885
|
+
readTrimmedString(streamEventMessage?.message_id) ??
|
|
886
|
+
readTrimmedString(messageContainer?.id) ??
|
|
887
|
+
readTrimmedString(messageContainer?.message_id) ??
|
|
888
|
+
(messageType === "user" ? readTrimmedString(root.uuid) : null) ??
|
|
889
|
+
null,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function isTaskNotificationUserContent(content) {
|
|
893
|
+
if (typeof content === "string") {
|
|
894
|
+
return content.includes("<task-notification>");
|
|
895
|
+
}
|
|
896
|
+
if (!Array.isArray(content)) {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
for (const block of content) {
|
|
900
|
+
if (block &&
|
|
901
|
+
typeof block === "object" &&
|
|
902
|
+
typeof block.text === "string" &&
|
|
903
|
+
block.text.includes("<task-notification>")) {
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
315
909
|
export class ClaudeAgentClient {
|
|
316
910
|
constructor(options) {
|
|
317
911
|
this.provider = "claude";
|
|
@@ -327,24 +921,7 @@ export class ClaudeAgentClient {
|
|
|
327
921
|
}
|
|
328
922
|
}
|
|
329
923
|
applyRuntimeSettings(options) {
|
|
330
|
-
|
|
331
|
-
const commandMode = this.runtimeSettings?.command?.mode;
|
|
332
|
-
const needsCustomSpawn = hasEnvOverrides || commandMode === "append" || commandMode === "replace";
|
|
333
|
-
if (!needsCustomSpawn) {
|
|
334
|
-
return options;
|
|
335
|
-
}
|
|
336
|
-
return {
|
|
337
|
-
...options,
|
|
338
|
-
spawnClaudeCodeProcess: (spawnOptions) => {
|
|
339
|
-
const resolved = resolveClaudeSpawnCommand(spawnOptions, this.runtimeSettings);
|
|
340
|
-
return spawn(resolved.command, resolved.args, {
|
|
341
|
-
cwd: spawnOptions.cwd,
|
|
342
|
-
env: applyProviderEnv(spawnOptions.env, this.runtimeSettings),
|
|
343
|
-
signal: spawnOptions.signal,
|
|
344
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
345
|
-
});
|
|
346
|
-
},
|
|
347
|
-
};
|
|
924
|
+
return applyRuntimeSettingsToClaudeOptions(options, this.runtimeSettings);
|
|
348
925
|
}
|
|
349
926
|
async createSession(config) {
|
|
350
927
|
const claudeConfig = this.assertConfig(config);
|
|
@@ -456,28 +1033,30 @@ class ClaudeAgentSession {
|
|
|
456
1033
|
this.toolUseIndexToId = new Map();
|
|
457
1034
|
this.toolUseInputBuffers = new Map();
|
|
458
1035
|
this.pendingPermissions = new Map();
|
|
459
|
-
this.
|
|
1036
|
+
this.activeForegroundTurn = null;
|
|
1037
|
+
this.liveEventQueue = new Pushable();
|
|
1038
|
+
this.runTracker = new RunTracker();
|
|
1039
|
+
this.timelineAssembler = new TimelineAssembler();
|
|
460
1040
|
this.persistedHistory = [];
|
|
461
1041
|
this.historyPending = false;
|
|
462
|
-
this.
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1042
|
+
this.turnState = "idle";
|
|
1043
|
+
this.preReplayMetadataSeen = false;
|
|
1044
|
+
this.pendingAutonomousWakeReservations = 0;
|
|
1045
|
+
this.nextRunOrdinal = 1;
|
|
466
1046
|
this.cancelCurrentTurn = null;
|
|
467
|
-
// Track the pending interrupt promise so we can await it in processPrompt
|
|
468
|
-
// This ensures the interrupt's response is consumed before we call query.next()
|
|
469
1047
|
this.pendingInterruptPromise = null;
|
|
470
|
-
// Track the current turn ID and active turn promise to serialize concurrent stream() calls
|
|
471
|
-
// and prevent race conditions where two processPrompt() loops run against the same query
|
|
472
|
-
this.currentTurnId = 0;
|
|
473
1048
|
this.activeTurnPromise = null;
|
|
474
1049
|
this.cachedRuntimeInfo = null;
|
|
475
1050
|
this.lastOptionsModel = null;
|
|
476
1051
|
this.selectableModelIds = null;
|
|
477
1052
|
this.activeSidechains = new Map();
|
|
478
1053
|
this.compacting = false;
|
|
1054
|
+
this.queryPumpPromise = null;
|
|
479
1055
|
this.queryRestartNeeded = false;
|
|
480
1056
|
this.userMessageIds = [];
|
|
1057
|
+
this.localUserMessageIds = new Set();
|
|
1058
|
+
this.suppressLocalReplayActivity = false;
|
|
1059
|
+
this.closed = false;
|
|
481
1060
|
this.handlePermissionRequest = async (toolName, input, options) => {
|
|
482
1061
|
const requestId = `permission-${randomUUID()}`;
|
|
483
1062
|
const kind = resolvePermissionKind(toolName, input);
|
|
@@ -520,19 +1099,6 @@ class ClaudeAgentSession {
|
|
|
520
1099
|
}
|
|
521
1100
|
}
|
|
522
1101
|
};
|
|
523
|
-
const timeout = setTimeout(() => {
|
|
524
|
-
this.pendingPermissions.delete(requestId);
|
|
525
|
-
cleanup();
|
|
526
|
-
const error = new Error("Permission request timed out");
|
|
527
|
-
this.pushEvent({
|
|
528
|
-
type: "permission_resolved",
|
|
529
|
-
provider: "claude",
|
|
530
|
-
requestId,
|
|
531
|
-
resolution: { behavior: "deny", message: "timeout" },
|
|
532
|
-
});
|
|
533
|
-
reject(error);
|
|
534
|
-
}, DEFAULT_PERMISSION_TIMEOUT_MS);
|
|
535
|
-
cleanupFns.push(() => clearTimeout(timeout));
|
|
536
1102
|
const abortHandler = () => {
|
|
537
1103
|
this.pendingPermissions.delete(requestId);
|
|
538
1104
|
cleanup();
|
|
@@ -640,81 +1206,124 @@ class ClaudeAgentSession {
|
|
|
640
1206
|
}
|
|
641
1207
|
async *stream(prompt, options) {
|
|
642
1208
|
void options;
|
|
643
|
-
// Increment turn ID to invalidate any in-flight processPrompt() loops from previous turns.
|
|
644
|
-
// This prevents race conditions where an interrupted turn's events get mixed with the new turn.
|
|
645
|
-
const turnId = ++this.currentTurnId;
|
|
646
|
-
// Cancel the previous turn if one exists. The caller of interrupt() is responsible
|
|
647
|
-
// for awaiting completion - the new turn just signals cancellation and proceeds.
|
|
648
1209
|
if (this.cancelCurrentTurn) {
|
|
649
1210
|
this.cancelCurrentTurn();
|
|
650
1211
|
}
|
|
651
|
-
|
|
652
|
-
this.
|
|
1212
|
+
this.suppressLocalReplayActivity = false;
|
|
1213
|
+
this.pendingAutonomousWakeReservations = 0;
|
|
653
1214
|
const slashCommand = this.resolveSlashCommandInvocation(prompt);
|
|
654
1215
|
if (slashCommand?.commandName === REWIND_COMMAND_NAME) {
|
|
655
1216
|
yield* this.streamRewindCommand(slashCommand);
|
|
656
1217
|
return;
|
|
657
1218
|
}
|
|
1219
|
+
await this.awaitPendingInterruptPromise();
|
|
1220
|
+
if (this.turnState === "autonomous" &&
|
|
1221
|
+
this.runTracker.hasActiveRuns("autonomous")) {
|
|
1222
|
+
await this.transitionAutonomousToForeground();
|
|
1223
|
+
}
|
|
658
1224
|
const sdkMessage = this.toSdkUserMessage(prompt);
|
|
659
1225
|
const queue = new Pushable();
|
|
660
|
-
this.
|
|
1226
|
+
const run = this.createRun("foreground", queue);
|
|
1227
|
+
this.runTracker.bindIdentifiers(run, {
|
|
1228
|
+
taskId: null,
|
|
1229
|
+
parentMessageId: null,
|
|
1230
|
+
messageId: typeof sdkMessage.uuid === "string" ? sdkMessage.uuid : null,
|
|
1231
|
+
});
|
|
1232
|
+
const foregroundTurn = {
|
|
1233
|
+
runId: run.id,
|
|
1234
|
+
queue,
|
|
1235
|
+
};
|
|
1236
|
+
this.activeForegroundTurn = foregroundTurn;
|
|
1237
|
+
this.preReplayMetadataSeen = false;
|
|
1238
|
+
this.transitionTurnState("foreground", "foreground stream started");
|
|
661
1239
|
let finishedNaturally = false;
|
|
662
1240
|
let cancelIssued = false;
|
|
1241
|
+
let queueDrainedWithoutTerminal = false;
|
|
1242
|
+
const turnPromise = Promise.resolve();
|
|
1243
|
+
this.activeTurnPromise = turnPromise;
|
|
663
1244
|
const requestCancel = () => {
|
|
664
1245
|
if (cancelIssued) {
|
|
665
1246
|
return;
|
|
666
1247
|
}
|
|
667
1248
|
cancelIssued = true;
|
|
668
|
-
this.
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1249
|
+
if (this.activeForegroundTurn?.runId === run.id) {
|
|
1250
|
+
this.activeForegroundTurn = null;
|
|
1251
|
+
}
|
|
1252
|
+
if (this.cancelCurrentTurn === requestCancel) {
|
|
1253
|
+
this.cancelCurrentTurn = null;
|
|
1254
|
+
}
|
|
1255
|
+
this.rejectAllPendingPermissions(new Error("Permission request aborted"));
|
|
1256
|
+
this.cancelRun(run, {
|
|
676
1257
|
type: "turn_canceled",
|
|
677
1258
|
provider: "claude",
|
|
678
1259
|
reason: "Interrupted",
|
|
679
1260
|
});
|
|
680
|
-
|
|
1261
|
+
this.pendingInterruptPromise = this.interruptActiveTurn().catch((error) => {
|
|
1262
|
+
this.logger.warn({ err: error }, "Failed to interrupt during cancel");
|
|
1263
|
+
});
|
|
681
1264
|
};
|
|
682
1265
|
this.cancelCurrentTurn = requestCancel;
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1266
|
+
try {
|
|
1267
|
+
await this.ensureQuery();
|
|
1268
|
+
if (!this.input) {
|
|
1269
|
+
throw new Error("Claude session input stream not initialized");
|
|
1270
|
+
}
|
|
1271
|
+
this.startQueryPump();
|
|
1272
|
+
this.input.push(sdkMessage);
|
|
1273
|
+
}
|
|
1274
|
+
catch (error) {
|
|
1275
|
+
this.failRun(run, error instanceof Error ? error.message : "Claude stream failed");
|
|
1276
|
+
finishedNaturally = true;
|
|
1277
|
+
}
|
|
689
1278
|
try {
|
|
690
1279
|
for await (const event of queue) {
|
|
691
|
-
|
|
692
|
-
if (event.type === "turn_completed" ||
|
|
1280
|
+
const isTerminalEvent = event.type === "turn_completed" ||
|
|
693
1281
|
event.type === "turn_failed" ||
|
|
694
|
-
event.type === "turn_canceled"
|
|
1282
|
+
event.type === "turn_canceled";
|
|
1283
|
+
if (isTerminalEvent) {
|
|
695
1284
|
finishedNaturally = true;
|
|
1285
|
+
}
|
|
1286
|
+
yield event;
|
|
1287
|
+
if (isTerminalEvent) {
|
|
696
1288
|
break;
|
|
697
1289
|
}
|
|
698
1290
|
}
|
|
1291
|
+
if (!finishedNaturally && !cancelIssued) {
|
|
1292
|
+
queueDrainedWithoutTerminal = true;
|
|
1293
|
+
}
|
|
699
1294
|
}
|
|
700
1295
|
finally {
|
|
701
|
-
if (!finishedNaturally && !cancelIssued) {
|
|
1296
|
+
if (!finishedNaturally && !cancelIssued && !queueDrainedWithoutTerminal) {
|
|
702
1297
|
requestCancel();
|
|
703
1298
|
}
|
|
704
|
-
if (this.
|
|
705
|
-
this.
|
|
1299
|
+
if (this.activeForegroundTurn === foregroundTurn) {
|
|
1300
|
+
this.activeForegroundTurn = null;
|
|
706
1301
|
}
|
|
707
1302
|
if (this.cancelCurrentTurn === requestCancel) {
|
|
708
1303
|
this.cancelCurrentTurn = null;
|
|
709
1304
|
}
|
|
710
|
-
|
|
711
|
-
if (this.activeTurnPromise === forwardPromise) {
|
|
1305
|
+
if (this.activeTurnPromise === turnPromise) {
|
|
712
1306
|
this.activeTurnPromise = null;
|
|
713
1307
|
}
|
|
714
1308
|
}
|
|
715
1309
|
}
|
|
716
1310
|
async interrupt() {
|
|
717
|
-
this.cancelCurrentTurn
|
|
1311
|
+
if (this.cancelCurrentTurn) {
|
|
1312
|
+
this.cancelCurrentTurn();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const autonomousRuns = this.runTracker.listActiveRuns("autonomous");
|
|
1316
|
+
if (autonomousRuns.length > 0) {
|
|
1317
|
+
this.flushPendingToolCalls();
|
|
1318
|
+
for (const run of autonomousRuns) {
|
|
1319
|
+
this.emitRunEvent(run, {
|
|
1320
|
+
type: "turn_canceled",
|
|
1321
|
+
provider: "claude",
|
|
1322
|
+
reason: "Interrupted",
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
await this.interruptActiveTurn();
|
|
718
1327
|
}
|
|
719
1328
|
async *streamHistory() {
|
|
720
1329
|
if (!this.historyPending || this.persistedHistory.length === 0) {
|
|
@@ -727,6 +1336,14 @@ class ClaudeAgentSession {
|
|
|
727
1336
|
yield { type: "timeline", item, provider: "claude" };
|
|
728
1337
|
}
|
|
729
1338
|
}
|
|
1339
|
+
async *streamLiveEvents() {
|
|
1340
|
+
if (this.claudeSessionId) {
|
|
1341
|
+
this.startQueryPump();
|
|
1342
|
+
}
|
|
1343
|
+
for await (const event of this.liveEventQueue) {
|
|
1344
|
+
yield event;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
730
1347
|
async getAvailableModes() {
|
|
731
1348
|
return this.availableModes;
|
|
732
1349
|
}
|
|
@@ -841,10 +1458,20 @@ class ClaudeAgentSession {
|
|
|
841
1458
|
return this.persistence;
|
|
842
1459
|
}
|
|
843
1460
|
async close() {
|
|
1461
|
+
this.closed = true;
|
|
844
1462
|
this.rejectAllPendingPermissions(new Error("Claude session closed"));
|
|
1463
|
+
this.cancelCurrentTurn?.();
|
|
1464
|
+
this.activeForegroundTurn?.queue.end();
|
|
1465
|
+
this.activeForegroundTurn = null;
|
|
1466
|
+
this.cancelCurrentTurn = null;
|
|
1467
|
+
this.turnState = "idle";
|
|
1468
|
+
this.suppressLocalReplayActivity = false;
|
|
1469
|
+
this.pendingAutonomousWakeReservations = 0;
|
|
1470
|
+
this.liveEventQueue.end();
|
|
1471
|
+
this.activeTurnPromise = null;
|
|
845
1472
|
this.input?.end();
|
|
846
|
-
await this.query?.interrupt?.();
|
|
847
|
-
await this.query?.return?.();
|
|
1473
|
+
await this.awaitWithTimeout(this.query?.interrupt?.(), "close query interrupt");
|
|
1474
|
+
await this.awaitWithTimeout(this.query?.return?.(), "close query return");
|
|
848
1475
|
this.query = null;
|
|
849
1476
|
this.input = null;
|
|
850
1477
|
}
|
|
@@ -898,11 +1525,6 @@ class ClaudeAgentSession {
|
|
|
898
1525
|
}
|
|
899
1526
|
async *streamRewindCommand(invocation) {
|
|
900
1527
|
yield { type: "turn_started", provider: "claude" };
|
|
901
|
-
yield {
|
|
902
|
-
type: "timeline",
|
|
903
|
-
provider: "claude",
|
|
904
|
-
item: { type: "user_message", text: invocation.rawInput },
|
|
905
|
-
};
|
|
906
1528
|
try {
|
|
907
1529
|
const rewindAttempt = await this.attemptRewind(invocation.args);
|
|
908
1530
|
if (!rewindAttempt.messageId || !rewindAttempt.result) {
|
|
@@ -1116,13 +1738,30 @@ class ClaudeAgentSession {
|
|
|
1116
1738
|
}
|
|
1117
1739
|
const input = new Pushable();
|
|
1118
1740
|
const options = this.buildOptions();
|
|
1119
|
-
this.logger.debug({ options }, "claude query");
|
|
1741
|
+
this.logger.debug({ options: summarizeClaudeOptionsForLog(options) }, "claude query");
|
|
1120
1742
|
this.input = input;
|
|
1121
1743
|
this.query = query({ prompt: input, options });
|
|
1122
|
-
|
|
1123
|
-
|
|
1744
|
+
// Do not block query readiness on control-plane calls. We need `next()` to
|
|
1745
|
+
// start immediately so autonomous wake events are not missed between turns.
|
|
1746
|
+
void this.awaitWithTimeout(this.primeSelectableModelIds(this.query), "prime selectable model ids");
|
|
1124
1747
|
return this.query;
|
|
1125
1748
|
}
|
|
1749
|
+
async awaitWithTimeout(promise, label) {
|
|
1750
|
+
if (!promise) {
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
await Promise.race([
|
|
1755
|
+
promise,
|
|
1756
|
+
new Promise((_, reject) => {
|
|
1757
|
+
setTimeout(() => reject(new Error("timeout")), 3000);
|
|
1758
|
+
}),
|
|
1759
|
+
]);
|
|
1760
|
+
}
|
|
1761
|
+
catch (error) {
|
|
1762
|
+
this.logger.warn({ err: error, label }, "Claude query operation did not settle cleanly");
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1126
1765
|
buildOptions() {
|
|
1127
1766
|
const configuredThinkingOptionId = this.config.thinkingOptionId;
|
|
1128
1767
|
const thinkingOptionId = configuredThinkingOptionId && configuredThinkingOptionId !== "default"
|
|
@@ -1186,24 +1825,7 @@ class ClaudeAgentSession {
|
|
|
1186
1825
|
return this.applyRuntimeSettings(base);
|
|
1187
1826
|
}
|
|
1188
1827
|
applyRuntimeSettings(options) {
|
|
1189
|
-
|
|
1190
|
-
const commandMode = this.runtimeSettings?.command?.mode;
|
|
1191
|
-
const needsCustomSpawn = hasEnvOverrides || commandMode === "append" || commandMode === "replace";
|
|
1192
|
-
if (!needsCustomSpawn) {
|
|
1193
|
-
return options;
|
|
1194
|
-
}
|
|
1195
|
-
return {
|
|
1196
|
-
...options,
|
|
1197
|
-
spawnClaudeCodeProcess: (spawnOptions) => {
|
|
1198
|
-
const resolved = resolveClaudeSpawnCommand(spawnOptions, this.runtimeSettings);
|
|
1199
|
-
return spawn(resolved.command, resolved.args, {
|
|
1200
|
-
cwd: spawnOptions.cwd,
|
|
1201
|
-
env: applyProviderEnv(spawnOptions.env, this.runtimeSettings),
|
|
1202
|
-
signal: spawnOptions.signal,
|
|
1203
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1204
|
-
});
|
|
1205
|
-
},
|
|
1206
|
-
};
|
|
1828
|
+
return applyRuntimeSettingsToClaudeOptions(options, this.runtimeSettings);
|
|
1207
1829
|
}
|
|
1208
1830
|
normalizeMcpServers(servers) {
|
|
1209
1831
|
const result = {};
|
|
@@ -1236,6 +1858,7 @@ class ClaudeAgentSession {
|
|
|
1236
1858
|
}
|
|
1237
1859
|
const messageId = randomUUID();
|
|
1238
1860
|
this.rememberUserMessageId(messageId);
|
|
1861
|
+
this.localUserMessageIds.add(messageId);
|
|
1239
1862
|
return {
|
|
1240
1863
|
type: "user",
|
|
1241
1864
|
message: {
|
|
@@ -1247,175 +1870,716 @@ class ClaudeAgentSession {
|
|
|
1247
1870
|
session_id: this.claudeSessionId ?? "",
|
|
1248
1871
|
};
|
|
1249
1872
|
}
|
|
1250
|
-
async
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
// so we must wait for it to complete before we try to get the query.
|
|
1254
|
-
if (this.pendingInterruptPromise) {
|
|
1255
|
-
await this.pendingInterruptPromise;
|
|
1256
|
-
this.pendingInterruptPromise = null;
|
|
1873
|
+
async awaitPendingInterruptPromise() {
|
|
1874
|
+
if (!this.pendingInterruptPromise) {
|
|
1875
|
+
return;
|
|
1257
1876
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1877
|
+
await this.pendingInterruptPromise;
|
|
1878
|
+
this.pendingInterruptPromise = null;
|
|
1879
|
+
}
|
|
1880
|
+
createRun(owner, queue) {
|
|
1881
|
+
const runId = `${owner}-run-${this.nextRunOrdinal++}`;
|
|
1882
|
+
const run = this.runTracker.createRun({
|
|
1883
|
+
id: runId,
|
|
1884
|
+
owner,
|
|
1885
|
+
queue,
|
|
1886
|
+
promptReplaySeen: owner === "autonomous",
|
|
1887
|
+
});
|
|
1888
|
+
this.logger.debug({ runId, owner, state: run.state }, "Created Claude run");
|
|
1889
|
+
return run;
|
|
1890
|
+
}
|
|
1891
|
+
transitionTurnState(next, reason) {
|
|
1892
|
+
if (this.turnState === next) {
|
|
1260
1893
|
return;
|
|
1261
1894
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1895
|
+
this.logger.debug({ from: this.turnState, to: next, reason }, "Claude turn state transition");
|
|
1896
|
+
this.turnState = next;
|
|
1897
|
+
}
|
|
1898
|
+
transitionTurnStateFromActiveRuns(reason) {
|
|
1899
|
+
if (this.runTracker.hasActiveRuns("foreground")) {
|
|
1900
|
+
this.transitionTurnState("foreground", reason);
|
|
1901
|
+
return;
|
|
1265
1902
|
}
|
|
1266
|
-
this.
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1903
|
+
if (this.runTracker.hasActiveRuns("autonomous")) {
|
|
1904
|
+
this.transitionTurnState("autonomous", reason);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
this.transitionTurnState("idle", reason);
|
|
1908
|
+
}
|
|
1909
|
+
failRun(run, errorMessage) {
|
|
1910
|
+
this.emitRunEvent(run, {
|
|
1911
|
+
type: "turn_failed",
|
|
1912
|
+
provider: "claude",
|
|
1913
|
+
error: errorMessage,
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
cancelRun(run, event) {
|
|
1917
|
+
this.flushPendingToolCalls();
|
|
1918
|
+
this.emitRunEvent(run, event);
|
|
1919
|
+
}
|
|
1920
|
+
emitRunEvent(run, event) {
|
|
1921
|
+
if (run.owner === "foreground" && run.queue) {
|
|
1922
|
+
run.queue.push(event);
|
|
1923
|
+
if (event.type === "turn_completed" ||
|
|
1924
|
+
event.type === "turn_failed" ||
|
|
1925
|
+
event.type === "turn_canceled") {
|
|
1926
|
+
run.queue.end();
|
|
1271
1927
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1928
|
+
}
|
|
1929
|
+
else {
|
|
1930
|
+
this.liveEventQueue.push(event);
|
|
1931
|
+
}
|
|
1932
|
+
this.handleRunTerminalEvent(run, event);
|
|
1933
|
+
}
|
|
1934
|
+
handleRunTerminalEvent(run, event) {
|
|
1935
|
+
if (event.type === "turn_completed") {
|
|
1936
|
+
this.runTracker.complete(run, "completed");
|
|
1937
|
+
}
|
|
1938
|
+
else if (event.type === "turn_failed") {
|
|
1939
|
+
this.runTracker.complete(run, "error");
|
|
1940
|
+
}
|
|
1941
|
+
else if (event.type === "turn_canceled") {
|
|
1942
|
+
this.runTracker.complete(run, "interrupted");
|
|
1943
|
+
}
|
|
1944
|
+
else {
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (this.activeForegroundTurn?.runId === run.id) {
|
|
1948
|
+
this.activeForegroundTurn = null;
|
|
1949
|
+
this.preReplayMetadataSeen = false;
|
|
1950
|
+
}
|
|
1951
|
+
this.transitionTurnStateFromActiveRuns(`run ${run.id} terminal`);
|
|
1952
|
+
}
|
|
1953
|
+
async transitionAutonomousToForeground() {
|
|
1954
|
+
const autonomousRuns = this.runTracker.listActiveRuns("autonomous");
|
|
1955
|
+
if (autonomousRuns.length === 0) {
|
|
1956
|
+
this.transitionTurnStateFromActiveRuns("no autonomous runs to transition");
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
this.logger.debug({ runIds: autonomousRuns.map((run) => run.id) }, "Transitioning autonomous runs to foreground ownership");
|
|
1960
|
+
this.flushPendingToolCalls();
|
|
1961
|
+
for (const run of autonomousRuns) {
|
|
1962
|
+
this.emitRunEvent(run, {
|
|
1963
|
+
type: "turn_canceled",
|
|
1964
|
+
provider: "claude",
|
|
1965
|
+
reason: "Interrupted by foreground prompt",
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
this.pendingInterruptPromise = this.interruptActiveTurn().catch((error) => {
|
|
1969
|
+
this.logger.warn({ err: error }, "Failed to interrupt autonomous run during foreground transition");
|
|
1970
|
+
});
|
|
1971
|
+
await this.awaitPendingInterruptPromise();
|
|
1972
|
+
this.transitionTurnStateFromActiveRuns("autonomous interrupted for foreground");
|
|
1973
|
+
}
|
|
1974
|
+
routeMessage(normalized) {
|
|
1975
|
+
if (normalized.metadataOnly) {
|
|
1976
|
+
if (normalized.message.type === "user" &&
|
|
1977
|
+
isTaskNotificationUserContent(normalized.message.message?.content)) {
|
|
1978
|
+
this.reserveAutonomousWake("task_notification");
|
|
1275
1979
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1980
|
+
this.notePreReplayMetadata(normalized.message);
|
|
1981
|
+
return { run: null, reason: "metadata" };
|
|
1982
|
+
}
|
|
1983
|
+
const hasIdentifiers = Boolean(normalized.identifiers.taskId ||
|
|
1984
|
+
normalized.identifiers.parentMessageId ||
|
|
1985
|
+
normalized.identifiers.messageId);
|
|
1986
|
+
const byIdentifiers = this.runTracker.resolveByIdentifiers(normalized.identifiers);
|
|
1987
|
+
if (byIdentifiers.run) {
|
|
1988
|
+
return byIdentifiers;
|
|
1989
|
+
}
|
|
1990
|
+
if (this.turnState === "autonomous") {
|
|
1991
|
+
const activeAutonomousRun = this.runTracker.getLatestActiveRun("autonomous");
|
|
1992
|
+
if (activeAutonomousRun) {
|
|
1993
|
+
return { run: activeAutonomousRun, reason: "unbound_autonomous" };
|
|
1278
1994
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1995
|
+
}
|
|
1996
|
+
const foregroundRun = this.activeForegroundTurn
|
|
1997
|
+
? this.runTracker.getRun(this.activeForegroundTurn.runId)
|
|
1998
|
+
: null;
|
|
1999
|
+
// A previously unseen task_id during foreground ownership is deterministic
|
|
2000
|
+
// evidence of a distinct autonomous wake/run, not foreground response text.
|
|
2001
|
+
if (this.turnState === "foreground" &&
|
|
2002
|
+
foregroundRun &&
|
|
2003
|
+
normalized.identifiers.taskId) {
|
|
2004
|
+
const incomingTaskId = normalized.identifiers.taskId;
|
|
2005
|
+
// Foreground must claim its first task_id; otherwise early foreground
|
|
2006
|
+
// result events can be misrouted to autonomous fallback runs.
|
|
2007
|
+
if (foregroundRun.taskIds.size === 0) {
|
|
2008
|
+
if (foregroundRun.state !== "finalizing") {
|
|
2009
|
+
return { run: foregroundRun, reason: "foreground" };
|
|
2010
|
+
}
|
|
1282
2011
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
2012
|
+
else if (foregroundRun.taskIds.has(incomingTaskId)) {
|
|
2013
|
+
return { run: foregroundRun, reason: "foreground" };
|
|
2014
|
+
}
|
|
2015
|
+
const autonomousRun = this.createRun("autonomous", null);
|
|
2016
|
+
this.emitRunEvent(autonomousRun, { type: "turn_started", provider: "claude" });
|
|
2017
|
+
return { run: autonomousRun, reason: "task_id_new" };
|
|
2018
|
+
}
|
|
2019
|
+
if (this.turnState === "foreground" &&
|
|
2020
|
+
foregroundRun &&
|
|
2021
|
+
this.shouldPreferForegroundRun({
|
|
2022
|
+
run: foregroundRun,
|
|
2023
|
+
message: normalized.message,
|
|
2024
|
+
})) {
|
|
2025
|
+
return { run: foregroundRun, reason: "foreground" };
|
|
2026
|
+
}
|
|
2027
|
+
if (!hasIdentifiers) {
|
|
2028
|
+
if (this.pendingAutonomousWakeReservations > 0) {
|
|
2029
|
+
const reservedAutonomousRun = this.claimOrCreateAutonomousRun("reservation_unbound");
|
|
2030
|
+
return {
|
|
2031
|
+
run: reservedAutonomousRun,
|
|
2032
|
+
reason: "unbound_autonomous",
|
|
2033
|
+
};
|
|
1286
2034
|
}
|
|
2035
|
+
const activeAutonomousRun = this.runTracker.getLatestActiveRun("autonomous");
|
|
2036
|
+
if (activeAutonomousRun) {
|
|
2037
|
+
return { run: activeAutonomousRun, reason: "unbound_autonomous" };
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
if (this.pendingAutonomousWakeReservations > 0) {
|
|
2041
|
+
const reservedAutonomousRun = this.claimOrCreateAutonomousRun("reservation_fallback");
|
|
2042
|
+
return { run: reservedAutonomousRun, reason: "fallback" };
|
|
1287
2043
|
}
|
|
2044
|
+
const autonomousRun = this.createRun("autonomous", null);
|
|
2045
|
+
this.emitRunEvent(autonomousRun, { type: "turn_started", provider: "claude" });
|
|
2046
|
+
return { run: autonomousRun, reason: "fallback" };
|
|
1288
2047
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
2048
|
+
shouldPreferForegroundRun(input) {
|
|
2049
|
+
const { run, message } = input;
|
|
2050
|
+
if (run.state === "completed" ||
|
|
2051
|
+
run.state === "interrupted" ||
|
|
2052
|
+
run.state === "error") {
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2055
|
+
// Before prompt replay is observed, prefer foreground by default so the
|
|
2056
|
+
// first turn cannot be stranded in autonomous fallback. If metadata churn
|
|
2057
|
+
// was observed pre-replay, stay conservative and wait for replay.
|
|
2058
|
+
if (!run.promptReplaySeen) {
|
|
2059
|
+
// Keep pre-replay result events with the foreground run so stale result
|
|
2060
|
+
// bursts cannot consume autonomous wake reservations.
|
|
2061
|
+
if (message.type === "result") {
|
|
2062
|
+
return true;
|
|
2063
|
+
}
|
|
2064
|
+
if (message.type === "assistant" ||
|
|
2065
|
+
message.type === "stream_event" ||
|
|
2066
|
+
message.type === "tool_progress") {
|
|
2067
|
+
return !this.preReplayMetadataSeen;
|
|
2068
|
+
}
|
|
2069
|
+
return true;
|
|
2070
|
+
}
|
|
2071
|
+
if (run.state === "finalizing" &&
|
|
2072
|
+
(message.type === "assistant" || message.type === "stream_event")) {
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
return true;
|
|
2076
|
+
}
|
|
2077
|
+
notePreReplayMetadata(message) {
|
|
2078
|
+
if (this.turnState !== "foreground") {
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
const foregroundRun = this.activeForegroundTurn
|
|
2082
|
+
? this.runTracker.getRun(this.activeForegroundTurn.runId)
|
|
2083
|
+
: null;
|
|
2084
|
+
if (!foregroundRun || foregroundRun.promptReplaySeen) {
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
this.preReplayMetadataSeen = true;
|
|
2091
|
+
}
|
|
2092
|
+
reserveAutonomousWake(reason) {
|
|
2093
|
+
this.pendingAutonomousWakeReservations += 1;
|
|
2094
|
+
this.logger.debug({
|
|
2095
|
+
reason,
|
|
2096
|
+
pendingAutonomousWakeReservations: this.pendingAutonomousWakeReservations,
|
|
2097
|
+
}, "Reserved autonomous wake");
|
|
2098
|
+
}
|
|
2099
|
+
claimOrCreateAutonomousRun(reason) {
|
|
2100
|
+
const existing = this.runTracker.getLatestActiveRun("autonomous");
|
|
2101
|
+
if (existing) {
|
|
2102
|
+
if (this.pendingAutonomousWakeReservations > 0) {
|
|
2103
|
+
this.pendingAutonomousWakeReservations -= 1;
|
|
2104
|
+
}
|
|
2105
|
+
this.logger.debug({
|
|
2106
|
+
reason,
|
|
2107
|
+
runId: existing.id,
|
|
2108
|
+
pendingAutonomousWakeReservations: this.pendingAutonomousWakeReservations,
|
|
2109
|
+
}, "Claimed autonomous wake reservation on existing run");
|
|
2110
|
+
return existing;
|
|
2111
|
+
}
|
|
2112
|
+
const run = this.createRun("autonomous", null);
|
|
2113
|
+
this.emitRunEvent(run, { type: "turn_started", provider: "claude" });
|
|
2114
|
+
if (this.pendingAutonomousWakeReservations > 0) {
|
|
2115
|
+
this.pendingAutonomousWakeReservations -= 1;
|
|
2116
|
+
}
|
|
2117
|
+
this.logger.debug({
|
|
2118
|
+
reason,
|
|
2119
|
+
runId: run.id,
|
|
2120
|
+
pendingAutonomousWakeReservations: this.pendingAutonomousWakeReservations,
|
|
2121
|
+
}, "Claimed autonomous wake reservation with new run");
|
|
2122
|
+
return run;
|
|
2123
|
+
}
|
|
2124
|
+
startQueryPump() {
|
|
2125
|
+
if (this.closed || this.queryPumpPromise) {
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
const pump = this.runQueryPump().catch((error) => {
|
|
2129
|
+
this.logger.warn({ err: error }, "Claude query pump exited unexpectedly");
|
|
2130
|
+
});
|
|
2131
|
+
this.queryPumpPromise = pump;
|
|
2132
|
+
pump.finally(() => {
|
|
2133
|
+
if (this.queryPumpPromise === pump) {
|
|
2134
|
+
this.queryPumpPromise = null;
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
async runQueryPump() {
|
|
2139
|
+
while (!this.closed) {
|
|
2140
|
+
if (!this.claudeSessionId && !this.activeForegroundTurn && !this.query) {
|
|
2141
|
+
await this.waitForLiveHistoryPoll();
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
let q;
|
|
2145
|
+
try {
|
|
2146
|
+
q = await this.ensureQuery();
|
|
2147
|
+
}
|
|
2148
|
+
catch (error) {
|
|
2149
|
+
this.logger.warn({ err: error }, "Failed to initialize Claude query pump");
|
|
2150
|
+
await this.waitForLiveHistoryPoll();
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
let next;
|
|
2154
|
+
try {
|
|
2155
|
+
next = await q.next();
|
|
2156
|
+
}
|
|
2157
|
+
catch (error) {
|
|
2158
|
+
this.logger.warn({ err: error }, "Claude query pump next() failed");
|
|
2159
|
+
for (const run of this.runTracker.listActiveRuns()) {
|
|
2160
|
+
this.failRun(run, error instanceof Error ? error.message : "Claude stream failed");
|
|
2161
|
+
}
|
|
2162
|
+
this.input?.end();
|
|
2163
|
+
await this.awaitWithTimeout(q.return?.(), "query pump return after failure");
|
|
2164
|
+
if (this.query === q) {
|
|
2165
|
+
this.query = null;
|
|
2166
|
+
this.input = null;
|
|
1302
2167
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
2168
|
+
await this.waitForLiveHistoryPoll();
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
if (next.done) {
|
|
2172
|
+
this.input?.end();
|
|
2173
|
+
await this.awaitWithTimeout(q.return?.(), "query pump return on done");
|
|
2174
|
+
if (this.query === q) {
|
|
2175
|
+
this.query = null;
|
|
2176
|
+
this.input = null;
|
|
2177
|
+
}
|
|
2178
|
+
const activeRuns = this.runTracker.listActiveRuns();
|
|
2179
|
+
if (activeRuns.length > 0) {
|
|
2180
|
+
for (const run of activeRuns) {
|
|
2181
|
+
this.failRun(run, "Claude stream ended before terminal result");
|
|
1308
2182
|
}
|
|
1309
2183
|
}
|
|
2184
|
+
await this.waitForLiveHistoryPoll();
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
const sdkMessage = next.value;
|
|
2188
|
+
if (!sdkMessage) {
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2191
|
+
try {
|
|
2192
|
+
this.routeSdkMessageFromPump(sdkMessage);
|
|
2193
|
+
}
|
|
2194
|
+
catch (error) {
|
|
2195
|
+
this.logger.warn({ err: error }, "Failed to route Claude SDK message from query pump");
|
|
1310
2196
|
}
|
|
1311
2197
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
2198
|
+
}
|
|
2199
|
+
routeSdkMessageFromPump(message) {
|
|
2200
|
+
if (this.shouldSuppressLocalReplayActivity(message)) {
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
const identifiers = readEventIdentifiers(message);
|
|
2204
|
+
const metadataOnly = isMetadataOnlySdkMessage(message);
|
|
2205
|
+
const route = this.routeMessage({
|
|
2206
|
+
message,
|
|
2207
|
+
identifiers,
|
|
2208
|
+
metadataOnly,
|
|
2209
|
+
});
|
|
2210
|
+
const suppressTerminalEvents = this.shouldSuppressReplayResultTerminal({
|
|
2211
|
+
run: route.run,
|
|
2212
|
+
message,
|
|
2213
|
+
});
|
|
2214
|
+
if (route.run) {
|
|
2215
|
+
this.transitionTurnStateFromActiveRuns(`routed via ${route.reason}`);
|
|
2216
|
+
this.runTracker.bindIdentifiers(route.run, identifiers);
|
|
2217
|
+
if (!suppressTerminalEvents) {
|
|
2218
|
+
this.updateRunLifecycleForMessage(route.run, message, identifiers);
|
|
1319
2219
|
}
|
|
1320
2220
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
2221
|
+
const messageEvents = this.translateMessageToEvents(message, {
|
|
2222
|
+
suppressAssistantText: true,
|
|
2223
|
+
suppressReasoning: true,
|
|
2224
|
+
suppressTerminalEvents,
|
|
2225
|
+
});
|
|
2226
|
+
const assistantTimelineItems = this.timelineAssembler.consume({
|
|
2227
|
+
message,
|
|
2228
|
+
runId: route.run?.id ?? null,
|
|
2229
|
+
messageIdHint: identifiers.messageId,
|
|
2230
|
+
});
|
|
2231
|
+
const assistantTimelineEvents = assistantTimelineItems.map((item) => ({
|
|
2232
|
+
type: "timeline",
|
|
2233
|
+
item,
|
|
2234
|
+
provider: "claude",
|
|
2235
|
+
}));
|
|
2236
|
+
const events = [...messageEvents, ...assistantTimelineEvents];
|
|
2237
|
+
if (events.length === 0) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (!route.run) {
|
|
2241
|
+
this.dispatchMetadataEvents(events);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
for (const event of events) {
|
|
2245
|
+
this.emitRunEvent(route.run, event);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
shouldSuppressReplayResultTerminal(input) {
|
|
2249
|
+
const { run, message } = input;
|
|
2250
|
+
if (!run || run.owner !== "foreground" || message.type !== "result") {
|
|
2251
|
+
return false;
|
|
2252
|
+
}
|
|
2253
|
+
if (run.promptReplaySeen) {
|
|
2254
|
+
return false;
|
|
2255
|
+
}
|
|
2256
|
+
if (run.state === "streaming" || run.state === "finalizing") {
|
|
2257
|
+
return false;
|
|
2258
|
+
}
|
|
2259
|
+
const resultSubtype = "subtype" in message && typeof message.subtype === "string"
|
|
2260
|
+
? message.subtype
|
|
2261
|
+
: null;
|
|
2262
|
+
// Pre-replay success results are stale in practice (leftover from an
|
|
2263
|
+
// earlier query segment) and must not end the current foreground run.
|
|
2264
|
+
if (resultSubtype === "success") {
|
|
2265
|
+
return true;
|
|
2266
|
+
}
|
|
2267
|
+
// For non-success results, keep the metadata-churn guard to avoid
|
|
2268
|
+
// suppressing legitimate hard failures.
|
|
2269
|
+
return this.preReplayMetadataSeen;
|
|
2270
|
+
}
|
|
2271
|
+
dispatchMetadataEvents(events) {
|
|
2272
|
+
for (const event of events) {
|
|
2273
|
+
this.pushEvent(event);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
updateRunLifecycleForMessage(run, message, identifiers) {
|
|
2277
|
+
if (message.type === "user" &&
|
|
2278
|
+
identifiers.messageId &&
|
|
2279
|
+
run.messageIds.has(identifiers.messageId)) {
|
|
2280
|
+
run.promptReplaySeen = true;
|
|
2281
|
+
this.preReplayMetadataSeen = false;
|
|
2282
|
+
}
|
|
2283
|
+
if (run.state === "queued") {
|
|
2284
|
+
this.runTracker.transition(run, "awaiting_response");
|
|
2285
|
+
}
|
|
2286
|
+
if (message.type === "assistant" ||
|
|
2287
|
+
message.type === "stream_event" ||
|
|
2288
|
+
message.type === "tool_progress") {
|
|
2289
|
+
this.runTracker.transition(run, "streaming");
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if (message.type === "result") {
|
|
2293
|
+
this.runTracker.transition(run, "finalizing");
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
shouldSuppressLocalReplayActivity(message) {
|
|
2298
|
+
const localReplay = this.isLocalReplayUserMessage(message);
|
|
2299
|
+
if (!this.activeForegroundTurn && localReplay) {
|
|
2300
|
+
this.suppressLocalReplayActivity = true;
|
|
2301
|
+
this.logger.debug({ uuid: message.uuid }, "Suppressing local replay user message from live pump");
|
|
2302
|
+
return true;
|
|
2303
|
+
}
|
|
2304
|
+
if (!this.suppressLocalReplayActivity) {
|
|
2305
|
+
return false;
|
|
2306
|
+
}
|
|
2307
|
+
// Suppress only replay scaffolding. Do not suppress autonomous
|
|
2308
|
+
// assistant/result events; otherwise task-notification replies can be dropped.
|
|
2309
|
+
if (localReplay) {
|
|
2310
|
+
return true;
|
|
2311
|
+
}
|
|
2312
|
+
if (message.type === "system") {
|
|
2313
|
+
return true;
|
|
2314
|
+
}
|
|
2315
|
+
const identifiers = readEventIdentifiers(message);
|
|
2316
|
+
const hasIdentifiers = Boolean(identifiers.taskId || identifiers.parentMessageId || identifiers.messageId);
|
|
2317
|
+
if (message.type !== "user" && !hasIdentifiers) {
|
|
2318
|
+
if (this.pendingAutonomousWakeReservations > 0) {
|
|
2319
|
+
this.suppressLocalReplayActivity = false;
|
|
2320
|
+
return false;
|
|
1333
2321
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
2322
|
+
return true;
|
|
2323
|
+
}
|
|
2324
|
+
if (message.type === "user") {
|
|
2325
|
+
this.suppressLocalReplayActivity = false;
|
|
2326
|
+
return false;
|
|
1336
2327
|
}
|
|
2328
|
+
this.suppressLocalReplayActivity = false;
|
|
2329
|
+
return false;
|
|
2330
|
+
}
|
|
2331
|
+
isLocalReplayUserMessage(message) {
|
|
2332
|
+
if (message.type !== "user") {
|
|
2333
|
+
return false;
|
|
2334
|
+
}
|
|
2335
|
+
const uuid = readTrimmedString(message.uuid);
|
|
2336
|
+
if (!uuid) {
|
|
2337
|
+
return false;
|
|
2338
|
+
}
|
|
2339
|
+
return this.localUserMessageIds.has(uuid);
|
|
1337
2340
|
}
|
|
1338
2341
|
async interruptActiveTurn() {
|
|
1339
2342
|
const queryToInterrupt = this.query;
|
|
1340
2343
|
if (!queryToInterrupt || typeof queryToInterrupt.interrupt !== "function") {
|
|
1341
|
-
this.logger.
|
|
2344
|
+
this.logger.debug("interruptActiveTurn: no query to interrupt");
|
|
1342
2345
|
return;
|
|
1343
2346
|
}
|
|
1344
2347
|
try {
|
|
1345
|
-
this.logger.
|
|
2348
|
+
this.logger.debug("interruptActiveTurn: calling query.interrupt()...");
|
|
1346
2349
|
const t0 = Date.now();
|
|
1347
2350
|
await queryToInterrupt.interrupt();
|
|
1348
|
-
this.logger.
|
|
2351
|
+
this.logger.debug({ durationMs: Date.now() - t0 }, "interruptActiveTurn: query.interrupt() returned");
|
|
1349
2352
|
// After interrupt(), the query iterator is done (returns done: true).
|
|
1350
2353
|
// Clear it so ensureQuery() creates a fresh query for the next turn.
|
|
1351
2354
|
// Also end the input stream and call return() to clean up the SDK process.
|
|
1352
2355
|
this.input?.end();
|
|
1353
|
-
this.logger.
|
|
2356
|
+
this.logger.debug("interruptActiveTurn: calling query.return()...");
|
|
1354
2357
|
const t1 = Date.now();
|
|
1355
2358
|
await queryToInterrupt.return?.();
|
|
1356
|
-
this.logger.
|
|
2359
|
+
this.logger.debug({ durationMs: Date.now() - t1 }, "interruptActiveTurn: query.return() returned");
|
|
1357
2360
|
this.query = null;
|
|
1358
2361
|
this.input = null;
|
|
1359
2362
|
this.queryRestartNeeded = false;
|
|
1360
2363
|
}
|
|
1361
2364
|
catch (error) {
|
|
1362
2365
|
this.logger.warn({ err: error }, "Failed to interrupt active turn");
|
|
2366
|
+
// If interrupt fails, the SDK iterator may remain in an indeterminate state.
|
|
2367
|
+
// Force a teardown/recreate path so the next turn cannot reuse stale query state.
|
|
2368
|
+
this.queryRestartNeeded = true;
|
|
1363
2369
|
}
|
|
1364
2370
|
}
|
|
1365
2371
|
handleSidechainMessage(message, parentToolUseId) {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
else if (message.type === "stream_event") {
|
|
1381
|
-
const event = message.event;
|
|
1382
|
-
if (event.type === "content_block_start") {
|
|
1383
|
-
const cb = isClaudeContentChunk(event.content_block) ? event.content_block : null;
|
|
1384
|
-
if (cb?.type === "tool_use" && typeof cb.name === "string") {
|
|
1385
|
-
toolName = cb.name;
|
|
1386
|
-
}
|
|
2372
|
+
const state = this.activeSidechains.get(parentToolUseId) ??
|
|
2373
|
+
{
|
|
2374
|
+
actions: [],
|
|
2375
|
+
actionKeys: [],
|
|
2376
|
+
nextActionIndex: 1,
|
|
2377
|
+
actionIndexByKey: new Map(),
|
|
2378
|
+
};
|
|
2379
|
+
this.activeSidechains.set(parentToolUseId, state);
|
|
2380
|
+
const contextUpdated = this.updateSubAgentContextFromTaskInput(state, parentToolUseId);
|
|
2381
|
+
const actionCandidates = this.extractSubAgentActionCandidates(message);
|
|
2382
|
+
let actionUpdated = false;
|
|
2383
|
+
for (const action of actionCandidates) {
|
|
2384
|
+
if (this.appendSubAgentAction(state, action)) {
|
|
2385
|
+
actionUpdated = true;
|
|
1387
2386
|
}
|
|
1388
2387
|
}
|
|
1389
|
-
|
|
1390
|
-
toolName = message.tool_name;
|
|
1391
|
-
}
|
|
1392
|
-
if (!toolName) {
|
|
1393
|
-
return [];
|
|
1394
|
-
}
|
|
1395
|
-
const prev = this.activeSidechains.get(parentToolUseId);
|
|
1396
|
-
if (prev === toolName) {
|
|
2388
|
+
if (!contextUpdated && !actionUpdated) {
|
|
1397
2389
|
return [];
|
|
1398
2390
|
}
|
|
1399
|
-
this.activeSidechains.set(parentToolUseId, toolName);
|
|
1400
2391
|
const toolCall = mapClaudeRunningToolCall({
|
|
1401
2392
|
name: "Task",
|
|
1402
2393
|
callId: parentToolUseId,
|
|
1403
2394
|
input: null,
|
|
1404
2395
|
output: null,
|
|
1405
|
-
metadata: { subAgentActivity: toolName },
|
|
1406
2396
|
});
|
|
1407
2397
|
if (!toolCall) {
|
|
1408
2398
|
return [];
|
|
1409
2399
|
}
|
|
2400
|
+
const detail = {
|
|
2401
|
+
type: "sub_agent",
|
|
2402
|
+
...(state.subAgentType ? { subAgentType: state.subAgentType } : {}),
|
|
2403
|
+
...(state.description ? { description: state.description } : {}),
|
|
2404
|
+
log: state.actions
|
|
2405
|
+
.map((action) => action.summary
|
|
2406
|
+
? `[${action.toolName}] ${action.summary}`
|
|
2407
|
+
: `[${action.toolName}]`)
|
|
2408
|
+
.join("\n"),
|
|
2409
|
+
actions: state.actions.map((action) => ({
|
|
2410
|
+
index: action.index,
|
|
2411
|
+
toolName: action.toolName,
|
|
2412
|
+
...(action.summary ? { summary: action.summary } : {}),
|
|
2413
|
+
})),
|
|
2414
|
+
};
|
|
1410
2415
|
return [
|
|
1411
2416
|
{
|
|
1412
2417
|
type: "timeline",
|
|
1413
|
-
item:
|
|
2418
|
+
item: {
|
|
2419
|
+
...toolCall,
|
|
2420
|
+
detail,
|
|
2421
|
+
},
|
|
1414
2422
|
provider: "claude",
|
|
1415
2423
|
},
|
|
1416
2424
|
];
|
|
1417
2425
|
}
|
|
1418
|
-
|
|
2426
|
+
updateSubAgentContextFromTaskInput(state, parentToolUseId) {
|
|
2427
|
+
const taskInput = this.toolUseCache.get(parentToolUseId)?.input;
|
|
2428
|
+
const nextSubAgentType = this.normalizeSubAgentText(taskInput?.subagent_type);
|
|
2429
|
+
const nextDescription = this.normalizeSubAgentText(taskInput?.description);
|
|
2430
|
+
let changed = false;
|
|
2431
|
+
if (nextSubAgentType && nextSubAgentType !== state.subAgentType) {
|
|
2432
|
+
state.subAgentType = nextSubAgentType;
|
|
2433
|
+
changed = true;
|
|
2434
|
+
}
|
|
2435
|
+
if (nextDescription && nextDescription !== state.description) {
|
|
2436
|
+
state.description = nextDescription;
|
|
2437
|
+
changed = true;
|
|
2438
|
+
}
|
|
2439
|
+
return changed;
|
|
2440
|
+
}
|
|
2441
|
+
normalizeSubAgentText(value) {
|
|
2442
|
+
const normalized = readTrimmedString(value)?.replace(/\s+/g, " ");
|
|
2443
|
+
if (!normalized) {
|
|
2444
|
+
return undefined;
|
|
2445
|
+
}
|
|
2446
|
+
if (normalized.length <= MAX_SUB_AGENT_SUMMARY_CHARS) {
|
|
2447
|
+
return normalized;
|
|
2448
|
+
}
|
|
2449
|
+
return `${normalized.slice(0, MAX_SUB_AGENT_SUMMARY_CHARS)}...`;
|
|
2450
|
+
}
|
|
2451
|
+
extractSubAgentActionCandidates(message) {
|
|
2452
|
+
if (message.type === "assistant") {
|
|
2453
|
+
const content = message.message?.content;
|
|
2454
|
+
if (!Array.isArray(content)) {
|
|
2455
|
+
return [];
|
|
2456
|
+
}
|
|
2457
|
+
const actions = [];
|
|
2458
|
+
for (const block of content) {
|
|
2459
|
+
if (!isClaudeContentChunk(block) ||
|
|
2460
|
+
!(block.type === "tool_use" ||
|
|
2461
|
+
block.type === "mcp_tool_use" ||
|
|
2462
|
+
block.type === "server_tool_use") ||
|
|
2463
|
+
typeof block.name !== "string") {
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
const key = readTrimmedString(block.id) ??
|
|
2467
|
+
`assistant:${block.name}:${actions.length}`;
|
|
2468
|
+
actions.push({
|
|
2469
|
+
key,
|
|
2470
|
+
toolName: block.name,
|
|
2471
|
+
input: block.input ?? null,
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
return actions;
|
|
2475
|
+
}
|
|
2476
|
+
if (message.type === "stream_event") {
|
|
2477
|
+
const event = message.event;
|
|
2478
|
+
if (event.type !== "content_block_start") {
|
|
2479
|
+
return [];
|
|
2480
|
+
}
|
|
2481
|
+
const block = isClaudeContentChunk(event.content_block)
|
|
2482
|
+
? event.content_block
|
|
2483
|
+
: null;
|
|
2484
|
+
if (!block ||
|
|
2485
|
+
!(block.type === "tool_use" ||
|
|
2486
|
+
block.type === "mcp_tool_use" ||
|
|
2487
|
+
block.type === "server_tool_use") ||
|
|
2488
|
+
typeof block.name !== "string") {
|
|
2489
|
+
return [];
|
|
2490
|
+
}
|
|
2491
|
+
const key = readTrimmedString(block.id) ??
|
|
2492
|
+
`stream:${block.name}:${typeof event.index === "number" ? event.index : 0}`;
|
|
2493
|
+
return [
|
|
2494
|
+
{
|
|
2495
|
+
key,
|
|
2496
|
+
toolName: block.name,
|
|
2497
|
+
input: block.input ?? null,
|
|
2498
|
+
},
|
|
2499
|
+
];
|
|
2500
|
+
}
|
|
2501
|
+
if (message.type === "tool_progress") {
|
|
2502
|
+
const toolName = readTrimmedString(message.tool_name);
|
|
2503
|
+
if (!toolName) {
|
|
2504
|
+
return [];
|
|
2505
|
+
}
|
|
2506
|
+
const key = readTrimmedString(message.tool_use_id) ?? `progress:${toolName}`;
|
|
2507
|
+
return [{ key, toolName, input: null }];
|
|
2508
|
+
}
|
|
2509
|
+
return [];
|
|
2510
|
+
}
|
|
2511
|
+
appendSubAgentAction(state, candidate) {
|
|
2512
|
+
const normalizedToolName = readTrimmedString(candidate.toolName);
|
|
2513
|
+
if (!normalizedToolName) {
|
|
2514
|
+
return false;
|
|
2515
|
+
}
|
|
2516
|
+
const summary = this.deriveSubAgentActionSummary(normalizedToolName, candidate.input);
|
|
2517
|
+
const existingIndex = state.actionIndexByKey.get(candidate.key);
|
|
2518
|
+
if (existingIndex !== undefined) {
|
|
2519
|
+
const existing = state.actions[existingIndex];
|
|
2520
|
+
if (!existing) {
|
|
2521
|
+
return false;
|
|
2522
|
+
}
|
|
2523
|
+
const nextSummary = existing.summary ?? summary;
|
|
2524
|
+
const unchanged = existing.toolName === normalizedToolName &&
|
|
2525
|
+
existing.summary === nextSummary;
|
|
2526
|
+
if (unchanged) {
|
|
2527
|
+
return false;
|
|
2528
|
+
}
|
|
2529
|
+
state.actions[existingIndex] = {
|
|
2530
|
+
...existing,
|
|
2531
|
+
toolName: normalizedToolName,
|
|
2532
|
+
...(nextSummary ? { summary: nextSummary } : {}),
|
|
2533
|
+
};
|
|
2534
|
+
return true;
|
|
2535
|
+
}
|
|
2536
|
+
const nextEntry = {
|
|
2537
|
+
index: state.nextActionIndex,
|
|
2538
|
+
toolName: normalizedToolName,
|
|
2539
|
+
...(summary ? { summary } : {}),
|
|
2540
|
+
};
|
|
2541
|
+
state.nextActionIndex += 1;
|
|
2542
|
+
state.actions.push(nextEntry);
|
|
2543
|
+
state.actionKeys.push(candidate.key);
|
|
2544
|
+
this.trimSubAgentTail(state);
|
|
2545
|
+
this.rebuildSubAgentActionIndex(state);
|
|
2546
|
+
return true;
|
|
2547
|
+
}
|
|
2548
|
+
trimSubAgentTail(state) {
|
|
2549
|
+
while (state.actions.length > MAX_SUB_AGENT_LOG_ENTRIES) {
|
|
2550
|
+
state.actions.shift();
|
|
2551
|
+
state.actionKeys.shift();
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
rebuildSubAgentActionIndex(state) {
|
|
2555
|
+
state.actionIndexByKey.clear();
|
|
2556
|
+
for (let index = 0; index < state.actionKeys.length; index += 1) {
|
|
2557
|
+
const key = state.actionKeys[index];
|
|
2558
|
+
if (key) {
|
|
2559
|
+
state.actionIndexByKey.set(key, index);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
deriveSubAgentActionSummary(toolName, input) {
|
|
2564
|
+
const runningToolCall = mapClaudeRunningToolCall({
|
|
2565
|
+
name: toolName,
|
|
2566
|
+
callId: `sub-agent-summary-${toolName}`,
|
|
2567
|
+
input,
|
|
2568
|
+
output: null,
|
|
2569
|
+
});
|
|
2570
|
+
if (!runningToolCall) {
|
|
2571
|
+
return undefined;
|
|
2572
|
+
}
|
|
2573
|
+
const display = buildToolCallDisplayModel({
|
|
2574
|
+
name: runningToolCall.name,
|
|
2575
|
+
status: runningToolCall.status,
|
|
2576
|
+
error: runningToolCall.error,
|
|
2577
|
+
detail: runningToolCall.detail,
|
|
2578
|
+
metadata: runningToolCall.metadata,
|
|
2579
|
+
});
|
|
2580
|
+
return this.normalizeSubAgentText(display.summary);
|
|
2581
|
+
}
|
|
2582
|
+
translateMessageToEvents(message, options) {
|
|
1419
2583
|
const parentToolUseId = "parent_tool_use_id" in message
|
|
1420
2584
|
? message.parent_tool_use_id
|
|
1421
2585
|
: null;
|
|
@@ -1423,6 +2587,14 @@ class ClaudeAgentSession {
|
|
|
1423
2587
|
return this.handleSidechainMessage(message, parentToolUseId);
|
|
1424
2588
|
}
|
|
1425
2589
|
const events = [];
|
|
2590
|
+
const fallbackThreadSessionId = this.captureSessionIdFromMessage(message);
|
|
2591
|
+
if (fallbackThreadSessionId) {
|
|
2592
|
+
events.push({
|
|
2593
|
+
type: "thread_started",
|
|
2594
|
+
provider: "claude",
|
|
2595
|
+
sessionId: fallbackThreadSessionId,
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
1426
2598
|
switch (message.type) {
|
|
1427
2599
|
case "system":
|
|
1428
2600
|
if (message.subtype === "init") {
|
|
@@ -1447,14 +2619,14 @@ class ClaudeAgentSession {
|
|
|
1447
2619
|
}
|
|
1448
2620
|
}
|
|
1449
2621
|
else if (message.subtype === "compact_boundary") {
|
|
1450
|
-
const
|
|
2622
|
+
const compactMetadata = readCompactionMetadata(message);
|
|
1451
2623
|
events.push({
|
|
1452
2624
|
type: "timeline",
|
|
1453
2625
|
item: {
|
|
1454
2626
|
type: "compaction",
|
|
1455
2627
|
status: "completed",
|
|
1456
|
-
trigger:
|
|
1457
|
-
preTokens:
|
|
2628
|
+
trigger: compactMetadata?.trigger === "manual" ? "manual" : "auto",
|
|
2629
|
+
preTokens: compactMetadata?.preTokens,
|
|
1458
2630
|
},
|
|
1459
2631
|
provider: "claude",
|
|
1460
2632
|
});
|
|
@@ -1483,7 +2655,7 @@ class ClaudeAgentSession {
|
|
|
1483
2655
|
});
|
|
1484
2656
|
}
|
|
1485
2657
|
else if (Array.isArray(content)) {
|
|
1486
|
-
const timelineItems = this.mapBlocksToTimeline(content
|
|
2658
|
+
const timelineItems = this.mapBlocksToTimeline(content);
|
|
1487
2659
|
for (const item of timelineItems) {
|
|
1488
2660
|
if (item.type === "user_message" && messageId && !item.messageId) {
|
|
1489
2661
|
events.push({
|
|
@@ -1500,9 +2672,8 @@ class ClaudeAgentSession {
|
|
|
1500
2672
|
}
|
|
1501
2673
|
case "assistant": {
|
|
1502
2674
|
const timelineItems = this.mapBlocksToTimeline(message.message.content, {
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
suppressReasoning: turnContext.streamedReasoningThisTurn,
|
|
2675
|
+
suppressAssistantText: options?.suppressAssistantText ?? false,
|
|
2676
|
+
suppressReasoning: options?.suppressReasoning ?? false,
|
|
1506
2677
|
});
|
|
1507
2678
|
for (const item of timelineItems) {
|
|
1508
2679
|
events.push({ type: "timeline", item, provider: "claude" });
|
|
@@ -1510,13 +2681,19 @@ class ClaudeAgentSession {
|
|
|
1510
2681
|
break;
|
|
1511
2682
|
}
|
|
1512
2683
|
case "stream_event": {
|
|
1513
|
-
const timelineItems = this.mapPartialEvent(message.event,
|
|
2684
|
+
const timelineItems = this.mapPartialEvent(message.event, {
|
|
2685
|
+
suppressAssistantText: options?.suppressAssistantText ?? false,
|
|
2686
|
+
suppressReasoning: options?.suppressReasoning ?? false,
|
|
2687
|
+
});
|
|
1514
2688
|
for (const item of timelineItems) {
|
|
1515
2689
|
events.push({ type: "timeline", item, provider: "claude" });
|
|
1516
2690
|
}
|
|
1517
2691
|
break;
|
|
1518
2692
|
}
|
|
1519
2693
|
case "result": {
|
|
2694
|
+
if (options?.suppressTerminalEvents) {
|
|
2695
|
+
break;
|
|
2696
|
+
}
|
|
1520
2697
|
const usage = this.convertUsage(message);
|
|
1521
2698
|
if (message.subtype === "success") {
|
|
1522
2699
|
events.push({ type: "turn_completed", provider: "claude", usage });
|
|
@@ -1534,6 +2711,31 @@ class ClaudeAgentSession {
|
|
|
1534
2711
|
}
|
|
1535
2712
|
return events;
|
|
1536
2713
|
}
|
|
2714
|
+
captureSessionIdFromMessage(message) {
|
|
2715
|
+
const msg = message;
|
|
2716
|
+
const sessionIdRaw = typeof msg.session_id === "string"
|
|
2717
|
+
? msg.session_id
|
|
2718
|
+
: typeof msg.sessionId === "string"
|
|
2719
|
+
? msg.sessionId
|
|
2720
|
+
: typeof msg.session?.id === "string"
|
|
2721
|
+
? msg.session.id
|
|
2722
|
+
: "";
|
|
2723
|
+
const sessionId = sessionIdRaw.trim();
|
|
2724
|
+
if (!sessionId) {
|
|
2725
|
+
return null;
|
|
2726
|
+
}
|
|
2727
|
+
if (this.claudeSessionId === null) {
|
|
2728
|
+
this.claudeSessionId = sessionId;
|
|
2729
|
+
this.persistence = null;
|
|
2730
|
+
return sessionId;
|
|
2731
|
+
}
|
|
2732
|
+
if (this.claudeSessionId === sessionId) {
|
|
2733
|
+
return null;
|
|
2734
|
+
}
|
|
2735
|
+
throw new Error(`CRITICAL: Claude session ID overwrite detected! ` +
|
|
2736
|
+
`Existing: ${this.claudeSessionId}, New: ${sessionId}. ` +
|
|
2737
|
+
`This indicates a session identity corruption bug.`);
|
|
2738
|
+
}
|
|
1537
2739
|
handleSystemMessage(message) {
|
|
1538
2740
|
if (message.subtype !== "init") {
|
|
1539
2741
|
return null;
|
|
@@ -1613,6 +2815,7 @@ class ClaudeAgentSession {
|
|
|
1613
2815
|
}
|
|
1614
2816
|
}
|
|
1615
2817
|
this.toolUseCache.clear();
|
|
2818
|
+
this.activeSidechains.clear();
|
|
1616
2819
|
}
|
|
1617
2820
|
pushToolCall(item, target) {
|
|
1618
2821
|
if (!item) {
|
|
@@ -1625,9 +2828,19 @@ class ClaudeAgentSession {
|
|
|
1625
2828
|
this.enqueueTimeline(item);
|
|
1626
2829
|
}
|
|
1627
2830
|
pushEvent(event) {
|
|
1628
|
-
|
|
1629
|
-
|
|
2831
|
+
const foregroundTurn = this.activeForegroundTurn;
|
|
2832
|
+
if (foregroundTurn) {
|
|
2833
|
+
const run = this.runTracker.getRun(foregroundTurn.runId);
|
|
2834
|
+
if (run &&
|
|
2835
|
+
run.owner === "foreground" &&
|
|
2836
|
+
run.queue === foregroundTurn.queue &&
|
|
2837
|
+
this.runTracker.isRunActive(run)) {
|
|
2838
|
+
foregroundTurn.queue.push(event);
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
this.activeForegroundTurn = null;
|
|
1630
2842
|
}
|
|
2843
|
+
this.liveEventQueue.push(event);
|
|
1631
2844
|
}
|
|
1632
2845
|
normalizePermissionUpdates(updates) {
|
|
1633
2846
|
if (!updates || updates.length === 0) {
|
|
@@ -1643,6 +2856,9 @@ class ClaudeAgentSession {
|
|
|
1643
2856
|
this.pendingPermissions.delete(id);
|
|
1644
2857
|
}
|
|
1645
2858
|
}
|
|
2859
|
+
waitForLiveHistoryPoll() {
|
|
2860
|
+
return new Promise((resolve) => setTimeout(resolve, 250));
|
|
2861
|
+
}
|
|
1646
2862
|
loadPersistedHistory(sessionId) {
|
|
1647
2863
|
try {
|
|
1648
2864
|
const historyPath = this.resolveHistoryPath(sessionId);
|
|
@@ -1692,20 +2908,15 @@ class ClaudeAgentSession {
|
|
|
1692
2908
|
return path.join(dir, `${sessionId}.jsonl`);
|
|
1693
2909
|
}
|
|
1694
2910
|
convertHistoryEntry(entry) {
|
|
1695
|
-
return convertClaudeHistoryEntry(entry, (content) => this.mapBlocksToTimeline(content
|
|
2911
|
+
return convertClaudeHistoryEntry(entry, (content) => this.mapBlocksToTimeline(content));
|
|
1696
2912
|
}
|
|
1697
2913
|
mapBlocksToTimeline(content, options) {
|
|
1698
|
-
const context = options?.context ?? "live";
|
|
1699
|
-
const turnContext = options?.turnContext;
|
|
1700
2914
|
const suppressAssistant = options?.suppressAssistantText ?? false;
|
|
1701
2915
|
const suppressReasoning = options?.suppressReasoning ?? false;
|
|
1702
2916
|
if (typeof content === "string") {
|
|
1703
|
-
if (!content || content ===
|
|
2917
|
+
if (!content || content === INTERRUPT_TOOL_USE_PLACEHOLDER) {
|
|
1704
2918
|
return [];
|
|
1705
2919
|
}
|
|
1706
|
-
if (context === "live" && turnContext) {
|
|
1707
|
-
turnContext.streamedAssistantTextThisTurn = true;
|
|
1708
|
-
}
|
|
1709
2920
|
if (suppressAssistant) {
|
|
1710
2921
|
return [];
|
|
1711
2922
|
}
|
|
@@ -1716,10 +2927,7 @@ class ClaudeAgentSession {
|
|
|
1716
2927
|
switch (block.type) {
|
|
1717
2928
|
case "text":
|
|
1718
2929
|
case "text_delta":
|
|
1719
|
-
if (block.text && block.text !==
|
|
1720
|
-
if (context === "live" && turnContext) {
|
|
1721
|
-
turnContext.streamedAssistantTextThisTurn = true;
|
|
1722
|
-
}
|
|
2930
|
+
if (block.text && block.text !== INTERRUPT_TOOL_USE_PLACEHOLDER) {
|
|
1723
2931
|
if (!suppressAssistant) {
|
|
1724
2932
|
items.push({ type: "assistant_message", text: block.text });
|
|
1725
2933
|
}
|
|
@@ -1728,9 +2936,6 @@ class ClaudeAgentSession {
|
|
|
1728
2936
|
case "thinking":
|
|
1729
2937
|
case "thinking_delta":
|
|
1730
2938
|
if (block.thinking) {
|
|
1731
|
-
if (context === "live" && turnContext) {
|
|
1732
|
-
turnContext.streamedReasoningThisTurn = true;
|
|
1733
|
-
}
|
|
1734
2939
|
if (!suppressReasoning) {
|
|
1735
2940
|
items.push({ type: "reasoning", text: block.thinking });
|
|
1736
2941
|
}
|
|
@@ -1802,6 +3007,7 @@ class ClaudeAgentSession {
|
|
|
1802
3007
|
}
|
|
1803
3008
|
if (typeof block.tool_use_id === "string") {
|
|
1804
3009
|
this.toolUseCache.delete(block.tool_use_id);
|
|
3010
|
+
this.activeSidechains.delete(block.tool_use_id);
|
|
1805
3011
|
}
|
|
1806
3012
|
}
|
|
1807
3013
|
buildToolOutput(block, entry) {
|
|
@@ -1810,7 +3016,7 @@ class ClaudeAgentSession {
|
|
|
1810
3016
|
}
|
|
1811
3017
|
const server = entry?.server ?? block.server ?? "tool";
|
|
1812
3018
|
const tool = entry?.name ?? block.tool_name ?? "tool";
|
|
1813
|
-
const content =
|
|
3019
|
+
const content = coerceToolResultContentToString(block.content);
|
|
1814
3020
|
const input = entry?.input;
|
|
1815
3021
|
// Build structured result based on tool type
|
|
1816
3022
|
const structured = this.buildStructuredToolResult(server, tool, content, input);
|
|
@@ -1899,7 +3105,7 @@ class ClaudeAgentSession {
|
|
|
1899
3105
|
}
|
|
1900
3106
|
return undefined;
|
|
1901
3107
|
}
|
|
1902
|
-
mapPartialEvent(event,
|
|
3108
|
+
mapPartialEvent(event, options) {
|
|
1903
3109
|
if (event.type === "content_block_start") {
|
|
1904
3110
|
const block = isClaudeContentChunk(event.content_block) ? event.content_block : null;
|
|
1905
3111
|
if (block?.type === "tool_use" && typeof event.index === "number" && typeof block.id === "string") {
|
|
@@ -1925,10 +3131,18 @@ class ClaudeAgentSession {
|
|
|
1925
3131
|
switch (event.type) {
|
|
1926
3132
|
case "content_block_start":
|
|
1927
3133
|
return isClaudeContentChunk(event.content_block)
|
|
1928
|
-
? this.mapBlocksToTimeline([event.content_block], {
|
|
3134
|
+
? this.mapBlocksToTimeline([event.content_block], {
|
|
3135
|
+
suppressAssistantText: options?.suppressAssistantText,
|
|
3136
|
+
suppressReasoning: options?.suppressReasoning,
|
|
3137
|
+
})
|
|
1929
3138
|
: [];
|
|
1930
3139
|
case "content_block_delta":
|
|
1931
|
-
return isClaudeContentChunk(event.delta)
|
|
3140
|
+
return isClaudeContentChunk(event.delta)
|
|
3141
|
+
? this.mapBlocksToTimeline([event.delta], {
|
|
3142
|
+
suppressAssistantText: options?.suppressAssistantText,
|
|
3143
|
+
suppressReasoning: options?.suppressReasoning,
|
|
3144
|
+
})
|
|
3145
|
+
: [];
|
|
1932
3146
|
default:
|
|
1933
3147
|
return [];
|
|
1934
3148
|
}
|
|
@@ -2127,6 +3341,24 @@ function hasToolLikeBlock(block) {
|
|
|
2127
3341
|
const type = typeof block.type === "string" ? block.type.toLowerCase() : "";
|
|
2128
3342
|
return type.includes("tool");
|
|
2129
3343
|
}
|
|
3344
|
+
function readCompactionMetadata(source) {
|
|
3345
|
+
const candidates = [
|
|
3346
|
+
source.compact_metadata,
|
|
3347
|
+
source.compactMetadata,
|
|
3348
|
+
source.compactionMetadata,
|
|
3349
|
+
];
|
|
3350
|
+
for (const candidate of candidates) {
|
|
3351
|
+
if (!candidate || typeof candidate !== "object") {
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
const metadata = candidate;
|
|
3355
|
+
const trigger = typeof metadata.trigger === "string" ? metadata.trigger : undefined;
|
|
3356
|
+
const preTokensRaw = metadata.preTokens ?? metadata.pre_tokens;
|
|
3357
|
+
const preTokens = typeof preTokensRaw === "number" ? preTokensRaw : undefined;
|
|
3358
|
+
return { trigger, preTokens };
|
|
3359
|
+
}
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
2130
3362
|
function normalizeHistoryBlocks(content) {
|
|
2131
3363
|
if (Array.isArray(content)) {
|
|
2132
3364
|
const blocks = content.filter((entry) => isClaudeContentChunk(entry));
|
|
@@ -2139,11 +3371,12 @@ function normalizeHistoryBlocks(content) {
|
|
|
2139
3371
|
}
|
|
2140
3372
|
export function convertClaudeHistoryEntry(entry, mapBlocks) {
|
|
2141
3373
|
if (entry.type === "system" && entry.subtype === "compact_boundary") {
|
|
3374
|
+
const compactMetadata = readCompactionMetadata(entry);
|
|
2142
3375
|
return [{
|
|
2143
3376
|
type: "compaction",
|
|
2144
3377
|
status: "completed",
|
|
2145
|
-
trigger:
|
|
2146
|
-
preTokens:
|
|
3378
|
+
trigger: compactMetadata?.trigger === "manual" ? "manual" : "auto",
|
|
3379
|
+
preTokens: compactMetadata?.preTokens,
|
|
2147
3380
|
}];
|
|
2148
3381
|
}
|
|
2149
3382
|
if (entry.isCompactSummary) {
|