@jskit-ai/assistant-core 0.1.199 → 0.1.201
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/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { claudeMessageBlocks, deleteClaudeConversationHistory, readClaudeHistory, requireClaudeSessionId, retireClaudeConversationHistory } from "./claudeHistory.js";
|
|
4
|
+
import { bindClaudeConversationAccount, claudeCodeArguments, createClaudeAccountQueries, createClaudeCodeProcess, stopClaudeCodeProcess } from "./claudeProcess.js";
|
|
5
|
+
import { claudeApplicationToolResponse } from "./claudeTools.js";
|
|
6
|
+
import { createClaudeConversationAdapter } from "./providers/claude.js";
|
|
7
|
+
|
|
8
|
+
function failure(message, code = "assistant_claude_turn_failed") {
|
|
9
|
+
return Object.assign(new Error(message), { code });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const text = value => String(value ?? "").trim();
|
|
13
|
+
const now = () => new Date().toISOString();
|
|
14
|
+
|
|
15
|
+
export function claudeNativeMessageId(id) {
|
|
16
|
+
const value = createHash("sha256").update(String(id)).digest("hex");
|
|
17
|
+
return `${value.slice(0, 8)}-${value.slice(8, 12)}-4${value.slice(13, 16)}-8${value.slice(17, 20)}-${value.slice(20, 32)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Wait on the conversation's existing completion and cleanup owner. */
|
|
21
|
+
export async function waitForClaudeConversationTurn(conversation, input = {}, {
|
|
22
|
+
signal, defaultTimeoutMs = 180_000, maximumTimeoutMs = Infinity, createError = failure,
|
|
23
|
+
interruptedMessage = "Claude was interrupted when the application disconnected."
|
|
24
|
+
} = {}) {
|
|
25
|
+
const entry = conversation.state;
|
|
26
|
+
if (entry.executionId && !entry.process) await conversation.stop(interruptedMessage);
|
|
27
|
+
if (!entry.turn?.active) return conversation.read(input);
|
|
28
|
+
const timeoutMs = Math.min(
|
|
29
|
+
Number(input.timeoutMs) > 0 ? Number(input.timeoutMs) : defaultTimeoutMs,
|
|
30
|
+
maximumTimeoutMs
|
|
31
|
+
);
|
|
32
|
+
let timer;
|
|
33
|
+
let timedOut = false;
|
|
34
|
+
const abort = () => { void conversation.interrupt().catch(() => {}); };
|
|
35
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
36
|
+
if (signal?.aborted) abort();
|
|
37
|
+
try {
|
|
38
|
+
if (timeoutMs === Infinity) return await entry.completion.promise;
|
|
39
|
+
return await Promise.race([entry.completion.promise, new Promise((_, reject) => {
|
|
40
|
+
timer = setTimeout(() => {
|
|
41
|
+
timedOut = true;
|
|
42
|
+
reject(createError("Claude did not finish within the time limit."));
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
})]);
|
|
45
|
+
} catch (failure) {
|
|
46
|
+
if (timedOut) await conversation.interrupt();
|
|
47
|
+
throw failure;
|
|
48
|
+
} finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
signal?.removeEventListener("abort", abort);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Native renewal receipt, completion and cleanup moved from the session provider. */
|
|
55
|
+
export async function runClaudeRenewalTurn(conversation, {
|
|
56
|
+
clientMessageId, prompt, outputSchema, expectedThreadId, forbiddenThreadId,
|
|
57
|
+
requireFreshHistory = false, timeoutMs = 180_000
|
|
58
|
+
}, {
|
|
59
|
+
createError = failure, unreadableCode = "assistant_claude_turn_unreadable",
|
|
60
|
+
failedCode = "assistant_claude_turn_failed", acceptedField = "inputAccepted"
|
|
61
|
+
} = {}) {
|
|
62
|
+
const uuid = claudeNativeMessageId(clientMessageId);
|
|
63
|
+
const entry = conversation.state;
|
|
64
|
+
if ((expectedThreadId && expectedThreadId !== entry.id) ||
|
|
65
|
+
(requireFreshHistory && forbiddenThreadId === entry.id)) {
|
|
66
|
+
throw createError("The Claude renewal conversation does not match the expected native history.");
|
|
67
|
+
}
|
|
68
|
+
const history = await conversation.readHistory();
|
|
69
|
+
if (requireFreshHistory && history.userIds.some((id) => id !== uuid)) throw createError("The successor Claude conversation already contains unrelated messages.");
|
|
70
|
+
const accepted = history.userIds.includes(uuid);
|
|
71
|
+
let result;
|
|
72
|
+
try {
|
|
73
|
+
if (accepted) {
|
|
74
|
+
result = { text: history.messages.filter((message) => message.userId === uuid && message.role === "assistant").map((message) => message.text).join("\n"),
|
|
75
|
+
threadId: entry.id, turnId: uuid, reconciled: true };
|
|
76
|
+
if (!result.text) throw createError("The accepted Claude renewal turn has no readable result; it will not be submitted again.", unreadableCode);
|
|
77
|
+
} else {
|
|
78
|
+
if (entry.turn?.active && entry.process) throw createError("Stop the current turn before renewing this conversation.");
|
|
79
|
+
await conversation.send({ message: prompt, messageId: clientMessageId, outputSchema }, { renewal: true });
|
|
80
|
+
result = await conversation.wait({ conversationId: entry.id, timeoutMs });
|
|
81
|
+
if (result.status !== "completed") throw createError(result.error || "Claude did not complete the renewal turn.", failedCode);
|
|
82
|
+
}
|
|
83
|
+
return { ...result, clientMessageId,
|
|
84
|
+
freshThread: history.userIds.length === 0, processExitProof: await conversation.stop() };
|
|
85
|
+
} catch (failure) {
|
|
86
|
+
failure.details = { ...failure.details, clientMessageId, [acceptedField]: accepted || entry.lastMessageId === uuid,
|
|
87
|
+
threadId: entry.id, turnId: entry.turn?.id || uuid };
|
|
88
|
+
await conversation.stop(failure.message);
|
|
89
|
+
throw failure;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createClaudeConversationGoals({ conversationId, readHistory, readState, interrupt, send }) {
|
|
94
|
+
const goalError = message => failure(message, "assistant_claude_goal_failed");
|
|
95
|
+
|
|
96
|
+
async function read() {
|
|
97
|
+
const { goal = null } = await readHistory();
|
|
98
|
+
const state = readState();
|
|
99
|
+
return goal ? { ...goal, status: goal.status === "complete" ? "complete" :
|
|
100
|
+
state.running && state.active ? "active" : "paused" } : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function update(input = {}, { interrupt: interruptGoal = interrupt, send: sendGoal = send } = {}) {
|
|
104
|
+
const goal = await read();
|
|
105
|
+
const action = input.action;
|
|
106
|
+
if (action !== "set" && (!goal || input.threadId !== conversationId ||
|
|
107
|
+
input.createdAt !== goal.createdAt || input.objective !== goal.objective)) {
|
|
108
|
+
throw goalError("The Claude goal changed. Refresh before trying again.");
|
|
109
|
+
}
|
|
110
|
+
if (input.tokenBudget != null) throw goalError("Claude goals do not support a token budget.");
|
|
111
|
+
if (action === "pause") {
|
|
112
|
+
await interruptGoal();
|
|
113
|
+
} else if (action === "cancel") {
|
|
114
|
+
await interruptGoal();
|
|
115
|
+
await sendGoal({ message: "/goal clear" });
|
|
116
|
+
} else if (action === "set" || action === "resume") {
|
|
117
|
+
const objective = String((action === "set" ? input.objective : goal.objective) ?? "").trim();
|
|
118
|
+
if (!objective || objective.length > 4000 || /^(clear|stop|off|reset|none|cancel)$/iu.test(objective)) {
|
|
119
|
+
throw goalError("Enter a goal condition of up to 4,000 characters.");
|
|
120
|
+
}
|
|
121
|
+
if (readState().active || (action === "set" && goal && goal.status !== "complete")) {
|
|
122
|
+
throw goalError("Stop the current turn and cancel the existing goal before setting another.");
|
|
123
|
+
}
|
|
124
|
+
await sendGoal({ message: `/goal ${objective}` });
|
|
125
|
+
} else throw goalError("Unknown Claude goal action.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return Object.freeze({ read, update });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The original retained entry and command owner; hosts supply authorized storage and message effects. */
|
|
132
|
+
export function createClaudeConversationOwner({
|
|
133
|
+
configRoot, store, preparation, process, onEvent, createError = failure,
|
|
134
|
+
disconnectedMessage = "Claude was interrupted when the application disconnected."
|
|
135
|
+
}) {
|
|
136
|
+
const entries = new Map();
|
|
137
|
+
const closingSessions = new Set();
|
|
138
|
+
let closing = false;
|
|
139
|
+
const createProcess = process.create || createClaudeCodeProcess;
|
|
140
|
+
const saveAccount = store.save;
|
|
141
|
+
|
|
142
|
+
function createAccountQueries({ accountIdentity, createProcess }) {
|
|
143
|
+
return createClaudeAccountQueries({ accountIdentity,
|
|
144
|
+
processes: () => entries.values(), isClosing: () => closing, createProcess });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function snapshot(entry) {
|
|
148
|
+
return entry?.turn ? { ...entry.turn, threadId: entry.id,
|
|
149
|
+
phase: entry.turn.active && !entry.observationError ? text(entry.turn.phase) : "" } : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readRetainedTurn(contextKey, conversationId, state) {
|
|
153
|
+
const entry = entries.get(`${contextKey}\0${conversationId}`);
|
|
154
|
+
const saved = state.saved;
|
|
155
|
+
return state.nativeResult?.turn || (entry ? snapshot(entry) : saved?.turnId ? {
|
|
156
|
+
id: saved.turnId, threadId: conversationId, state: saved.state,
|
|
157
|
+
active: ["starting", "active", "finalizing"].includes(saved.state)
|
|
158
|
+
} : null);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readHistory(entry) {
|
|
162
|
+
return readClaudeHistory({ configRoot, workdir: entry.nativeWorkdir, conversationId: entry.id });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function open(context, { conversationId = "", mainId, create = false,
|
|
166
|
+
cleanupExecutionId, recoveringCleanup = false, nativeWorkdir = context.workdir } = {}) {
|
|
167
|
+
const main = !conversationId || conversationId === mainId;
|
|
168
|
+
const id = conversationId || text(mainId) ||
|
|
169
|
+
[...entries.values()].find(entry => entry.main && entry.context.key === context.key)?.id || randomUUID();
|
|
170
|
+
requireClaudeSessionId(id);
|
|
171
|
+
if (!conversationId) {
|
|
172
|
+
const selected = store.select(context, id);
|
|
173
|
+
if (selected) await selected;
|
|
174
|
+
}
|
|
175
|
+
const key = `${context.key}\0${id}`;
|
|
176
|
+
if (entries.has(key)) {
|
|
177
|
+
const entry = entries.get(key);
|
|
178
|
+
entry.context = context;
|
|
179
|
+
return entry;
|
|
180
|
+
}
|
|
181
|
+
let saved = store.read(context, id);
|
|
182
|
+
if (saved && typeof saved.then === "function") saved = await saved;
|
|
183
|
+
if (!main && !saved && !create && !recoveringCleanup) throw createError("This Claude conversation is unavailable.");
|
|
184
|
+
const state = saved ? JSON.parse(saved) : {};
|
|
185
|
+
const entry = { id, key, main, persistent: state.persistent === true, context, process: null,
|
|
186
|
+
executionId: state.executionId || (recoveringCleanup ? cleanupExecutionId : ""),
|
|
187
|
+
accountIdentities: state.accountIdentities || (state.accountIdentity ? { anthropic: state.accountIdentity } : {}),
|
|
188
|
+
accountIdentity: state.accountIdentity || "", sent: state.sent === true, nativeWorkdir: state.nativeWorkdir || nativeWorkdir,
|
|
189
|
+
lastMessageId: state.lastMessageId || "", turn: state.turnId ? {
|
|
190
|
+
id: state.turnId, active: ["starting", "active", "finalizing"].includes(state.state),
|
|
191
|
+
state: state.state, startedAt: now(), updatedAt: now()
|
|
192
|
+
} : null };
|
|
193
|
+
entry.nativeTurn = createClaudeConversationTurn({ conversationId: id, entry,
|
|
194
|
+
onEvent: event => onEvent(entry, event),
|
|
195
|
+
process: {
|
|
196
|
+
stopExecution: process.stopExecution,
|
|
197
|
+
...(store.releaseExecution ? { releaseExecution: () => store.releaseExecution(entry) } : {}),
|
|
198
|
+
getProcess: () => entry.stopping ? null : entry.process,
|
|
199
|
+
isActive: () => store.releaseExecution ? entry.nativeTurn.read().active : Boolean(entry.turn?.active),
|
|
200
|
+
startProcess: input => startProcess(entry, input),
|
|
201
|
+
stopProcess: reason => entry.process || entry.executionId ? entry.nativeTurn.stopProcess(reason) : Promise.resolve()
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
entry.goals = createClaudeConversationGoals({ conversationId: id,
|
|
205
|
+
readHistory: () => readHistory(entry),
|
|
206
|
+
readState: () => ({ running: Boolean(entry.process), active: store.releaseExecution ? entry.nativeTurn.read().active : Boolean(entry.turn?.active) }),
|
|
207
|
+
interrupt: () => interrupt(entry), send: input => send(entry, input) });
|
|
208
|
+
entries.set(key, entry);
|
|
209
|
+
await store.save(entry);
|
|
210
|
+
return entry;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function acquire(context, conversationId = "", { create = false, cleanupExecutionId, operation, input } = {}) {
|
|
214
|
+
const ctx = await preparation.context(context);
|
|
215
|
+
const prepared = preparation.entry(ctx, context, conversationId, { create, cleanupExecutionId, operation, input });
|
|
216
|
+
const entry = await open(ctx, prepared.options);
|
|
217
|
+
prepared.check(entry);
|
|
218
|
+
return entry;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function createConversation(input = {}, { context, acquire }) {
|
|
222
|
+
const entry = await acquire(context, randomUUID(), { create: true });
|
|
223
|
+
const executionProfile = input.executionProfile;
|
|
224
|
+
if (executionProfile !== undefined) entry.profile = executionProfile;
|
|
225
|
+
entry.persistent = input.persistent === true;
|
|
226
|
+
await store.save(entry);
|
|
227
|
+
return { ok: true, conversationId: entry.id, ephemeral: input.ephemeral === true, status: "ready" };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function bindAccount(entry) {
|
|
231
|
+
const account = preparation.account(entry);
|
|
232
|
+
const identity = await account.identity;
|
|
233
|
+
const providerId = account.providerId;
|
|
234
|
+
try {
|
|
235
|
+
await bindClaudeConversationAccount(entry, { identity, providerId,
|
|
236
|
+
stop: (entry, reason) => entry.nativeTurn.stopProcess(reason), save: saveAccount });
|
|
237
|
+
} catch (failure) {
|
|
238
|
+
if (failure.code !== "claude_account_changed") throw failure;
|
|
239
|
+
throw createError(failure.message, account.changedCode || failure.code);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function ensureReady(entry) {
|
|
244
|
+
if (entry.process) await ensureProcess(entry);
|
|
245
|
+
else {
|
|
246
|
+
await bindAccount(entry);
|
|
247
|
+
if (entry.executionId) await entry.nativeTurn.stopProcess("Claude's previous process exit could not be confirmed.");
|
|
248
|
+
// The first Send prepares host hooks before starting the native process.
|
|
249
|
+
}
|
|
250
|
+
return { ok: true, thread: { id: entry.id }, turn: snapshot(entry), workdir: entry.context.workdir };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function ensureProcess(entry, input = {}) {
|
|
254
|
+
if (preparation.checkConfiguration) {
|
|
255
|
+
const application = preparation.checkConfiguration(entry, input);
|
|
256
|
+
if (closing || closingSessions.has(entry.context.key) || application?.sessionClosing) throw createError("This session is closing.");
|
|
257
|
+
}
|
|
258
|
+
if (preparation.account) await bindAccount(entry);
|
|
259
|
+
return entry.nativeTurn.prepare(await preparation.configuration(entry, input));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function stopForTerminal(entry, application) {
|
|
263
|
+
if (entry.turn?.active) throw createError("Stop the current turn before opening the Claude Code terminal.");
|
|
264
|
+
if (closing || closingSessions.has(entry.context.key) || application?.sessionClosing) throw createError("This session is closing.");
|
|
265
|
+
await bindAccount(entry);
|
|
266
|
+
await entry.nativeTurn.stopProcess();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function prepareTerminal(entry, context) {
|
|
270
|
+
const history = await readClaudeHistory({ configRoot, workdir: context.workdir, conversationId: entry.id });
|
|
271
|
+
entry.sent ||= history.exists;
|
|
272
|
+
const configuration = await process.configureTerminal(entry, context);
|
|
273
|
+
const options = { terminal: true, sessionId: entry.id, resume: entry.sent,
|
|
274
|
+
instructionArguments: entry.nativeTurn.instructionArguments({ systemPrompt: await configuration.systemPrompt }),
|
|
275
|
+
model: configuration.model, effort: configuration.effort };
|
|
276
|
+
return { args: claudeCodeArguments({ permissionMode: configuration.permissionMode, settings: configuration.settings, ...options }),
|
|
277
|
+
env: { ...configuration.env, DISABLE_AUTOUPDATER: "1" } };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function startProcess(entry, configuration) {
|
|
281
|
+
const context = entry.context;
|
|
282
|
+
let prepared = process.prepare ? process.prepare(entry, configuration, context) : { workdir: entry.nativeWorkdir };
|
|
283
|
+
if (prepared && typeof prepared.then === "function") prepared = await prepared;
|
|
284
|
+
entry.nativeWorkdir = prepared.workdir;
|
|
285
|
+
const history = await readHistory(entry);
|
|
286
|
+
entry.sent ||= history.exists;
|
|
287
|
+
entry.stopping = false;
|
|
288
|
+
entry.profile = configuration.profile;
|
|
289
|
+
let options = process.configure(entry, configuration, { context, prepared });
|
|
290
|
+
if (options && typeof options.then === "function") options = await options;
|
|
291
|
+
try { entry.process = await createProcess({ ...options,
|
|
292
|
+
workdir: entry.nativeWorkdir, sessionId: entry.id, resume: entry.sent,
|
|
293
|
+
onStarted: async executionId => {
|
|
294
|
+
entry.executionId = executionId;
|
|
295
|
+
await store.save(entry, { execution: true });
|
|
296
|
+
await onEvent(entry, { type: "execution", context, executionId });
|
|
297
|
+
},
|
|
298
|
+
async onEvent(frame) {
|
|
299
|
+
const current = entry.command;
|
|
300
|
+
if (current && frame.type === "assistant" && !frame.parent_tool_use_id) {
|
|
301
|
+
for (const tool of frame.message?.content || []) if (tool.type === "tool_use" && tool.id && tool.name?.startsWith("mcp__application__")) {
|
|
302
|
+
if (current.nativeToolCount >= current.maximumToolCalls && !current.nativeTools.has(tool.id)) {
|
|
303
|
+
throw new Error("Claude exceeded the application tool-call limit.");
|
|
304
|
+
}
|
|
305
|
+
if (!current.nativeTools.has(tool.id)) {
|
|
306
|
+
current.nativeToolCount++;
|
|
307
|
+
current.nativeTools.set(tool.id, { name: tool.name, input: tool.input, messageId: current.messageId });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
await entry.nativeTurn.receive(frame);
|
|
312
|
+
},
|
|
313
|
+
...(store.releaseExecution ? {
|
|
314
|
+
async onControlRequest(request, { signal }) {
|
|
315
|
+
const active = entry.command;
|
|
316
|
+
const work = claudeApplicationToolResponse(request, { schemas: entry.toolSchemas || [], turn: active, signal });
|
|
317
|
+
active?.toolWork.add(work);
|
|
318
|
+
try { return await work; }
|
|
319
|
+
finally { active?.toolWork.delete(work); }
|
|
320
|
+
}
|
|
321
|
+
} : {}),
|
|
322
|
+
onFailure: async error => {
|
|
323
|
+
if (store.releaseExecution) {
|
|
324
|
+
entry.nativeTurn.invalidateInstructions();
|
|
325
|
+
try { await entry.nativeTurn.stopProcess(error.message); }
|
|
326
|
+
catch (cleanupError) { error = cleanupError; await context.reportFailure(cleanupError); }
|
|
327
|
+
entry.command?.completion.reject(error);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
entry.observationError = error.message;
|
|
331
|
+
await entry.nativeTurn.stopProcess(error.message);
|
|
332
|
+
}
|
|
333
|
+
}); } catch (error) {
|
|
334
|
+
if (store.releaseExecution && error.stopProof?.scopeEmpty) await store.releaseExecution(entry);
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
return entry.process;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function send(entry, input = {}, { renewal = false, command } = {}) {
|
|
341
|
+
const message = command ? input.text : text(input.message || input.prompt);
|
|
342
|
+
if (!command && !message) throw createError("Enter a message for Claude.");
|
|
343
|
+
const context = entry.context;
|
|
344
|
+
const messageId = command ? input.messageId : text(input.messageId) || randomUUID();
|
|
345
|
+
const uuid = claudeNativeMessageId(messageId);
|
|
346
|
+
if (!command) {
|
|
347
|
+
if (entry.main && await store.hasMessage(context, messageId)) {
|
|
348
|
+
return { ok: true, delivered: true, duplicate: true, thread: { id: entry.id }, turn: snapshot(entry) };
|
|
349
|
+
}
|
|
350
|
+
preparation.check(entry, context);
|
|
351
|
+
const history = await readHistory(entry);
|
|
352
|
+
if (history.userIds.includes(uuid)) {
|
|
353
|
+
return { ok: true, delivered: true, duplicate: true, thread: { id: entry.id }, turn: snapshot(entry) };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const native = command ? command.steering ? entry.process : await entry.nativeTurn.prepare(command.prepared) : await ensureProcess(entry, input);
|
|
357
|
+
if (command && !command.steering) command.signal.throwIfAborted();
|
|
358
|
+
const steering = command ? command.steering === true : Boolean(entry.turn?.active);
|
|
359
|
+
if (steering) {
|
|
360
|
+
if (command) {
|
|
361
|
+
await command.beforeDispatch({ threadId: entry.id });
|
|
362
|
+
command.signal.throwIfAborted();
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
await entry.nativeTurn.interruptGeneration(command ? entry.process.client : native.client,
|
|
366
|
+
command ? { timeoutMs: command.timeoutMs } : undefined);
|
|
367
|
+
if (command) command.interrupted = true;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (!command) await entry.nativeTurn.stopProcess(error.message);
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const prompt = command ? undefined : await preparation.message(entry, input, { context, message, steering, renewal });
|
|
374
|
+
if (command) {
|
|
375
|
+
await command.beforeDispatch({ threadId: entry.id });
|
|
376
|
+
command.signal.throwIfAborted();
|
|
377
|
+
}
|
|
378
|
+
if (!steering) {
|
|
379
|
+
entry.renewal = renewal;
|
|
380
|
+
entry.nativeTurn.begin({ id: uuid, maxOutputCharacters: command ? command.maximumOutput : entry.profile?.limits.maxOutputCharacters });
|
|
381
|
+
entry.observationError = "";
|
|
382
|
+
entry.turn = { id: uuid, active: true, state: "starting", startedAt: now(), updatedAt: now(), error: "" };
|
|
383
|
+
if (!command) entry.completion = Promise.withResolvers();
|
|
384
|
+
if (!command) await entry.nativeTurn.updateState("starting");
|
|
385
|
+
}
|
|
386
|
+
entry.onEvent = context.onEvent;
|
|
387
|
+
entry.lastMessageId = uuid;
|
|
388
|
+
const actorMetadata = command ? undefined : await onEvent(entry, { type: "message-metadata", input, context });
|
|
389
|
+
let conversationTurn;
|
|
390
|
+
async function accept() {
|
|
391
|
+
if (command) {
|
|
392
|
+
if (!steering) await onEvent(entry, { type: "binding-admitted" });
|
|
393
|
+
await command.accept();
|
|
394
|
+
if (steering) {
|
|
395
|
+
command.active.messageId = input.messageId;
|
|
396
|
+
command.active.nativeToolCount = 0;
|
|
397
|
+
} else command.active.admitted = true;
|
|
398
|
+
command.admitted = true;
|
|
399
|
+
} else {
|
|
400
|
+
const admission = onEvent(entry, { type: "admit-message", input, context, renewal, message, messageId, uuid, actorMetadata });
|
|
401
|
+
if (admission) conversationTurn = await admission;
|
|
402
|
+
}
|
|
403
|
+
if (!command) await entry.nativeTurn.updateState("active");
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
if (command) command.attempted = true;
|
|
407
|
+
else await input.onPromptSending?.({ threadId: entry.id, displayAttachments: input.displayAttachments, turnMetadata: actorMetadata });
|
|
408
|
+
await entry.nativeTurn.send(command ? entry.process.client : native.client, command ? preparation.message(entry, input) : prompt,
|
|
409
|
+
{ messageId: uuid, accept, ...(command ? { timeoutMs: command.timeoutMs } : {}) });
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (!command) await entry.nativeTurn.stopProcess(error.message);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
return { ok: true, delivered: true, deliveryMode: steering ? "steer" : "new_turn",
|
|
415
|
+
thread: { id: entry.id }, turn: snapshot(entry), workdir: context.workdir, conversationTurn };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function startTurn(entry, input = {}) {
|
|
419
|
+
entry.persistent ||= input.persistent === true;
|
|
420
|
+
if (entry.turn?.active && entry.process && input.steer !== true) throw createError("This conversation is still working.");
|
|
421
|
+
await send(entry, input);
|
|
422
|
+
return { ...entry.nativeTurn.readResult(), ok: true, started: true };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function interrupt(entry) {
|
|
426
|
+
if (!entry.turn?.active) return { ok: true, interrupted: false, turn: snapshot(entry) };
|
|
427
|
+
await entry.nativeTurn.cancel(entry.process?.client, () => entry.nativeTurn.stopProcess());
|
|
428
|
+
return { ok: true, interrupted: true, thread: { id: entry.id }, turn: snapshot(entry) };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function read(entry, input = {}) {
|
|
432
|
+
if (entry.executionId && !entry.process) await entry.nativeTurn.stopProcess(disconnectedMessage);
|
|
433
|
+
const history = await readHistory(entry);
|
|
434
|
+
const messages = new Map(history.messages.map(message => [message.id, message]));
|
|
435
|
+
const current = entry.nativeTurn.read();
|
|
436
|
+
for (const message of current.messages) messages.set(message.id, message);
|
|
437
|
+
return { ...entry.nativeTurn.readResult(), ok: true, messages: [...messages.values()], text: current.text || history.text,
|
|
438
|
+
admitted: history.userIds.includes(claudeNativeMessageId(input.messageId || "")) };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function inspectAdmission(entry, input) {
|
|
442
|
+
if (input.threadId && input.threadId !== entry.id) return { ok: true, admission: "unknown", messageId: input.messageId, threadId: entry.id };
|
|
443
|
+
const history = await readHistory(entry);
|
|
444
|
+
const turnId = claudeNativeMessageId(input.messageId);
|
|
445
|
+
const accepted = history.userIds.includes(turnId);
|
|
446
|
+
return { ok: true, admission: accepted ? "accepted" : "unknown", messageId: input.messageId,
|
|
447
|
+
threadId: entry.id, turnId: accepted ? turnId : "" };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function hasActiveTemporaryConversation(context, { acquire }) {
|
|
451
|
+
await restoreSessionEntries(context, { acquire });
|
|
452
|
+
return [...entries.values()].some((entry) => entry.context.key === context.key && !entry.main && entry.turn?.active);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function hasActiveConversation(binding) {
|
|
456
|
+
return [...entries.values()].some((entry) => entry.id === binding.conversationId &&
|
|
457
|
+
(entry.turn?.active || entry.process || entry.executionId));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function retireConversationHistory(prepared) {
|
|
461
|
+
const { binding, application } = prepared;
|
|
462
|
+
return retireClaudeConversationHistory({ configRoot: prepared.configRoot, binding,
|
|
463
|
+
beforeDelete: prepared.beforeDelete, signal: prepared.signal,
|
|
464
|
+
requireIdle: async () => {
|
|
465
|
+
const saved = application.saved;
|
|
466
|
+
const active = hasActiveConversation(binding);
|
|
467
|
+
if (saved?.executionId || active || application.terminalRunning) {
|
|
468
|
+
throw createError("Stop the saved Claude process and its terminal before retiring native history.");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function reconcileSessions(sessions, options = {}, application) {
|
|
475
|
+
const results = [];
|
|
476
|
+
for (const session of sessions) {
|
|
477
|
+
const ctx = await application.readContext({ ...options, session, sessionId: session.sessionId });
|
|
478
|
+
await recover(await restoreSessionEntries(ctx, { acquire }));
|
|
479
|
+
results.push({ ok: true, sessionId: session.sessionId, resumed: false });
|
|
480
|
+
}
|
|
481
|
+
return { ok: true, results, failed: [], sessionCount: results.length };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function restoreSessionEntries(context, { acquire }) {
|
|
485
|
+
let saved = store.readSessionConversations(context);
|
|
486
|
+
if (saved && typeof saved.then === "function") saved = await saved;
|
|
487
|
+
for (const conversationId of saved.ids) await acquire(saved.context, conversationId);
|
|
488
|
+
return [...entries.values()].filter((entry) => entry.context.key === context.key);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function closeSession(context, application) {
|
|
492
|
+
const ctx = await application.readContext(context);
|
|
493
|
+
closingSessions.add(ctx.key);
|
|
494
|
+
try {
|
|
495
|
+
await restoreSessionEntries(ctx, { acquire });
|
|
496
|
+
const proofs = await drain(ctx.key);
|
|
497
|
+
const terminal = await application.terminals.close(ctx.sessionId);
|
|
498
|
+
if (terminal?.ok !== false && context.forgetConversationBinding === true) {
|
|
499
|
+
forget(ctx.key);
|
|
500
|
+
}
|
|
501
|
+
return { ok: terminal?.ok !== false, closed: proofs.length + Number(terminal?.closed || 0),
|
|
502
|
+
processExitProof: proofs.at(-1) || { exited: true, scopeEmpty: true }, processExitProofs: proofs };
|
|
503
|
+
} finally {
|
|
504
|
+
closingSessions.delete(ctx.key);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function closeProject(input = {}, application) {
|
|
509
|
+
const root = text(input.projectContextRoot);
|
|
510
|
+
const contexts = new Map([...entries.values()]
|
|
511
|
+
.filter((entry) => !root || path.resolve(store.projectContextRoot(entry)) === path.resolve(root))
|
|
512
|
+
.map((entry) => [entry.context.key, entry.context]));
|
|
513
|
+
let closed = 0;
|
|
514
|
+
let ok = true;
|
|
515
|
+
for (const ctx of contexts.values()) {
|
|
516
|
+
const result = await closeSession(ctx, application);
|
|
517
|
+
closed += result.closed;
|
|
518
|
+
ok &&= result.ok;
|
|
519
|
+
}
|
|
520
|
+
return { ok, closed };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function invalidateRuntimes(input = {}, application, account) {
|
|
524
|
+
if (input.provider && input.provider !== "claude") return { ok: true, closed: 0 };
|
|
525
|
+
if (input.modelProviderId) {
|
|
526
|
+
return stopSelected(entry => store.modelProviderId(entry) === input.modelProviderId,
|
|
527
|
+
"The provider connection changed.");
|
|
528
|
+
}
|
|
529
|
+
closing = true;
|
|
530
|
+
try {
|
|
531
|
+
await account.queries.invalidate().catch(account.failure);
|
|
532
|
+
return await closeProject(undefined, application);
|
|
533
|
+
} finally {
|
|
534
|
+
closing = input.reason === "server-shutdown";
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function drain(key) {
|
|
539
|
+
await Promise.allSettled([...entries.values()].filter(entry => entry.context.key === key).map(entry => entry.nativeTurn.whenIdle()));
|
|
540
|
+
const proofs = [];
|
|
541
|
+
for (const entry of entries.values()) if (entry.context.key === key) proofs.push(await entry.nativeTurn.stopProcess());
|
|
542
|
+
return proofs;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function forget(contextKey) {
|
|
546
|
+
for (const [key, entry] of entries) if (entry.context.key === contextKey) entries.delete(key);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function close(entry) {
|
|
550
|
+
await entry.nativeTurn.stopProcess("Conversation closed.");
|
|
551
|
+
entry.disposed = true;
|
|
552
|
+
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function deleteConversation(entry) {
|
|
556
|
+
await entry.nativeTurn.stopProcess();
|
|
557
|
+
await deleteClaudeConversationHistory({ configRoot, workdir: entry.nativeWorkdir, conversationId: entry.id });
|
|
558
|
+
await store.remove(entry);
|
|
559
|
+
entries.delete(entry.key);
|
|
560
|
+
return { ok: true, deleted: true, conversationId: entry.id };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function recover(restoredEntries) {
|
|
564
|
+
for (const entry of restoredEntries) {
|
|
565
|
+
if (!entry.process && (entry.executionId || entry.turn?.active)) await entry.nativeTurn.stopProcess(disconnectedMessage);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function stopSelected(select, reason) {
|
|
570
|
+
let closed = 0;
|
|
571
|
+
for (const entry of entries.values()) if (select(entry)) {
|
|
572
|
+
await entry.nativeTurn.stopProcess(reason); closed++;
|
|
573
|
+
}
|
|
574
|
+
return { ok: true, closed };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function wait(entry, input, { context, acquire }) {
|
|
578
|
+
return waitForClaudeConversationTurn({ state: entry,
|
|
579
|
+
stop: reason => entry.nativeTurn.stopProcess(reason),
|
|
580
|
+
read: async current => read(await acquire(context, current.conversationId || current.threadId), current),
|
|
581
|
+
interrupt: () => interrupt(entry)
|
|
582
|
+
}, input, { signal: context.signal, defaultTimeoutMs: entry.persistent ? Infinity : 180_000,
|
|
583
|
+
maximumTimeoutMs: entry.profile?.limits.timeoutMs || Infinity, createError,
|
|
584
|
+
interruptedMessage: disconnectedMessage });
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function stopConversation(entry, reason) {
|
|
588
|
+
await entry.nativeTurn.stopProcess(reason);
|
|
589
|
+
return { ok: true, stopped: true, conversationId: entry.id };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function readGoal(entry) {
|
|
593
|
+
await bindAccount(entry);
|
|
594
|
+
return { status: "available", threadId: entry.id, goal: await entry.goals.read() };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function updateGoal(entry, input, operations) {
|
|
598
|
+
await bindAccount(entry);
|
|
599
|
+
try { await entry.goals.update(input, operations); }
|
|
600
|
+
catch (error) {
|
|
601
|
+
if (error.code !== "assistant_claude_goal_failed") throw error;
|
|
602
|
+
throw createError(error.message);
|
|
603
|
+
}
|
|
604
|
+
await onEvent(entry, { type: "goal-updated" });
|
|
605
|
+
return { ok: true, status: "available", threadId: entry.id, goal: await entry.goals.read() };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function renewal(entry, input, options, { context, acquire }) {
|
|
609
|
+
return runClaudeRenewalTurn({ state: entry, readHistory: () => readHistory(entry),
|
|
610
|
+
send: (message, sendOptions) => send(entry, message, sendOptions),
|
|
611
|
+
wait: async current => wait(await acquire(context, current.conversationId || current.threadId), current, { context, acquire }),
|
|
612
|
+
stop: reason => entry.nativeTurn.stopProcess(reason)
|
|
613
|
+
}, input, options);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function cancel(control, { context, acquire, reportFailure, entry }) {
|
|
617
|
+
if (store.releaseExecution) {
|
|
618
|
+
await entry.nativeTurn.stopProcess("Work stopped.");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const value = await interrupt(await acquire(context || control.current?.context));
|
|
623
|
+
if (control.cleanupFailure) {
|
|
624
|
+
await reportFailure(control.cleanupFailure, { recovered: true });
|
|
625
|
+
control.cleanupFailure = null;
|
|
626
|
+
}
|
|
627
|
+
return value;
|
|
628
|
+
} catch (error) {
|
|
629
|
+
error.cleanupFailed = true;
|
|
630
|
+
control.cleanupFailure = error;
|
|
631
|
+
control.current?.failure.reject(error);
|
|
632
|
+
await reportFailure(error);
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function dispatch(control, command, { acquire, entry: selectedEntry, dispatch: nativeDispatch }) {
|
|
638
|
+
if (store.releaseExecution) {
|
|
639
|
+
await send(selectedEntry, command.input, { command: nativeDispatch });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
command.signal.throwIfAborted();
|
|
643
|
+
const entry = await acquire(command.context);
|
|
644
|
+
const value = await send(entry, { ...command.input.nativeMessage, onPromptSending: command.beforeDispatch });
|
|
645
|
+
const completion = value.conversationTurn ? entry.completion?.promise : null;
|
|
646
|
+
const delivered = { value, completion };
|
|
647
|
+
if (value.delivered === true && value.conversationTurn) {
|
|
648
|
+
if (control.current) control.current.delivery = delivered;
|
|
649
|
+
await command.accept({ nativeResult: { value }, conversationTurn: value.conversationTurn, nativeTurnId: value.turn?.id });
|
|
650
|
+
}
|
|
651
|
+
return delivered;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function steer(control, command, options) {
|
|
655
|
+
if (!store.releaseExecution) return dispatch(control, command, options).then(delivered => ({ value: delivered.value }));
|
|
656
|
+
if (!control.current?.steer || !control.current.entry.nativeTurn.read().active) throw new Error("Claude has no active turn to steer.");
|
|
657
|
+
return control.current.steer(command);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function run(control, command, { acquire, reportFailure, entry: selectedEntry, maximumOutput, maximumToolCalls = 32, timeoutMs } = {}) {
|
|
661
|
+
if (control.disposed || control.current) throw new Error("This Claude conversation is closed or already working.");
|
|
662
|
+
const binding = Boolean(store.releaseExecution);
|
|
663
|
+
const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent } = command;
|
|
664
|
+
const completion = Promise.withResolvers();
|
|
665
|
+
completion.promise.catch(() => {});
|
|
666
|
+
const active = { completion, onMessage, onEvent, signal, tools, admitted: false, nativeTools: new Map(),
|
|
667
|
+
messageId: input.messageId, goal: input.goal, nativeToolCount: 0, toolWork: new Set(), maximumToolCalls,
|
|
668
|
+
context, failure: Promise.withResolvers() };
|
|
669
|
+
let failure;
|
|
670
|
+
active.failure.promise.catch(error => { failure = error; });
|
|
671
|
+
control.current = active;
|
|
672
|
+
const ready = Promise.withResolvers();
|
|
673
|
+
ready.promise.catch(() => {});
|
|
674
|
+
active.steer = async ({ input, beforeDispatch, accept }) => {
|
|
675
|
+
await ready.promise;
|
|
676
|
+
const nativeDispatch = { active, signal, beforeDispatch, accept, timeoutMs, steering: true, interrupted: false, attempted: false, admitted: false };
|
|
677
|
+
try { await dispatch(control, { input, context, signal }, { acquire, entry, dispatch: nativeDispatch }); }
|
|
678
|
+
catch (error) {
|
|
679
|
+
if (nativeDispatch.attempted && !nativeDispatch.admitted) error.delivery = "uncertain";
|
|
680
|
+
if (nativeDispatch.interrupted || nativeDispatch.attempted) completion.reject(error);
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
const nativeDispatch = { active, signal, beforeDispatch, accept, timeoutMs, maximumOutput, attempted: false, admitted: false };
|
|
685
|
+
let entry;
|
|
686
|
+
let cancellation;
|
|
687
|
+
let problem;
|
|
688
|
+
let delivered;
|
|
689
|
+
function abort() {
|
|
690
|
+
if (cancellation) return;
|
|
691
|
+
if (binding) {
|
|
692
|
+
cancellation = entry.nativeTurn.cancel(entry.process?.client, () => entry.nativeTurn.stopProcess("Work stopped."));
|
|
693
|
+
cancellation.then(() => completion.reject(signal.reason), error => completion.reject(error));
|
|
694
|
+
} else {
|
|
695
|
+
cancellation = cancel(control, { context, acquire, reportFailure }).then(() => { throw signal.reason; });
|
|
696
|
+
cancellation.catch(active.failure.reject);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
if (binding) {
|
|
701
|
+
entry = selectedEntry;
|
|
702
|
+
if (entry.disposed || entry.command) throw new Error("This Claude conversation is closed or already working.");
|
|
703
|
+
active.entry = entry;
|
|
704
|
+
entry.command = active;
|
|
705
|
+
entry.completion = completion;
|
|
706
|
+
entry.toolSchemas = tools?.schemas || [];
|
|
707
|
+
active.delivery = { completion: completion.promise };
|
|
708
|
+
nativeDispatch.prepared = await preparation.configuration(entry, { configuration, context, signal, tools });
|
|
709
|
+
}
|
|
710
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
711
|
+
let dispatchFailure;
|
|
712
|
+
try {
|
|
713
|
+
delivered = await dispatch(control, command, { acquire, entry, dispatch: nativeDispatch });
|
|
714
|
+
if (binding) ready.resolve();
|
|
715
|
+
} catch (error) { dispatchFailure = error; }
|
|
716
|
+
for (;;) {
|
|
717
|
+
const settling = active.delivery;
|
|
718
|
+
let result;
|
|
719
|
+
problem = dispatchFailure || failure;
|
|
720
|
+
if (!problem && settling?.completion) {
|
|
721
|
+
try { result = await Promise.race([settling.completion, active.failure.promise]); }
|
|
722
|
+
catch (error) { problem = error; }
|
|
723
|
+
}
|
|
724
|
+
if (command.finishIfCurrent && !await command.finishIfCurrent(() => active.delivery === settling)) continue;
|
|
725
|
+
problem = dispatchFailure || failure || problem;
|
|
726
|
+
if (!problem && settling?.completion && (binding ? result.status !== "completed" : !result.ok)) {
|
|
727
|
+
problem = Object.assign(new Error(result.error || (binding ? "Claude interrupted this answer." : `Claude turn ${result.status}.`)), { status: result.status });
|
|
728
|
+
}
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
} catch (error) { problem = error; }
|
|
732
|
+
ready.reject(problem || new Error("Claude work ended."));
|
|
733
|
+
signal.removeEventListener("abort", abort);
|
|
734
|
+
if (binding) {
|
|
735
|
+
try {
|
|
736
|
+
if (problem || signal.aborted) await (cancellation || entry.nativeTurn.stopProcess(problem?.message || "Work stopped."));
|
|
737
|
+
} catch (error) { problem = error; }
|
|
738
|
+
finally {
|
|
739
|
+
await Promise.allSettled([...active.toolWork]);
|
|
740
|
+
if (entry?.command === active) entry.command = null;
|
|
741
|
+
}
|
|
742
|
+
} else await cancellation?.catch(() => {});
|
|
743
|
+
if (control.current === active) control.current = null;
|
|
744
|
+
if (problem) {
|
|
745
|
+
if (binding && nativeDispatch.attempted && !nativeDispatch.admitted) problem.delivery = "uncertain";
|
|
746
|
+
throw problem;
|
|
747
|
+
}
|
|
748
|
+
if (!binding) return { value: delivered.value };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return Object.freeze({ entries, closingSessions, get closing() { return closing; }, createAccountQueries, open, acquire, createConversation, snapshot, readRetainedTurn, readHistory, ensureReady, ensureProcess, stopForTerminal, prepareTerminal, send, startTurn,
|
|
752
|
+
interrupt, read, inspectAdmission, hasActiveTemporaryConversation, hasActiveConversation, retireConversationHistory, reconcileSessions, restoreSessionEntries, closeSession, closeProject, invalidateRuntimes, drain, forget, close, deleteConversation, recover, stopSelected, wait, stopConversation,
|
|
753
|
+
readGoal, updateGoal, renewal, run, steer, cancel });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Native turn handling moved from the production session provider; hosts persist its events. */
|
|
757
|
+
export function createClaudeConversationTurn({ conversationId, onEvent = async () => {}, process, entry } = {}) {
|
|
758
|
+
if (!conversationId) throw new TypeError("A Claude conversation id is required.");
|
|
759
|
+
const instructions = process ? createClaudeConversationAdapter(process) : null;
|
|
760
|
+
const publish = entry ? publishEntryEvent : onEvent;
|
|
761
|
+
const messages = new Map();
|
|
762
|
+
const admissions = new Map();
|
|
763
|
+
const inFlight = new Set();
|
|
764
|
+
const tasks = new Set();
|
|
765
|
+
let turnId = "";
|
|
766
|
+
let messageId = "";
|
|
767
|
+
let currentCommandId = "";
|
|
768
|
+
let result = "";
|
|
769
|
+
let phase = "";
|
|
770
|
+
let outcome;
|
|
771
|
+
let active = false;
|
|
772
|
+
let stopped = false;
|
|
773
|
+
let cancelling = false;
|
|
774
|
+
let steering;
|
|
775
|
+
let maximumOutput = 4 * 1024 * 1024;
|
|
776
|
+
|
|
777
|
+
function read() {
|
|
778
|
+
return { turnId, active, text: result, phase, status: active ? "inProgress" : outcome?.status || "ready",
|
|
779
|
+
error: outcome?.error || "", messages: structuredClone([...messages.values()]),
|
|
780
|
+
pendingCommands: inFlight.size, backgroundTasks: tasks.size };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function readResult() {
|
|
784
|
+
const current = read();
|
|
785
|
+
const failed = entry.turn?.state === "failed";
|
|
786
|
+
let status = "completed";
|
|
787
|
+
if (entry.turn?.active) status = "inProgress";
|
|
788
|
+
else if (failed) status = "failed";
|
|
789
|
+
else if (entry.turn?.state === "interrupted") status = "interrupted";
|
|
790
|
+
return { ok: !failed, conversationId: entry.id, threadId: entry.id,
|
|
791
|
+
runId: entry.turn?.id || "", turnId: entry.turn?.id || "", text: current.text,
|
|
792
|
+
messages: current.messages, error: entry.turn?.error || "", status };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function updateState(state, message = "") {
|
|
796
|
+
if (!entry.turn) return;
|
|
797
|
+
const active = ["starting", "active", "finalizing"].includes(state);
|
|
798
|
+
const checkpoint = onEvent({ type: "before-state", state, active, message });
|
|
799
|
+
if (checkpoint) await checkpoint;
|
|
800
|
+
setState(state, message);
|
|
801
|
+
await onEvent({ type: "save" });
|
|
802
|
+
await onEvent({ type: "state", state, active, message });
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function setState(state, message = "") {
|
|
806
|
+
if (!entry.turn) return;
|
|
807
|
+
const active = ["starting", "active", "finalizing"].includes(state);
|
|
808
|
+
Object.assign(entry.turn, { active, state, error: message, updatedAt: new Date().toISOString(),
|
|
809
|
+
phase: active && !entry.observationError ? String(entry.turn.phase ?? "").trim() : "" });
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function stopProcess(reason = "") {
|
|
813
|
+
// Runtime bindings commit execution release before dropping the handle.
|
|
814
|
+
// Retained host entries keep their original post-proof checkpoint order.
|
|
815
|
+
const binding = Boolean(process.releaseExecution);
|
|
816
|
+
if (binding && entry.stopPending) return entry.stopPending;
|
|
817
|
+
const active = read().active;
|
|
818
|
+
if (!binding) entry.stopping = true;
|
|
819
|
+
const operation = async () => {
|
|
820
|
+
let proof;
|
|
821
|
+
try {
|
|
822
|
+
if (!binding || entry.process || entry.executionId) {
|
|
823
|
+
proof = await stopClaudeCodeProcess({ process: entry.process, executionId: entry.executionId,
|
|
824
|
+
stopExecution: process.stopExecution });
|
|
825
|
+
}
|
|
826
|
+
} catch (error) {
|
|
827
|
+
if (binding || error.code !== "claude_stop_unconfirmed") throw error;
|
|
828
|
+
entry.observationError = reason || "Claude process exit has not been confirmed. Try Stop again.";
|
|
829
|
+
await onEvent({ type: "save" });
|
|
830
|
+
await onEvent({ type: "stop-unconfirmed" });
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
833
|
+
if (binding && (entry.process || entry.executionId)) await process.releaseExecution();
|
|
834
|
+
entry.process = null;
|
|
835
|
+
entry.executionId = "";
|
|
836
|
+
entry.exitProof = proof;
|
|
837
|
+
stop(binding ? reason : reason || "Claude stopped before acknowledging the prompt.");
|
|
838
|
+
if (binding) {
|
|
839
|
+
if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (entry.turn?.active) await updateState("interrupted", reason);
|
|
843
|
+
else await onEvent({ type: "save" });
|
|
844
|
+
entry.completion?.resolve(readResult());
|
|
845
|
+
return entry.exitProof;
|
|
846
|
+
};
|
|
847
|
+
if (!binding) return operation();
|
|
848
|
+
entry.stopPending = operation().catch(error => { error.cleanupFailed = true; throw error; })
|
|
849
|
+
.finally(() => { entry.stopPending = null; });
|
|
850
|
+
return entry.stopPending;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async function publishEntryEvent(event) {
|
|
854
|
+
if (entry.stopping) return;
|
|
855
|
+
const binding = Boolean(process.releaseExecution);
|
|
856
|
+
if (event.type === "admitted") {
|
|
857
|
+
if (event.userEcho) entry.sent = true;
|
|
858
|
+
if (!binding) await onEvent({ type: "save" });
|
|
859
|
+
} else if (event.type === "phase") {
|
|
860
|
+
entry.turn.phase = event.phase;
|
|
861
|
+
if (!binding) await updateState(entry.turn.state);
|
|
862
|
+
} else if (event.type === "settled") {
|
|
863
|
+
const state = event.status === "interrupted" ? "interrupted" : event.status === "failed" ? "failed" : "completed";
|
|
864
|
+
if (binding) setState(state, event.error);
|
|
865
|
+
else await updateState(state, event.error);
|
|
866
|
+
}
|
|
867
|
+
await onEvent(event);
|
|
868
|
+
if (event.type === "provider-event") {
|
|
869
|
+
const frame = event.event;
|
|
870
|
+
const change = frame.type === "active_goal" ? "goal" : frame.type === "rate_limit_event" ? "usage" : "";
|
|
871
|
+
if (change) await onEvent({ type: "provider-update", change });
|
|
872
|
+
}
|
|
873
|
+
if (event.type === "settled") {
|
|
874
|
+
if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
|
|
875
|
+
entry.completion?.resolve(binding ? event : readResult());
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function checkOutput(value, role = "assistant") {
|
|
880
|
+
if (value.length > (role === "thinking" ? 4 * 1024 * 1024 : maximumOutput)) {
|
|
881
|
+
throw failure("Claude output exceeded its size limit.");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function publishMessage(message) {
|
|
886
|
+
messages.set(message.id, message);
|
|
887
|
+
await publish({ type: "message", message });
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function receive(frame) {
|
|
891
|
+
if (stopped) return;
|
|
892
|
+
if (frame.session_id && frame.session_id !== conversationId) throw failure("Claude returned a different conversation id.");
|
|
893
|
+
if (frame.parent_tool_use_id) return; // Nested agents do not replace the main reply.
|
|
894
|
+
await publish({ type: "provider-event", event: frame });
|
|
895
|
+
if (frame.type === "command_lifecycle" || frame.type === "user") {
|
|
896
|
+
const uuid = frame.type === "user" ? frame.uuid : frame.command_uuid;
|
|
897
|
+
if (frame.type === "user" && !inFlight.has(uuid) && !admissions.has(uuid)) return;
|
|
898
|
+
if (frame.state === "started" || frame.type === "user") currentCommandId = uuid;
|
|
899
|
+
if (frame.state === "cancelled" && steering) {
|
|
900
|
+
inFlight.delete(uuid);
|
|
901
|
+
steering.resolve();
|
|
902
|
+
}
|
|
903
|
+
if (frame.state === "queued" || frame.type === "user") {
|
|
904
|
+
const pending = admissions.get(uuid);
|
|
905
|
+
if (pending) {
|
|
906
|
+
await pending.accept();
|
|
907
|
+
admissions.delete(uuid);
|
|
908
|
+
pending.resolve();
|
|
909
|
+
}
|
|
910
|
+
await publish({ type: "admitted", messageId: uuid, userEcho: frame.type === "user" });
|
|
911
|
+
}
|
|
912
|
+
} else if (frame.type === "system") {
|
|
913
|
+
if (active && ["status", "compact_boundary"].includes(frame.subtype)) {
|
|
914
|
+
const nextPhase = frame.subtype === "status" && frame.status === "compacting" ? "compacting" : "";
|
|
915
|
+
if (nextPhase !== phase) {
|
|
916
|
+
phase = nextPhase;
|
|
917
|
+
await publish({ type: "phase", phase });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (frame.subtype === "local_command_output" && frame.content) {
|
|
921
|
+
await publishMessage({ id: `claude_${frame.uuid}`, role: "commentary", text: frame.content, complete: true });
|
|
922
|
+
}
|
|
923
|
+
if (frame.subtype === "task_started" && ["local_agent", "local_workflow"].includes(frame.task_type)) tasks.add(frame.task_id);
|
|
924
|
+
if (["task_notification", "task_updated"].includes(frame.subtype) &&
|
|
925
|
+
["completed", "failed", "stopped", "killed"].includes(frame.status || frame.patch?.status)) tasks.delete(frame.task_id);
|
|
926
|
+
} else if (frame.type === "stream_event") {
|
|
927
|
+
const event = frame.event || {};
|
|
928
|
+
if (event.type === "message_start") messageId = event.message?.id || frame.uuid;
|
|
929
|
+
const id = `claude_${messageId}_${event.index}`;
|
|
930
|
+
if (event.type === "content_block_start" && ["text", "thinking"].includes(event.content_block?.type)) {
|
|
931
|
+
messages.set(id, { id, outputId: id, complete: false, role: event.content_block.type === "thinking" ? "thinking" : "assistant",
|
|
932
|
+
text: event.content_block.text || event.content_block.thinking || "" });
|
|
933
|
+
} else if (event.type === "content_block_delta") {
|
|
934
|
+
const block = messages.get(id);
|
|
935
|
+
const delta = event.delta?.text ?? event.delta?.thinking;
|
|
936
|
+
if (block && typeof delta === "string") {
|
|
937
|
+
block.text += delta;
|
|
938
|
+
checkOutput(block.text, block.role);
|
|
939
|
+
await publishMessage(block);
|
|
940
|
+
await publish({ type: block.role === "thinking" ? "thinking" : "text", text: delta, messageId: id });
|
|
941
|
+
}
|
|
942
|
+
} else if (event.type === "content_block_stop") {
|
|
943
|
+
messages.delete(id);
|
|
944
|
+
await publish({ type: "message-complete", messageId: id });
|
|
945
|
+
}
|
|
946
|
+
} else if (frame.type === "assistant") {
|
|
947
|
+
for (const block of claudeMessageBlocks(frame)) {
|
|
948
|
+
checkOutput(block.text, block.role);
|
|
949
|
+
// Native split frames arrive before their block's stop event. Keep that
|
|
950
|
+
// existing live identity without replacing the saved frame UUID.
|
|
951
|
+
if (typeof messageId === "string" && messageId && frame.message.id === messageId && frame.message.content.length === 1) {
|
|
952
|
+
const prefix = `claude_${messageId}_`;
|
|
953
|
+
const live = [...messages.values()].filter(message => !message.complete && message.role === block.role &&
|
|
954
|
+
message.id.startsWith(prefix) && /^\d+$/u.test(message.id.slice(prefix.length)));
|
|
955
|
+
if (live.length === 1 && (!Object.hasOwn(frame, "apiBlockIndex") ||
|
|
956
|
+
Number.isSafeInteger(frame.apiBlockIndex) && frame.apiBlockIndex >= 0 &&
|
|
957
|
+
live[0].id === `${prefix}${frame.apiBlockIndex}`)) block.outputId = live[0].id;
|
|
958
|
+
}
|
|
959
|
+
await publishMessage(block);
|
|
960
|
+
}
|
|
961
|
+
} else if (frame.type === "result") {
|
|
962
|
+
result = typeof frame.structured_output === "object" ? JSON.stringify(frame.structured_output) : String(frame.result || "");
|
|
963
|
+
checkOutput(result);
|
|
964
|
+
const failed = frame.is_error || frame.subtype !== "success";
|
|
965
|
+
const message = failed ? (frame.errors || []).join("; ") || result || frame.subtype : "";
|
|
966
|
+
if (result && ![...messages.values()].some((block) => block.complete && block.role === "assistant" && block.text === result)) {
|
|
967
|
+
await publishMessage({ id: `claude_${frame.uuid || turnId}_result`, role: "assistant", text: result, complete: true });
|
|
968
|
+
}
|
|
969
|
+
inFlight.delete(currentCommandId);
|
|
970
|
+
if (steering) {
|
|
971
|
+
steering.resolve();
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (cancelling) return;
|
|
975
|
+
const interrupted = ["aborted_streaming", "aborted_tools"].includes(frame.terminal_reason);
|
|
976
|
+
outcome = { status: interrupted ? "interrupted" : failed ? "failed" : "completed", error: message };
|
|
977
|
+
}
|
|
978
|
+
if ((frame.type === "system" || frame.type === "result") &&
|
|
979
|
+
outcome && inFlight.size === 0 && tasks.size === 0) {
|
|
980
|
+
active = false;
|
|
981
|
+
phase = "";
|
|
982
|
+
await publish({ type: "settled", ...read(), source: frame.type });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function stop(reason = "Claude stopped before acknowledging the prompt.") {
|
|
987
|
+
stopped = true;
|
|
988
|
+
for (const pending of admissions.values()) {
|
|
989
|
+
pending.reject(failure(reason));
|
|
990
|
+
}
|
|
991
|
+
admissions.clear();
|
|
992
|
+
inFlight.clear();
|
|
993
|
+
tasks.clear();
|
|
994
|
+
steering?.reject(failure(reason));
|
|
995
|
+
if (active) outcome = { status: "interrupted", error: reason };
|
|
996
|
+
active = false;
|
|
997
|
+
phase = "";
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function interrupt(client) {
|
|
1001
|
+
cancelling = true;
|
|
1002
|
+
if (client) await client.interrupt();
|
|
1003
|
+
// The process host still must drain its owned scope and call stop().
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return Object.freeze({
|
|
1007
|
+
receive, read, stop, interrupt,
|
|
1008
|
+
...(entry ? { readResult, updateState, stopProcess } : {}),
|
|
1009
|
+
...(instructions ? {
|
|
1010
|
+
prepare: instructions.prepare,
|
|
1011
|
+
whenIdle: instructions.whenIdle,
|
|
1012
|
+
instructionArguments: instructions.instructionArguments,
|
|
1013
|
+
invalidateInstructions: instructions.invalidate
|
|
1014
|
+
} : {}),
|
|
1015
|
+
begin({ id, maxOutputCharacters = 4 * 1024 * 1024 } = {}) {
|
|
1016
|
+
if (active) throw failure("This Claude turn is still working.");
|
|
1017
|
+
if (!id || !Number.isSafeInteger(maxOutputCharacters) || maxOutputCharacters < 1) throw new TypeError("Invalid Claude turn configuration.");
|
|
1018
|
+
turnId = id;
|
|
1019
|
+
maximumOutput = maxOutputCharacters;
|
|
1020
|
+
messages.clear();
|
|
1021
|
+
result = "";
|
|
1022
|
+
phase = "";
|
|
1023
|
+
messageId = "";
|
|
1024
|
+
currentCommandId = "";
|
|
1025
|
+
outcome = null;
|
|
1026
|
+
cancelling = false;
|
|
1027
|
+
stopped = false;
|
|
1028
|
+
active = true;
|
|
1029
|
+
},
|
|
1030
|
+
reset() {
|
|
1031
|
+
if (active || inFlight.size || tasks.size) throw failure("Stop Claude before resetting its turn view.");
|
|
1032
|
+
messages.clear();
|
|
1033
|
+
result = "";
|
|
1034
|
+
outcome = null;
|
|
1035
|
+
},
|
|
1036
|
+
async send(client, message, { messageId: uuid, accept, timeoutMs = 30_000 } = {}) {
|
|
1037
|
+
if (!active || stopped || cancelling) throw failure("Claude has no active turn for this message.");
|
|
1038
|
+
if (!uuid || typeof accept !== "function" || admissions.has(uuid)) throw new TypeError("A distinct message id and admission writer are required.");
|
|
1039
|
+
const admitted = Promise.withResolvers();
|
|
1040
|
+
void admitted.promise.catch(() => {});
|
|
1041
|
+
admissions.set(uuid, { ...admitted, accept });
|
|
1042
|
+
inFlight.add(uuid);
|
|
1043
|
+
const timer = setTimeout(() => admitted.reject(Object.assign(
|
|
1044
|
+
failure("Claude has not acknowledged this prompt. Its delivery is uncertain.", "assistant_claude_admission_unknown"),
|
|
1045
|
+
{ delivery: "uncertain" }
|
|
1046
|
+
)), timeoutMs);
|
|
1047
|
+
try {
|
|
1048
|
+
await Promise.all([client.send(message, { messageId: uuid, sessionId: conversationId }), admitted.promise]);
|
|
1049
|
+
} finally {
|
|
1050
|
+
clearTimeout(timer);
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
async interruptGeneration(client, { timeoutMs = 30_000 } = {}) {
|
|
1054
|
+
if (!active || steering || cancelling) throw failure("This Claude turn cannot accept steering now.");
|
|
1055
|
+
// Streaming user input otherwise queues behind the entire running turn.
|
|
1056
|
+
// Claude's supported steering path interrupts generation, then continues
|
|
1057
|
+
// the same native history with the new instruction.
|
|
1058
|
+
steering = Promise.withResolvers();
|
|
1059
|
+
let timer;
|
|
1060
|
+
try {
|
|
1061
|
+
await Promise.all([client.interrupt(), Promise.race([
|
|
1062
|
+
steering.promise,
|
|
1063
|
+
new Promise((_, reject) => {
|
|
1064
|
+
timer = setTimeout(() => reject(failure("Claude did not confirm the interrupted generation.")), timeoutMs);
|
|
1065
|
+
})
|
|
1066
|
+
])]);
|
|
1067
|
+
}
|
|
1068
|
+
finally { clearTimeout(timer); steering = null; }
|
|
1069
|
+
},
|
|
1070
|
+
async cancel(client, stopProcess) {
|
|
1071
|
+
try {
|
|
1072
|
+
await interrupt(client);
|
|
1073
|
+
} catch {
|
|
1074
|
+
// A verified managed-scope stop also handles an unresponsive control pipe.
|
|
1075
|
+
} finally {
|
|
1076
|
+
// A control acknowledgement is not a process-exit proof. Draining also
|
|
1077
|
+
// cancels queued steering prompts and native background tools.
|
|
1078
|
+
await stopProcess();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
}
|