@gkorepanov/ccodex 0.3.0 → 0.3.4
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/README.md +27 -9
- package/RELEASING.md +14 -15
- package/SSH_VERIFY.md +11 -0
- package/archive/README.md +23 -0
- package/compatibility.json +1 -2
- package/config.example.toml +12 -4
- package/dist/claude/asyncQueue.d.ts +3 -0
- package/dist/claude/asyncQueue.js +13 -2
- package/dist/claude/asyncQueue.js.map +1 -1
- package/dist/claude/commandActions.d.ts +6 -0
- package/dist/claude/commandActions.js +57 -0
- package/dist/claude/commandActions.js.map +1 -0
- package/dist/claude/compactCommand.d.ts +2 -0
- package/dist/claude/compactCommand.js +7 -0
- package/dist/claude/compactCommand.js.map +1 -0
- package/dist/claude/goalTools.d.ts +15 -0
- package/dist/claude/goalTools.js +173 -0
- package/dist/claude/goalTools.js.map +1 -0
- package/dist/claude/hookMapper.d.ts +17 -4
- package/dist/claude/hookMapper.js +7 -5
- package/dist/claude/hookMapper.js.map +1 -1
- package/dist/claude/rateLimits.d.ts +72 -0
- package/dist/claude/rateLimits.js +330 -0
- package/dist/claude/rateLimits.js.map +1 -0
- package/dist/claude/responseItemValidation.d.ts +2 -0
- package/dist/claude/responseItemValidation.js +40 -0
- package/dist/claude/responseItemValidation.js.map +1 -0
- package/dist/claude/resultClassifier.d.ts +8 -2
- package/dist/claude/resultClassifier.js +1 -1
- package/dist/claude/resultClassifier.js.map +1 -1
- package/dist/claude/service.d.ts +108 -28
- package/dist/claude/service.js +1276 -668
- package/dist/claude/service.js.map +1 -1
- package/dist/claude/session/backgroundOutput.d.ts +2 -0
- package/dist/claude/session/backgroundOutput.js +29 -0
- package/dist/claude/session/backgroundOutput.js.map +1 -0
- package/dist/claude/session/commands.d.ts +855 -0
- package/dist/claude/session/commands.js +2 -0
- package/dist/claude/session/commands.js.map +1 -0
- package/dist/claude/session/goalState.d.ts +49 -0
- package/dist/claude/session/goalState.js +407 -0
- package/dist/claude/session/goalState.js.map +1 -0
- package/dist/claude/session/mailbox.d.ts +52 -0
- package/dist/claude/session/mailbox.js +190 -0
- package/dist/claude/session/mailbox.js.map +1 -0
- package/dist/claude/session/outputAdapter.d.ts +11 -0
- package/dist/claude/session/outputAdapter.js +31 -0
- package/dist/claude/session/outputAdapter.js.map +1 -0
- package/dist/claude/session/providerFacts.d.ts +47 -0
- package/dist/claude/session/providerFacts.js +84 -0
- package/dist/claude/session/providerFacts.js.map +1 -0
- package/dist/claude/session/providerRuntime.d.ts +1 -0
- package/dist/claude/session/providerRuntime.js +2 -0
- package/dist/claude/session/providerRuntime.js.map +1 -0
- package/dist/claude/session/providerRuntimeFactory.d.ts +49 -0
- package/dist/claude/session/providerRuntimeFactory.js +127 -0
- package/dist/claude/session/providerRuntimeFactory.js.map +1 -0
- package/dist/claude/session/repository.d.ts +57 -0
- package/dist/claude/session/repository.js +161 -0
- package/dist/claude/session/repository.js.map +1 -0
- package/dist/claude/session/runtime.d.ts +46 -0
- package/dist/claude/session/runtime.js +140 -0
- package/dist/claude/session/runtime.js.map +1 -0
- package/dist/claude/session/scopeState.d.ts +61 -0
- package/dist/claude/session/scopeState.js +74 -0
- package/dist/claude/session/scopeState.js.map +1 -0
- package/dist/claude/session/session.d.ts +281 -0
- package/dist/claude/session/session.js +6664 -0
- package/dist/claude/session/session.js.map +1 -0
- package/dist/claude/session/shellRunner.d.ts +13 -0
- package/dist/claude/session/shellRunner.js +103 -0
- package/dist/claude/session/shellRunner.js.map +1 -0
- package/dist/claude/session/usage.d.ts +2 -0
- package/dist/claude/session/usage.js +10 -0
- package/dist/claude/session/usage.js.map +1 -0
- package/dist/claude/sessionRegistry.d.ts +26 -0
- package/dist/claude/sessionRegistry.js +118 -0
- package/dist/claude/sessionRegistry.js.map +1 -0
- package/dist/claude/shellSupervisor.d.ts +1 -0
- package/dist/claude/shellSupervisor.js +69 -0
- package/dist/claude/shellSupervisor.js.map +1 -0
- package/dist/claude/statusCommand.d.ts +21 -0
- package/dist/claude/statusCommand.js +97 -0
- package/dist/claude/statusCommand.js.map +1 -0
- package/dist/claude/toolMapper.d.ts +9 -1
- package/dist/claude/toolMapper.js +92 -16
- package/dist/claude/toolMapper.js.map +1 -1
- package/dist/claude/transcriptBrancher.d.ts +37 -0
- package/dist/claude/transcriptBrancher.js +103 -0
- package/dist/claude/transcriptBrancher.js.map +1 -0
- package/dist/cli/delegate.js +23 -1
- package/dist/cli/delegate.js.map +1 -1
- package/dist/cli/main.js +28 -12
- package/dist/cli/main.js.map +1 -1
- package/dist/codex/generated/ResponseItemRuntimeSchema.d.ts +8 -0
- package/dist/codex/generated/ResponseItemRuntimeSchema.js +7 -0
- package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -0
- package/dist/compatibility/probe.js +11 -6
- package/dist/compatibility/probe.js.map +1 -1
- package/dist/config/config.d.ts +9 -0
- package/dist/config/config.js +80 -11
- package/dist/config/config.js.map +1 -1
- package/dist/daemon/daemon.js +91 -43
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/ownership.d.ts +15 -0
- package/dist/daemon/ownership.js +185 -0
- package/dist/daemon/ownership.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +3 -0
- package/dist/daemon/supervisor.js +37 -1
- package/dist/daemon/supervisor.js.map +1 -1
- package/dist/gateway/clientConnection.d.ts +7 -1
- package/dist/gateway/clientConnection.js +435 -48
- package/dist/gateway/clientConnection.js.map +1 -1
- package/dist/gateway/remoteRelay.js +0 -1
- package/dist/gateway/remoteRelay.js.map +1 -1
- package/dist/gateway/server.d.ts +1 -1
- package/dist/gateway/server.js +37 -9
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/subscriptions.d.ts +9 -2
- package/dist/gateway/subscriptions.js +87 -9
- package/dist/gateway/subscriptions.js.map +1 -1
- package/dist/gateway/transientNotice.d.ts +2 -0
- package/dist/gateway/transientNotice.js +42 -1
- package/dist/gateway/transientNotice.js.map +1 -1
- package/dist/handoff/service.d.ts +32 -3
- package/dist/handoff/service.js +304 -26
- package/dist/handoff/service.js.map +1 -1
- package/dist/handoff/titleGeneration.d.ts +28 -0
- package/dist/handoff/titleGeneration.js +151 -0
- package/dist/handoff/titleGeneration.js.map +1 -0
- package/dist/management/doctor.d.ts +1 -1
- package/dist/management/doctor.js +32 -23
- package/dist/management/doctor.js.map +1 -1
- package/dist/management/lifecycle.js +14 -2
- package/dist/management/lifecycle.js.map +1 -1
- package/dist/management/remoteShim.d.ts +10 -0
- package/dist/management/remoteShim.js +65 -0
- package/dist/management/remoteShim.js.map +1 -0
- package/dist/management/setup.d.ts +4 -0
- package/dist/management/setup.js +108 -39
- package/dist/management/setup.js.map +1 -1
- package/dist/observability/metrics.d.ts +2 -0
- package/dist/observability/metrics.js +6 -0
- package/dist/observability/metrics.js.map +1 -1
- package/dist/protocol/errors.d.ts +1 -0
- package/dist/protocol/errors.js +4 -1
- package/dist/protocol/errors.js.map +1 -1
- package/dist/runtime/dependencies.js +2 -2
- package/dist/runtime/dependencies.js.map +1 -1
- package/dist/runtime/providerAvailability.d.ts +24 -0
- package/dist/runtime/providerAvailability.js +107 -0
- package/dist/runtime/providerAvailability.js.map +1 -0
- package/dist/state/stateCommand.d.ts +21 -0
- package/dist/state/stateCommand.js +198 -0
- package/dist/state/stateCommand.js.map +1 -0
- package/dist/state/stockStateTracker.d.ts +14 -0
- package/dist/state/stockStateTracker.js +120 -0
- package/dist/state/stockStateTracker.js.map +1 -0
- package/dist/store/HybridStore.d.ts +118 -2
- package/dist/store/HybridStore.js +23 -1
- package/dist/store/HybridStore.js.map +1 -1
- package/dist/store/memoryStore.d.ts +57 -7
- package/dist/store/memoryStore.js +337 -5
- package/dist/store/memoryStore.js.map +1 -1
- package/dist/store/sqliteStore.d.ts +30 -4
- package/dist/store/sqliteStore.js +395 -42
- package/dist/store/sqliteStore.js.map +1 -1
- package/install.sh +2 -2
- package/package.json +18 -10
- package/dist/claude/interactions.d.ts +0 -23
- package/dist/claude/interactions.js +0 -77
- package/dist/claude/interactions.js.map +0 -1
- package/dist/claude/sessionRuntime.d.ts +0 -102
- package/dist/claude/sessionRuntime.js +0 -1361
- package/dist/claude/sessionRuntime.js.map +0 -1
- /package/{BUILD_BENCHMARKS.md → archive/BUILD_BENCHMARKS.md} +0 -0
|
@@ -1,1361 +0,0 @@
|
|
|
1
|
-
import { v7 as uuidv7 } from "uuid";
|
|
2
|
-
import { AsyncQueue } from "./asyncQueue.js";
|
|
3
|
-
import { mapUserInput } from "./inputMapper.js";
|
|
4
|
-
import { completeTool, planSteps, startTool, updateToolInput } from "./toolMapper.js";
|
|
5
|
-
import { diffFile, snapshotFile } from "./fileSnapshots.js";
|
|
6
|
-
import { appendHookProgress, completeHookRun, startHookRun } from "./hookMapper.js";
|
|
7
|
-
import { isFileMutationTool, toolPolicy } from "./permissionPolicy.js";
|
|
8
|
-
import { interruptAndCancelOwned } from "./interruptCompat.js";
|
|
9
|
-
import { classifyClaudeResult, classifyClaudeRuntimeError } from "./resultClassifier.js";
|
|
10
|
-
import { invalidParams } from "../protocol/errors.js";
|
|
11
|
-
import { MetricsRegistry } from "../observability/metrics.js";
|
|
12
|
-
import { normalizeClaudeModelIdentifier } from "./modelSelection.js";
|
|
13
|
-
function nowSeconds() {
|
|
14
|
-
return Math.floor(Date.now() / 1_000);
|
|
15
|
-
}
|
|
16
|
-
function eventDedupKey(method, params) {
|
|
17
|
-
if (!params || typeof params !== "object")
|
|
18
|
-
return null;
|
|
19
|
-
const value = params;
|
|
20
|
-
const item = value.item && typeof value.item === "object" ? value.item : undefined;
|
|
21
|
-
const run = value.run && typeof value.run === "object" ? value.run : undefined;
|
|
22
|
-
const turn = value.turn && typeof value.turn === "object" ? value.turn : undefined;
|
|
23
|
-
const turnId = typeof value.turnId === "string" ? value.turnId : typeof turn?.id === "string" ? turn.id : null;
|
|
24
|
-
if (["turn/started", "turn/completed", "thread/compacted"].includes(method) && turnId) {
|
|
25
|
-
return `${method}:${turnId}`;
|
|
26
|
-
}
|
|
27
|
-
if (["item/started", "item/completed"].includes(method) && typeof item?.id === "string") {
|
|
28
|
-
return `${method}:${item.id}`;
|
|
29
|
-
}
|
|
30
|
-
if (["hook/started", "hook/completed"].includes(method) && typeof run?.id === "string") {
|
|
31
|
-
return `${method}:${run.id}`;
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
function appendInstructions(record) {
|
|
36
|
-
const parts = [record.baseInstructions, record.developerInstructions];
|
|
37
|
-
if (record.personality === "friendly")
|
|
38
|
-
parts.push("Use a friendly, collaborative communication style.");
|
|
39
|
-
if (record.personality === "pragmatic")
|
|
40
|
-
parts.push("Be direct, pragmatic, and focused on concrete outcomes.");
|
|
41
|
-
const value = parts.filter((part) => Boolean(part)).join("\n\n");
|
|
42
|
-
return value || undefined;
|
|
43
|
-
}
|
|
44
|
-
function effort(value) {
|
|
45
|
-
return value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max"
|
|
46
|
-
? value
|
|
47
|
-
: undefined;
|
|
48
|
-
}
|
|
49
|
-
function permissionMode(record) {
|
|
50
|
-
if (record.approvalsReviewer !== "user")
|
|
51
|
-
return "auto";
|
|
52
|
-
const sandbox = record.sandboxPolicy && typeof record.sandboxPolicy === "object" && "type" in record.sandboxPolicy
|
|
53
|
-
? record.sandboxPolicy
|
|
54
|
-
: undefined;
|
|
55
|
-
if (record.approvalPolicy === "never" && sandbox?.type === "dangerFullAccess")
|
|
56
|
-
return "bypassPermissions";
|
|
57
|
-
if (record.approvalPolicy === "never")
|
|
58
|
-
return "dontAsk";
|
|
59
|
-
return "default";
|
|
60
|
-
}
|
|
61
|
-
function claudeOutputSchema(value) {
|
|
62
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
63
|
-
return undefined;
|
|
64
|
-
const { $schema: _dialect, ...schema } = value;
|
|
65
|
-
return schema;
|
|
66
|
-
}
|
|
67
|
-
function textFromAssistant(message) {
|
|
68
|
-
if (message.type !== "assistant" || !Array.isArray(message.message.content))
|
|
69
|
-
return [];
|
|
70
|
-
return message.message.content.flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
|
|
71
|
-
}
|
|
72
|
-
function outputText(value) {
|
|
73
|
-
if (typeof value === "string")
|
|
74
|
-
return value;
|
|
75
|
-
if (!Array.isArray(value))
|
|
76
|
-
return "";
|
|
77
|
-
return value.flatMap((block) => {
|
|
78
|
-
if (!block || typeof block !== "object")
|
|
79
|
-
return [];
|
|
80
|
-
const candidate = block;
|
|
81
|
-
return typeof candidate.text === "string" ? [candidate.text] : [];
|
|
82
|
-
}).join("\n");
|
|
83
|
-
}
|
|
84
|
-
function toolResults(message) {
|
|
85
|
-
if (message.type !== "user" || !Array.isArray(message.message.content))
|
|
86
|
-
return [];
|
|
87
|
-
return message.message.content.flatMap((block) => {
|
|
88
|
-
if (!block || typeof block !== "object" || block.type !== "tool_result")
|
|
89
|
-
return [];
|
|
90
|
-
return [{ toolUseId: block.tool_use_id, output: outputText(block.content), isError: block.is_error === true }];
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
function serverToolResult(value) {
|
|
94
|
-
if (!value || typeof value !== "object")
|
|
95
|
-
return undefined;
|
|
96
|
-
const block = value;
|
|
97
|
-
if (typeof block.type !== "string" || !block.type.endsWith("_tool_result") || typeof block.tool_use_id !== "string")
|
|
98
|
-
return undefined;
|
|
99
|
-
const content = block.content;
|
|
100
|
-
const errorType = content && typeof content === "object" && !Array.isArray(content)
|
|
101
|
-
? content.type
|
|
102
|
-
: undefined;
|
|
103
|
-
return {
|
|
104
|
-
toolUseId: block.tool_use_id,
|
|
105
|
-
output: typeof content === "string" ? content : content === undefined ? "" : JSON.stringify(content),
|
|
106
|
-
isError: typeof errorType === "string" && errorType.endsWith("_error"),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
export class ClaudeSessionRuntime {
|
|
110
|
-
record;
|
|
111
|
-
store;
|
|
112
|
-
hub;
|
|
113
|
-
logger;
|
|
114
|
-
interactions;
|
|
115
|
-
invalidateModelCatalog;
|
|
116
|
-
metrics;
|
|
117
|
-
promptQueue = new AsyncQueue();
|
|
118
|
-
abort = new AbortController();
|
|
119
|
-
sdkQuery;
|
|
120
|
-
active;
|
|
121
|
-
consumePromise;
|
|
122
|
-
stopped = false;
|
|
123
|
-
lastActivityMs = Date.now();
|
|
124
|
-
pendingCompaction;
|
|
125
|
-
fileSnapshots = new Map();
|
|
126
|
-
hookRuns = new Map();
|
|
127
|
-
hookDisplayOrder = 0;
|
|
128
|
-
tasks = new Map();
|
|
129
|
-
hiddenTaskIds = new Set();
|
|
130
|
-
backgroundTaskIds = new Set();
|
|
131
|
-
backgroundTurn;
|
|
132
|
-
capabilities = new Set();
|
|
133
|
-
ownedMessageIds = new Set();
|
|
134
|
-
sessionApprovedTools = new Set();
|
|
135
|
-
hasSubmittedInput = false;
|
|
136
|
-
providerEventId = null;
|
|
137
|
-
providerEventType = null;
|
|
138
|
-
constructor(record, store, hub, logger, config, resume, queryFactory, interactions, invalidateModelCatalog, metrics) {
|
|
139
|
-
this.record = record;
|
|
140
|
-
this.store = store;
|
|
141
|
-
this.hub = hub;
|
|
142
|
-
this.logger = logger;
|
|
143
|
-
this.interactions = interactions;
|
|
144
|
-
this.invalidateModelCatalog = invalidateModelCatalog;
|
|
145
|
-
this.metrics = metrics;
|
|
146
|
-
const append = appendInstructions(record);
|
|
147
|
-
const selectedPermissionMode = permissionMode(record);
|
|
148
|
-
const selectedEffort = effort(record.reasoningEffort);
|
|
149
|
-
if (record.reasoningEffort && !selectedEffort)
|
|
150
|
-
throw new Error(`Unsupported Claude effort '${record.reasoningEffort}'.`);
|
|
151
|
-
const outputSchema = claudeOutputSchema(record.outputSchema);
|
|
152
|
-
this.sdkQuery = queryFactory({
|
|
153
|
-
prompt: this.promptQueue,
|
|
154
|
-
options: {
|
|
155
|
-
cwd: record.thread.cwd,
|
|
156
|
-
model: normalizeClaudeModelIdentifier(record.claudeModelValue),
|
|
157
|
-
...(resume ? { resume: record.claudeSessionId } : { sessionId: record.claudeSessionId }),
|
|
158
|
-
pathToClaudeCodeExecutable: config.claudeBinary,
|
|
159
|
-
persistSession: !record.thread.ephemeral,
|
|
160
|
-
abortController: this.abort,
|
|
161
|
-
includePartialMessages: true,
|
|
162
|
-
includeHookEvents: true,
|
|
163
|
-
forwardSubagentText: true,
|
|
164
|
-
settingSources: ["user", "project", "local"],
|
|
165
|
-
systemPrompt: { type: "preset", preset: "claude_code", ...(append ? { append } : {}) },
|
|
166
|
-
permissionMode: selectedPermissionMode,
|
|
167
|
-
...(selectedPermissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
|
|
168
|
-
...(selectedPermissionMode === "auto"
|
|
169
|
-
? {}
|
|
170
|
-
: { canUseTool: (name, input, options) => this.canUseTool(name, input, options) }),
|
|
171
|
-
onElicitation: (request, options) => this.onElicitation(request, options.signal),
|
|
172
|
-
hooks: {
|
|
173
|
-
PreToolUse: [{ hooks: [(input, toolUseId) => this.beforeToolUse(input, toolUseId)] }],
|
|
174
|
-
PostToolUse: [{ matcher: "Edit|Write|NotebookEdit", hooks: [(input, toolUseId) => this.captureFileAfter(input, toolUseId)] }],
|
|
175
|
-
PostToolUseFailure: [{ matcher: "Edit|Write|NotebookEdit", hooks: [(input, toolUseId) => this.captureFileAfter(input, toolUseId)] }],
|
|
176
|
-
},
|
|
177
|
-
...(record.serviceTier === "fast" ? { settings: { fastMode: true } } : {}),
|
|
178
|
-
...(selectedEffort ? { effort: selectedEffort } : {}),
|
|
179
|
-
...(record.reasoningSummary === "none"
|
|
180
|
-
? { thinking: { type: "adaptive", display: "omitted" } }
|
|
181
|
-
: record.reasoningSummary
|
|
182
|
-
? { thinking: { type: "adaptive", display: "summarized" } }
|
|
183
|
-
: {}),
|
|
184
|
-
...(outputSchema
|
|
185
|
-
? { outputFormat: { type: "json_schema", schema: outputSchema } }
|
|
186
|
-
: {}),
|
|
187
|
-
env: process.env,
|
|
188
|
-
stderr: (line) => logger.debug("claude.stderr", { threadId: record.thread.id, output: line }),
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
this.consumePromise = this.consume();
|
|
192
|
-
}
|
|
193
|
-
static async create(record, store, hub, logger, config, resume, queryFactory, interactions, invalidateModelCatalog = () => undefined, metrics = new MetricsRegistry()) {
|
|
194
|
-
const runtime = new ClaudeSessionRuntime(record, store, hub, logger, config, resume, queryFactory, interactions, invalidateModelCatalog, metrics);
|
|
195
|
-
let timer;
|
|
196
|
-
try {
|
|
197
|
-
await Promise.race([
|
|
198
|
-
runtime.sdkQuery.initializationResult(),
|
|
199
|
-
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("Claude session initialization timed out.")), 15_000); }),
|
|
200
|
-
]);
|
|
201
|
-
return runtime;
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
await runtime.stop("Claude session initialization failed.");
|
|
205
|
-
throw error;
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
if (timer)
|
|
209
|
-
clearTimeout(timer);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
get threadId() {
|
|
213
|
-
return this.record.thread.id;
|
|
214
|
-
}
|
|
215
|
-
get isActive() {
|
|
216
|
-
return this.active !== undefined;
|
|
217
|
-
}
|
|
218
|
-
get activeTurnId() {
|
|
219
|
-
return this.active?.turn.id;
|
|
220
|
-
}
|
|
221
|
-
get isEphemeral() {
|
|
222
|
-
return this.record.thread.ephemeral;
|
|
223
|
-
}
|
|
224
|
-
get canRestartEphemeral() {
|
|
225
|
-
return this.record.thread.ephemeral && !this.hasSubmittedInput && !this.active;
|
|
226
|
-
}
|
|
227
|
-
synchronizeRecord(record) {
|
|
228
|
-
if (record.thread.id !== this.threadId)
|
|
229
|
-
throw new Error("Cannot synchronize a different Claude thread.");
|
|
230
|
-
this.record = record;
|
|
231
|
-
}
|
|
232
|
-
isIdleFor(milliseconds) {
|
|
233
|
-
return !this.active && Date.now() - this.lastActivityMs >= milliseconds;
|
|
234
|
-
}
|
|
235
|
-
async prepareTurn(params, review) {
|
|
236
|
-
if (this.active)
|
|
237
|
-
throw invalidParams(`Thread '${this.threadId}' already has an active turn.`);
|
|
238
|
-
this.completeBackgroundTurn();
|
|
239
|
-
this.lastActivityMs = Date.now();
|
|
240
|
-
if (params.model && params.model !== this.record.modelPickerId) {
|
|
241
|
-
if (!params.model.startsWith("claude:"))
|
|
242
|
-
throw invalidParams("Cannot switch a Claude thread to a Codex model.");
|
|
243
|
-
await this.sdkQuery.setModel(params.model.slice("claude:".length));
|
|
244
|
-
this.record = {
|
|
245
|
-
...this.record,
|
|
246
|
-
modelPickerId: params.model,
|
|
247
|
-
claudeModelValue: params.model.slice("claude:".length),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
const userItem = {
|
|
251
|
-
type: "userMessage",
|
|
252
|
-
id: uuidv7(),
|
|
253
|
-
clientId: params.clientUserMessageId ?? null,
|
|
254
|
-
content: params.input,
|
|
255
|
-
};
|
|
256
|
-
const reviewItem = review ? { type: "enteredReviewMode", id: uuidv7(), review } : undefined;
|
|
257
|
-
const turn = {
|
|
258
|
-
id: uuidv7(),
|
|
259
|
-
items: [...(reviewItem ? [reviewItem] : []), userItem],
|
|
260
|
-
itemsView: "full",
|
|
261
|
-
status: "inProgress",
|
|
262
|
-
error: null,
|
|
263
|
-
startedAt: nowSeconds(),
|
|
264
|
-
completedAt: null,
|
|
265
|
-
durationMs: null,
|
|
266
|
-
};
|
|
267
|
-
const messageUuid = uuidv7();
|
|
268
|
-
const message = await mapUserInput(params.input, messageUuid, { cwd: this.record.thread.cwd, sandboxPolicy: this.record.sandboxPolicy });
|
|
269
|
-
this.ownedMessageIds.add(messageUuid);
|
|
270
|
-
this.active = {
|
|
271
|
-
turn,
|
|
272
|
-
blockItems: new Map(),
|
|
273
|
-
tools: new Map(),
|
|
274
|
-
completedItems: new Set([userItem.id, ...(reviewItem ? [reviewItem.id] : [])]),
|
|
275
|
-
...(review ? { review } : {}),
|
|
276
|
-
};
|
|
277
|
-
this.store.createTurn(this.threadId, turn);
|
|
278
|
-
const preview = this.record.thread.preview || params.input.flatMap((item) => item.type === "text" ? [item.text] : []).join(" ").slice(0, 200);
|
|
279
|
-
this.updateThread({
|
|
280
|
-
...this.record.thread,
|
|
281
|
-
preview,
|
|
282
|
-
status: { type: "active", activeFlags: [] },
|
|
283
|
-
updatedAt: nowSeconds(),
|
|
284
|
-
recencyAt: nowSeconds(),
|
|
285
|
-
});
|
|
286
|
-
return {
|
|
287
|
-
turn,
|
|
288
|
-
announce: () => {
|
|
289
|
-
this.emit("turn/started", { threadId: this.threadId, turn });
|
|
290
|
-
if (reviewItem) {
|
|
291
|
-
this.emit("item/started", { item: reviewItem, threadId: this.threadId, turnId: turn.id, startedAtMs: Date.now() });
|
|
292
|
-
this.emit("item/completed", { item: reviewItem, threadId: this.threadId, turnId: turn.id, completedAtMs: Date.now() });
|
|
293
|
-
}
|
|
294
|
-
this.emit("item/started", { item: userItem, threadId: this.threadId, turnId: turn.id, startedAtMs: Date.now() });
|
|
295
|
-
this.emit("item/completed", { item: userItem, threadId: this.threadId, turnId: turn.id, completedAtMs: Date.now() });
|
|
296
|
-
},
|
|
297
|
-
start: () => {
|
|
298
|
-
this.hasSubmittedInput = true;
|
|
299
|
-
this.promptQueue.push(message);
|
|
300
|
-
},
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
async interrupt() {
|
|
304
|
-
if (!this.active)
|
|
305
|
-
return;
|
|
306
|
-
this.interactions.cancelThread(this.threadId);
|
|
307
|
-
const cancellation = await interruptAndCancelOwned(this.sdkQuery, this.ownedMessageIds, this.capabilities);
|
|
308
|
-
for (const id of [...cancellation.cancelled, ...cancellation.raced])
|
|
309
|
-
this.ownedMessageIds.delete(id);
|
|
310
|
-
if (cancellation.raced.length > 0) {
|
|
311
|
-
this.logger.warn("claude.interrupt.queued-message-race", { threadId: this.threadId, count: cancellation.raced.length });
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
async steer(params) {
|
|
315
|
-
if (!this.active || this.active.turn.id !== params.expectedTurnId) {
|
|
316
|
-
throw invalidParams(`Expected active turn '${params.expectedTurnId}' does not match the Claude thread.`);
|
|
317
|
-
}
|
|
318
|
-
const item = {
|
|
319
|
-
type: "userMessage",
|
|
320
|
-
id: uuidv7(),
|
|
321
|
-
clientId: params.clientUserMessageId ?? null,
|
|
322
|
-
content: params.input,
|
|
323
|
-
};
|
|
324
|
-
const messageUuid = uuidv7();
|
|
325
|
-
const message = await mapUserInput(params.input, messageUuid, { cwd: this.record.thread.cwd, sandboxPolicy: this.record.sandboxPolicy });
|
|
326
|
-
this.ownedMessageIds.add(messageUuid);
|
|
327
|
-
this.active.turn.items.push(item);
|
|
328
|
-
this.active.completedItems.add(item.id);
|
|
329
|
-
this.persistActive();
|
|
330
|
-
this.emit("item/started", { item, threadId: this.threadId, turnId: this.active.turn.id, startedAtMs: Date.now() });
|
|
331
|
-
this.emit("item/completed", { item, threadId: this.threadId, turnId: this.active.turn.id, completedAtMs: Date.now() });
|
|
332
|
-
this.promptQueue.push(message);
|
|
333
|
-
return this.active.turn.id;
|
|
334
|
-
}
|
|
335
|
-
async reinitialize() {
|
|
336
|
-
await this.sdkQuery.reinitialize();
|
|
337
|
-
}
|
|
338
|
-
async compact() {
|
|
339
|
-
if (this.active)
|
|
340
|
-
throw invalidParams("Cannot compact a Claude thread during an active turn.");
|
|
341
|
-
if (this.pendingCompaction)
|
|
342
|
-
throw invalidParams("Claude compaction is already in progress.");
|
|
343
|
-
const completion = new Promise((resolve, reject) => {
|
|
344
|
-
const timer = setTimeout(() => {
|
|
345
|
-
this.pendingCompaction = undefined;
|
|
346
|
-
reject(new Error("Installed Claude CLI did not confirm compaction; this version may not support SDK-driven /compact."));
|
|
347
|
-
}, 60_000);
|
|
348
|
-
this.pendingCompaction = { resolve, reject, timer };
|
|
349
|
-
});
|
|
350
|
-
this.hasSubmittedInput = true;
|
|
351
|
-
this.promptQueue.push(await mapUserInput([{ type: "text", text: "/compact", text_elements: [] }], uuidv7()));
|
|
352
|
-
await completion;
|
|
353
|
-
}
|
|
354
|
-
async injectItems(items) {
|
|
355
|
-
if (this.active)
|
|
356
|
-
throw invalidParams("Cannot inject history during an active Claude turn.");
|
|
357
|
-
const visible = items.map((item) => JSON.stringify(item)).join("\n");
|
|
358
|
-
const input = [{ type: "text", text: `[Injected model-visible history]\n${visible}`, text_elements: [] }];
|
|
359
|
-
const messageUuid = uuidv7();
|
|
360
|
-
const message = await mapUserInput(input, messageUuid);
|
|
361
|
-
this.hasSubmittedInput = true;
|
|
362
|
-
this.promptQueue.push({ ...message, shouldQuery: false });
|
|
363
|
-
const now = nowSeconds();
|
|
364
|
-
const userItem = { type: "userMessage", id: uuidv7(), clientId: null, content: input };
|
|
365
|
-
const turn = {
|
|
366
|
-
id: uuidv7(), items: [userItem], itemsView: "full", status: "completed", error: null,
|
|
367
|
-
startedAt: now, completedAt: now, durationMs: 0,
|
|
368
|
-
};
|
|
369
|
-
this.store.createTurn(this.threadId, turn);
|
|
370
|
-
this.store.setTurnClaudeMessageUuid(this.threadId, turn.id, messageUuid);
|
|
371
|
-
this.record = { ...this.record, lastClaudeMessageUuid: messageUuid };
|
|
372
|
-
this.record = { ...this.record, lastCompletedTurnId: turn.id };
|
|
373
|
-
this.updateThread({ ...this.record.thread, updatedAt: now, recencyAt: now });
|
|
374
|
-
this.emit("item/completed", { item: userItem, threadId: this.threadId, turnId: turn.id, completedAtMs: Date.now() });
|
|
375
|
-
}
|
|
376
|
-
async stop(reason = "Claude runtime unloaded.") {
|
|
377
|
-
if (this.stopped)
|
|
378
|
-
return;
|
|
379
|
-
if (this.pendingCompaction) {
|
|
380
|
-
const pending = this.pendingCompaction;
|
|
381
|
-
this.pendingCompaction = undefined;
|
|
382
|
-
clearTimeout(pending.timer);
|
|
383
|
-
pending.reject(new Error(reason));
|
|
384
|
-
}
|
|
385
|
-
if (this.active)
|
|
386
|
-
await this.failActive(reason);
|
|
387
|
-
this.completeBackgroundTurn("failed", reason);
|
|
388
|
-
this.stopped = true;
|
|
389
|
-
this.promptQueue.close();
|
|
390
|
-
this.abort.abort();
|
|
391
|
-
await this.sdkQuery.return(undefined).catch(() => undefined);
|
|
392
|
-
await this.consumePromise.catch(() => undefined);
|
|
393
|
-
if (this.record.thread.status.type !== "notLoaded") {
|
|
394
|
-
this.updateThread({ ...this.record.thread, status: { type: "notLoaded" }, updatedAt: nowSeconds() });
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
async consume() {
|
|
398
|
-
try {
|
|
399
|
-
for await (const message of this.sdkQuery)
|
|
400
|
-
await this.handleMessage(message);
|
|
401
|
-
if (!this.stopped) {
|
|
402
|
-
if (this.active)
|
|
403
|
-
await this.failActive("Claude runtime exited before the turn completed.");
|
|
404
|
-
this.updateThread({ ...this.record.thread, status: { type: "systemError" }, updatedAt: nowSeconds() });
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
catch (error) {
|
|
408
|
-
if (!this.stopped) {
|
|
409
|
-
await this.failActive(error instanceof Error ? error.message : String(error));
|
|
410
|
-
this.updateThread({ ...this.record.thread, status: { type: "systemError" }, updatedAt: nowSeconds() });
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
async canUseTool(name, input, options) {
|
|
415
|
-
if (!this.active)
|
|
416
|
-
return { behavior: "deny", message: "No active Claude turn." };
|
|
417
|
-
if (name === "AskUserQuestion")
|
|
418
|
-
return this.askUser(input, options);
|
|
419
|
-
if (name === "ExitPlanMode") {
|
|
420
|
-
return { behavior: "deny", message: "The plan is visible in the client. Wait for the user's next instruction before executing it." };
|
|
421
|
-
}
|
|
422
|
-
if (permissionMode(this.record) === "bypassPermissions")
|
|
423
|
-
return { behavior: "allow", updatedInput: input };
|
|
424
|
-
const policy = toolPolicy(name, input, this.record.thread.cwd, this.record.approvalPolicy, this.record.sandboxPolicy);
|
|
425
|
-
if (policy.decision === "deny")
|
|
426
|
-
return { behavior: "deny", message: policy.reason ?? `Tool '${name}' is denied by the Codex thread policy.` };
|
|
427
|
-
if (policy.decision === "defer" && isFileMutationTool(name))
|
|
428
|
-
return { behavior: "allow", updatedInput: input };
|
|
429
|
-
if (this.sessionApprovedTools.has(name))
|
|
430
|
-
return { behavior: "allow", updatedInput: input };
|
|
431
|
-
if (this.record.approvalPolicy === "never") {
|
|
432
|
-
return { behavior: "deny", message: `Tool '${name}' requires permission, but approvalPolicy is never.` };
|
|
433
|
-
}
|
|
434
|
-
const itemId = options.toolUseID;
|
|
435
|
-
const tool = this.active.tools.get(itemId);
|
|
436
|
-
const item = tool && this.active.turn.items.find((candidate) => candidate.id === tool.itemId);
|
|
437
|
-
if (tool && item) {
|
|
438
|
-
updateToolInput(item, tool, input);
|
|
439
|
-
this.persistActive();
|
|
440
|
-
}
|
|
441
|
-
const common = {
|
|
442
|
-
threadId: this.threadId,
|
|
443
|
-
turnId: this.active.turn.id,
|
|
444
|
-
itemId,
|
|
445
|
-
startedAtMs: Date.now(),
|
|
446
|
-
};
|
|
447
|
-
let method;
|
|
448
|
-
let params;
|
|
449
|
-
if (name === "Bash") {
|
|
450
|
-
method = "item/commandExecution/requestApproval";
|
|
451
|
-
params = {
|
|
452
|
-
...common, environmentId: null, reason: options.decisionReason ?? options.description ?? null,
|
|
453
|
-
command: typeof input.command === "string" ? input.command : null, cwd: this.record.thread.cwd,
|
|
454
|
-
commandActions: typeof input.command === "string" ? [{ type: "unknown", command: input.command }] : null,
|
|
455
|
-
additionalPermissions: null, proposedExecpolicyAmendment: null,
|
|
456
|
-
proposedNetworkPolicyAmendments: null, availableDecisions: ["accept", "acceptForSession", "decline", "cancel"],
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
else if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
|
|
460
|
-
method = "item/fileChange/requestApproval";
|
|
461
|
-
params = { ...common, reason: options.decisionReason ?? options.description ?? null, grantRoot: options.blockedPath ?? null };
|
|
462
|
-
}
|
|
463
|
-
else {
|
|
464
|
-
method = "item/permissions/requestApproval";
|
|
465
|
-
const path = options.blockedPath ? [options.blockedPath] : null;
|
|
466
|
-
params = {
|
|
467
|
-
...common, environmentId: null, cwd: this.record.thread.cwd,
|
|
468
|
-
reason: options.title ?? options.decisionReason ?? `Claude requests permission for ${name}.`,
|
|
469
|
-
permissions: { network: null, fileSystem: path ? { read: path, write: path } : null },
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
this.updateThread({ ...this.record.thread, status: { type: "active", activeFlags: ["waitingOnApproval"] } });
|
|
473
|
-
if (tool && item)
|
|
474
|
-
this.startToolItem(tool, item, true);
|
|
475
|
-
let response;
|
|
476
|
-
try {
|
|
477
|
-
response = await this.interactions.open({
|
|
478
|
-
threadId: this.threadId,
|
|
479
|
-
turnId: this.active.turn.id,
|
|
480
|
-
claudeRequestId: options.requestId,
|
|
481
|
-
method,
|
|
482
|
-
params,
|
|
483
|
-
signal: options.signal,
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
finally {
|
|
487
|
-
if (this.active)
|
|
488
|
-
this.updateThread({ ...this.record.thread, status: { type: "active", activeFlags: [] } });
|
|
489
|
-
}
|
|
490
|
-
if (response && typeof response === "object" && "cancelled" in response) {
|
|
491
|
-
return { behavior: "deny", message: "User cancelled tool execution." };
|
|
492
|
-
}
|
|
493
|
-
const decision = response && typeof response === "object" && "decision" in response
|
|
494
|
-
? response.decision
|
|
495
|
-
: response && typeof response === "object" && "scope" in response
|
|
496
|
-
? "accept"
|
|
497
|
-
: "decline";
|
|
498
|
-
if (decision === "accept" || decision === "acceptForSession") {
|
|
499
|
-
if (decision === "acceptForSession")
|
|
500
|
-
this.sessionApprovedTools.add(name);
|
|
501
|
-
return {
|
|
502
|
-
behavior: "allow",
|
|
503
|
-
updatedInput: input,
|
|
504
|
-
...(decision === "acceptForSession" && options.suggestions ? { updatedPermissions: options.suggestions } : {}),
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
return { behavior: "deny", message: decision === "cancel" ? "User cancelled tool execution." : "User declined tool execution." };
|
|
508
|
-
}
|
|
509
|
-
async askUser(input, options) {
|
|
510
|
-
if (!this.active)
|
|
511
|
-
return { behavior: "deny", message: "No active Claude turn." };
|
|
512
|
-
const rawQuestions = Array.isArray(input.questions) ? input.questions : [];
|
|
513
|
-
const questions = rawQuestions.map((value, index) => {
|
|
514
|
-
const question = value && typeof value === "object" ? value : {};
|
|
515
|
-
const prompt = typeof question.question === "string" ? question.question : `Question ${index + 1}`;
|
|
516
|
-
const choices = Array.isArray(question.options) ? question.options : [];
|
|
517
|
-
return {
|
|
518
|
-
id: prompt,
|
|
519
|
-
header: typeof question.header === "string" ? question.header : `Question ${index + 1}`,
|
|
520
|
-
question: prompt,
|
|
521
|
-
isOther: true,
|
|
522
|
-
isSecret: false,
|
|
523
|
-
options: choices.map((choice) => {
|
|
524
|
-
const option = choice && typeof choice === "object" ? choice : {};
|
|
525
|
-
return { label: String(option.label ?? ""), description: String(option.description ?? "") };
|
|
526
|
-
}),
|
|
527
|
-
};
|
|
528
|
-
});
|
|
529
|
-
const response = await this.interactions.open({
|
|
530
|
-
threadId: this.threadId,
|
|
531
|
-
turnId: this.active.turn.id,
|
|
532
|
-
claudeRequestId: options.requestId,
|
|
533
|
-
method: "item/tool/requestUserInput",
|
|
534
|
-
params: { threadId: this.threadId, turnId: this.active.turn.id, itemId: options.toolUseID, questions, autoResolutionMs: null },
|
|
535
|
-
signal: options.signal,
|
|
536
|
-
});
|
|
537
|
-
if (!response || typeof response !== "object" || "cancelled" in response) {
|
|
538
|
-
return { behavior: "deny", message: "User cancelled the question." };
|
|
539
|
-
}
|
|
540
|
-
const rawAnswers = "answers" in response ? response.answers : undefined;
|
|
541
|
-
const answers = {};
|
|
542
|
-
if (rawAnswers && typeof rawAnswers === "object") {
|
|
543
|
-
for (const [id, value] of Object.entries(rawAnswers)) {
|
|
544
|
-
const values = value && typeof value === "object" && "answers" in value
|
|
545
|
-
? value.answers
|
|
546
|
-
: undefined;
|
|
547
|
-
if (Array.isArray(values))
|
|
548
|
-
answers[id] = values.map(String).join(", ");
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
return { behavior: "allow", updatedInput: { ...input, answers } };
|
|
552
|
-
}
|
|
553
|
-
async onElicitation(request, signal) {
|
|
554
|
-
const response = await this.interactions.open({
|
|
555
|
-
threadId: this.threadId,
|
|
556
|
-
turnId: this.active?.turn.id ?? null,
|
|
557
|
-
claudeRequestId: request.elicitationId ?? null,
|
|
558
|
-
method: "mcpServer/elicitation/request",
|
|
559
|
-
params: request.mode === "url"
|
|
560
|
-
? { threadId: this.threadId, turnId: this.active?.turn.id ?? null, serverName: request.serverName, mode: "url", _meta: null, message: request.message, url: request.url ?? "", elicitationId: request.elicitationId ?? "" }
|
|
561
|
-
: { threadId: this.threadId, turnId: this.active?.turn.id ?? null, serverName: request.serverName, mode: "form", _meta: null, message: request.message, requestedSchema: request.requestedSchema ?? {} },
|
|
562
|
-
signal,
|
|
563
|
-
});
|
|
564
|
-
if (!response || typeof response !== "object" || "cancelled" in response)
|
|
565
|
-
return { action: "cancel" };
|
|
566
|
-
const result = response;
|
|
567
|
-
return {
|
|
568
|
-
action: result.action ?? "cancel",
|
|
569
|
-
...(result.content ? { content: result.content } : {}),
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
async beforeToolUse(input, toolUseId) {
|
|
573
|
-
if (input.hook_event_name !== "PreToolUse" || !toolUseId || !this.active)
|
|
574
|
-
return { continue: true };
|
|
575
|
-
const toolInput = input.tool_input && typeof input.tool_input === "object"
|
|
576
|
-
? input.tool_input
|
|
577
|
-
: {};
|
|
578
|
-
const selected = toolPolicy(input.tool_name, toolInput, this.record.thread.cwd, this.record.approvalPolicy, this.record.sandboxPolicy);
|
|
579
|
-
const policy = permissionMode(this.record) === "auto" && selected.decision === "ask"
|
|
580
|
-
? { decision: "defer" }
|
|
581
|
-
: selected;
|
|
582
|
-
if (policy.decision !== "deny") {
|
|
583
|
-
const snapshot = await snapshotFile(input.tool_name, toolInput, this.record.thread.cwd);
|
|
584
|
-
if (snapshot)
|
|
585
|
-
this.fileSnapshots.set(toolUseId, snapshot);
|
|
586
|
-
}
|
|
587
|
-
if (policy.decision !== "deny" && this.sessionApprovedTools.has(input.tool_name))
|
|
588
|
-
return { continue: true };
|
|
589
|
-
if (policy.decision === "defer")
|
|
590
|
-
return { continue: true };
|
|
591
|
-
return {
|
|
592
|
-
continue: true,
|
|
593
|
-
hookSpecificOutput: {
|
|
594
|
-
hookEventName: "PreToolUse",
|
|
595
|
-
permissionDecision: policy.decision,
|
|
596
|
-
...(policy.reason ? { permissionDecisionReason: policy.reason } : {}),
|
|
597
|
-
},
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
async captureFileAfter(input, toolUseId) {
|
|
601
|
-
if ((input.hook_event_name !== "PostToolUse" && input.hook_event_name !== "PostToolUseFailure") || !toolUseId) {
|
|
602
|
-
return { continue: true };
|
|
603
|
-
}
|
|
604
|
-
const snapshot = this.fileSnapshots.get(toolUseId);
|
|
605
|
-
this.fileSnapshots.delete(toolUseId);
|
|
606
|
-
if (!snapshot || !this.active)
|
|
607
|
-
return { continue: true };
|
|
608
|
-
const change = await diffFile(snapshot);
|
|
609
|
-
const tool = this.active.tools.get(toolUseId);
|
|
610
|
-
const index = tool && this.active.turn.items.findIndex((item) => item.id === tool.itemId);
|
|
611
|
-
const item = index !== undefined && index >= 0 ? this.active.turn.items[index] : undefined;
|
|
612
|
-
if (!change || !item || item.type !== "fileChange")
|
|
613
|
-
return { continue: true };
|
|
614
|
-
item.changes = [change];
|
|
615
|
-
this.persistActive();
|
|
616
|
-
this.emitFileDiff(item);
|
|
617
|
-
return { continue: true };
|
|
618
|
-
}
|
|
619
|
-
async handleMessage(message) {
|
|
620
|
-
const previousId = this.providerEventId;
|
|
621
|
-
const previousType = this.providerEventType;
|
|
622
|
-
this.providerEventId = "uuid" in message && typeof message.uuid === "string" ? message.uuid : null;
|
|
623
|
-
this.providerEventType = message.type === "system" && "subtype" in message ? `system/${message.subtype}` : message.type;
|
|
624
|
-
if (this.providerEventId && this.store.hasProcessedProviderEvent(this.threadId, this.providerEventId)) {
|
|
625
|
-
this.metrics.eventDeduplicated();
|
|
626
|
-
this.providerEventId = previousId;
|
|
627
|
-
this.providerEventType = previousType;
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
try {
|
|
631
|
-
await this.handleProviderMessage(message);
|
|
632
|
-
if (this.providerEventId && this.providerEventType) {
|
|
633
|
-
this.store.markProviderEventProcessed(this.threadId, this.providerEventType, this.providerEventId);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
finally {
|
|
637
|
-
this.providerEventId = previousId;
|
|
638
|
-
this.providerEventType = previousType;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
async handleProviderMessage(message) {
|
|
642
|
-
if (message.type === "system" && message.subtype === "hook_started") {
|
|
643
|
-
const run = startHookRun(message, this.record.thread.cwd, Boolean(this.active), this.hookDisplayOrder++);
|
|
644
|
-
if (run) {
|
|
645
|
-
this.hookRuns.set(message.hook_id, run);
|
|
646
|
-
this.emit("hook/started", { threadId: this.threadId, turnId: this.active?.turn.id ?? null, run });
|
|
647
|
-
}
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
if (message.type === "system" && message.subtype === "hook_progress") {
|
|
651
|
-
const run = this.hookRuns.get(message.hook_id);
|
|
652
|
-
if (run)
|
|
653
|
-
appendHookProgress(run, message);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
if (message.type === "system" && message.subtype === "hook_response") {
|
|
657
|
-
const run = this.hookRuns.get(message.hook_id);
|
|
658
|
-
if (run) {
|
|
659
|
-
completeHookRun(run, message);
|
|
660
|
-
this.hookRuns.delete(message.hook_id);
|
|
661
|
-
this.emit("hook/completed", { threadId: this.threadId, turnId: this.active?.turn.id ?? null, run });
|
|
662
|
-
}
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
if (message.type === "system" && message.subtype === "task_started") {
|
|
666
|
-
this.startTask(message);
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
if (message.type === "system" && message.subtype === "task_progress") {
|
|
670
|
-
this.progressTask(message);
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
if (message.type === "system" && message.subtype === "task_updated") {
|
|
674
|
-
this.updateTask(message);
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
if (message.type === "system" && message.subtype === "task_notification") {
|
|
678
|
-
this.completeTask(message);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
if (message.type === "system" && message.subtype === "background_tasks_changed") {
|
|
682
|
-
this.backgroundTaskIds = new Set(message.tasks.map((task) => task.task_id));
|
|
683
|
-
if (!this.active) {
|
|
684
|
-
if (this.backgroundTaskIds.size === 0)
|
|
685
|
-
this.completeBackgroundTurn();
|
|
686
|
-
else
|
|
687
|
-
this.updateThread({ ...this.record.thread, status: { type: "active", activeFlags: [] }, updatedAt: nowSeconds() });
|
|
688
|
-
}
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
if (message.type === "system" && message.subtype === "status" && this.pendingCompaction && message.compact_result === "failed") {
|
|
692
|
-
const pending = this.pendingCompaction;
|
|
693
|
-
this.pendingCompaction = undefined;
|
|
694
|
-
clearTimeout(pending.timer);
|
|
695
|
-
pending.reject(new Error(message.compact_error ?? "Claude compaction failed."));
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
if (message.type === "system" && message.subtype === "compact_boundary") {
|
|
699
|
-
await this.handleCompaction(message.compact_metadata);
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
if (message.type === "system" && message.subtype === "init") {
|
|
703
|
-
if (message.session_id !== this.record.claudeSessionId) {
|
|
704
|
-
throw new Error(`Claude initialized session '${message.session_id}', expected '${this.record.claudeSessionId}'.`);
|
|
705
|
-
}
|
|
706
|
-
this.record = {
|
|
707
|
-
...this.record,
|
|
708
|
-
resolvedModel: message.model,
|
|
709
|
-
claudeCodeVersion: message.claude_code_version,
|
|
710
|
-
thread: { ...this.record.thread, cliVersion: message.claude_code_version },
|
|
711
|
-
};
|
|
712
|
-
this.capabilities.clear();
|
|
713
|
-
for (const capability of message.capabilities ?? [])
|
|
714
|
-
this.capabilities.add(capability);
|
|
715
|
-
this.store.updateThread(this.record);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
if (message.type === "stream_event") {
|
|
719
|
-
await this.handleStreamEvent(message);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
if (message.type === "assistant") {
|
|
723
|
-
if (this.active) {
|
|
724
|
-
if (message.error) {
|
|
725
|
-
this.active.providerError = message.error;
|
|
726
|
-
if (message.error === "authentication_failed" || message.error === "oauth_org_not_allowed" || message.error === "model_not_found") {
|
|
727
|
-
this.invalidateModelCatalog();
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
this.store.setTurnClaudeMessageUuid(this.threadId, this.active.turn.id, message.uuid);
|
|
731
|
-
this.record = { ...this.record, lastClaudeMessageUuid: message.uuid };
|
|
732
|
-
this.store.updateThread(this.record);
|
|
733
|
-
if (Array.isArray(message.message.content)) {
|
|
734
|
-
for (const block of message.message.content)
|
|
735
|
-
this.completeServerToolResult(serverToolResult(block));
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
await this.backfillAssistant(message);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
if (message.type === "user") {
|
|
742
|
-
if ("uuid" in message && typeof message.uuid === "string")
|
|
743
|
-
this.ownedMessageIds.delete(message.uuid);
|
|
744
|
-
this.handleToolResults(message);
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
if (message.type === "result") {
|
|
748
|
-
if (!this.active && this.pendingCompaction && message.subtype !== "success") {
|
|
749
|
-
const pending = this.pendingCompaction;
|
|
750
|
-
this.pendingCompaction = undefined;
|
|
751
|
-
clearTimeout(pending.timer);
|
|
752
|
-
pending.reject(new Error(message.errors[0] ?? "Claude compaction failed."));
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
const result = classifyClaudeResult(message, this.active?.providerError);
|
|
756
|
-
if (result.codexErrorInfo === "unauthorized" || result.codexErrorInfo === "badRequest")
|
|
757
|
-
this.invalidateModelCatalog();
|
|
758
|
-
if (result.status === "completed" && message.subtype === "success") {
|
|
759
|
-
if (message.structured_output !== undefined)
|
|
760
|
-
this.appendStructuredOutput(message.structured_output);
|
|
761
|
-
this.emitUsage(message);
|
|
762
|
-
await this.completeActive("completed");
|
|
763
|
-
}
|
|
764
|
-
else {
|
|
765
|
-
await this.completeActive(result.status === "completed" ? "failed" : result.status, result.error, result.codexErrorInfo);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
appendStructuredOutput(value) {
|
|
770
|
-
if (!this.active)
|
|
771
|
-
return;
|
|
772
|
-
const text = JSON.stringify(value, null, 2);
|
|
773
|
-
const item = { type: "agentMessage", id: uuidv7(), text, phase: null, memoryCitation: null };
|
|
774
|
-
this.active.turn.items.push(item);
|
|
775
|
-
this.active.completedItems.add(item.id);
|
|
776
|
-
this.persistActive();
|
|
777
|
-
this.emit("item/started", { item, threadId: this.threadId, turnId: this.active.turn.id, startedAtMs: Date.now() });
|
|
778
|
-
this.emit("item/agentMessage/delta", { threadId: this.threadId, turnId: this.active.turn.id, itemId: item.id, delta: text });
|
|
779
|
-
this.emit("item/completed", { item, threadId: this.threadId, turnId: this.active.turn.id, completedAtMs: Date.now() });
|
|
780
|
-
}
|
|
781
|
-
startTask(message) {
|
|
782
|
-
if (message.skip_transcript) {
|
|
783
|
-
this.hiddenTaskIds.add(message.task_id);
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
const turn = this.taskTurn();
|
|
787
|
-
const existing = message.tool_use_id ? this.active?.tools.get(message.tool_use_id) : undefined;
|
|
788
|
-
const existingItem = existing && turn.items.find((item) => item.id === existing.itemId);
|
|
789
|
-
const subagent = Boolean(message.subagent_type) || message.task_type === "agent" || message.task_type === "subagent";
|
|
790
|
-
const item = existingItem ?? (subagent
|
|
791
|
-
? {
|
|
792
|
-
type: "collabAgentToolCall", id: message.tool_use_id ?? `claude-task:${uuidv7()}`, tool: "spawnAgent",
|
|
793
|
-
status: "inProgress", senderThreadId: this.threadId, receiverThreadIds: [message.task_id],
|
|
794
|
-
prompt: message.prompt ?? message.description, model: null, reasoningEffort: null,
|
|
795
|
-
agentsStates: { [message.task_id]: { status: "running", message: message.description } },
|
|
796
|
-
}
|
|
797
|
-
: {
|
|
798
|
-
type: "dynamicToolCall", id: message.tool_use_id ?? `claude-task:${uuidv7()}`, namespace: "claude",
|
|
799
|
-
tool: message.task_type ?? "backgroundTask", arguments: { taskId: message.task_id, description: message.description },
|
|
800
|
-
status: "inProgress", contentItems: null, success: null, durationMs: null,
|
|
801
|
-
});
|
|
802
|
-
if (item.type === "collabAgentToolCall") {
|
|
803
|
-
item.receiverThreadIds = [message.task_id];
|
|
804
|
-
item.agentsStates[message.task_id] = { status: "running", message: message.description };
|
|
805
|
-
}
|
|
806
|
-
if (!existingItem) {
|
|
807
|
-
turn.items.push(item);
|
|
808
|
-
this.persistTaskTurn(turn);
|
|
809
|
-
this.emit("item/started", { item, threadId: this.threadId, turnId: turn.id, startedAtMs: Date.now() }, turn.id);
|
|
810
|
-
}
|
|
811
|
-
this.tasks.set(message.task_id, {
|
|
812
|
-
taskId: message.task_id, itemId: item.id, turnId: turn.id, subagent, description: message.description, terminal: false,
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
progressTask(message) {
|
|
816
|
-
if (this.hiddenTaskIds.has(message.task_id))
|
|
817
|
-
return;
|
|
818
|
-
const task = this.taskForEvent(message.task_id, message.description, Boolean(message.subagent_type));
|
|
819
|
-
if (!task)
|
|
820
|
-
return;
|
|
821
|
-
task.description = message.summary ?? message.description;
|
|
822
|
-
const { turn, item } = this.taskItem(task);
|
|
823
|
-
if (!turn || !item)
|
|
824
|
-
return;
|
|
825
|
-
if (item.type === "collabAgentToolCall") {
|
|
826
|
-
item.agentsStates[message.task_id] = { status: "running", message: task.description };
|
|
827
|
-
}
|
|
828
|
-
else if (item.type === "dynamicToolCall") {
|
|
829
|
-
item.contentItems = [{ type: "inputText", text: task.description }];
|
|
830
|
-
}
|
|
831
|
-
this.persistTaskTurn(turn);
|
|
832
|
-
}
|
|
833
|
-
updateTask(message) {
|
|
834
|
-
if (this.hiddenTaskIds.has(message.task_id))
|
|
835
|
-
return;
|
|
836
|
-
const task = this.tasks.get(message.task_id);
|
|
837
|
-
if (!task)
|
|
838
|
-
return;
|
|
839
|
-
if (message.patch.description)
|
|
840
|
-
task.description = message.patch.description;
|
|
841
|
-
const { turn, item } = this.taskItem(task);
|
|
842
|
-
if (!turn || !item || item.type !== "collabAgentToolCall")
|
|
843
|
-
return;
|
|
844
|
-
const status = message.patch.status;
|
|
845
|
-
item.agentsStates[message.task_id] = {
|
|
846
|
-
status: status === "failed" ? "errored" : status === "killed" ? "interrupted" : status === "completed" ? "completed" : "running",
|
|
847
|
-
message: message.patch.error ?? task.description,
|
|
848
|
-
};
|
|
849
|
-
this.persistTaskTurn(turn);
|
|
850
|
-
}
|
|
851
|
-
completeTask(message) {
|
|
852
|
-
if (this.hiddenTaskIds.delete(message.task_id))
|
|
853
|
-
return;
|
|
854
|
-
const task = this.taskForEvent(message.task_id, message.summary, false);
|
|
855
|
-
if (!task)
|
|
856
|
-
return;
|
|
857
|
-
const { turn, item } = this.taskItem(task);
|
|
858
|
-
if (!turn || !item)
|
|
859
|
-
return;
|
|
860
|
-
task.terminal = true;
|
|
861
|
-
if (item.type === "collabAgentToolCall") {
|
|
862
|
-
item.status = message.status === "completed" ? "completed" : "failed";
|
|
863
|
-
item.agentsStates[message.task_id] = {
|
|
864
|
-
status: message.status === "completed" ? "completed" : message.status === "stopped" ? "interrupted" : "errored",
|
|
865
|
-
message: message.summary,
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
else if (item.type === "dynamicToolCall") {
|
|
869
|
-
item.status = message.status === "completed" ? "completed" : "failed";
|
|
870
|
-
item.success = message.status === "completed";
|
|
871
|
-
item.durationMs = message.usage?.duration_ms ?? null;
|
|
872
|
-
item.contentItems = message.summary ? [{ type: "inputText", text: message.summary }] : [];
|
|
873
|
-
}
|
|
874
|
-
this.persistTaskTurn(turn);
|
|
875
|
-
if (this.active?.turn.id === turn.id)
|
|
876
|
-
this.active.completedItems.add(item.id);
|
|
877
|
-
this.emit("item/completed", { item, threadId: this.threadId, turnId: turn.id, completedAtMs: Date.now() }, turn.id);
|
|
878
|
-
if (!this.active && !this.backgroundTaskIds.has(message.task_id))
|
|
879
|
-
this.completeBackgroundTurn();
|
|
880
|
-
}
|
|
881
|
-
taskForEvent(taskId, description, subagent) {
|
|
882
|
-
if (this.hiddenTaskIds.has(taskId))
|
|
883
|
-
return undefined;
|
|
884
|
-
const existing = this.tasks.get(taskId);
|
|
885
|
-
if (existing && this.taskItem(existing).turn)
|
|
886
|
-
return existing;
|
|
887
|
-
const projectedSubagent = existing?.subagent ?? subagent;
|
|
888
|
-
this.startTask({
|
|
889
|
-
type: "system", subtype: "task_started", task_id: taskId, description,
|
|
890
|
-
...(projectedSubagent ? { subagent_type: "agent" } : {}),
|
|
891
|
-
uuid: "00000000-0000-4000-8000-000000000001", session_id: this.record.claudeSessionId,
|
|
892
|
-
});
|
|
893
|
-
return this.tasks.get(taskId);
|
|
894
|
-
}
|
|
895
|
-
taskTurn() {
|
|
896
|
-
if (this.active)
|
|
897
|
-
return this.active.turn;
|
|
898
|
-
if (this.backgroundTurn)
|
|
899
|
-
return this.backgroundTurn;
|
|
900
|
-
const turn = {
|
|
901
|
-
id: uuidv7(), items: [], itemsView: "full", status: "inProgress", error: null,
|
|
902
|
-
startedAt: nowSeconds(), completedAt: null, durationMs: null,
|
|
903
|
-
};
|
|
904
|
-
this.backgroundTurn = turn;
|
|
905
|
-
this.store.createTurn(this.threadId, turn);
|
|
906
|
-
this.updateThread({ ...this.record.thread, status: { type: "active", activeFlags: [] }, updatedAt: nowSeconds(), recencyAt: nowSeconds() });
|
|
907
|
-
this.emit("turn/started", { threadId: this.threadId, turn }, turn.id);
|
|
908
|
-
return turn;
|
|
909
|
-
}
|
|
910
|
-
taskItem(task) {
|
|
911
|
-
const turn = this.active?.turn.id === task.turnId
|
|
912
|
-
? this.active.turn
|
|
913
|
-
: this.backgroundTurn?.id === task.turnId
|
|
914
|
-
? this.backgroundTurn
|
|
915
|
-
: undefined;
|
|
916
|
-
return { turn, item: turn?.items.find((item) => item.id === task.itemId) };
|
|
917
|
-
}
|
|
918
|
-
persistTaskTurn(turn) {
|
|
919
|
-
if (this.active?.turn.id === turn.id)
|
|
920
|
-
this.persistActive();
|
|
921
|
-
else
|
|
922
|
-
this.store.updateTurn(this.threadId, turn);
|
|
923
|
-
}
|
|
924
|
-
completeBackgroundTurn(status = "completed", errorMessage) {
|
|
925
|
-
const turn = this.backgroundTurn;
|
|
926
|
-
if (!turn)
|
|
927
|
-
return;
|
|
928
|
-
const completedAt = nowSeconds();
|
|
929
|
-
for (const item of turn.items) {
|
|
930
|
-
if (item.type === "collabAgentToolCall" && item.status === "inProgress")
|
|
931
|
-
item.status = "completed";
|
|
932
|
-
else if (item.type === "dynamicToolCall" && item.status === "inProgress")
|
|
933
|
-
item.status = "completed";
|
|
934
|
-
}
|
|
935
|
-
turn.status = status;
|
|
936
|
-
turn.completedAt = completedAt;
|
|
937
|
-
turn.durationMs = Math.max(0, (completedAt - (turn.startedAt ?? completedAt)) * 1_000);
|
|
938
|
-
turn.error = status === "failed" ? { message: errorMessage ?? "Background Claude work failed.", codexErrorInfo: null, additionalDetails: null } : null;
|
|
939
|
-
this.store.updateTurn(this.threadId, turn);
|
|
940
|
-
this.backgroundTurn = undefined;
|
|
941
|
-
for (const task of this.tasks.values())
|
|
942
|
-
if (task.turnId === turn.id)
|
|
943
|
-
task.turnId = null;
|
|
944
|
-
this.record = { ...this.record, lastCompletedTurnId: turn.id };
|
|
945
|
-
this.updateThread({
|
|
946
|
-
...this.record.thread,
|
|
947
|
-
status: this.backgroundTaskIds.size > 0 ? { type: "active", activeFlags: [] } : { type: "idle" },
|
|
948
|
-
updatedAt: completedAt, recencyAt: completedAt,
|
|
949
|
-
});
|
|
950
|
-
if (turn.error) {
|
|
951
|
-
this.emit("error", { error: turn.error, willRetry: false, threadId: this.threadId, turnId: turn.id }, turn.id);
|
|
952
|
-
}
|
|
953
|
-
this.emit("turn/completed", { threadId: this.threadId, turn }, turn.id);
|
|
954
|
-
}
|
|
955
|
-
async handleCompaction(metadata) {
|
|
956
|
-
const item = { type: "contextCompaction", id: uuidv7() };
|
|
957
|
-
let turnId;
|
|
958
|
-
if (this.active) {
|
|
959
|
-
this.active.turn.items.push(item);
|
|
960
|
-
this.active.completedItems.add(item.id);
|
|
961
|
-
this.persistActive();
|
|
962
|
-
turnId = this.active.turn.id;
|
|
963
|
-
}
|
|
964
|
-
else {
|
|
965
|
-
const now = nowSeconds();
|
|
966
|
-
const turn = {
|
|
967
|
-
id: uuidv7(), items: [item], itemsView: "full", status: "completed", error: null,
|
|
968
|
-
startedAt: now, completedAt: now, durationMs: 0,
|
|
969
|
-
};
|
|
970
|
-
this.store.createTurn(this.threadId, turn);
|
|
971
|
-
turnId = turn.id;
|
|
972
|
-
this.record = { ...this.record, lastCompletedTurnId: turn.id };
|
|
973
|
-
this.updateThread({ ...this.record.thread, updatedAt: now, recencyAt: now, status: { type: "idle" } });
|
|
974
|
-
}
|
|
975
|
-
const timestamp = Date.now();
|
|
976
|
-
this.emit("item/started", { item, threadId: this.threadId, turnId, startedAtMs: timestamp });
|
|
977
|
-
this.emit("item/completed", { item, threadId: this.threadId, turnId, completedAtMs: timestamp });
|
|
978
|
-
this.emit("thread/compacted", { threadId: this.threadId, turnId });
|
|
979
|
-
let context;
|
|
980
|
-
try {
|
|
981
|
-
context = await this.sdkQuery.getContextUsage();
|
|
982
|
-
}
|
|
983
|
-
catch (error) {
|
|
984
|
-
this.logger.debug("claude.context-usage.failed", { threadId: this.threadId, error: String(error) });
|
|
985
|
-
}
|
|
986
|
-
const contextTokens = context?.totalTokens ?? metadata.post_tokens;
|
|
987
|
-
if (contextTokens !== undefined) {
|
|
988
|
-
const last = {
|
|
989
|
-
totalTokens: contextTokens, inputTokens: contextTokens,
|
|
990
|
-
cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0,
|
|
991
|
-
};
|
|
992
|
-
this.emit("thread/tokenUsage/updated", {
|
|
993
|
-
threadId: this.threadId, turnId,
|
|
994
|
-
tokenUsage: { total: this.record.tokenUsageTotal, last, modelContextWindow: context?.maxTokens ?? null },
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
const pending = this.pendingCompaction;
|
|
998
|
-
if (pending) {
|
|
999
|
-
this.pendingCompaction = undefined;
|
|
1000
|
-
clearTimeout(pending.timer);
|
|
1001
|
-
pending.resolve();
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
emitUsage(message) {
|
|
1005
|
-
if (!this.active)
|
|
1006
|
-
return;
|
|
1007
|
-
const inputTokens = message.usage.input_tokens + message.usage.cache_creation_input_tokens + message.usage.cache_read_input_tokens;
|
|
1008
|
-
const last = {
|
|
1009
|
-
totalTokens: inputTokens + message.usage.output_tokens,
|
|
1010
|
-
inputTokens,
|
|
1011
|
-
cachedInputTokens: message.usage.cache_read_input_tokens,
|
|
1012
|
-
outputTokens: message.usage.output_tokens,
|
|
1013
|
-
reasoningOutputTokens: 0,
|
|
1014
|
-
};
|
|
1015
|
-
const total = {
|
|
1016
|
-
totalTokens: this.record.tokenUsageTotal.totalTokens + last.totalTokens,
|
|
1017
|
-
inputTokens: this.record.tokenUsageTotal.inputTokens + last.inputTokens,
|
|
1018
|
-
cachedInputTokens: this.record.tokenUsageTotal.cachedInputTokens + last.cachedInputTokens,
|
|
1019
|
-
outputTokens: this.record.tokenUsageTotal.outputTokens + last.outputTokens,
|
|
1020
|
-
reasoningOutputTokens: this.record.tokenUsageTotal.reasoningOutputTokens + last.reasoningOutputTokens,
|
|
1021
|
-
};
|
|
1022
|
-
this.record = { ...this.record, tokenUsageTotal: total };
|
|
1023
|
-
this.store.updateThread(this.record);
|
|
1024
|
-
const contextWindows = Object.values(message.modelUsage).map((usage) => usage.contextWindow);
|
|
1025
|
-
this.emit("thread/tokenUsage/updated", {
|
|
1026
|
-
threadId: this.threadId,
|
|
1027
|
-
turnId: this.active.turn.id,
|
|
1028
|
-
tokenUsage: {
|
|
1029
|
-
total,
|
|
1030
|
-
last,
|
|
1031
|
-
modelContextWindow: contextWindows.length > 0 ? Math.max(...contextWindows) : null,
|
|
1032
|
-
},
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
async handleStreamEvent(message) {
|
|
1036
|
-
const event = message.event;
|
|
1037
|
-
if (!this.active)
|
|
1038
|
-
return;
|
|
1039
|
-
if (event.type === "message_start") {
|
|
1040
|
-
this.active.blockItems.clear();
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
if (event.type === "content_block_start") {
|
|
1044
|
-
const block = event.content_block;
|
|
1045
|
-
if (block.type === "text")
|
|
1046
|
-
this.createAgentItem(event.index);
|
|
1047
|
-
else if (block.type === "thinking")
|
|
1048
|
-
this.createReasoningItem(event.index);
|
|
1049
|
-
else if (block.type === "tool_use" || block.type === "server_tool_use" || block.type === "mcp_tool_use") {
|
|
1050
|
-
this.createToolItem(event.index, block);
|
|
1051
|
-
}
|
|
1052
|
-
else
|
|
1053
|
-
this.completeServerToolResult(serverToolResult(block));
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
if (event.type === "content_block_delta") {
|
|
1057
|
-
if (event.delta.type === "text_delta")
|
|
1058
|
-
this.appendAgentText(event.index, event.delta.text);
|
|
1059
|
-
else if (event.delta.type === "thinking_delta")
|
|
1060
|
-
this.appendReasoning(event.index, event.delta.thinking);
|
|
1061
|
-
else if (event.delta.type === "input_json_delta")
|
|
1062
|
-
this.appendToolInput(event.index, event.delta.partial_json);
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
if (event.type === "content_block_stop" && !this.toolAt(event.index))
|
|
1066
|
-
this.completeItem(event.index);
|
|
1067
|
-
}
|
|
1068
|
-
createToolItem(index, block) {
|
|
1069
|
-
if (!this.active)
|
|
1070
|
-
throw new Error("No active turn.");
|
|
1071
|
-
const existing = this.itemAt(index);
|
|
1072
|
-
if (existing)
|
|
1073
|
-
return existing;
|
|
1074
|
-
const { state, item } = startTool(index, block, this.record.thread.cwd, this.threadId);
|
|
1075
|
-
this.active.blockItems.set(index, item.id);
|
|
1076
|
-
this.active.tools.set(state.providerId, state);
|
|
1077
|
-
this.active.turn.items.push(item);
|
|
1078
|
-
this.persistActive();
|
|
1079
|
-
this.startToolItem(state, item);
|
|
1080
|
-
return item;
|
|
1081
|
-
}
|
|
1082
|
-
appendToolInput(index, delta) {
|
|
1083
|
-
const tool = this.toolAt(index);
|
|
1084
|
-
const item = this.itemAt(index);
|
|
1085
|
-
if (!tool || !item)
|
|
1086
|
-
return;
|
|
1087
|
-
tool.partialInput += delta;
|
|
1088
|
-
try {
|
|
1089
|
-
const input = JSON.parse(tool.partialInput);
|
|
1090
|
-
updateToolInput(item, tool, input);
|
|
1091
|
-
this.persistActive();
|
|
1092
|
-
this.startToolItem(tool, item);
|
|
1093
|
-
this.emitPlan(item, input);
|
|
1094
|
-
}
|
|
1095
|
-
catch {
|
|
1096
|
-
// Partial JSON is expected until the final input_json_delta.
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
handleToolResults(message) {
|
|
1100
|
-
if (!this.active)
|
|
1101
|
-
return;
|
|
1102
|
-
const structured = message.tool_use_result;
|
|
1103
|
-
const result = structured && typeof structured === "object" ? structured : undefined;
|
|
1104
|
-
for (const toolResult of toolResults(message)) {
|
|
1105
|
-
const tool = this.active.tools.get(toolResult.toolUseId);
|
|
1106
|
-
if (!tool || this.active.completedItems.has(tool.itemId))
|
|
1107
|
-
continue;
|
|
1108
|
-
const index = this.active.turn.items.findIndex((item) => item.id === tool.itemId);
|
|
1109
|
-
const item = this.active.turn.items[index];
|
|
1110
|
-
if (!item)
|
|
1111
|
-
continue;
|
|
1112
|
-
this.startToolItem(tool, item, true);
|
|
1113
|
-
const previousChanges = item.type === "fileChange" ? JSON.stringify(item.changes) : undefined;
|
|
1114
|
-
const completed = completeTool(item, toolResult.output, toolResult.isError, result, tool.startedAtMs);
|
|
1115
|
-
this.active.turn.items[index] = completed;
|
|
1116
|
-
this.active.completedItems.add(completed.id);
|
|
1117
|
-
this.persistActive();
|
|
1118
|
-
if (completed.type === "commandExecution" && toolResult.output) {
|
|
1119
|
-
this.emit("item/commandExecution/outputDelta", { threadId: this.threadId, turnId: this.active.turn.id, itemId: completed.id, delta: toolResult.output });
|
|
1120
|
-
}
|
|
1121
|
-
if (completed.type === "fileChange" && completed.changes.length > 0 && JSON.stringify(completed.changes) !== previousChanges)
|
|
1122
|
-
this.emitFileDiff(completed);
|
|
1123
|
-
this.emit("item/completed", { item: completed, threadId: this.threadId, turnId: this.active.turn.id, completedAtMs: Date.now() });
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
completeServerToolResult(result) {
|
|
1127
|
-
if (!this.active || !result)
|
|
1128
|
-
return;
|
|
1129
|
-
const tool = this.active.tools.get(result.toolUseId);
|
|
1130
|
-
if (!tool || this.active.completedItems.has(tool.itemId))
|
|
1131
|
-
return;
|
|
1132
|
-
const index = this.active.turn.items.findIndex((item) => item.id === tool.itemId);
|
|
1133
|
-
const item = this.active.turn.items[index];
|
|
1134
|
-
if (!item)
|
|
1135
|
-
return;
|
|
1136
|
-
this.startToolItem(tool, item, true);
|
|
1137
|
-
const completed = completeTool(item, result.output, result.isError, undefined, tool.startedAtMs);
|
|
1138
|
-
this.active.turn.items[index] = completed;
|
|
1139
|
-
this.active.completedItems.add(completed.id);
|
|
1140
|
-
this.persistActive();
|
|
1141
|
-
this.emit("item/completed", {
|
|
1142
|
-
item: completed, threadId: this.threadId, turnId: this.active.turn.id, completedAtMs: Date.now(),
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
emitFileDiff(item) {
|
|
1146
|
-
if (!this.active)
|
|
1147
|
-
return;
|
|
1148
|
-
this.emit("item/fileChange/patchUpdated", {
|
|
1149
|
-
threadId: this.threadId, turnId: this.active.turn.id, itemId: item.id, changes: item.changes,
|
|
1150
|
-
});
|
|
1151
|
-
const diff = this.active.turn.items.flatMap((candidate) => candidate.type === "fileChange" ? candidate.changes.map((change) => change.diff) : []).join("\n");
|
|
1152
|
-
this.emit("turn/diff/updated", { threadId: this.threadId, turnId: this.active.turn.id, diff });
|
|
1153
|
-
}
|
|
1154
|
-
emitPlan(item, input) {
|
|
1155
|
-
if (!this.active || item.type !== "plan")
|
|
1156
|
-
return;
|
|
1157
|
-
const plan = planSteps(input);
|
|
1158
|
-
if (plan.length > 0)
|
|
1159
|
-
this.emit("turn/plan/updated", { threadId: this.threadId, turnId: this.active.turn.id, explanation: null, plan });
|
|
1160
|
-
}
|
|
1161
|
-
startToolItem(tool, item, force = false) {
|
|
1162
|
-
if (!this.active || tool.started || (item.type === "commandExecution" && !item.command))
|
|
1163
|
-
return;
|
|
1164
|
-
if (!force && !this.sessionApprovedTools.has(tool.name)) {
|
|
1165
|
-
const policy = toolPolicy(tool.name, tool.input, this.record.thread.cwd, this.record.approvalPolicy, this.record.sandboxPolicy);
|
|
1166
|
-
if (policy.decision === "ask")
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
tool.started = true;
|
|
1170
|
-
this.emit("item/started", {
|
|
1171
|
-
item, threadId: this.threadId, turnId: this.active.turn.id, startedAtMs: tool.startedAtMs,
|
|
1172
|
-
});
|
|
1173
|
-
this.emitPlan(item, tool.input);
|
|
1174
|
-
}
|
|
1175
|
-
itemAt(index) {
|
|
1176
|
-
if (!this.active)
|
|
1177
|
-
return undefined;
|
|
1178
|
-
const itemId = this.active.blockItems.get(index);
|
|
1179
|
-
return itemId ? this.active.turn.items.find((item) => item.id === itemId) : undefined;
|
|
1180
|
-
}
|
|
1181
|
-
toolAt(index) {
|
|
1182
|
-
if (!this.active)
|
|
1183
|
-
return undefined;
|
|
1184
|
-
const itemId = this.active.blockItems.get(index);
|
|
1185
|
-
return itemId ? [...this.active.tools.values()].find((tool) => tool.itemId === itemId) : undefined;
|
|
1186
|
-
}
|
|
1187
|
-
createAgentItem(index) {
|
|
1188
|
-
if (!this.active)
|
|
1189
|
-
throw new Error("No active turn.");
|
|
1190
|
-
const existingId = this.active.blockItems.get(index);
|
|
1191
|
-
const existing = existingId && this.active.turn.items.find((item) => item.id === existingId);
|
|
1192
|
-
if (existing)
|
|
1193
|
-
return existing;
|
|
1194
|
-
const item = { type: "agentMessage", id: uuidv7(), text: "", phase: null, memoryCitation: null };
|
|
1195
|
-
this.active.blockItems.set(index, item.id);
|
|
1196
|
-
this.active.turn.items.push(item);
|
|
1197
|
-
this.persistActive();
|
|
1198
|
-
this.emit("item/started", { item, threadId: this.threadId, turnId: this.active.turn.id, startedAtMs: Date.now() });
|
|
1199
|
-
return item;
|
|
1200
|
-
}
|
|
1201
|
-
createReasoningItem(index) {
|
|
1202
|
-
if (!this.active)
|
|
1203
|
-
throw new Error("No active turn.");
|
|
1204
|
-
const existingId = this.active.blockItems.get(index);
|
|
1205
|
-
const existing = existingId && this.active.turn.items.find((item) => item.id === existingId);
|
|
1206
|
-
if (existing)
|
|
1207
|
-
return existing;
|
|
1208
|
-
const item = { type: "reasoning", id: uuidv7(), summary: [], content: [] };
|
|
1209
|
-
this.active.blockItems.set(index, item.id);
|
|
1210
|
-
this.active.turn.items.push(item);
|
|
1211
|
-
this.persistActive();
|
|
1212
|
-
this.emit("item/started", { item, threadId: this.threadId, turnId: this.active.turn.id, startedAtMs: Date.now() });
|
|
1213
|
-
return item;
|
|
1214
|
-
}
|
|
1215
|
-
appendAgentText(index, delta) {
|
|
1216
|
-
if (!this.active)
|
|
1217
|
-
return;
|
|
1218
|
-
const item = this.createAgentItem(index);
|
|
1219
|
-
if (item.type !== "agentMessage")
|
|
1220
|
-
return;
|
|
1221
|
-
item.text += delta;
|
|
1222
|
-
this.persistActive();
|
|
1223
|
-
this.emit("item/agentMessage/delta", { threadId: this.threadId, turnId: this.active.turn.id, itemId: item.id, delta });
|
|
1224
|
-
}
|
|
1225
|
-
appendReasoning(index, delta) {
|
|
1226
|
-
if (!this.active)
|
|
1227
|
-
return;
|
|
1228
|
-
const item = this.createReasoningItem(index);
|
|
1229
|
-
if (item.type !== "reasoning")
|
|
1230
|
-
return;
|
|
1231
|
-
item.content[0] = `${item.content[0] ?? ""}${delta}`;
|
|
1232
|
-
this.persistActive();
|
|
1233
|
-
this.emit("item/reasoning/textDelta", { threadId: this.threadId, turnId: this.active.turn.id, itemId: item.id, delta, contentIndex: 0 });
|
|
1234
|
-
}
|
|
1235
|
-
completeItem(index) {
|
|
1236
|
-
if (!this.active)
|
|
1237
|
-
return;
|
|
1238
|
-
const itemId = this.active.blockItems.get(index);
|
|
1239
|
-
const item = itemId && this.active.turn.items.find((candidate) => candidate.id === itemId);
|
|
1240
|
-
if (!item || this.active.completedItems.has(item.id))
|
|
1241
|
-
return;
|
|
1242
|
-
this.active.completedItems.add(item.id);
|
|
1243
|
-
this.persistActive();
|
|
1244
|
-
this.emit("item/completed", { item, threadId: this.threadId, turnId: this.active.turn.id, completedAtMs: Date.now() });
|
|
1245
|
-
}
|
|
1246
|
-
async backfillAssistant(message) {
|
|
1247
|
-
if (!this.active)
|
|
1248
|
-
return;
|
|
1249
|
-
for (const text of textFromAssistant(message)) {
|
|
1250
|
-
const alreadyPresent = this.active.turn.items.some((item) => item.type === "agentMessage" && item.text === text);
|
|
1251
|
-
if (alreadyPresent)
|
|
1252
|
-
continue;
|
|
1253
|
-
const item = { type: "agentMessage", id: uuidv7(), text, phase: null, memoryCitation: null };
|
|
1254
|
-
this.active.turn.items.push(item);
|
|
1255
|
-
this.active.completedItems.add(item.id);
|
|
1256
|
-
this.persistActive();
|
|
1257
|
-
this.emit("item/started", { item, threadId: this.threadId, turnId: this.active.turn.id, startedAtMs: Date.now() });
|
|
1258
|
-
this.emit("item/agentMessage/delta", { threadId: this.threadId, turnId: this.active.turn.id, itemId: item.id, delta: text });
|
|
1259
|
-
this.emit("item/completed", { item, threadId: this.threadId, turnId: this.active.turn.id, completedAtMs: Date.now() });
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
async failActive(message) {
|
|
1263
|
-
await this.completeActive("failed", message, classifyClaudeRuntimeError(message));
|
|
1264
|
-
}
|
|
1265
|
-
async completeActive(status, errorMessage, codexErrorInfo = null) {
|
|
1266
|
-
if (!this.active)
|
|
1267
|
-
return;
|
|
1268
|
-
for (const tool of this.active.tools.values()) {
|
|
1269
|
-
if (this.active.completedItems.has(tool.itemId))
|
|
1270
|
-
continue;
|
|
1271
|
-
const index = this.active.turn.items.findIndex((item) => item.id === tool.itemId);
|
|
1272
|
-
const item = this.active.turn.items[index];
|
|
1273
|
-
if (!item)
|
|
1274
|
-
continue;
|
|
1275
|
-
this.active.turn.items[index] = completeTool(item, "Claude turn ended before the tool returned a result.", true, undefined, tool.startedAtMs);
|
|
1276
|
-
}
|
|
1277
|
-
for (const index of this.active.blockItems.keys())
|
|
1278
|
-
this.completeItem(index);
|
|
1279
|
-
if (this.active.review) {
|
|
1280
|
-
const item = { type: "exitedReviewMode", id: uuidv7(), review: this.active.review };
|
|
1281
|
-
this.active.turn.items.push(item);
|
|
1282
|
-
this.active.completedItems.add(item.id);
|
|
1283
|
-
this.persistActive();
|
|
1284
|
-
this.emit("item/started", { item, threadId: this.threadId, turnId: this.active.turn.id, startedAtMs: Date.now() });
|
|
1285
|
-
this.emit("item/completed", { item, threadId: this.threadId, turnId: this.active.turn.id, completedAtMs: Date.now() });
|
|
1286
|
-
}
|
|
1287
|
-
for (const task of this.tasks.values()) {
|
|
1288
|
-
if (task.turnId !== this.active.turn.id || task.terminal)
|
|
1289
|
-
continue;
|
|
1290
|
-
const item = this.active.turn.items.find((candidate) => candidate.id === task.itemId);
|
|
1291
|
-
if (item?.type === "collabAgentToolCall")
|
|
1292
|
-
item.status = "completed";
|
|
1293
|
-
else if (item?.type === "dynamicToolCall")
|
|
1294
|
-
item.status = "completed";
|
|
1295
|
-
if (item) {
|
|
1296
|
-
this.active.completedItems.add(item.id);
|
|
1297
|
-
this.emit("item/completed", { item, threadId: this.threadId, turnId: this.active.turn.id, completedAtMs: Date.now() });
|
|
1298
|
-
}
|
|
1299
|
-
task.turnId = null;
|
|
1300
|
-
}
|
|
1301
|
-
const completedAt = nowSeconds();
|
|
1302
|
-
const startedAt = this.active.turn.startedAt ?? completedAt;
|
|
1303
|
-
this.active.turn = {
|
|
1304
|
-
...this.active.turn,
|
|
1305
|
-
status,
|
|
1306
|
-
completedAt,
|
|
1307
|
-
durationMs: Math.max(0, (completedAt - startedAt) * 1_000),
|
|
1308
|
-
error: status === "failed"
|
|
1309
|
-
? { message: errorMessage ?? "Claude turn failed.", codexErrorInfo, additionalDetails: null }
|
|
1310
|
-
: null,
|
|
1311
|
-
};
|
|
1312
|
-
this.persistActive();
|
|
1313
|
-
const turn = this.active.turn;
|
|
1314
|
-
this.active = undefined;
|
|
1315
|
-
this.lastActivityMs = Date.now();
|
|
1316
|
-
this.record = { ...this.record, lastCompletedTurnId: turn.id };
|
|
1317
|
-
this.updateThread({
|
|
1318
|
-
...this.record.thread,
|
|
1319
|
-
status: this.backgroundTaskIds.size > 0 ? { type: "active", activeFlags: [] } : { type: "idle" },
|
|
1320
|
-
updatedAt: completedAt, recencyAt: completedAt,
|
|
1321
|
-
});
|
|
1322
|
-
if (turn.error) {
|
|
1323
|
-
this.emit("error", { error: turn.error, willRetry: false, threadId: this.threadId, turnId: turn.id }, turn.id);
|
|
1324
|
-
}
|
|
1325
|
-
this.emit("turn/completed", { threadId: this.threadId, turn });
|
|
1326
|
-
}
|
|
1327
|
-
persistActive() {
|
|
1328
|
-
if (this.active)
|
|
1329
|
-
this.store.updateTurn(this.threadId, this.active.turn);
|
|
1330
|
-
}
|
|
1331
|
-
updateThread(thread) {
|
|
1332
|
-
this.record = { ...this.record, thread };
|
|
1333
|
-
this.store.updateThread(this.record);
|
|
1334
|
-
this.emit("thread/status/changed", { threadId: this.threadId, status: thread.status });
|
|
1335
|
-
}
|
|
1336
|
-
emit(method, params, turnId = this.active?.turn.id ?? null) {
|
|
1337
|
-
const value = params && typeof params === "object" ? params : undefined;
|
|
1338
|
-
const parameterTurn = value?.turn && typeof value.turn === "object" && "items" in value.turn
|
|
1339
|
-
? value.turn
|
|
1340
|
-
: undefined;
|
|
1341
|
-
const runtimeTurn = this.active?.turn.id === turnId
|
|
1342
|
-
? this.active.turn
|
|
1343
|
-
: this.backgroundTurn?.id === turnId
|
|
1344
|
-
? this.backgroundTurn
|
|
1345
|
-
: undefined;
|
|
1346
|
-
const persistedTurn = parameterTurn ?? runtimeTurn;
|
|
1347
|
-
const sequence = this.store.appendEvent(this.threadId, turnId, method, params, {
|
|
1348
|
-
...(persistedTurn ? { turn: persistedTurn } : {}),
|
|
1349
|
-
providerEventId: this.providerEventId,
|
|
1350
|
-
providerEventType: this.providerEventType,
|
|
1351
|
-
dedupKey: eventDedupKey(method, params),
|
|
1352
|
-
});
|
|
1353
|
-
if (sequence === 0)
|
|
1354
|
-
return;
|
|
1355
|
-
if (method === "turn/completed" && parameterTurn && parameterTurn.status !== "inProgress") {
|
|
1356
|
-
this.metrics.turnCompleted(parameterTurn.status);
|
|
1357
|
-
}
|
|
1358
|
-
this.hub.emit(this.threadId, method, params);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
//# sourceMappingURL=sessionRuntime.js.map
|