@bbigbang/agent-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentHost.js +483 -0
- package/dist/appVersion.js +14 -0
- package/dist/assetCachePaths.js +35 -0
- package/dist/attachmentInput.js +588 -0
- package/dist/attachmentMaterializer.js +230 -0
- package/dist/bigbangCli.js +17 -0
- package/dist/bigbangMessageSendDetection.js +284 -0
- package/dist/builtinSkillRoots.js +54 -0
- package/dist/claudeConfig.js +32 -0
- package/dist/claudeDirectRuntime.js +1960 -0
- package/dist/claudeSessionControls.js +78 -0
- package/dist/claudeTranscriptFs.js +147 -0
- package/dist/codexAppServerClient.js +188 -0
- package/dist/codexAppServerEnv.js +14 -0
- package/dist/codexAppServerRpc.js +273 -0
- package/dist/codexAppServerRuntime.js +3495 -0
- package/dist/codexBuiltinPrompt.js +117 -0
- package/dist/codexConversationSummarizer.js +76 -0
- package/dist/codexTranscriptFs.js +145 -0
- package/dist/config.js +129 -0
- package/dist/connection.js +151 -0
- package/dist/dispatchQueueStore.js +39 -0
- package/dist/dreamEnv.js +1 -0
- package/dist/dreamMemoryFallback.js +118 -0
- package/dist/dreamToolPolicy.js +293 -0
- package/dist/droidMissionRunner.js +808 -0
- package/dist/executor.js +1078 -0
- package/dist/hostRuntime.js +1 -0
- package/dist/libraryAuthorityFs.js +74 -0
- package/dist/libraryMirror.js +183 -0
- package/dist/main.js +1659 -0
- package/dist/native-worker/native-worker.mjs +475 -0
- package/dist/nativeMissionAgentDispatch.js +463 -0
- package/dist/nativeMissionRunner.js +461 -0
- package/dist/nativeSkillMounts.js +204 -0
- package/dist/nativeWorkerHost.js +142 -0
- package/dist/nodeSink.js +142 -0
- package/dist/panelHttpFetch.js +334 -0
- package/dist/runtimeDrivers.js +62 -0
- package/dist/skillFs.js +229 -0
- package/dist/soloHost.js +165 -0
- package/dist/soloNodeSink.js +138 -0
- package/dist/terminalManager.js +254 -0
- package/dist/workspaceFs.js +1020 -0
- package/dist/workspaceGit.js +694 -0
- package/dist/workspaceInspect.js +22 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1960 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
5
|
+
import { clearAcpSessionId, getLatestClaudeSessionUserMessage, getSession, insertClaudeSessionUserMessage, log, updateClaudeSessionControls, updateSessionRuntimeState, } from '@bbigbang/runtime-acp';
|
|
6
|
+
import { buildInlineTextAttachmentPreview, formatAttachmentTextReference, promptAlreadyReferencesAttachment, resolveClaudeInlineImageAttachment, resolveLocalAttachmentPath, } from './attachmentInput.js';
|
|
7
|
+
import { extractAttachmentIdFromUri, RuntimeAttachmentMaterializer, } from './attachmentMaterializer.js';
|
|
8
|
+
import { buildClaudeSessionControlState, isClaudeSessionCommandName, isClaudeSessionModelId, isClaudeSessionModeId, normalizeClaudeSessionModeId, } from './claudeSessionControls.js';
|
|
9
|
+
import { buildBigbangMessageSendRepairGuidance, getBigbangMessageSendInfo, } from './bigbangMessageSendDetection.js';
|
|
10
|
+
import { buildDreamMemoryWriteRepairPrompt, buildDreamWatermarkRepairPrompt, } from '@bbigbang/memory';
|
|
11
|
+
import { executeDreamMemoryConsolidationFallback, executeDreamWatermarkFallback, readDreamAuthTokenFromEnv, } from './dreamMemoryFallback.js';
|
|
12
|
+
import { buildDreamAdditionalDisallowedTools, evaluateDreamToolPermission, isDreamMemoryWatermarkTool, isDreamMemoryWriteTool, } from './dreamToolPolicy.js';
|
|
13
|
+
const CLAUDE_SETTING_SOURCES = ['user', 'project'];
|
|
14
|
+
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;
|
|
15
|
+
const CLAUDE_STEER_CONSUME_TIMEOUT_MS = 5 * 60_000;
|
|
16
|
+
const CLAUDE_ASSET_FETCH_TIMEOUT_MS = 15_000;
|
|
17
|
+
// If no event arrives within this window the model is considered hung; the stream is interrupted and the run is marked failed.
|
|
18
|
+
const CLAUDE_STREAM_IDLE_TIMEOUT_MS = 10 * 60_000;
|
|
19
|
+
const CLAUDE_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'max']);
|
|
20
|
+
const CLAUDE_ASSISTANT_TEXT_REPAIR_MAX_ATTEMPTS = 2;
|
|
21
|
+
const DREAM_MEMORY_WRITE_REPAIR_MAX_ATTEMPTS = 4;
|
|
22
|
+
const CLAUDE_ASSISTANT_TEXT_REPAIR_PROMPT = 'Your previous turn emitted ordinary assistant text directly instead of sending it through the platform chat tool. '
|
|
23
|
+
+ 'The user cannot see plain assistant text from that turn. Do not run tools, inspect files, revise prior work, or do more work. '
|
|
24
|
+
+ 'Your only allowed user-visible action now is exactly one successful {toolName}(content="...", kind="final") call using the intended reply below. '
|
|
25
|
+
+ 'If the captured text is incomplete, reconstruct only the complete intended user-visible reply from the current conversation context. '
|
|
26
|
+
+ 'Do not emit ordinary assistant text again.';
|
|
27
|
+
const CLAUDE_BIGBANG_ASSISTANT_TEXT_REPAIR_PROMPT = 'Your previous turn emitted ordinary assistant text directly or failed to prove delivery through `bigbang message send`. '
|
|
28
|
+
+ 'The user cannot see plain assistant text from that turn. Do not inspect files, revise prior work, or do more work. '
|
|
29
|
+
+ 'Your only allowed user-visible action now is exactly one successful shell command using `bigbang message send --kind final`. '
|
|
30
|
+
+ 'If captured text is provided below, send that intended reply; otherwise reconstruct only the complete intended user-visible reply from the current conversation context. '
|
|
31
|
+
+ 'Do not emit ordinary assistant text again.';
|
|
32
|
+
const CLAUDE_BIGBANG_PROGRESS_ALREADY_DELIVERED_FINAL_GUIDANCE = 'A visible progress message was already delivered successfully in this run. The final message is still required for completion proof, but it must be a concise completion marker and must not repeat the already posted progress content. Prefer a short final such as "已完成,详见上面的进度消息。".';
|
|
33
|
+
// Tool kinds that may write to the workspace and therefore need the workspace
|
|
34
|
+
// write lock before we let Claude SDK execute them.
|
|
35
|
+
const CLAUDE_WORKSPACE_WRITE_TOOL_KINDS = new Set([
|
|
36
|
+
'edit',
|
|
37
|
+
'delete',
|
|
38
|
+
'move',
|
|
39
|
+
'execute',
|
|
40
|
+
]);
|
|
41
|
+
const CLAUDE_WORKSPACE_READ_TOOL_KINDS = new Set([
|
|
42
|
+
'read',
|
|
43
|
+
'search',
|
|
44
|
+
]);
|
|
45
|
+
const CLAUDE_DISALLOWED_TOOLS_BY_KIND = {
|
|
46
|
+
read: ['Read'],
|
|
47
|
+
edit: ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'],
|
|
48
|
+
search: ['Grep', 'Glob', 'LS'],
|
|
49
|
+
execute: ['Bash', 'KillBash'],
|
|
50
|
+
think: ['Task', 'TodoWrite'],
|
|
51
|
+
fetch: ['WebFetch', 'WebSearch'],
|
|
52
|
+
switch_mode: ['ExitPlanMode'],
|
|
53
|
+
};
|
|
54
|
+
class ClaudeInputQueue {
|
|
55
|
+
messages = [];
|
|
56
|
+
waiters = [];
|
|
57
|
+
closed = false;
|
|
58
|
+
enqueue(message) {
|
|
59
|
+
return this.enqueueItem({ message });
|
|
60
|
+
}
|
|
61
|
+
enqueueAndWaitConsumed(message, timeoutMs = CLAUDE_STEER_CONSUME_TIMEOUT_MS) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const item = {
|
|
64
|
+
message,
|
|
65
|
+
onConsumed: resolve,
|
|
66
|
+
};
|
|
67
|
+
if (!this.enqueueItem(item)) {
|
|
68
|
+
this.settleItem(item, false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (timeoutMs > 0 && !item.settled) {
|
|
72
|
+
item.timer = setTimeout(() => {
|
|
73
|
+
this.removeItem(item);
|
|
74
|
+
this.settleItem(item, false);
|
|
75
|
+
}, timeoutMs);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
enqueueItem(item) {
|
|
80
|
+
if (this.closed)
|
|
81
|
+
return false;
|
|
82
|
+
const waiter = this.waiters.shift();
|
|
83
|
+
if (waiter) {
|
|
84
|
+
waiter({ value: item.message, done: false });
|
|
85
|
+
this.settleItem(item, true);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
this.messages.push(item);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
settleItem(item, consumed) {
|
|
92
|
+
if (item.settled)
|
|
93
|
+
return;
|
|
94
|
+
item.settled = true;
|
|
95
|
+
if (item.timer) {
|
|
96
|
+
clearTimeout(item.timer);
|
|
97
|
+
item.timer = undefined;
|
|
98
|
+
}
|
|
99
|
+
item.onConsumed?.(consumed);
|
|
100
|
+
}
|
|
101
|
+
removeItem(item) {
|
|
102
|
+
const index = this.messages.indexOf(item);
|
|
103
|
+
if (index >= 0) {
|
|
104
|
+
this.messages.splice(index, 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
close() {
|
|
108
|
+
if (this.closed)
|
|
109
|
+
return;
|
|
110
|
+
this.closed = true;
|
|
111
|
+
while (this.messages.length > 0) {
|
|
112
|
+
const item = this.messages.shift();
|
|
113
|
+
if (item)
|
|
114
|
+
this.settleItem(item, false);
|
|
115
|
+
}
|
|
116
|
+
while (this.waiters.length > 0) {
|
|
117
|
+
this.waiters.shift()?.({ value: undefined, done: true });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
[Symbol.asyncIterator]() {
|
|
121
|
+
return {
|
|
122
|
+
next: async () => {
|
|
123
|
+
while (this.messages.length > 0) {
|
|
124
|
+
const nextMessage = this.messages.shift();
|
|
125
|
+
if (!nextMessage || nextMessage.settled)
|
|
126
|
+
continue;
|
|
127
|
+
this.settleItem(nextMessage, true);
|
|
128
|
+
return { value: nextMessage.message, done: false };
|
|
129
|
+
}
|
|
130
|
+
if (this.closed)
|
|
131
|
+
return { value: undefined, done: true };
|
|
132
|
+
return await new Promise((resolve) => {
|
|
133
|
+
this.waiters.push(resolve);
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export class ClaudeDirectRuntime {
|
|
140
|
+
db;
|
|
141
|
+
sessionKey;
|
|
142
|
+
bindingKey;
|
|
143
|
+
workspaceRoot;
|
|
144
|
+
env;
|
|
145
|
+
disabledToolKinds;
|
|
146
|
+
channelBridgeMcpEntry;
|
|
147
|
+
agentSurfaceMode;
|
|
148
|
+
toolAuth;
|
|
149
|
+
workspaceLockManager;
|
|
150
|
+
attachmentMaterializer;
|
|
151
|
+
queryImpl;
|
|
152
|
+
sessionId;
|
|
153
|
+
sessionSystemPromptText = null;
|
|
154
|
+
useAgentRuntimeConfig = false;
|
|
155
|
+
agentModelId = null;
|
|
156
|
+
agentReasoningEffort = null;
|
|
157
|
+
claudeModeId = 'default';
|
|
158
|
+
claudeModelId = null;
|
|
159
|
+
activeRunId = null;
|
|
160
|
+
activeQuery = null;
|
|
161
|
+
activeSink = null;
|
|
162
|
+
activeUiMode = 'summary';
|
|
163
|
+
activeClaudeModeId = null;
|
|
164
|
+
cancelRequested = false;
|
|
165
|
+
streamIdleTimedOut = false;
|
|
166
|
+
pendingToolCalls = new Map();
|
|
167
|
+
suppressedToolResultIds = new Set();
|
|
168
|
+
pendingPermission = null;
|
|
169
|
+
activeInputQueue = null;
|
|
170
|
+
activeSteerInputQueue = null;
|
|
171
|
+
deliveredFinalChatMessage = false;
|
|
172
|
+
deliveredProgressChatMessage = false;
|
|
173
|
+
receivedRuntimeOutput = false;
|
|
174
|
+
assistantTextRepairAttempts = 0;
|
|
175
|
+
assistantTextRepairSourceText = null;
|
|
176
|
+
currentTurnAssistantTextBuffer = '';
|
|
177
|
+
currentTurnAssistantTextRepairTriggered = false;
|
|
178
|
+
currentTurnBigbangMessageSendFailures = [];
|
|
179
|
+
lastBigbangMessageSendFailureForRepair = null;
|
|
180
|
+
planApprovalDenied = false;
|
|
181
|
+
activeTurnWorkspaceLease = null;
|
|
182
|
+
activeTurnWorkspaceLeasePromise = null;
|
|
183
|
+
workspaceLockAbortController = null;
|
|
184
|
+
activeDreamMode = false;
|
|
185
|
+
dreamMemoryWriteObserved = false;
|
|
186
|
+
dreamWatermarkObserved = false;
|
|
187
|
+
dreamTurnCompleted = false;
|
|
188
|
+
dreamMemoryRepairAttempts = 0;
|
|
189
|
+
activeDreamContextText = null;
|
|
190
|
+
dreamCachedAuthToken = null;
|
|
191
|
+
preservedChatSessionId = null;
|
|
192
|
+
preservedChatSystemPromptText = null;
|
|
193
|
+
constructor(params) {
|
|
194
|
+
this.db = params.db;
|
|
195
|
+
this.sessionKey = params.sessionKey;
|
|
196
|
+
this.bindingKey = params.bindingKey?.trim() || null;
|
|
197
|
+
this.workspaceRoot = params.workspaceRoot;
|
|
198
|
+
this.env = { ...(params.env ?? {}) };
|
|
199
|
+
this.useAgentRuntimeConfig = hasAgentRuntimeConfigInput(params);
|
|
200
|
+
this.agentModelId = normalizeOptionalString(params.model);
|
|
201
|
+
this.agentReasoningEffort = normalizeClaudeReasoningEffort(params.reasoningEffort);
|
|
202
|
+
this.disabledToolKinds = [...(params.disabledToolKinds ?? [])];
|
|
203
|
+
this.channelBridgeMcpEntry = params.channelBridgeMcpEntry;
|
|
204
|
+
this.agentSurfaceMode = params.agentSurfaceMode ?? 'mcp';
|
|
205
|
+
this.toolAuth = params.toolAuth ?? null;
|
|
206
|
+
this.workspaceLockManager = params.workspaceLockManager;
|
|
207
|
+
this.attachmentMaterializer = new RuntimeAttachmentMaterializer({
|
|
208
|
+
sessionKey: this.sessionKey,
|
|
209
|
+
runtimeLabel: 'claude-sdk',
|
|
210
|
+
assetAccessConfig: params.assetAccessConfig,
|
|
211
|
+
assetCacheRoot: params.assetCacheRoot,
|
|
212
|
+
fetchTimeoutMs: params.assetFetchTimeoutMs ?? CLAUDE_ASSET_FETCH_TIMEOUT_MS,
|
|
213
|
+
isCancelled: () => this.cancelRequested,
|
|
214
|
+
onMaterialized: params.onAssetMaterialized,
|
|
215
|
+
});
|
|
216
|
+
this.queryImpl = params.queryImpl ?? query;
|
|
217
|
+
const existingSession = getSession(this.db, this.sessionKey);
|
|
218
|
+
this.sessionId = existingSession?.acpSessionId?.trim() || null;
|
|
219
|
+
this.sessionSystemPromptText = existingSession?.systemPromptText?.trim() || null;
|
|
220
|
+
this.claudeModeId = normalizeClaudeSessionModeId(existingSession?.claudeModeId ?? null);
|
|
221
|
+
this.claudeModelId = isClaudeSessionModelId(existingSession?.claudeModelId ?? null)
|
|
222
|
+
? existingSession?.claudeModelId ?? null
|
|
223
|
+
: null;
|
|
224
|
+
}
|
|
225
|
+
hasPendingPermission() {
|
|
226
|
+
return Boolean(this.pendingPermission);
|
|
227
|
+
}
|
|
228
|
+
updateConfig(config) {
|
|
229
|
+
this.useAgentRuntimeConfig = true;
|
|
230
|
+
this.agentModelId = normalizeOptionalString(config.model);
|
|
231
|
+
this.agentReasoningEffort = normalizeClaudeReasoningEffort(config.reasoningEffort);
|
|
232
|
+
this.disabledToolKinds = [...(config.disabledToolKinds ?? [])];
|
|
233
|
+
if (config.envVars) {
|
|
234
|
+
this.env = { ...config.envVars };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async respondToPermission(requestId, decision, selectedActionId, responseText, _answers) {
|
|
238
|
+
const pending = this.pendingPermission;
|
|
239
|
+
if (!pending || pending.requestId !== requestId)
|
|
240
|
+
return false;
|
|
241
|
+
if (pending.kind === 'plan') {
|
|
242
|
+
if (decision === 'deny' || selectedActionId === 'reject') {
|
|
243
|
+
this.planApprovalDenied = true;
|
|
244
|
+
pending.settleDeny('Plan rejected by user.', true);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (selectedActionId === 'continue_planning') {
|
|
248
|
+
pending.settleDeny(buildClaudeContinuePlanningMessage(responseText), false);
|
|
249
|
+
this.sendPlanPhaseBestEffort('planning');
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (decision === 'allow' && selectedActionId === 'implement') {
|
|
253
|
+
pending.settleAllow();
|
|
254
|
+
this.sendPlanPhaseBestEffort('implementation');
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
pending.settleDeny('Plan approval requires an explicit implement, continue planning, or reject action.');
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
if (decision === 'allow') {
|
|
261
|
+
pending.settleAllow();
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
pending.settleDeny('Denied by user.');
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
async cancelCurrentRun(runId) {
|
|
269
|
+
if (this.activeRunId !== runId) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
this.cancelRequested = true;
|
|
273
|
+
this.activeInputQueue?.close();
|
|
274
|
+
this.attachmentMaterializer.abort();
|
|
275
|
+
this.abortWorkspaceLockWait();
|
|
276
|
+
this.rejectPendingPermission('Permission request cancelled.');
|
|
277
|
+
if (this.activeQuery && typeof this.activeQuery.interrupt === 'function') {
|
|
278
|
+
await this.activeQuery.interrupt();
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
async steerCurrentRun(runId, promptText, attachments) {
|
|
283
|
+
if (this.activeRunId !== runId || this.cancelRequested)
|
|
284
|
+
return false;
|
|
285
|
+
if (this.deliveredFinalChatMessage)
|
|
286
|
+
return false;
|
|
287
|
+
const inputQueue = this.activeInputQueue ?? this.activeSteerInputQueue;
|
|
288
|
+
if (!inputQueue)
|
|
289
|
+
return false;
|
|
290
|
+
const materializedAttachments = attachments?.length
|
|
291
|
+
? await this.materializeAssetAttachments(attachments, runId)
|
|
292
|
+
: [];
|
|
293
|
+
if (!promptText?.trim() && materializedAttachments.length === 0)
|
|
294
|
+
return false;
|
|
295
|
+
const consumed = await inputQueue.enqueueAndWaitConsumed({
|
|
296
|
+
...this.toSdkUserMessage(promptText, materializedAttachments),
|
|
297
|
+
isSynthetic: true,
|
|
298
|
+
priority: 'now',
|
|
299
|
+
});
|
|
300
|
+
return consumed;
|
|
301
|
+
}
|
|
302
|
+
async getClaudeControls() {
|
|
303
|
+
return this.buildClaudeControls();
|
|
304
|
+
}
|
|
305
|
+
async setClaudeMode(modeId) {
|
|
306
|
+
if (!isClaudeSessionModeId(modeId)) {
|
|
307
|
+
throw new Error(`Unsupported Claude mode: ${modeId}`);
|
|
308
|
+
}
|
|
309
|
+
this.claudeModeId = modeId;
|
|
310
|
+
updateClaudeSessionControls(this.db, {
|
|
311
|
+
sessionKey: this.sessionKey,
|
|
312
|
+
claudeModeId: this.claudeModeId,
|
|
313
|
+
claudeModelId: this.claudeModelId,
|
|
314
|
+
});
|
|
315
|
+
return this.buildClaudeControls();
|
|
316
|
+
}
|
|
317
|
+
async setClaudeModel(modelId) {
|
|
318
|
+
const normalizedModelId = modelId?.trim() ? modelId.trim() : null;
|
|
319
|
+
if (normalizedModelId && !isClaudeSessionModelId(normalizedModelId)) {
|
|
320
|
+
throw new Error(`Unsupported Claude model: ${normalizedModelId}`);
|
|
321
|
+
}
|
|
322
|
+
this.claudeModelId = normalizedModelId;
|
|
323
|
+
updateClaudeSessionControls(this.db, {
|
|
324
|
+
sessionKey: this.sessionKey,
|
|
325
|
+
claudeModeId: this.claudeModeId,
|
|
326
|
+
claudeModelId: this.claudeModelId,
|
|
327
|
+
});
|
|
328
|
+
return this.buildClaudeControls();
|
|
329
|
+
}
|
|
330
|
+
async executeClaudeCommand(commandName, args) {
|
|
331
|
+
if (!isClaudeSessionCommandName(commandName)) {
|
|
332
|
+
throw new Error(`Unsupported Claude command: ${commandName}`);
|
|
333
|
+
}
|
|
334
|
+
switch (commandName) {
|
|
335
|
+
case 'rewind':
|
|
336
|
+
return this.executeRewindCommand(args);
|
|
337
|
+
default:
|
|
338
|
+
throw new Error(`Unsupported Claude command: ${commandName}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async close() {
|
|
342
|
+
this.cancelRequested = true;
|
|
343
|
+
this.activeInputQueue?.close();
|
|
344
|
+
this.attachmentMaterializer.abort();
|
|
345
|
+
this.abortWorkspaceLockWait();
|
|
346
|
+
this.rejectPendingPermission('Permission request cancelled.');
|
|
347
|
+
if (this.activeQuery && typeof this.activeQuery.interrupt === 'function') {
|
|
348
|
+
await this.activeQuery.interrupt().catch(() => { });
|
|
349
|
+
}
|
|
350
|
+
this.releaseActiveTurnWorkspaceLock();
|
|
351
|
+
this.activeQuery = null;
|
|
352
|
+
this.activeInputQueue = null;
|
|
353
|
+
this.activeSteerInputQueue = null;
|
|
354
|
+
this.activeRunId = null;
|
|
355
|
+
this.activeSink = null;
|
|
356
|
+
this.deliveredProgressChatMessage = false;
|
|
357
|
+
this.pendingToolCalls.clear();
|
|
358
|
+
this.suppressedToolResultIds.clear();
|
|
359
|
+
}
|
|
360
|
+
async prompt(params) {
|
|
361
|
+
if (this.activeRunId) {
|
|
362
|
+
throw new Error('Claude SDK runtime already has an active run.');
|
|
363
|
+
}
|
|
364
|
+
this.activeRunId = params.runId;
|
|
365
|
+
this.activeSink = params.sink;
|
|
366
|
+
this.activeUiMode = params.uiMode;
|
|
367
|
+
this.activeDreamMode = params.dreamMode === true;
|
|
368
|
+
if (this.activeDreamMode) {
|
|
369
|
+
this.preservedChatSessionId = this.sessionId;
|
|
370
|
+
this.preservedChatSystemPromptText = this.sessionSystemPromptText;
|
|
371
|
+
this.sessionId = null;
|
|
372
|
+
this.dreamMemoryWriteObserved = false;
|
|
373
|
+
this.dreamWatermarkObserved = false;
|
|
374
|
+
this.dreamTurnCompleted = false;
|
|
375
|
+
this.dreamMemoryRepairAttempts = 0;
|
|
376
|
+
this.activeDreamContextText = params.contextText?.trim() || null;
|
|
377
|
+
this.dreamCachedAuthToken = readDreamAuthTokenFromEnv(this.env);
|
|
378
|
+
}
|
|
379
|
+
this.cancelRequested = false;
|
|
380
|
+
this.streamIdleTimedOut = false;
|
|
381
|
+
this.deliveredFinalChatMessage = false;
|
|
382
|
+
this.deliveredProgressChatMessage = false;
|
|
383
|
+
this.receivedRuntimeOutput = false;
|
|
384
|
+
this.clearAssistantTextRepairState();
|
|
385
|
+
this.planApprovalDenied = false;
|
|
386
|
+
this.currentTurnBigbangMessageSendFailures = [];
|
|
387
|
+
this.activeClaudeModeId = params.runtimeOverrides?.planMode === true
|
|
388
|
+
? 'plan'
|
|
389
|
+
: this.getEffectivePermissionMode();
|
|
390
|
+
this.pendingToolCalls.clear();
|
|
391
|
+
this.suppressedToolResultIds.clear();
|
|
392
|
+
const isFreshSession = !this.sessionId;
|
|
393
|
+
const effectiveContextText = isFreshSession
|
|
394
|
+
? joinPromptContext(params.resumeContextText, params.recoveryContextText, params.contextText)
|
|
395
|
+
: joinPromptContext(params.contextText);
|
|
396
|
+
const effectiveSystemPromptText = params.systemPromptText?.trim() || undefined;
|
|
397
|
+
const promptAttachments = params.attachments ?? params.promptResources;
|
|
398
|
+
const materializedAttachments = promptAttachments?.length
|
|
399
|
+
? await this.materializeAssetAttachments(promptAttachments, params.runId)
|
|
400
|
+
: [];
|
|
401
|
+
if (this.cancelRequested) {
|
|
402
|
+
const cancelledSessionId = this.sessionId ?? '';
|
|
403
|
+
this.activeRunId = null;
|
|
404
|
+
this.activeSink = null;
|
|
405
|
+
this.activeUiMode = 'summary';
|
|
406
|
+
this.activeClaudeModeId = null;
|
|
407
|
+
this.cancelRequested = false;
|
|
408
|
+
return {
|
|
409
|
+
stopReason: 'cancelled',
|
|
410
|
+
isFreshSession,
|
|
411
|
+
sessionId: cancelledSessionId,
|
|
412
|
+
effectiveSystemPromptText,
|
|
413
|
+
effectiveContextText: effectiveContextText || undefined,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const sdkMessage = this.toSdkUserMessage(joinPromptContext(effectiveContextText, params.promptText), materializedAttachments);
|
|
417
|
+
const inputQueue = new ClaudeInputQueue();
|
|
418
|
+
inputQueue.enqueue(sdkMessage);
|
|
419
|
+
let preparedSessionId = null;
|
|
420
|
+
let finalStopReason = 'completed';
|
|
421
|
+
let finalError = null;
|
|
422
|
+
const dreamModeRun = this.activeDreamMode;
|
|
423
|
+
const syncPreparedSession = async () => {
|
|
424
|
+
if (dreamModeRun)
|
|
425
|
+
return;
|
|
426
|
+
if (!this.sessionId || preparedSessionId === this.sessionId)
|
|
427
|
+
return;
|
|
428
|
+
preparedSessionId = this.sessionId;
|
|
429
|
+
this.sessionSystemPromptText = effectiveSystemPromptText ?? null;
|
|
430
|
+
updateSessionRuntimeState(this.db, {
|
|
431
|
+
sessionKey: this.sessionKey,
|
|
432
|
+
acpSessionId: this.sessionId,
|
|
433
|
+
systemPromptText: effectiveSystemPromptText ?? null,
|
|
434
|
+
});
|
|
435
|
+
await params.onPrepared?.({
|
|
436
|
+
sessionId: this.sessionId,
|
|
437
|
+
isFreshSession,
|
|
438
|
+
effectiveSystemPromptText,
|
|
439
|
+
effectiveContextText: effectiveContextText || undefined,
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
try {
|
|
443
|
+
let promptInput = inputQueue;
|
|
444
|
+
let promptInputQueue = inputQueue;
|
|
445
|
+
while (true) {
|
|
446
|
+
this.resetAssistantTextRepairTurnState();
|
|
447
|
+
this.currentTurnBigbangMessageSendFailures = [];
|
|
448
|
+
finalStopReason = 'completed';
|
|
449
|
+
finalError = null;
|
|
450
|
+
const activeQuery = this.queryImpl({
|
|
451
|
+
prompt: promptInput,
|
|
452
|
+
options: this.buildOptions(effectiveSystemPromptText, this.activeClaudeModeId, dreamModeRun),
|
|
453
|
+
});
|
|
454
|
+
this.activeQuery = activeQuery;
|
|
455
|
+
this.activeInputQueue = promptInputQueue;
|
|
456
|
+
this.activeSteerInputQueue = promptInputQueue;
|
|
457
|
+
let streamIdleTimer = null;
|
|
458
|
+
try {
|
|
459
|
+
const armIdleTimer = () => {
|
|
460
|
+
if (streamIdleTimer)
|
|
461
|
+
clearTimeout(streamIdleTimer);
|
|
462
|
+
streamIdleTimer = setTimeout(async () => {
|
|
463
|
+
this.streamIdleTimedOut = true;
|
|
464
|
+
if (this.activeQuery && typeof this.activeQuery.interrupt === 'function') {
|
|
465
|
+
await this.activeQuery.interrupt().catch(() => { });
|
|
466
|
+
}
|
|
467
|
+
}, CLAUDE_STREAM_IDLE_TIMEOUT_MS);
|
|
468
|
+
};
|
|
469
|
+
armIdleTimer();
|
|
470
|
+
for await (const message of activeQuery) {
|
|
471
|
+
armIdleTimer();
|
|
472
|
+
this.captureSessionId(message);
|
|
473
|
+
await syncPreparedSession();
|
|
474
|
+
await this.handleSdkMessage(message, params.sink);
|
|
475
|
+
if (dreamModeRun && this.dreamMemoryWriteObserved && this.dreamWatermarkObserved) {
|
|
476
|
+
this.dreamTurnCompleted = true;
|
|
477
|
+
if (this.activeQuery && typeof this.activeQuery.interrupt === 'function') {
|
|
478
|
+
await this.activeQuery.interrupt().catch(() => { });
|
|
479
|
+
}
|
|
480
|
+
finalStopReason = 'end_turn';
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
if (message.type === 'result') {
|
|
484
|
+
if (this.cancelRequested) {
|
|
485
|
+
finalStopReason = 'cancelled';
|
|
486
|
+
}
|
|
487
|
+
else if (message.subtype === 'success') {
|
|
488
|
+
finalStopReason = 'completed';
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
finalStopReason = 'failed';
|
|
492
|
+
finalError = readSdkResultError(message) ?? 'Claude SDK run failed';
|
|
493
|
+
if (this.isBenignPostFinalMessageAbort(finalError)) {
|
|
494
|
+
finalStopReason = 'completed';
|
|
495
|
+
finalError = null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
const errorMessage = String(error?.message ?? error);
|
|
503
|
+
if (this.dreamTurnCompleted) {
|
|
504
|
+
finalStopReason = 'end_turn';
|
|
505
|
+
finalError = null;
|
|
506
|
+
}
|
|
507
|
+
else if (this.streamIdleTimedOut) {
|
|
508
|
+
finalStopReason = 'failed';
|
|
509
|
+
finalError = `Model stream idle for ${CLAUDE_STREAM_IDLE_TIMEOUT_MS / 60_000} minutes with no events — possible hang`;
|
|
510
|
+
}
|
|
511
|
+
else if (this.cancelRequested) {
|
|
512
|
+
finalStopReason = 'cancelled';
|
|
513
|
+
}
|
|
514
|
+
else if (this.planApprovalDenied) {
|
|
515
|
+
finalStopReason = 'approval_denied';
|
|
516
|
+
}
|
|
517
|
+
else if (this.currentTurnAssistantTextRepairTriggered && isAbortLikeError(error)) {
|
|
518
|
+
finalStopReason = 'interrupted';
|
|
519
|
+
finalError = null;
|
|
520
|
+
}
|
|
521
|
+
else if (isAbortLikeError(error) && this.isBenignPostFinalMessageAbort(errorMessage)) {
|
|
522
|
+
finalStopReason = 'completed';
|
|
523
|
+
finalError = null;
|
|
524
|
+
}
|
|
525
|
+
else if (isAbortLikeError(error)) {
|
|
526
|
+
finalStopReason = 'cancelled';
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
finalStopReason = 'failed';
|
|
530
|
+
finalError = errorMessage;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
if (streamIdleTimer)
|
|
535
|
+
clearTimeout(streamIdleTimer);
|
|
536
|
+
this.streamIdleTimedOut = false;
|
|
537
|
+
this.releaseActiveTurnWorkspaceLock();
|
|
538
|
+
this.rejectPendingPermission('Permission request cancelled.');
|
|
539
|
+
await this.flushPendingToolCalls(params.sink, finalStopReason === 'cancelled' ? 'cancelled' : 'failed', finalError);
|
|
540
|
+
this.activeQuery = null;
|
|
541
|
+
if (promptInputQueue) {
|
|
542
|
+
promptInputQueue.close();
|
|
543
|
+
if (this.activeInputQueue === promptInputQueue) {
|
|
544
|
+
this.activeInputQueue = null;
|
|
545
|
+
}
|
|
546
|
+
if (this.activeSteerInputQueue === promptInputQueue) {
|
|
547
|
+
this.activeSteerInputQueue = null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
this.activeInputQueue = null;
|
|
552
|
+
this.activeSteerInputQueue = null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const repairText = !dreamModeRun && !this.cancelRequested && !this.deliveredFinalChatMessage
|
|
556
|
+
? this.getPendingAssistantTextRepairSource()
|
|
557
|
+
: null;
|
|
558
|
+
if (repairText) {
|
|
559
|
+
finalError = null;
|
|
560
|
+
if (!this.sessionId) {
|
|
561
|
+
finalStopReason = 'failed';
|
|
562
|
+
finalError = 'Claude SDK assistant text repair cannot continue without a session id.';
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
if (this.assistantTextRepairAttempts >= CLAUDE_ASSISTANT_TEXT_REPAIR_MAX_ATTEMPTS) {
|
|
566
|
+
finalStopReason = 'failed';
|
|
567
|
+
finalError = 'Claude SDK assistant text repair exceeded retry cap.';
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
this.assistantTextRepairAttempts += 1;
|
|
571
|
+
const repairQueue = new ClaudeInputQueue();
|
|
572
|
+
repairQueue.enqueue(this.toSdkUserMessage(this.buildAssistantTextRepairPrompt(repairText)));
|
|
573
|
+
promptInput = repairQueue;
|
|
574
|
+
promptInputQueue = repairQueue;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const dreamNeedsRepair = dreamModeRun
|
|
578
|
+
&& !this.cancelRequested
|
|
579
|
+
&& !finalError
|
|
580
|
+
&& (!this.dreamMemoryWriteObserved || !this.dreamWatermarkObserved);
|
|
581
|
+
if (dreamNeedsRepair && this.dreamMemoryRepairAttempts < DREAM_MEMORY_WRITE_REPAIR_MAX_ATTEMPTS) {
|
|
582
|
+
this.dreamMemoryRepairAttempts += 1;
|
|
583
|
+
const repairQueue = new ClaudeInputQueue();
|
|
584
|
+
const repairPrompt = !this.dreamMemoryWriteObserved
|
|
585
|
+
? buildDreamMemoryWriteRepairPrompt(this.activeDreamContextText ?? undefined)
|
|
586
|
+
: buildDreamWatermarkRepairPrompt(this.activeDreamContextText ?? undefined);
|
|
587
|
+
repairQueue.enqueue(this.toSdkUserMessage(repairPrompt));
|
|
588
|
+
promptInput = repairQueue;
|
|
589
|
+
promptInputQueue = repairQueue;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
if (dreamModeRun && !this.cancelRequested && !finalError) {
|
|
597
|
+
if (this.dreamMemoryWriteObserved && !this.dreamWatermarkObserved) {
|
|
598
|
+
const watermarkOk = await executeDreamWatermarkFallback({
|
|
599
|
+
env: this.env,
|
|
600
|
+
workspaceRoot: this.workspaceRoot,
|
|
601
|
+
contextText: this.activeDreamContextText,
|
|
602
|
+
sink: params.sink,
|
|
603
|
+
authToken: this.dreamCachedAuthToken,
|
|
604
|
+
});
|
|
605
|
+
if (watermarkOk) {
|
|
606
|
+
this.dreamWatermarkObserved = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else if (!this.dreamMemoryWriteObserved) {
|
|
610
|
+
const fallback = await executeDreamMemoryConsolidationFallback({
|
|
611
|
+
env: this.env,
|
|
612
|
+
workspaceRoot: this.workspaceRoot,
|
|
613
|
+
contextText: this.activeDreamContextText,
|
|
614
|
+
sink: params.sink,
|
|
615
|
+
authToken: this.dreamCachedAuthToken,
|
|
616
|
+
});
|
|
617
|
+
if (fallback.wroteMemory) {
|
|
618
|
+
this.dreamMemoryWriteObserved = true;
|
|
619
|
+
}
|
|
620
|
+
if (fallback.advancedWatermark) {
|
|
621
|
+
this.dreamWatermarkObserved = true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
this.activeRunId = null;
|
|
626
|
+
this.activeSink = null;
|
|
627
|
+
this.activeUiMode = 'summary';
|
|
628
|
+
if (dreamModeRun) {
|
|
629
|
+
this.sessionId = this.preservedChatSessionId;
|
|
630
|
+
this.sessionSystemPromptText = this.preservedChatSystemPromptText;
|
|
631
|
+
}
|
|
632
|
+
this.activeDreamMode = false;
|
|
633
|
+
this.dreamCachedAuthToken = null;
|
|
634
|
+
this.preservedChatSessionId = null;
|
|
635
|
+
this.preservedChatSystemPromptText = null;
|
|
636
|
+
this.activeClaudeModeId = null;
|
|
637
|
+
this.cancelRequested = false;
|
|
638
|
+
this.streamIdleTimedOut = false;
|
|
639
|
+
}
|
|
640
|
+
if (finalError && this.isBenignPostFinalMessageAbort(finalError)) {
|
|
641
|
+
finalStopReason = 'completed';
|
|
642
|
+
finalError = null;
|
|
643
|
+
}
|
|
644
|
+
if (this.planApprovalDenied) {
|
|
645
|
+
finalStopReason = 'approval_denied';
|
|
646
|
+
finalError = null;
|
|
647
|
+
}
|
|
648
|
+
if (dreamModeRun && !finalError && !this.deliveredFinalChatMessage) {
|
|
649
|
+
finalStopReason = 'end_turn';
|
|
650
|
+
}
|
|
651
|
+
if (finalError) {
|
|
652
|
+
this.deliveredFinalChatMessage = false;
|
|
653
|
+
this.deliveredProgressChatMessage = false;
|
|
654
|
+
this.receivedRuntimeOutput = false;
|
|
655
|
+
this.clearAssistantTextRepairState();
|
|
656
|
+
if (!dreamModeRun && isMissingResumeError(finalError)) {
|
|
657
|
+
this.sessionId = null;
|
|
658
|
+
clearAcpSessionId(this.db, this.sessionKey);
|
|
659
|
+
}
|
|
660
|
+
throw new Error(finalError);
|
|
661
|
+
}
|
|
662
|
+
if (dreamModeRun) {
|
|
663
|
+
this.deliveredFinalChatMessage = false;
|
|
664
|
+
this.deliveredProgressChatMessage = false;
|
|
665
|
+
this.receivedRuntimeOutput = false;
|
|
666
|
+
this.clearAssistantTextRepairState();
|
|
667
|
+
return {
|
|
668
|
+
stopReason: finalStopReason,
|
|
669
|
+
isFreshSession: !this.sessionId,
|
|
670
|
+
sessionId: this.sessionId ?? '',
|
|
671
|
+
effectiveSystemPromptText,
|
|
672
|
+
effectiveContextText: effectiveContextText || undefined,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
if (!this.sessionId) {
|
|
676
|
+
this.deliveredFinalChatMessage = false;
|
|
677
|
+
this.deliveredProgressChatMessage = false;
|
|
678
|
+
this.receivedRuntimeOutput = false;
|
|
679
|
+
this.clearAssistantTextRepairState();
|
|
680
|
+
throw new Error('Claude SDK run completed without a session id.');
|
|
681
|
+
}
|
|
682
|
+
await syncPreparedSession();
|
|
683
|
+
if (finalStopReason === 'completed' && sdkMessage.uuid) {
|
|
684
|
+
insertClaudeSessionUserMessage(this.db, {
|
|
685
|
+
sessionKey: this.sessionKey,
|
|
686
|
+
runId: params.runId,
|
|
687
|
+
messageUuid: sdkMessage.uuid,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
this.deliveredFinalChatMessage = false;
|
|
691
|
+
this.deliveredProgressChatMessage = false;
|
|
692
|
+
this.receivedRuntimeOutput = false;
|
|
693
|
+
this.clearAssistantTextRepairState();
|
|
694
|
+
return {
|
|
695
|
+
stopReason: finalStopReason,
|
|
696
|
+
isFreshSession,
|
|
697
|
+
sessionId: this.sessionId,
|
|
698
|
+
effectiveSystemPromptText,
|
|
699
|
+
effectiveContextText: effectiveContextText || undefined,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
buildOptions(systemPromptText, modeId = this.claudeModeId, dreamMode = false) {
|
|
703
|
+
const effectiveSystemPromptText = systemPromptText?.trim() || this.sessionSystemPromptText || undefined;
|
|
704
|
+
const effectiveEnv = {
|
|
705
|
+
...process.env,
|
|
706
|
+
...this.env,
|
|
707
|
+
};
|
|
708
|
+
const effectiveModelId = this.resolveEffectiveModelId(effectiveEnv);
|
|
709
|
+
const effectiveDisabledToolKinds = dreamMode
|
|
710
|
+
? this.disabledToolKinds.filter((kind) => kind !== 'execute')
|
|
711
|
+
: this.disabledToolKinds;
|
|
712
|
+
const disallowedTools = [
|
|
713
|
+
...buildClaudeDisallowedTools(effectiveDisabledToolKinds),
|
|
714
|
+
...(dreamMode ? buildDreamAdditionalDisallowedTools() : []),
|
|
715
|
+
];
|
|
716
|
+
const base = {
|
|
717
|
+
cwd: this.workspaceRoot,
|
|
718
|
+
permissionMode: modeId,
|
|
719
|
+
...(modeId === 'bypassPermissions' ? { allowDangerouslySkipPermissions: true } : {}),
|
|
720
|
+
canUseTool: this.handlePermissionRequest,
|
|
721
|
+
...(disallowedTools.length > 0 ? { disallowedTools } : {}),
|
|
722
|
+
settingSources: [...CLAUDE_SETTING_SOURCES],
|
|
723
|
+
systemPrompt: effectiveSystemPromptText
|
|
724
|
+
? { type: 'preset', preset: 'claude_code', append: effectiveSystemPromptText }
|
|
725
|
+
: { type: 'preset', preset: 'claude_code' },
|
|
726
|
+
env: {
|
|
727
|
+
...effectiveEnv,
|
|
728
|
+
MCP_TIMEOUT: '600000',
|
|
729
|
+
MCP_TOOL_TIMEOUT: '600000',
|
|
730
|
+
},
|
|
731
|
+
enableFileCheckpointing: true,
|
|
732
|
+
...(effectiveModelId ? { model: effectiveModelId } : {}),
|
|
733
|
+
...(this.agentReasoningEffort ? { effort: this.agentReasoningEffort } : {}),
|
|
734
|
+
...(this.sessionId && !dreamMode ? { resume: this.sessionId } : {}),
|
|
735
|
+
};
|
|
736
|
+
if (this.channelBridgeMcpEntry) {
|
|
737
|
+
const normalized = toClaudeSdkMcpConfig(this.channelBridgeMcpEntry);
|
|
738
|
+
if (normalized) {
|
|
739
|
+
base.mcpServers = {
|
|
740
|
+
[this.channelBridgeMcpEntry.name]: normalized,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return base;
|
|
745
|
+
}
|
|
746
|
+
resolveEffectiveModelId(env) {
|
|
747
|
+
const envModel = normalizeOptionalString(env.ANTHROPIC_MODEL);
|
|
748
|
+
if (envModel)
|
|
749
|
+
return envModel;
|
|
750
|
+
if (normalizeOptionalString(env.ANTHROPIC_DEFAULT_OPUS_MODEL)
|
|
751
|
+
|| normalizeOptionalString(env.ANTHROPIC_DEFAULT_SONNET_MODEL)
|
|
752
|
+
|| normalizeOptionalString(env.ANTHROPIC_DEFAULT_HAIKU_MODEL)) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
if (this.useAgentRuntimeConfig) {
|
|
756
|
+
return this.agentModelId;
|
|
757
|
+
}
|
|
758
|
+
return this.agentModelId ?? this.claudeModelId;
|
|
759
|
+
}
|
|
760
|
+
resolveDisplayedEffectiveModelId(env) {
|
|
761
|
+
return normalizeOptionalString(env.ANTHROPIC_MODEL)
|
|
762
|
+
?? normalizeOptionalString(env.ANTHROPIC_DEFAULT_OPUS_MODEL)
|
|
763
|
+
?? normalizeOptionalString(env.ANTHROPIC_DEFAULT_SONNET_MODEL)
|
|
764
|
+
?? normalizeOptionalString(env.ANTHROPIC_DEFAULT_HAIKU_MODEL)
|
|
765
|
+
?? this.resolveEffectiveModelId(env);
|
|
766
|
+
}
|
|
767
|
+
getEffectivePermissionMode() {
|
|
768
|
+
return 'bypassPermissions';
|
|
769
|
+
}
|
|
770
|
+
handlePermissionRequest = async (toolName, input, options) => {
|
|
771
|
+
const toolKind = resolveClaudeToolKind(toolName);
|
|
772
|
+
const effectiveModeId = this.activeClaudeModeId ?? this.claudeModeId;
|
|
773
|
+
const isExitPlanRequest = isClaudePlanApprovalTool(toolName, toolKind);
|
|
774
|
+
const isPlanExitRequest = effectiveModeId === 'plan' && isExitPlanRequest;
|
|
775
|
+
const planText = isPlanExitRequest ? extractClaudePlanText(input) : null;
|
|
776
|
+
const isPlanApprovalRequest = isPlanExitRequest && Boolean(planText);
|
|
777
|
+
const permissionInput = isExitPlanRequest && !isPlanApprovalRequest
|
|
778
|
+
? stripClaudeExitPlanAllowedPrompts(input)
|
|
779
|
+
: input;
|
|
780
|
+
if (isClaudePlatformChatTool(toolName, this.channelBridgeMcpEntry?.name)) {
|
|
781
|
+
if (this.activeDreamMode) {
|
|
782
|
+
return {
|
|
783
|
+
behavior: 'deny',
|
|
784
|
+
message: 'Dream mode cannot send chat messages.',
|
|
785
|
+
toolUseID: options.toolUseID,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
behavior: 'allow',
|
|
790
|
+
updatedInput: permissionInput,
|
|
791
|
+
toolUseID: options.toolUseID,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
if (this.activeDreamMode) {
|
|
795
|
+
const dreamDecision = evaluateDreamToolPermission({
|
|
796
|
+
toolName,
|
|
797
|
+
input: permissionInput,
|
|
798
|
+
toolKind,
|
|
799
|
+
});
|
|
800
|
+
if (dreamDecision === 'deny_blocked_memory') {
|
|
801
|
+
return {
|
|
802
|
+
behavior: 'deny',
|
|
803
|
+
message: 'Dream mode blocks unrelated memory MCP tools; use bigbang memory * shell commands.',
|
|
804
|
+
toolUseID: options.toolUseID,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
if (dreamDecision === 'deny_memory_read') {
|
|
808
|
+
return {
|
|
809
|
+
behavior: 'deny',
|
|
810
|
+
message: 'Dream mode blocks bigbang memory list/search; create or update draft nodes directly from the provided new messages.',
|
|
811
|
+
toolUseID: options.toolUseID,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
if (dreamDecision === 'allow') {
|
|
815
|
+
return {
|
|
816
|
+
behavior: 'allow',
|
|
817
|
+
updatedInput: permissionInput,
|
|
818
|
+
toolUseID: options.toolUseID,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
if (dreamDecision === 'deny_execute') {
|
|
822
|
+
return {
|
|
823
|
+
behavior: 'deny',
|
|
824
|
+
message: 'Dream mode only allows bigbang memory write shell commands via Bash.',
|
|
825
|
+
toolUseID: options.toolUseID,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
behavior: 'deny',
|
|
830
|
+
message: 'Dream mode blocks exploration tools; use Bash with bigbang memory node create/update/edge create or dream-watermark.',
|
|
831
|
+
toolUseID: options.toolUseID,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
if (toolKind && this.disabledToolKinds.includes(toolKind)) {
|
|
835
|
+
return {
|
|
836
|
+
behavior: 'deny',
|
|
837
|
+
message: `Tool kind disabled by platform policy: ${toolKind}`,
|
|
838
|
+
toolUseID: options.toolUseID,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
const targetsWorkspace = Boolean(toolKind) && claudeToolTargetsWorkspace(toolKind, permissionInput, this.workspaceRoot);
|
|
842
|
+
const needsWorkspaceWriteLock = Boolean(toolKind) && CLAUDE_WORKSPACE_WRITE_TOOL_KINDS.has(toolKind) && targetsWorkspace;
|
|
843
|
+
const needsWorkspaceReadBarrier = Boolean(toolKind) && CLAUDE_WORKSPACE_READ_TOOL_KINDS.has(toolKind) && targetsWorkspace;
|
|
844
|
+
// Check ToolAuth persistent policy before interactive permission flow
|
|
845
|
+
if (this.toolAuth && this.bindingKey && toolKind) {
|
|
846
|
+
const context = buildClaudeToolMatchContext(toolName, permissionInput, toolKind, this.workspaceRoot);
|
|
847
|
+
const policy = this.toolAuth.evaluatePersistentPolicy(this.bindingKey, toolKind, context);
|
|
848
|
+
if (policy === 'allow') {
|
|
849
|
+
if (needsWorkspaceWriteLock) {
|
|
850
|
+
try {
|
|
851
|
+
await this.ensureActiveTurnWorkspaceWriteLock();
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
return {
|
|
855
|
+
behavior: 'deny',
|
|
856
|
+
message: `Workspace lock unavailable: ${String(error?.message ?? error)}`,
|
|
857
|
+
interrupt: true,
|
|
858
|
+
toolUseID: options.toolUseID,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
else if (needsWorkspaceReadBarrier) {
|
|
863
|
+
try {
|
|
864
|
+
await this.waitForWorkspaceWritesBeforeRead();
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
return {
|
|
868
|
+
behavior: 'deny',
|
|
869
|
+
message: `Workspace lock unavailable: ${String(error?.message ?? error)}`,
|
|
870
|
+
interrupt: true,
|
|
871
|
+
toolUseID: options.toolUseID,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
behavior: 'allow',
|
|
877
|
+
updatedInput: permissionInput,
|
|
878
|
+
toolUseID: options.toolUseID,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
if (policy === 'reject') {
|
|
882
|
+
return {
|
|
883
|
+
behavior: 'deny',
|
|
884
|
+
message: `Tool rejected by persistent policy: ${toolKind}`,
|
|
885
|
+
toolUseID: options.toolUseID,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const sink = this.activeSink;
|
|
890
|
+
const requestPermission = sink?.requestPermission;
|
|
891
|
+
if (!requestPermission) {
|
|
892
|
+
return {
|
|
893
|
+
behavior: 'deny',
|
|
894
|
+
message: 'Permission UI unavailable.',
|
|
895
|
+
toolUseID: options.toolUseID,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
if (this.pendingPermission) {
|
|
899
|
+
return {
|
|
900
|
+
behavior: 'deny',
|
|
901
|
+
message: 'Another permission request is already pending.',
|
|
902
|
+
interrupt: true,
|
|
903
|
+
toolUseID: options.toolUseID,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const requestId = options.toolUseID || `perm-${randomUUID()}`;
|
|
907
|
+
const toolTitle = options.title || options.displayName || toolName;
|
|
908
|
+
return await new Promise((resolve) => {
|
|
909
|
+
let settled = false;
|
|
910
|
+
const settle = (result) => {
|
|
911
|
+
if (settled)
|
|
912
|
+
return;
|
|
913
|
+
settled = true;
|
|
914
|
+
this.pendingPermission = null;
|
|
915
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
916
|
+
resolve(result);
|
|
917
|
+
};
|
|
918
|
+
const onAbort = () => {
|
|
919
|
+
settle({
|
|
920
|
+
behavior: 'deny',
|
|
921
|
+
message: 'Permission request cancelled.',
|
|
922
|
+
interrupt: true,
|
|
923
|
+
toolUseID: options.toolUseID,
|
|
924
|
+
});
|
|
925
|
+
};
|
|
926
|
+
this.pendingPermission = {
|
|
927
|
+
requestId,
|
|
928
|
+
kind: isPlanApprovalRequest ? 'plan' : 'tool',
|
|
929
|
+
settleAllow: () => {
|
|
930
|
+
if (!needsWorkspaceWriteLock && !needsWorkspaceReadBarrier) {
|
|
931
|
+
settle({
|
|
932
|
+
behavior: 'allow',
|
|
933
|
+
updatedInput: permissionInput,
|
|
934
|
+
toolUseID: options.toolUseID,
|
|
935
|
+
});
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const lockAction = needsWorkspaceWriteLock
|
|
939
|
+
? this.ensureActiveTurnWorkspaceWriteLock()
|
|
940
|
+
: this.waitForWorkspaceWritesBeforeRead();
|
|
941
|
+
lockAction.then(() => {
|
|
942
|
+
settle({
|
|
943
|
+
behavior: 'allow',
|
|
944
|
+
updatedInput: permissionInput,
|
|
945
|
+
toolUseID: options.toolUseID,
|
|
946
|
+
});
|
|
947
|
+
}).catch((error) => {
|
|
948
|
+
settle({
|
|
949
|
+
behavior: 'deny',
|
|
950
|
+
message: `Workspace lock unavailable: ${String(error?.message ?? error)}`,
|
|
951
|
+
interrupt: true,
|
|
952
|
+
toolUseID: options.toolUseID,
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
},
|
|
956
|
+
settleDeny: (message, interrupt = false) => {
|
|
957
|
+
settle({
|
|
958
|
+
behavior: 'deny',
|
|
959
|
+
message,
|
|
960
|
+
interrupt,
|
|
961
|
+
toolUseID: options.toolUseID,
|
|
962
|
+
});
|
|
963
|
+
},
|
|
964
|
+
};
|
|
965
|
+
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
966
|
+
void requestPermission({
|
|
967
|
+
uiMode: this.activeUiMode,
|
|
968
|
+
sessionKey: this.sessionKey,
|
|
969
|
+
requestId,
|
|
970
|
+
toolTitle: isPlanApprovalRequest ? 'Claude plan approval' : toolTitle,
|
|
971
|
+
toolKind,
|
|
972
|
+
toolName: isPlanApprovalRequest ? 'plan' : toolTitle,
|
|
973
|
+
toolArgs: isPlanApprovalRequest ? { plan: planText ?? '', rawInput: input } : buildPermissionArgs(permissionInput, options),
|
|
974
|
+
...(isPlanApprovalRequest
|
|
975
|
+
? {
|
|
976
|
+
approvalKind: 'plan',
|
|
977
|
+
title: 'Plan',
|
|
978
|
+
description: 'Review the proposed plan before implementation starts.',
|
|
979
|
+
input: { plan: planText ?? '' },
|
|
980
|
+
actions: buildPlanApprovalActions(),
|
|
981
|
+
}
|
|
982
|
+
: {}),
|
|
983
|
+
}).catch((error) => {
|
|
984
|
+
settle({
|
|
985
|
+
behavior: 'deny',
|
|
986
|
+
message: String(error?.message ?? error ?? 'Failed to deliver permission request.'),
|
|
987
|
+
interrupt: true,
|
|
988
|
+
toolUseID: options.toolUseID,
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
});
|
|
992
|
+
};
|
|
993
|
+
shouldEnforceToolReplyContract() {
|
|
994
|
+
if (this.activeDreamMode)
|
|
995
|
+
return false;
|
|
996
|
+
return this.agentSurfaceMode === 'bigbang'
|
|
997
|
+
|| (this.agentSurfaceMode === 'mcp' && Boolean(this.channelBridgeMcpEntry));
|
|
998
|
+
}
|
|
999
|
+
getPlatformSendMessageToolName() {
|
|
1000
|
+
const serverName = this.channelBridgeMcpEntry?.name?.trim() || 'chat';
|
|
1001
|
+
return `mcp__${serverName}__send_message`;
|
|
1002
|
+
}
|
|
1003
|
+
buildCurrentTurnBigbangRepairGuidance() {
|
|
1004
|
+
if (this.agentSurfaceMode !== 'bigbang')
|
|
1005
|
+
return null;
|
|
1006
|
+
const currentFailures = this.currentTurnBigbangMessageSendFailures;
|
|
1007
|
+
const latestFinalFailure = [...currentFailures].reverse().find((failure) => failure.messageKind === 'final');
|
|
1008
|
+
const selectedFailure = latestFinalFailure ?? currentFailures.at(-1) ?? this.lastBigbangMessageSendFailureForRepair;
|
|
1009
|
+
if (!selectedFailure)
|
|
1010
|
+
return null;
|
|
1011
|
+
this.lastBigbangMessageSendFailureForRepair = selectedFailure;
|
|
1012
|
+
return buildBigbangMessageSendRepairGuidance(selectedFailure.failureKind);
|
|
1013
|
+
}
|
|
1014
|
+
buildAssistantTextRepairPrompt(assistantText) {
|
|
1015
|
+
if (this.agentSurfaceMode === 'bigbang') {
|
|
1016
|
+
return [
|
|
1017
|
+
CLAUDE_BIGBANG_ASSISTANT_TEXT_REPAIR_PROMPT,
|
|
1018
|
+
this.deliveredProgressChatMessage ? CLAUDE_BIGBANG_PROGRESS_ALREADY_DELIVERED_FINAL_GUIDANCE : null,
|
|
1019
|
+
this.buildCurrentTurnBigbangRepairGuidance(),
|
|
1020
|
+
'',
|
|
1021
|
+
'Previous plain assistant text:',
|
|
1022
|
+
'',
|
|
1023
|
+
assistantText,
|
|
1024
|
+
].filter((line) => typeof line === 'string').join('\n');
|
|
1025
|
+
}
|
|
1026
|
+
const toolName = this.getPlatformSendMessageToolName();
|
|
1027
|
+
return [
|
|
1028
|
+
CLAUDE_ASSISTANT_TEXT_REPAIR_PROMPT.replaceAll('{toolName}', toolName),
|
|
1029
|
+
'',
|
|
1030
|
+
'Previous plain assistant text:',
|
|
1031
|
+
'',
|
|
1032
|
+
assistantText,
|
|
1033
|
+
].join('\n');
|
|
1034
|
+
}
|
|
1035
|
+
resetAssistantTextRepairTurnState() {
|
|
1036
|
+
this.currentTurnAssistantTextBuffer = '';
|
|
1037
|
+
this.currentTurnAssistantTextRepairTriggered = false;
|
|
1038
|
+
}
|
|
1039
|
+
clearAssistantTextRepairState() {
|
|
1040
|
+
this.assistantTextRepairAttempts = 0;
|
|
1041
|
+
this.assistantTextRepairSourceText = null;
|
|
1042
|
+
this.lastBigbangMessageSendFailureForRepair = null;
|
|
1043
|
+
this.resetAssistantTextRepairTurnState();
|
|
1044
|
+
}
|
|
1045
|
+
getPendingAssistantTextRepairSource() {
|
|
1046
|
+
if (this.currentTurnAssistantTextRepairTriggered) {
|
|
1047
|
+
return normalizeOptionalString(this.currentTurnAssistantTextBuffer)
|
|
1048
|
+
?? normalizeOptionalString(this.assistantTextRepairSourceText);
|
|
1049
|
+
}
|
|
1050
|
+
if (this.agentSurfaceMode === 'bigbang' && this.currentTurnBigbangMessageSendFailures.length > 0) {
|
|
1051
|
+
return '[No plain assistant text was captured; send the final user-visible result using the current conversation context.]';
|
|
1052
|
+
}
|
|
1053
|
+
if (this.agentSurfaceMode === 'bigbang'
|
|
1054
|
+
&& this.activeClaudeModeId !== 'plan'
|
|
1055
|
+
&& !this.deliveredFinalChatMessage) {
|
|
1056
|
+
return '[No final user-visible reply was delivered; send the final result using the current conversation context.]';
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
async handleAssistantText(text, sink) {
|
|
1061
|
+
if (this.activeDreamMode) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (!this.shouldEnforceToolReplyContract()) {
|
|
1065
|
+
this.receivedRuntimeOutput = true;
|
|
1066
|
+
await sink.sendText(text);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
this.receivedRuntimeOutput = true;
|
|
1070
|
+
await sink.sendActivityText?.(text);
|
|
1071
|
+
if (this.deliveredFinalChatMessage) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (text.trim().length === 0) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
this.currentTurnAssistantTextBuffer += text;
|
|
1078
|
+
this.assistantTextRepairSourceText = this.currentTurnAssistantTextBuffer;
|
|
1079
|
+
if (!this.currentTurnAssistantTextRepairTriggered) {
|
|
1080
|
+
this.currentTurnAssistantTextRepairTriggered = true;
|
|
1081
|
+
this.activeInputQueue?.close();
|
|
1082
|
+
await this.activeQuery?.interrupt().catch(() => { });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async handleSdkMessage(message, sink) {
|
|
1086
|
+
if (message.type === 'assistant') {
|
|
1087
|
+
const assistantMessage = message;
|
|
1088
|
+
const blocks = Array.isArray(assistantMessage.message?.content) ? assistantMessage.message?.content : [];
|
|
1089
|
+
let sawFinalChatToolUseInAssistantMessage = false;
|
|
1090
|
+
for (const block of blocks) {
|
|
1091
|
+
if (!block || typeof block !== 'object')
|
|
1092
|
+
continue;
|
|
1093
|
+
const typed = block;
|
|
1094
|
+
if (typed.type === 'text' && typeof typed.text === 'string' && typed.text.length > 0) {
|
|
1095
|
+
if (sawFinalChatToolUseInAssistantMessage) {
|
|
1096
|
+
this.receivedRuntimeOutput = true;
|
|
1097
|
+
await sink.sendActivityText?.(typed.text);
|
|
1098
|
+
if (typed.text.trim().length > 0) {
|
|
1099
|
+
this.currentTurnAssistantTextBuffer += typed.text;
|
|
1100
|
+
this.assistantTextRepairSourceText = this.currentTurnAssistantTextBuffer;
|
|
1101
|
+
this.currentTurnAssistantTextRepairTriggered = true;
|
|
1102
|
+
}
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
await this.handleAssistantText(typed.text, sink);
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if ((typed.type === 'thinking' || typed.type === 'redacted_thinking') && typeof typed.thinking === 'string') {
|
|
1109
|
+
this.receivedRuntimeOutput = true;
|
|
1110
|
+
await sink.sendThinkingText?.(typed.thinking);
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (typed.type === 'tool_use') {
|
|
1114
|
+
const name = typeof typed.name === 'string' ? typed.name : 'tool';
|
|
1115
|
+
const input = typed.input ?? null;
|
|
1116
|
+
const bigbangMessageSend = getClaudeBigbangMessageSendInfo(name, input);
|
|
1117
|
+
if (this.deliveredFinalChatMessage || sawFinalChatToolUseInAssistantMessage) {
|
|
1118
|
+
const toolCallId = typeof typed.id === 'string' ? typed.id : null;
|
|
1119
|
+
if (toolCallId) {
|
|
1120
|
+
this.suppressedToolResultIds.add(toolCallId);
|
|
1121
|
+
}
|
|
1122
|
+
this.receivedRuntimeOutput = true;
|
|
1123
|
+
await sink.sendActivityText?.(`Ignored post-final Claude tool use: ${name}`);
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
this.receivedRuntimeOutput = true;
|
|
1127
|
+
const toolCallId = typeof typed.id === 'string' ? typed.id : `tool-${randomUUID()}`;
|
|
1128
|
+
if ((this.agentSurfaceMode === 'mcp' && name === this.getPlatformSendMessageToolName() && isFinalChatSendMessageInput(input))
|
|
1129
|
+
|| (this.agentSurfaceMode === 'bigbang' && bigbangMessageSend?.kind === 'final')) {
|
|
1130
|
+
sawFinalChatToolUseInAssistantMessage = true;
|
|
1131
|
+
}
|
|
1132
|
+
this.pendingToolCalls.set(toolCallId, { name, input });
|
|
1133
|
+
await sink.sendUi?.({
|
|
1134
|
+
kind: 'tool',
|
|
1135
|
+
mode: 'summary',
|
|
1136
|
+
title: name,
|
|
1137
|
+
input,
|
|
1138
|
+
toolCallId,
|
|
1139
|
+
stage: 'start',
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (message.type === 'user') {
|
|
1146
|
+
const userMessage = message;
|
|
1147
|
+
const blocks = Array.isArray(userMessage.message?.content) ? userMessage.message?.content : [];
|
|
1148
|
+
for (const block of blocks) {
|
|
1149
|
+
if (!block || typeof block !== 'object')
|
|
1150
|
+
continue;
|
|
1151
|
+
const typed = block;
|
|
1152
|
+
if (typed.type !== 'tool_result')
|
|
1153
|
+
continue;
|
|
1154
|
+
const toolCallId = typeof typed.tool_use_id === 'string' ? typed.tool_use_id : null;
|
|
1155
|
+
if (!toolCallId)
|
|
1156
|
+
continue;
|
|
1157
|
+
if (this.suppressedToolResultIds.has(toolCallId)) {
|
|
1158
|
+
this.suppressedToolResultIds.delete(toolCallId);
|
|
1159
|
+
this.receivedRuntimeOutput = true;
|
|
1160
|
+
await sink.sendActivityText?.(`Ignored post-final Claude tool result: ${toolCallId}`);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
const existing = this.pendingToolCalls.get(toolCallId);
|
|
1164
|
+
if (this.deliveredFinalChatMessage) {
|
|
1165
|
+
this.receivedRuntimeOutput = true;
|
|
1166
|
+
await sink.sendActivityText?.(`Ignored post-final Claude tool result: ${toolCallId}`);
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const title = existing?.name ?? 'tool';
|
|
1170
|
+
const output = extractToolResultText(typed.content);
|
|
1171
|
+
const status = typed.is_error === true ? 'failed' : 'completed';
|
|
1172
|
+
this.receivedRuntimeOutput = true;
|
|
1173
|
+
if (this.activeDreamMode && status === 'completed') {
|
|
1174
|
+
if (isDreamMemoryWriteTool(title, existing?.input)) {
|
|
1175
|
+
this.dreamMemoryWriteObserved = true;
|
|
1176
|
+
}
|
|
1177
|
+
if (isDreamMemoryWatermarkTool(title, existing?.input)) {
|
|
1178
|
+
this.dreamWatermarkObserved = true;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const bigbangMessageSend = existing
|
|
1182
|
+
? getClaudeBigbangMessageSendInfo(existing.name, existing.input, output, {
|
|
1183
|
+
status,
|
|
1184
|
+
success: status === 'completed',
|
|
1185
|
+
})
|
|
1186
|
+
: null;
|
|
1187
|
+
if (this.agentSurfaceMode === 'bigbang') {
|
|
1188
|
+
if (bigbangMessageSend?.success === true) {
|
|
1189
|
+
this.lastBigbangMessageSendFailureForRepair = null;
|
|
1190
|
+
if (bigbangMessageSend.kind === 'progress') {
|
|
1191
|
+
this.deliveredProgressChatMessage = true;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
else if (bigbangMessageSend?.failureKind) {
|
|
1195
|
+
this.currentTurnBigbangMessageSendFailures.push({
|
|
1196
|
+
messageKind: bigbangMessageSend.kind,
|
|
1197
|
+
failureKind: bigbangMessageSend.failureKind,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const nonPostedResult = isNonPostedSendMessageResult(typed.content)
|
|
1202
|
+
|| isNonPostedSendMessageResult(output);
|
|
1203
|
+
const deliveredFinalReply = status === 'completed'
|
|
1204
|
+
&& !nonPostedResult
|
|
1205
|
+
&& ((this.agentSurfaceMode === 'mcp'
|
|
1206
|
+
&& title === this.getPlatformSendMessageToolName()
|
|
1207
|
+
&& isFinalChatSendMessageInput(existing?.input))
|
|
1208
|
+
|| (this.agentSurfaceMode === 'bigbang'
|
|
1209
|
+
&& bigbangMessageSend?.success === true
|
|
1210
|
+
&& bigbangMessageSend.kind === 'final'));
|
|
1211
|
+
if (deliveredFinalReply) {
|
|
1212
|
+
this.deliveredFinalChatMessage = true;
|
|
1213
|
+
}
|
|
1214
|
+
await sink.sendUi?.({
|
|
1215
|
+
kind: 'tool',
|
|
1216
|
+
mode: 'summary',
|
|
1217
|
+
title,
|
|
1218
|
+
output,
|
|
1219
|
+
toolCallId,
|
|
1220
|
+
stage: 'complete',
|
|
1221
|
+
status,
|
|
1222
|
+
});
|
|
1223
|
+
this.pendingToolCalls.delete(toolCallId);
|
|
1224
|
+
if (deliveredFinalReply) {
|
|
1225
|
+
this.pendingToolCalls.clear();
|
|
1226
|
+
this.activeInputQueue?.close();
|
|
1227
|
+
await this.activeQuery?.interrupt().catch(() => { });
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (message.type === 'system') {
|
|
1233
|
+
const typed = message;
|
|
1234
|
+
switch (typed.subtype) {
|
|
1235
|
+
case 'status': {
|
|
1236
|
+
const detail = describeClaudeStatus(typed);
|
|
1237
|
+
if (detail) {
|
|
1238
|
+
await sink.sendUi?.({
|
|
1239
|
+
kind: 'plan',
|
|
1240
|
+
mode: 'summary',
|
|
1241
|
+
title: 'Claude status',
|
|
1242
|
+
detail,
|
|
1243
|
+
silent: true,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
case 'notification': {
|
|
1249
|
+
if (typeof typed.text === 'string' && typed.text.trim()) {
|
|
1250
|
+
await sink.sendUi?.({
|
|
1251
|
+
kind: 'plan',
|
|
1252
|
+
mode: 'summary',
|
|
1253
|
+
title: 'Notification',
|
|
1254
|
+
detail: typed.text,
|
|
1255
|
+
silent: true,
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
case 'task_started': {
|
|
1261
|
+
await sink.sendUi?.({
|
|
1262
|
+
kind: 'task',
|
|
1263
|
+
mode: 'summary',
|
|
1264
|
+
title: 'Task started',
|
|
1265
|
+
detail: typeof typed.description === 'string' ? typed.description : undefined,
|
|
1266
|
+
silent: true,
|
|
1267
|
+
});
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
case 'task_progress': {
|
|
1271
|
+
const detail = typeof typed.description === 'string'
|
|
1272
|
+
? typed.description
|
|
1273
|
+
: typeof typed.summary === 'string'
|
|
1274
|
+
? typed.summary
|
|
1275
|
+
: undefined;
|
|
1276
|
+
await sink.sendUi?.({
|
|
1277
|
+
kind: 'task',
|
|
1278
|
+
mode: 'summary',
|
|
1279
|
+
title: 'Task progress',
|
|
1280
|
+
detail,
|
|
1281
|
+
silent: true,
|
|
1282
|
+
});
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
case 'task_updated': {
|
|
1286
|
+
const detail = formatClaudeTaskPatch(typed.patch);
|
|
1287
|
+
await sink.sendUi?.({
|
|
1288
|
+
kind: 'task',
|
|
1289
|
+
mode: 'summary',
|
|
1290
|
+
title: 'Task updated',
|
|
1291
|
+
detail,
|
|
1292
|
+
silent: true,
|
|
1293
|
+
});
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
case 'task_notification': {
|
|
1297
|
+
await sink.sendUi?.({
|
|
1298
|
+
kind: 'task',
|
|
1299
|
+
mode: 'summary',
|
|
1300
|
+
title: `Task ${String(typed.status ?? 'updated')}`,
|
|
1301
|
+
detail: typeof typed.summary === 'string' ? typed.summary : undefined,
|
|
1302
|
+
silent: true,
|
|
1303
|
+
});
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
default:
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (message.type === 'tool_progress') {
|
|
1311
|
+
const typed = message;
|
|
1312
|
+
const toolName = typeof typed.tool_name === 'string' ? typed.tool_name : 'tool';
|
|
1313
|
+
const elapsed = typeof typed.elapsed_time_seconds === 'number'
|
|
1314
|
+
? `${typed.elapsed_time_seconds.toFixed(1)}s elapsed`
|
|
1315
|
+
: undefined;
|
|
1316
|
+
await sink.sendUi?.({
|
|
1317
|
+
kind: 'task',
|
|
1318
|
+
mode: 'summary',
|
|
1319
|
+
title: `${toolName} running`,
|
|
1320
|
+
detail: elapsed,
|
|
1321
|
+
silent: true,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
isBenignPostFinalMessageAbort(message) {
|
|
1326
|
+
return this.deliveredFinalChatMessage
|
|
1327
|
+
&& (isClaudeSdkPostFinalMessageAbort(message) || isAbortLikeError(message));
|
|
1328
|
+
}
|
|
1329
|
+
rejectPendingPermission(message) {
|
|
1330
|
+
const pending = this.pendingPermission;
|
|
1331
|
+
if (!pending)
|
|
1332
|
+
return;
|
|
1333
|
+
pending.settleDeny(message, true);
|
|
1334
|
+
}
|
|
1335
|
+
sendPlanPhaseBestEffort(phase) {
|
|
1336
|
+
const sink = this.activeSink;
|
|
1337
|
+
if (!sink?.sendUi)
|
|
1338
|
+
return;
|
|
1339
|
+
try {
|
|
1340
|
+
void sink.sendUi({
|
|
1341
|
+
kind: 'plan_phase',
|
|
1342
|
+
mode: this.activeUiMode,
|
|
1343
|
+
phase,
|
|
1344
|
+
}).catch((error) => {
|
|
1345
|
+
log.warn('[claude-sdk] failed to send plan phase event', {
|
|
1346
|
+
phase,
|
|
1347
|
+
error: String(error?.message ?? error),
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
log.warn('[claude-sdk] failed to send plan phase event', {
|
|
1353
|
+
phase,
|
|
1354
|
+
error: String(error?.message ?? error),
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
/** Acquire the workspace write lock the first time this turn requests a write
|
|
1359
|
+
* tool. Subsequent writes within the same turn reuse the same lease. The
|
|
1360
|
+
* lease is released in the prompt() finally block. Mirrors the Codex App
|
|
1361
|
+
* Server pattern.
|
|
1362
|
+
*/
|
|
1363
|
+
async ensureActiveTurnWorkspaceWriteLock() {
|
|
1364
|
+
if (!this.workspaceLockManager)
|
|
1365
|
+
return;
|
|
1366
|
+
if (this.activeTurnWorkspaceLease)
|
|
1367
|
+
return;
|
|
1368
|
+
if (this.activeTurnWorkspaceLeasePromise) {
|
|
1369
|
+
await this.activeTurnWorkspaceLeasePromise;
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const abortController = new AbortController();
|
|
1373
|
+
this.workspaceLockAbortController = abortController;
|
|
1374
|
+
const acquirePromise = (async () => {
|
|
1375
|
+
const lease = await this.workspaceLockManager.acquire(this.workspaceRoot, {
|
|
1376
|
+
signal: abortController.signal,
|
|
1377
|
+
});
|
|
1378
|
+
this.activeTurnWorkspaceLease = lease;
|
|
1379
|
+
})();
|
|
1380
|
+
this.activeTurnWorkspaceLeasePromise = acquirePromise;
|
|
1381
|
+
try {
|
|
1382
|
+
await acquirePromise;
|
|
1383
|
+
}
|
|
1384
|
+
finally {
|
|
1385
|
+
if (this.workspaceLockAbortController === abortController) {
|
|
1386
|
+
this.workspaceLockAbortController = null;
|
|
1387
|
+
}
|
|
1388
|
+
if (this.activeTurnWorkspaceLeasePromise === acquirePromise) {
|
|
1389
|
+
this.activeTurnWorkspaceLeasePromise = null;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
async waitForWorkspaceWritesBeforeRead() {
|
|
1394
|
+
if (!this.workspaceLockManager)
|
|
1395
|
+
return;
|
|
1396
|
+
await this.ensureActiveTurnWorkspaceWriteLock();
|
|
1397
|
+
}
|
|
1398
|
+
abortWorkspaceLockWait() {
|
|
1399
|
+
const controller = this.workspaceLockAbortController;
|
|
1400
|
+
if (!controller)
|
|
1401
|
+
return;
|
|
1402
|
+
this.workspaceLockAbortController = null;
|
|
1403
|
+
controller.abort();
|
|
1404
|
+
}
|
|
1405
|
+
releaseActiveTurnWorkspaceLock() {
|
|
1406
|
+
this.activeTurnWorkspaceLeasePromise = null;
|
|
1407
|
+
const lease = this.activeTurnWorkspaceLease;
|
|
1408
|
+
if (!lease)
|
|
1409
|
+
return;
|
|
1410
|
+
this.activeTurnWorkspaceLease = null;
|
|
1411
|
+
try {
|
|
1412
|
+
lease.release();
|
|
1413
|
+
}
|
|
1414
|
+
catch (error) {
|
|
1415
|
+
log.warn('[claude-direct] failed to release workspace lock', {
|
|
1416
|
+
sessionKey: this.sessionKey,
|
|
1417
|
+
error: String(error?.message ?? error),
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
async flushPendingToolCalls(sink, status, output) {
|
|
1422
|
+
if (this.pendingToolCalls.size === 0)
|
|
1423
|
+
return;
|
|
1424
|
+
for (const [toolCallId, entry] of this.pendingToolCalls.entries()) {
|
|
1425
|
+
await sink.sendUi?.({
|
|
1426
|
+
kind: 'tool',
|
|
1427
|
+
mode: 'summary',
|
|
1428
|
+
title: entry.name,
|
|
1429
|
+
output: output ?? (status === 'cancelled' ? 'Cancelled' : 'Failed'),
|
|
1430
|
+
toolCallId,
|
|
1431
|
+
stage: 'complete',
|
|
1432
|
+
status,
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
this.pendingToolCalls.clear();
|
|
1436
|
+
}
|
|
1437
|
+
captureSessionId(message) {
|
|
1438
|
+
const candidate = readSessionId(message);
|
|
1439
|
+
if (candidate) {
|
|
1440
|
+
this.sessionId = candidate;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
buildClaudeControls() {
|
|
1444
|
+
const effectiveEnv = {
|
|
1445
|
+
...process.env,
|
|
1446
|
+
...this.env,
|
|
1447
|
+
};
|
|
1448
|
+
return buildClaudeSessionControlState({
|
|
1449
|
+
sessionId: this.sessionId,
|
|
1450
|
+
modeId: this.claudeModeId,
|
|
1451
|
+
modelId: this.claudeModelId,
|
|
1452
|
+
effectiveModelId: this.resolveDisplayedEffectiveModelId(effectiveEnv),
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
async executeRewindCommand(args) {
|
|
1456
|
+
if (!this.sessionId) {
|
|
1457
|
+
throw new Error('Claude rewind is only available after the conversation has started.');
|
|
1458
|
+
}
|
|
1459
|
+
const requestedUuid = args?.trim() ?? '';
|
|
1460
|
+
const targetUserMessageUuid = requestedUuid
|
|
1461
|
+
? validateClaudeUserMessageUuid(requestedUuid)
|
|
1462
|
+
: this.resolveLatestRewindCandidate();
|
|
1463
|
+
const controlQuery = this.queryImpl({
|
|
1464
|
+
prompt: emptyPromptIterable(),
|
|
1465
|
+
options: this.buildOptions(),
|
|
1466
|
+
});
|
|
1467
|
+
try {
|
|
1468
|
+
const result = await controlQuery.rewindFiles(targetUserMessageUuid, { dryRun: false });
|
|
1469
|
+
if (!result.canRewind) {
|
|
1470
|
+
throw new Error(result.error?.trim() || 'Claude could not rewind files for that message.');
|
|
1471
|
+
}
|
|
1472
|
+
const fileCount = result.filesChanged?.length ?? 0;
|
|
1473
|
+
const summaryParts = [
|
|
1474
|
+
`Rewound tracked files to ${targetUserMessageUuid}.`,
|
|
1475
|
+
fileCount > 0 ? `${fileCount} file${fileCount === 1 ? '' : 's'} changed.` : 'No file changes were needed.',
|
|
1476
|
+
];
|
|
1477
|
+
if ((result.insertions ?? 0) > 0 || (result.deletions ?? 0) > 0) {
|
|
1478
|
+
summaryParts.push(`Diff summary: +${result.insertions ?? 0} / -${result.deletions ?? 0}.`);
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
commandName: 'rewind',
|
|
1482
|
+
ok: true,
|
|
1483
|
+
targetUserMessageUuid,
|
|
1484
|
+
canRewind: true,
|
|
1485
|
+
filesChanged: result.filesChanged,
|
|
1486
|
+
insertions: result.insertions,
|
|
1487
|
+
deletions: result.deletions,
|
|
1488
|
+
summary: summaryParts.join(' '),
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
catch (error) {
|
|
1492
|
+
const message = String(error?.message ?? error);
|
|
1493
|
+
if (isMissingResumeError(message)) {
|
|
1494
|
+
this.sessionId = null;
|
|
1495
|
+
clearAcpSessionId(this.db, this.sessionKey);
|
|
1496
|
+
}
|
|
1497
|
+
throw new Error(message);
|
|
1498
|
+
}
|
|
1499
|
+
finally {
|
|
1500
|
+
controlQuery.close();
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
resolveLatestRewindCandidate() {
|
|
1504
|
+
const latest = getLatestClaudeSessionUserMessage(this.db, this.sessionKey);
|
|
1505
|
+
if (!latest?.messageUuid) {
|
|
1506
|
+
throw new Error('No rewind checkpoint is available for this Claude SDK session yet.');
|
|
1507
|
+
}
|
|
1508
|
+
return latest.messageUuid;
|
|
1509
|
+
}
|
|
1510
|
+
async materializeAssetAttachments(attachments, runId) {
|
|
1511
|
+
return this.attachmentMaterializer.materialize(attachments, { runId });
|
|
1512
|
+
}
|
|
1513
|
+
toSdkUserMessage(promptText, attachments) {
|
|
1514
|
+
return {
|
|
1515
|
+
type: 'user',
|
|
1516
|
+
message: {
|
|
1517
|
+
role: 'user',
|
|
1518
|
+
content: buildClaudeUserContent(promptText, attachments),
|
|
1519
|
+
},
|
|
1520
|
+
parent_tool_use_id: null,
|
|
1521
|
+
uuid: randomUUID(),
|
|
1522
|
+
session_id: this.sessionId ?? '',
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function joinPromptContext(...parts) {
|
|
1527
|
+
return parts
|
|
1528
|
+
.map((value) => value?.trim() ?? '')
|
|
1529
|
+
.filter(Boolean)
|
|
1530
|
+
.join('\n\n');
|
|
1531
|
+
}
|
|
1532
|
+
function emptyPromptIterable() {
|
|
1533
|
+
return {
|
|
1534
|
+
async *[Symbol.asyncIterator]() {
|
|
1535
|
+
return;
|
|
1536
|
+
},
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
function buildClaudeUserContent(promptText, attachments) {
|
|
1540
|
+
const blocks = [];
|
|
1541
|
+
const trimmedPrompt = promptText.trim();
|
|
1542
|
+
if (trimmedPrompt) {
|
|
1543
|
+
blocks.push({ type: 'text', text: trimmedPrompt });
|
|
1544
|
+
}
|
|
1545
|
+
for (const [index, attachment] of (attachments ?? []).entries()) {
|
|
1546
|
+
const inlineImage = resolveClaudeInlineImageAttachment(attachment);
|
|
1547
|
+
if (inlineImage) {
|
|
1548
|
+
blocks.push({
|
|
1549
|
+
type: 'text',
|
|
1550
|
+
text: formatClaudeImageAttachmentIntro(attachment, index, inlineImage.path, inlineImage.mediaType),
|
|
1551
|
+
});
|
|
1552
|
+
try {
|
|
1553
|
+
blocks.push({
|
|
1554
|
+
type: 'image',
|
|
1555
|
+
source: {
|
|
1556
|
+
type: 'base64',
|
|
1557
|
+
media_type: inlineImage.mediaType,
|
|
1558
|
+
data: fs.readFileSync(inlineImage.path).toString('base64'),
|
|
1559
|
+
},
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
catch {
|
|
1563
|
+
blocks.push({
|
|
1564
|
+
type: 'text',
|
|
1565
|
+
text: formatAttachmentTextReference(attachment, index, inlineImage.path, normalizeOptionalString(attachment.assetId)),
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
const localPath = normalizeOptionalString(attachment.preferredLocalPath) ?? resolveLocalAttachmentPath(attachment);
|
|
1571
|
+
const attachmentId = normalizeOptionalString(attachment.assetId) ?? extractAttachmentIdFromUri(attachment.uri);
|
|
1572
|
+
if (!localPath && attachmentId && promptAlreadyReferencesAttachment(trimmedPrompt, attachmentId)) {
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
const preview = localPath ? safelyBuildInlineTextAttachmentPreview(attachment, localPath, index) : null;
|
|
1576
|
+
blocks.push({
|
|
1577
|
+
type: 'text',
|
|
1578
|
+
text: preview ?? formatAttachmentTextReference(attachment, index, localPath, attachmentId),
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
if (blocks.length === 0) {
|
|
1582
|
+
blocks.push({ type: 'text', text: '' });
|
|
1583
|
+
}
|
|
1584
|
+
return blocks;
|
|
1585
|
+
}
|
|
1586
|
+
function formatClaudeImageAttachmentIntro(attachment, index, localPath, mediaType) {
|
|
1587
|
+
const filename = normalizeOptionalString(attachment.filename);
|
|
1588
|
+
return `Attachment ${index + 1}${filename ? ` (${filename})` : ''}: local_path="${localPath}" (${mediaType}). Inline image follows.`;
|
|
1589
|
+
}
|
|
1590
|
+
function safelyBuildInlineTextAttachmentPreview(attachment, localPath, index) {
|
|
1591
|
+
try {
|
|
1592
|
+
return buildInlineTextAttachmentPreview(attachment, localPath, index);
|
|
1593
|
+
}
|
|
1594
|
+
catch {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function toClaudeSdkMcpConfig(entry) {
|
|
1599
|
+
if ('command' in entry) {
|
|
1600
|
+
return {
|
|
1601
|
+
type: 'stdio',
|
|
1602
|
+
command: entry.command,
|
|
1603
|
+
args: [...entry.args],
|
|
1604
|
+
env: Object.fromEntries((entry.env ?? []).map((item) => [item.name, item.value])),
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
if (entry.type === 'http' || entry.type === 'sse') {
|
|
1608
|
+
return {
|
|
1609
|
+
type: entry.type,
|
|
1610
|
+
url: entry.url,
|
|
1611
|
+
headers: Object.fromEntries((entry.headers ?? []).map((item) => [item.name, item.value])),
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
function readSessionId(message) {
|
|
1617
|
+
const candidate = message;
|
|
1618
|
+
const raw = typeof candidate.session_id === 'string'
|
|
1619
|
+
? candidate.session_id
|
|
1620
|
+
: typeof candidate.sessionId === 'string'
|
|
1621
|
+
? candidate.sessionId
|
|
1622
|
+
: typeof candidate.session?.id === 'string'
|
|
1623
|
+
? candidate.session.id
|
|
1624
|
+
: '';
|
|
1625
|
+
const sessionId = raw.trim();
|
|
1626
|
+
return sessionId || null;
|
|
1627
|
+
}
|
|
1628
|
+
function readSdkResultError(message) {
|
|
1629
|
+
const typed = message;
|
|
1630
|
+
if (Array.isArray(typed.errors) && typed.errors.length > 0) {
|
|
1631
|
+
const combined = typed.errors.filter((value) => typeof value === 'string').join('\n').trim();
|
|
1632
|
+
if (combined)
|
|
1633
|
+
return combined;
|
|
1634
|
+
}
|
|
1635
|
+
const result = message.result;
|
|
1636
|
+
return typeof result === 'string' && result.trim() ? result.trim() : null;
|
|
1637
|
+
}
|
|
1638
|
+
function validateClaudeUserMessageUuid(value) {
|
|
1639
|
+
if (!UUID_PATTERN.test(value)) {
|
|
1640
|
+
throw new Error('Claude rewind expects a valid user message UUID.');
|
|
1641
|
+
}
|
|
1642
|
+
return value;
|
|
1643
|
+
}
|
|
1644
|
+
function isAbortLikeError(error) {
|
|
1645
|
+
const message = String(error?.message ?? error ?? '');
|
|
1646
|
+
return /\babort(ed)?\b/i.test(message) || /\binterrupt(ed)?\b/i.test(message);
|
|
1647
|
+
}
|
|
1648
|
+
function isMissingResumeError(message) {
|
|
1649
|
+
return /No conversation found with session ID:/i.test(message);
|
|
1650
|
+
}
|
|
1651
|
+
function normalizeOptionalString(value) {
|
|
1652
|
+
const trimmed = value?.trim() ?? '';
|
|
1653
|
+
return trimmed ? trimmed : null;
|
|
1654
|
+
}
|
|
1655
|
+
function hasAgentRuntimeConfigInput(params) {
|
|
1656
|
+
return Object.prototype.hasOwnProperty.call(params, 'model')
|
|
1657
|
+
|| Object.prototype.hasOwnProperty.call(params, 'reasoningEffort')
|
|
1658
|
+
|| Object.prototype.hasOwnProperty.call(params, 'claudePermissionMode');
|
|
1659
|
+
}
|
|
1660
|
+
function normalizeClaudeReasoningEffort(value) {
|
|
1661
|
+
const trimmed = value?.trim() ?? '';
|
|
1662
|
+
return CLAUDE_REASONING_EFFORTS.has(trimmed) ? trimmed : null;
|
|
1663
|
+
}
|
|
1664
|
+
function isClaudeSdkPostFinalMessageAbort(message) {
|
|
1665
|
+
return message.includes('[ede_diagnostic]') && /Request was aborted/i.test(message);
|
|
1666
|
+
}
|
|
1667
|
+
function isFinalChatSendMessageInput(input) {
|
|
1668
|
+
if (!input || typeof input !== 'object')
|
|
1669
|
+
return false;
|
|
1670
|
+
const kind = input.kind;
|
|
1671
|
+
return typeof kind === 'string' && kind.trim() === 'final';
|
|
1672
|
+
}
|
|
1673
|
+
function getClaudeBigbangMessageSendInfo(toolName, input, outputText, result) {
|
|
1674
|
+
if (!input || typeof input !== 'object')
|
|
1675
|
+
return null;
|
|
1676
|
+
const command = normalizeOptionalString(input.command);
|
|
1677
|
+
if (!command)
|
|
1678
|
+
return null;
|
|
1679
|
+
const normalizedToolName = toolName.trim().toLowerCase();
|
|
1680
|
+
if (normalizedToolName !== 'bash' && normalizedToolName !== 'shell')
|
|
1681
|
+
return null;
|
|
1682
|
+
return getBigbangMessageSendInfo({
|
|
1683
|
+
type: 'commandExecution',
|
|
1684
|
+
command,
|
|
1685
|
+
status: result?.status,
|
|
1686
|
+
success: result?.success,
|
|
1687
|
+
aggregatedOutput: outputText ?? null,
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
function isHeldStaleSendMessageResult(value) {
|
|
1691
|
+
if (isHeldStaleResultShape(value))
|
|
1692
|
+
return true;
|
|
1693
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
|
|
1694
|
+
return text.includes('Draft held; nothing was posted')
|
|
1695
|
+
|| text.includes('Your draft was not sent because newer messages exist on this surface');
|
|
1696
|
+
}
|
|
1697
|
+
function isNonPostedSendMessageResult(value) {
|
|
1698
|
+
if (isHeldStaleSendMessageResult(value))
|
|
1699
|
+
return true;
|
|
1700
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
|
|
1701
|
+
return text.includes('Nothing was confirmed posted')
|
|
1702
|
+
|| text.includes('success without a messageId');
|
|
1703
|
+
}
|
|
1704
|
+
function isHeldStaleResultShape(value, depth = 0) {
|
|
1705
|
+
if (depth > 4 || value == null)
|
|
1706
|
+
return false;
|
|
1707
|
+
if (Array.isArray(value)) {
|
|
1708
|
+
return value.some((item) => isHeldStaleResultShape(item, depth + 1));
|
|
1709
|
+
}
|
|
1710
|
+
if (typeof value !== 'object')
|
|
1711
|
+
return false;
|
|
1712
|
+
const record = value;
|
|
1713
|
+
if (record.held === true && record.stale === true)
|
|
1714
|
+
return true;
|
|
1715
|
+
for (const key of ['structuredContent', 'result', 'data', 'content', 'text', 'output']) {
|
|
1716
|
+
if (isHeldStaleResultShape(record[key], depth + 1))
|
|
1717
|
+
return true;
|
|
1718
|
+
}
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
function isClaudePlanApprovalTool(toolName, toolKind) {
|
|
1722
|
+
return toolKind === 'switch_mode' && toolName.trim().toLowerCase() === 'exitplanmode';
|
|
1723
|
+
}
|
|
1724
|
+
function extractClaudePlanText(input) {
|
|
1725
|
+
const direct = extractPlanLikeText(input, 0);
|
|
1726
|
+
if (direct)
|
|
1727
|
+
return direct;
|
|
1728
|
+
if (typeof input === 'string')
|
|
1729
|
+
return input.trim() || null;
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
function stripClaudeExitPlanAllowedPrompts(input) {
|
|
1733
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
1734
|
+
return input;
|
|
1735
|
+
if (!Object.prototype.hasOwnProperty.call(input, 'allowedPrompts'))
|
|
1736
|
+
return input;
|
|
1737
|
+
const { allowedPrompts: _allowedPrompts, ...rest } = input;
|
|
1738
|
+
return rest;
|
|
1739
|
+
}
|
|
1740
|
+
function extractPlanLikeText(value, depth) {
|
|
1741
|
+
if (depth > 4 || value == null)
|
|
1742
|
+
return null;
|
|
1743
|
+
if (typeof value === 'string') {
|
|
1744
|
+
const trimmed = value.trim();
|
|
1745
|
+
return trimmed || null;
|
|
1746
|
+
}
|
|
1747
|
+
if (Array.isArray(value)) {
|
|
1748
|
+
const parts = value
|
|
1749
|
+
.map((item) => extractPlanLikeText(item, depth + 1))
|
|
1750
|
+
.filter((item) => Boolean(item));
|
|
1751
|
+
return parts.length > 0 ? parts.join('\n') : null;
|
|
1752
|
+
}
|
|
1753
|
+
if (typeof value !== 'object')
|
|
1754
|
+
return null;
|
|
1755
|
+
const record = value;
|
|
1756
|
+
for (const key of ['plan', 'proposedPlan', 'proposed_plan', 'markdown', 'content', 'text']) {
|
|
1757
|
+
const text = extractPlanLikeText(record[key], depth + 1);
|
|
1758
|
+
if (text)
|
|
1759
|
+
return text;
|
|
1760
|
+
}
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
function buildClaudeContinuePlanningMessage(responseText) {
|
|
1764
|
+
const feedback = responseText?.trim();
|
|
1765
|
+
return feedback
|
|
1766
|
+
? `Please continue planning and revise the plan with this feedback: ${feedback}`
|
|
1767
|
+
: 'Please continue planning and revise the plan further.';
|
|
1768
|
+
}
|
|
1769
|
+
function buildPlanApprovalActions() {
|
|
1770
|
+
return [
|
|
1771
|
+
{ id: 'reject', label: 'Reject', variant: 'danger' },
|
|
1772
|
+
{
|
|
1773
|
+
id: 'continue_planning',
|
|
1774
|
+
label: 'Continue planning',
|
|
1775
|
+
variant: 'secondary',
|
|
1776
|
+
requiresInput: true,
|
|
1777
|
+
inputPlaceholder: 'Tell the agent what to change or clarify in the plan...',
|
|
1778
|
+
},
|
|
1779
|
+
{ id: 'implement', label: 'Implement', variant: 'primary' },
|
|
1780
|
+
];
|
|
1781
|
+
}
|
|
1782
|
+
function extractToolResultText(content) {
|
|
1783
|
+
if (typeof content === 'string')
|
|
1784
|
+
return content;
|
|
1785
|
+
if (!Array.isArray(content))
|
|
1786
|
+
return stringifyToolResultFallback(content);
|
|
1787
|
+
const parts = [];
|
|
1788
|
+
for (const block of content) {
|
|
1789
|
+
if (!block || typeof block !== 'object')
|
|
1790
|
+
continue;
|
|
1791
|
+
const typed = block;
|
|
1792
|
+
if (typeof typed.text === 'string' && typed.text.trim()) {
|
|
1793
|
+
parts.push(typed.text);
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
if (typeof typed.input === 'string' && typed.input.trim()) {
|
|
1797
|
+
parts.push(typed.input);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
return parts.join('\n\n') || stringifyToolResultFallback(content);
|
|
1801
|
+
}
|
|
1802
|
+
function stringifyToolResultFallback(content) {
|
|
1803
|
+
if (content == null)
|
|
1804
|
+
return '';
|
|
1805
|
+
try {
|
|
1806
|
+
return JSON.stringify(content);
|
|
1807
|
+
}
|
|
1808
|
+
catch {
|
|
1809
|
+
return String(content);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function describeClaudeStatus(message) {
|
|
1813
|
+
const status = typeof message.status === 'string' ? message.status : null;
|
|
1814
|
+
if (status === 'compacting')
|
|
1815
|
+
return 'Compacting context';
|
|
1816
|
+
if (status === 'requesting')
|
|
1817
|
+
return 'Preparing next model request';
|
|
1818
|
+
if (typeof message.compact_error === 'string' && message.compact_error.trim()) {
|
|
1819
|
+
return `Compaction error: ${message.compact_error}`;
|
|
1820
|
+
}
|
|
1821
|
+
return undefined;
|
|
1822
|
+
}
|
|
1823
|
+
function formatClaudeTaskPatch(patch) {
|
|
1824
|
+
if (!patch || typeof patch !== 'object')
|
|
1825
|
+
return undefined;
|
|
1826
|
+
const typed = patch;
|
|
1827
|
+
const parts = [];
|
|
1828
|
+
if (typeof typed.status === 'string')
|
|
1829
|
+
parts.push(`status=${typed.status}`);
|
|
1830
|
+
if (typeof typed.description === 'string' && typed.description.trim())
|
|
1831
|
+
parts.push(typed.description);
|
|
1832
|
+
if (typeof typed.error === 'string' && typed.error.trim())
|
|
1833
|
+
parts.push(`error=${typed.error}`);
|
|
1834
|
+
return parts.length > 0 ? parts.join(' • ') : undefined;
|
|
1835
|
+
}
|
|
1836
|
+
function buildClaudeToolMatchContext(_toolName, input, toolKind, workspaceRoot) {
|
|
1837
|
+
const params = {};
|
|
1838
|
+
if (toolKind === 'read' || toolKind === 'edit' || toolKind === 'delete' || toolKind === 'move') {
|
|
1839
|
+
if (typeof input.file_path === 'string') {
|
|
1840
|
+
params.file = input.file_path;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
if (toolKind === 'execute') {
|
|
1844
|
+
if (typeof input.command === 'string') {
|
|
1845
|
+
params.command = input.command;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (typeof input.pattern === 'string') {
|
|
1849
|
+
params.pattern = input.pattern;
|
|
1850
|
+
}
|
|
1851
|
+
if (typeof input.query === 'string') {
|
|
1852
|
+
params.query = input.query;
|
|
1853
|
+
}
|
|
1854
|
+
if (typeof input.text === 'string') {
|
|
1855
|
+
params.text = input.text;
|
|
1856
|
+
}
|
|
1857
|
+
return {
|
|
1858
|
+
params,
|
|
1859
|
+
workspaceRoot,
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function claudeToolTargetsWorkspace(toolKind, input, workspaceRoot) {
|
|
1863
|
+
if (toolKind === 'execute') {
|
|
1864
|
+
const cwd = normalizeUnknownString(input.cwd) ?? normalizeUnknownString(input.workdir);
|
|
1865
|
+
if (cwd && isPathInsideOrEqual(cwd, workspaceRoot))
|
|
1866
|
+
return true;
|
|
1867
|
+
const command = normalizeUnknownString(input.command);
|
|
1868
|
+
return !cwd || Boolean(command && command.replace(/\\ /g, ' ').includes(path.resolve(workspaceRoot)));
|
|
1869
|
+
}
|
|
1870
|
+
const paths = [
|
|
1871
|
+
normalizeUnknownString(input.file_path),
|
|
1872
|
+
normalizeUnknownString(input.filePath),
|
|
1873
|
+
normalizeUnknownString(input.path),
|
|
1874
|
+
normalizeUnknownString(input.cwd),
|
|
1875
|
+
normalizeUnknownString(input.directory),
|
|
1876
|
+
].filter((value) => Boolean(value));
|
|
1877
|
+
if (paths.length === 0) {
|
|
1878
|
+
return toolKind === 'search';
|
|
1879
|
+
}
|
|
1880
|
+
return paths.some((value) => pathTargetsWorkspace(value, workspaceRoot, workspaceRoot));
|
|
1881
|
+
}
|
|
1882
|
+
function normalizeUnknownString(value) {
|
|
1883
|
+
return typeof value === 'string' ? normalizeOptionalString(value) : null;
|
|
1884
|
+
}
|
|
1885
|
+
function pathTargetsWorkspace(value, cwd, workspaceRoot) {
|
|
1886
|
+
const normalized = normalizeOptionalString(value);
|
|
1887
|
+
if (!normalized)
|
|
1888
|
+
return false;
|
|
1889
|
+
const candidate = path.isAbsolute(normalized)
|
|
1890
|
+
? normalized
|
|
1891
|
+
: path.resolve(cwd, normalized);
|
|
1892
|
+
return isPathInsideOrEqual(candidate, workspaceRoot);
|
|
1893
|
+
}
|
|
1894
|
+
function isPathInsideOrEqual(candidatePath, rootPath) {
|
|
1895
|
+
const candidate = path.resolve(candidatePath);
|
|
1896
|
+
const root = path.resolve(rootPath);
|
|
1897
|
+
const relative = path.relative(root, candidate);
|
|
1898
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
1899
|
+
}
|
|
1900
|
+
function resolveClaudeToolKind(toolName) {
|
|
1901
|
+
const normalized = toolName.trim().toLowerCase();
|
|
1902
|
+
if (!normalized)
|
|
1903
|
+
return null;
|
|
1904
|
+
if (normalized === 'read')
|
|
1905
|
+
return 'read';
|
|
1906
|
+
if (normalized === 'edit' || normalized === 'multiedit' || normalized === 'write' || normalized === 'notebookedit')
|
|
1907
|
+
return 'edit';
|
|
1908
|
+
if (normalized === 'grep' || normalized === 'glob' || normalized === 'ls')
|
|
1909
|
+
return 'search';
|
|
1910
|
+
if (normalized === 'bash' || normalized === 'killbash')
|
|
1911
|
+
return 'execute';
|
|
1912
|
+
if (normalized === 'webfetch' || normalized === 'websearch')
|
|
1913
|
+
return 'fetch';
|
|
1914
|
+
if (normalized === 'exitplanmode')
|
|
1915
|
+
return 'switch_mode';
|
|
1916
|
+
if (normalized === 'task' || normalized === 'todowrite')
|
|
1917
|
+
return 'think';
|
|
1918
|
+
return 'other';
|
|
1919
|
+
}
|
|
1920
|
+
function buildClaudeDisallowedTools(disabledToolKinds) {
|
|
1921
|
+
const out = [];
|
|
1922
|
+
const seen = new Set();
|
|
1923
|
+
for (const kind of disabledToolKinds) {
|
|
1924
|
+
for (const toolName of CLAUDE_DISALLOWED_TOOLS_BY_KIND[kind] ?? []) {
|
|
1925
|
+
const key = toolName.toLowerCase();
|
|
1926
|
+
if (seen.has(key))
|
|
1927
|
+
continue;
|
|
1928
|
+
seen.add(key);
|
|
1929
|
+
out.push(toolName);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return out;
|
|
1933
|
+
}
|
|
1934
|
+
function isClaudePlatformChatTool(toolName, channelBridgeServerName) {
|
|
1935
|
+
const serverName = channelBridgeServerName?.trim().toLowerCase();
|
|
1936
|
+
if (!serverName)
|
|
1937
|
+
return false;
|
|
1938
|
+
return toolName.trim().toLowerCase() === `mcp__${serverName}__send_message`;
|
|
1939
|
+
}
|
|
1940
|
+
function buildPermissionArgs(input, options) {
|
|
1941
|
+
const meta = {
|
|
1942
|
+
toolUseId: options.toolUseID,
|
|
1943
|
+
};
|
|
1944
|
+
if (typeof options.title === 'string' && options.title.trim())
|
|
1945
|
+
meta.title = options.title;
|
|
1946
|
+
if (typeof options.displayName === 'string' && options.displayName.trim())
|
|
1947
|
+
meta.displayName = options.displayName;
|
|
1948
|
+
if (typeof options.description === 'string' && options.description.trim())
|
|
1949
|
+
meta.description = options.description;
|
|
1950
|
+
if (typeof options.blockedPath === 'string' && options.blockedPath.trim())
|
|
1951
|
+
meta.blockedPath = options.blockedPath;
|
|
1952
|
+
if (typeof options.decisionReason === 'string' && options.decisionReason.trim())
|
|
1953
|
+
meta.decisionReason = options.decisionReason;
|
|
1954
|
+
if (Array.isArray(options.suggestions) && options.suggestions.length > 0)
|
|
1955
|
+
meta.suggestions = options.suggestions;
|
|
1956
|
+
return {
|
|
1957
|
+
input,
|
|
1958
|
+
_permission: meta,
|
|
1959
|
+
};
|
|
1960
|
+
}
|