@jskit-ai/assistant-core 0.2.6 → 0.2.8
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/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +18 -12
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { claudeMessageBlocks } from "./claudeHistory.js";
|
|
3
|
+
import { stopClaudeCodeProcess } from "./claudeProcess.js";
|
|
4
|
+
import { createClaudeConversationAdapter } from "./providers/claude.js";
|
|
5
|
+
|
|
6
|
+
export function claudeTurnFailure(message, { code = "assistant_claude_turn_failed" } = {}) {
|
|
7
|
+
return Object.assign(new Error(message), { code });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Input for a turn that already finished is the caller's refusal; input that
|
|
11
|
+
// meets a stopping turn stays an internal turn failure.
|
|
12
|
+
function steeringRefusal(message, { messageCode, finished }) {
|
|
13
|
+
return finished
|
|
14
|
+
? new AppError(409, message, { code: "conversation_steer_turn_finished", messageCode })
|
|
15
|
+
: Object.assign(claudeTurnFailure(message), { messageCode, messageParams: {} });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CLAUDE_SIGNED_OUT_MESSAGE = "Claude isn't connected. Sign in to Claude, then send your message again.";
|
|
19
|
+
|
|
20
|
+
function setClaudeTurnState(entry, state, message = "") {
|
|
21
|
+
if (!entry.turn) return;
|
|
22
|
+
const active = ["starting", "active", "finalizing"].includes(state);
|
|
23
|
+
Object.assign(entry.turn, { active, state, error: message, updatedAt: new Date().toISOString(),
|
|
24
|
+
phase: active && !entry.observationError ? String(entry.turn.phase ?? "").trim() : "" });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The entry's turn result, with the text and messages the native view holds.
|
|
28
|
+
function claudeTurnResult(entry, current) {
|
|
29
|
+
const failed = entry.turn?.state === "failed";
|
|
30
|
+
let status = "completed";
|
|
31
|
+
if (entry.turn?.active) status = "inProgress";
|
|
32
|
+
else if (failed) status = "failed";
|
|
33
|
+
else if (entry.turn?.state === "interrupted") status = "interrupted";
|
|
34
|
+
return { ok: !failed, conversationId: entry.id, threadId: entry.id,
|
|
35
|
+
runId: entry.turn?.id || "", turnId: entry.turn?.id || "", text: current.text,
|
|
36
|
+
messages: current.messages, error: entry.turn?.error || "", status };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The provider state a native frame changes: its goal or its usage.
|
|
40
|
+
function claudeProviderChange(frame) {
|
|
41
|
+
return frame.type === "active_goal" ? "goal" : frame.type === "rate_limit_event" ? "usage" : "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A frame that shows Claude working on this turn again.
|
|
45
|
+
function continuesClaudeTurn(frame) {
|
|
46
|
+
return (frame.type === "user" && frame.origin?.kind === "task-notification") ||
|
|
47
|
+
(frame.type === "command_lifecycle" && frame.state === "started") ||
|
|
48
|
+
(frame.type === "stream_event" && frame.event?.type === "message_start") || frame.type === "assistant";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The native background tasks a system frame starts, lists or finishes.
|
|
52
|
+
function updateClaudeBackgroundTasks(tasks, frame) {
|
|
53
|
+
if (frame.subtype === "background_tasks_changed") {
|
|
54
|
+
tasks.clear();
|
|
55
|
+
for (const task of frame.tasks || []) if (task.task_id && !task.ambient) tasks.add(task.task_id);
|
|
56
|
+
}
|
|
57
|
+
if (frame.subtype === "task_started" && frame.task_id && !frame.ambient) tasks.add(frame.task_id);
|
|
58
|
+
if (["task_notification", "task_updated"].includes(frame.subtype) &&
|
|
59
|
+
["completed", "failed", "stopped", "killed"].includes(frame.status || frame.patch?.status)) {
|
|
60
|
+
tasks.delete(frame.task_id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Native split frames arrive before their block's stop event. The one live
|
|
65
|
+
// block a frame continues keeps that identity without replacing the saved frame UUID.
|
|
66
|
+
function liveClaudeBlock(messages, messageId, frame, role) {
|
|
67
|
+
if (typeof messageId === "string" && messageId && frame.message.id === messageId && frame.message.content.length === 1) {
|
|
68
|
+
const prefix = `claude_${messageId}_`;
|
|
69
|
+
const live = [...messages.values()].filter(message => !message.complete && message.role === role &&
|
|
70
|
+
message.id.startsWith(prefix) && /^\d+$/u.test(message.id.slice(prefix.length)));
|
|
71
|
+
if (live.length === 1 && (!Object.hasOwn(frame, "apiBlockIndex") ||
|
|
72
|
+
Number.isSafeInteger(frame.apiBlockIndex) && frame.apiBlockIndex >= 0 &&
|
|
73
|
+
live[0].id === `${prefix}${frame.apiBlockIndex}`)) return live[0];
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A conversation whose account signed out under it ends its next request
|
|
79
|
+
// at once with Claude Code's own "Not logged in" answer.
|
|
80
|
+
function claudeResultError(frame, output) {
|
|
81
|
+
return /^Not logged in\b/u.test(output) ? CLAUDE_SIGNED_OUT_MESSAGE
|
|
82
|
+
: (frame.errors || []).join("; ") || output || frame.subtype;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The final answer a result frame carries, with the local command that wrote it.
|
|
86
|
+
function claudeResultMessage(frame, turnId, result) {
|
|
87
|
+
return { id: `claude_${frame.uuid || turnId}_result`, role: "assistant", text: result, complete: true,
|
|
88
|
+
...(typeof frame.local_command === "string" && frame.local_command ? {
|
|
89
|
+
data: { native: { engineId: "claude", type: "result", command: frame.local_command, output: "stdout" } }
|
|
90
|
+
} : {}) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Verified process cleanup can precede Claude's terminal generation frame.
|
|
94
|
+
// Retain already displayed text for the transcript reader, with each block's
|
|
95
|
+
// input/output identity and no completed-answer receipt.
|
|
96
|
+
function retainStoppedClaudeMessages(messages, pendingAssistantMessages) {
|
|
97
|
+
for (const pending of pendingAssistantMessages.values()) for (const block of pending) {
|
|
98
|
+
if (block.outputId && block.outputId !== block.id) messages.delete(block.outputId);
|
|
99
|
+
messages.set(block.id, { ...block, role: "commentary" });
|
|
100
|
+
}
|
|
101
|
+
for (const [id, block] of messages) {
|
|
102
|
+
if (block.complete === false && block.role === "assistant" && block.text) {
|
|
103
|
+
messages.set(id, { ...block, complete: true, role: "commentary" });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The owner's control of one native process, as a native turn uses it.
|
|
110
|
+
* @typedef {import("./providers/claude.js").ClaudeProcessControl & {
|
|
111
|
+
* stopExecution?: (executionId: string, options?: object) => Promise<object>,
|
|
112
|
+
* releaseExecution?: () => Promise<unknown>
|
|
113
|
+
* }} ClaudeTurnProcess
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* One native Claude turn: its admission, events and results; hosts persist its events.
|
|
118
|
+
* A turn with an owner's retained entry also reads results and stops its process.
|
|
119
|
+
* @param {{
|
|
120
|
+
* conversationId: string,
|
|
121
|
+
* onEvent: (event: Record<string, any>) => unknown,
|
|
122
|
+
* process?: ClaudeTurnProcess,
|
|
123
|
+
* entry?: Record<string, any>,
|
|
124
|
+
* bound?: boolean
|
|
125
|
+
* }} options
|
|
126
|
+
*/
|
|
127
|
+
export function createClaudeConversationTurn({ conversationId, onEvent, process, entry, bound = false }) {
|
|
128
|
+
if (!conversationId) throw new TypeError("A Claude conversation id is required.");
|
|
129
|
+
const instructions = process ? createClaudeConversationAdapter(process) : null;
|
|
130
|
+
const publish = entry ? publishEntryEvent : onEvent;
|
|
131
|
+
const messages = new Map();
|
|
132
|
+
const pendingAssistantMessages = new Map();
|
|
133
|
+
const assistantRoles = new Map();
|
|
134
|
+
const admissions = new Map();
|
|
135
|
+
const inFlight = new Set();
|
|
136
|
+
// Inputs Claude has begun reading. A queued steer joins the running loop, so
|
|
137
|
+
// one result can finish several of them; queued-only input stays in flight.
|
|
138
|
+
const startedInputs = new Set();
|
|
139
|
+
// Steers written into a running loop that Claude has not begun reading. Claude
|
|
140
|
+
// Code reads queued input only when the running tool call returns.
|
|
141
|
+
const waitingSteers = new Set();
|
|
142
|
+
let steeringClient = null;
|
|
143
|
+
const tasks = new Set();
|
|
144
|
+
let turnId = "";
|
|
145
|
+
let messageId = "";
|
|
146
|
+
let currentCommandId = "";
|
|
147
|
+
let result = "";
|
|
148
|
+
let phase = "";
|
|
149
|
+
let outcome;
|
|
150
|
+
let active = false;
|
|
151
|
+
let generation = 0;
|
|
152
|
+
let stopped = false;
|
|
153
|
+
let cancelling = false;
|
|
154
|
+
let maximumOutput = 4 * 1024 * 1024;
|
|
155
|
+
let inputOwner = null;
|
|
156
|
+
let acceptedInput = null;
|
|
157
|
+
let publishedAssistant = null;
|
|
158
|
+
let finalAssistantResult = null;
|
|
159
|
+
let resolvedCompletion;
|
|
160
|
+
let latestSettlement;
|
|
161
|
+
// A common command fences its first checkpoint; later native continuations
|
|
162
|
+
// keep the same input but cannot enqueue onto that already resolved command.
|
|
163
|
+
let checkpointedCommand;
|
|
164
|
+
|
|
165
|
+
function clearFinalAssistantResult() {
|
|
166
|
+
publishedAssistant = null;
|
|
167
|
+
finalAssistantResult = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function invalidateFinalAssistantResult() {
|
|
171
|
+
inputOwner = null;
|
|
172
|
+
acceptedInput = null;
|
|
173
|
+
clearFinalAssistantResult();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function clearTurnView() {
|
|
177
|
+
messages.clear();
|
|
178
|
+
pendingAssistantMessages.clear();
|
|
179
|
+
assistantRoles.clear();
|
|
180
|
+
result = "";
|
|
181
|
+
outcome = null;
|
|
182
|
+
invalidateFinalAssistantResult();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// A waiting steer would otherwise sit behind a long foreground command (or a
|
|
186
|
+
// wait loop) until it returns. Claude's Ctrl+B control moves foreground Bash
|
|
187
|
+
// and subagents to the background: they keep running and report completion,
|
|
188
|
+
// and the blocked tool call returns so Claude reads the steer now.
|
|
189
|
+
function backgroundForegroundWork() {
|
|
190
|
+
const client = steeringClient;
|
|
191
|
+
if (!waitingSteers.size || !client?.request || stopped || cancelling) return;
|
|
192
|
+
void client.request({ subtype: "background_tasks" }).then(
|
|
193
|
+
() => publish({ type: "diagnostic", stage: "steering_backgrounded", pendingCommands: inFlight.size, backgroundTasks: tasks.size }),
|
|
194
|
+
// An older CLI still reads the steer when its tool call returns.
|
|
195
|
+
error => publish({ type: "diagnostic", stage: "steering_background_failed", error: error.message })
|
|
196
|
+
).catch(() => {});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function currentReceiptOwner(owner) {
|
|
200
|
+
return Boolean(entry && owner && inputOwner === owner && acceptedInput === owner &&
|
|
201
|
+
owner.commandId === currentCommandId && entry.process && entry.process === owner.process &&
|
|
202
|
+
entry.executionId === owner.executionId && entry.accountIdentity === owner.accountIdentity &&
|
|
203
|
+
entry.turn?.id === turnId && !entry.stopping && !entry.stopPending && !entry.disposed &&
|
|
204
|
+
!entry.observationError && !entry.command?.toolWork.size && !entry.command?.toolFailure);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readFinalAssistantResult(threadId, expectedTurnId) {
|
|
208
|
+
if (threadId !== conversationId || expectedTurnId !== turnId || !finalAssistantResult ||
|
|
209
|
+
active || stopped || cancelling || outcome?.status !== "completed" || outcome.error ||
|
|
210
|
+
inFlight.size || tasks.size || admissions.size || !currentReceiptOwner(acceptedInput)) return null;
|
|
211
|
+
return structuredClone(finalAssistantResult);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function read() {
|
|
215
|
+
return { turnId, active, text: result, phase, status: active ? "inProgress" : outcome?.status || "ready",
|
|
216
|
+
error: outcome?.error || "", messages: structuredClone([...messages.values()]),
|
|
217
|
+
pendingCommands: inFlight.size, backgroundTasks: tasks.size };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readResult() {
|
|
221
|
+
return claudeTurnResult(entry, read());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveCompletion(result) {
|
|
225
|
+
entry.completion?.resolve(result);
|
|
226
|
+
resolvedCompletion = entry.completion;
|
|
227
|
+
if (entry.command?.finishIfCurrent) entry.command.completion.resolve(result);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function updateState(state, message = "", expectedGeneration = generation) {
|
|
231
|
+
if (!entry.turn || expectedGeneration !== generation) return false;
|
|
232
|
+
const active = ["starting", "active", "finalizing"].includes(state);
|
|
233
|
+
const checkpoint = onEvent({ type: "before-state", state, active, message });
|
|
234
|
+
if (checkpoint) await checkpoint;
|
|
235
|
+
if (expectedGeneration !== generation) return false;
|
|
236
|
+
setClaudeTurnState(entry, state, message);
|
|
237
|
+
await onEvent({ type: "save" });
|
|
238
|
+
if (expectedGeneration !== generation) return false;
|
|
239
|
+
await onEvent({ type: "state", state, active, message });
|
|
240
|
+
return expectedGeneration === generation;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function stopProcess(reason = "", { preparing = false } = {}) {
|
|
244
|
+
clearTimeout(entry.goalToolStop?.timer);
|
|
245
|
+
entry.goalToolStop = null;
|
|
246
|
+
if (!preparing) entry.preparationRevision++;
|
|
247
|
+
invalidateFinalAssistantResult();
|
|
248
|
+
// Runtime bindings commit execution release before dropping the handle.
|
|
249
|
+
// Retained host entries keep their original post-proof checkpoint order.
|
|
250
|
+
if (entry.stopPending) return entry.stopPending;
|
|
251
|
+
const active = read().active;
|
|
252
|
+
if (!bound) entry.stopping = true;
|
|
253
|
+
const operation = async () => {
|
|
254
|
+
const startup = entry.startupCleanup?.executionId === entry.executionId ? entry.startupCleanup : null;
|
|
255
|
+
let proof;
|
|
256
|
+
try {
|
|
257
|
+
if (!bound || entry.process || entry.executionId) {
|
|
258
|
+
proof = await stopClaudeCodeProcess({ process: entry.process || startup, executionId: entry.executionId,
|
|
259
|
+
stopExecution: process.stopExecution });
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (bound || error.code !== "claude_stop_unconfirmed") throw error;
|
|
263
|
+
entry.observationError = reason || "Claude process exit has not been confirmed. Try Stop again.";
|
|
264
|
+
await onEvent({ type: "save" });
|
|
265
|
+
await onEvent({ type: "stop-unconfirmed" });
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
if (bound && (entry.process || entry.executionId)) await process.releaseExecution();
|
|
269
|
+
entry.process = null;
|
|
270
|
+
entry.executionId = "";
|
|
271
|
+
entry.exitProof = proof;
|
|
272
|
+
stop(bound ? reason : reason || "Claude stopped before acknowledging the prompt.");
|
|
273
|
+
if (bound) {
|
|
274
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
275
|
+
if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (entry.turn?.active) {
|
|
279
|
+
try { await updateState("interrupted", reason); }
|
|
280
|
+
catch (error) {
|
|
281
|
+
// Native cleanup is verified even when the host refuses its checkpoint.
|
|
282
|
+
// Retain that refusal without publishing a completed answer or active work.
|
|
283
|
+
setClaudeTurnState(entry, "interrupted", error.message);
|
|
284
|
+
await onEvent({ type: "save" });
|
|
285
|
+
await onEvent({ type: "state", state: "interrupted", active: false, message: error.message });
|
|
286
|
+
resolveCompletion(readResult());
|
|
287
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
} else await onEvent({ type: "save" });
|
|
291
|
+
resolveCompletion(readResult());
|
|
292
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
293
|
+
return entry.exitProof;
|
|
294
|
+
};
|
|
295
|
+
entry.stopPending = operation().catch(error => { if (bound) error.cleanupFailed = true; throw error; })
|
|
296
|
+
.finally(() => { entry.stopPending = null; });
|
|
297
|
+
return entry.stopPending;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function publishEntryEvent(event, deferred = false) {
|
|
301
|
+
if (entry.stopping) return;
|
|
302
|
+
if (event.type === "settled" && event.generation !== generation) return;
|
|
303
|
+
const command = entry.command;
|
|
304
|
+
if (event.type === "settled" && !bound && !deferred && command?.finishIfCurrent && command !== checkpointedCommand) {
|
|
305
|
+
const delivery = command.delivery;
|
|
306
|
+
const completion = entry.completion;
|
|
307
|
+
const owner = acceptedInput;
|
|
308
|
+
// Release the native queue before the common run joins reserved steering.
|
|
309
|
+
// That same accepted delivery then performs the original retained checkpoint.
|
|
310
|
+
command.completion.resolve(async () => {
|
|
311
|
+
try {
|
|
312
|
+
for (;;) {
|
|
313
|
+
if (entry.command !== command || command.delivery !== delivery || entry.completion !== completion ||
|
|
314
|
+
command.signal.aborted || !currentReceiptOwner(owner)) {
|
|
315
|
+
throw claudeTurnFailure("Claude's completion belongs to a retired native input.");
|
|
316
|
+
}
|
|
317
|
+
// Follow-ups share this input, but can finish while its checkpoint
|
|
318
|
+
// awaits storage. Apply the latest settlement after the same fence.
|
|
319
|
+
if (active) return readResult();
|
|
320
|
+
const settlement = latestSettlement;
|
|
321
|
+
await publishEntryEvent(settlement, true);
|
|
322
|
+
if (settlement === latestSettlement) return readResult();
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
clearFinalAssistantResult();
|
|
326
|
+
throw error;
|
|
327
|
+
} finally {
|
|
328
|
+
checkpointedCommand = command;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (event.type === "continued") {
|
|
334
|
+
if (entry.completion === resolvedCompletion) entry.completion = Promise.withResolvers();
|
|
335
|
+
if (bound) setClaudeTurnState(entry, "active");
|
|
336
|
+
else await updateState("active");
|
|
337
|
+
} else if (event.type === "admitted") {
|
|
338
|
+
if (event.userEcho) entry.sent = true;
|
|
339
|
+
if (!bound) await onEvent({ type: "save" });
|
|
340
|
+
} else if (event.type === "phase") {
|
|
341
|
+
entry.turn.phase = event.phase;
|
|
342
|
+
if (!bound) await updateState(entry.turn.state);
|
|
343
|
+
} else if (event.type === "settled") {
|
|
344
|
+
const state = event.status === "interrupted" ? "interrupted" : event.status === "failed" ? "failed" : "completed";
|
|
345
|
+
if (bound) setClaudeTurnState(entry, state, event.error);
|
|
346
|
+
else if (!await updateState(state, event.error, event.generation)) return;
|
|
347
|
+
}
|
|
348
|
+
const published = await onEvent(event);
|
|
349
|
+
const change = event.type === "provider-event" ? claudeProviderChange(event.event) : "";
|
|
350
|
+
if (change) await onEvent({ type: "provider-update", change });
|
|
351
|
+
if (event.type === "settled") {
|
|
352
|
+
if (event.generation !== generation) return;
|
|
353
|
+
if (event.source === "result") await onEvent({ type: "provider-update", change: "goal" });
|
|
354
|
+
resolveCompletion(bound ? event : readResult());
|
|
355
|
+
}
|
|
356
|
+
return published;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function checkOutput(value, role = "assistant") {
|
|
360
|
+
if (value.length > (role === "thinking" ? 4 * 1024 * 1024 : maximumOutput)) {
|
|
361
|
+
throw claudeTurnFailure("Claude output exceeded its size limit.");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function publishMessage(message) {
|
|
366
|
+
if (!message.userId && currentCommandId) message = { ...message, userId: currentCommandId };
|
|
367
|
+
messages.set(message.id, message);
|
|
368
|
+
const owner = acceptedInput;
|
|
369
|
+
const completeAssistant = message.complete && message.role === "assistant";
|
|
370
|
+
if (completeAssistant) clearFinalAssistantResult();
|
|
371
|
+
const conversationTurn = await publish({ type: "message", message });
|
|
372
|
+
if (completeAssistant && message.text && currentReceiptOwner(owner) && !stopped && !cancelling &&
|
|
373
|
+
conversationTurn && typeof conversationTurn === "object" && !Array.isArray(conversationTurn)) {
|
|
374
|
+
publishedAssistant = { owner, itemId: message.id, outputId: message.outputId || message.id,
|
|
375
|
+
text: message.text, conversationTurn: structuredClone(conversationTurn) };
|
|
376
|
+
}
|
|
377
|
+
return conversationTurn;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function flushAssistantMessages(apiMessageId, role) {
|
|
381
|
+
assistantRoles.set(apiMessageId, role);
|
|
382
|
+
const pending = pendingAssistantMessages.get(apiMessageId) || [];
|
|
383
|
+
pendingAssistantMessages.delete(apiMessageId);
|
|
384
|
+
for (const block of pending) {
|
|
385
|
+
await publishMessage({ ...block, role });
|
|
386
|
+
if (block.outputId) {
|
|
387
|
+
if (block.outputId !== block.id) messages.delete(block.outputId);
|
|
388
|
+
await publish({ type: "message-complete", messageId: block.outputId });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function receive(frame) {
|
|
394
|
+
if (stopped) return;
|
|
395
|
+
if (frame.session_id && frame.session_id !== conversationId) throw claudeTurnFailure("Claude returned a different conversation id.");
|
|
396
|
+
if (frame.parent_tool_use_id) return; // Nested agents do not replace the main reply.
|
|
397
|
+
await publish({ type: "provider-event", event: frame });
|
|
398
|
+
if (continuesClaudeTurn(frame) && turnId && !cancelling) {
|
|
399
|
+
const wasActive = active;
|
|
400
|
+
if (outcome || !active) {
|
|
401
|
+
generation++;
|
|
402
|
+
outcome = null;
|
|
403
|
+
result = "";
|
|
404
|
+
clearFinalAssistantResult();
|
|
405
|
+
}
|
|
406
|
+
active = true;
|
|
407
|
+
if (!wasActive) await publish({ type: "continued", source: frame.type });
|
|
408
|
+
if (phase === "background") {
|
|
409
|
+
phase = "";
|
|
410
|
+
await publish({ type: "phase", phase });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (frame.type === "command_lifecycle" || frame.type === "user") {
|
|
414
|
+
const uuid = frame.type === "user" ? frame.uuid : frame.command_uuid;
|
|
415
|
+
if (frame.type === "user" && !inFlight.has(uuid) && !admissions.has(uuid)) return;
|
|
416
|
+
// Native notifications have their own lifecycle UUIDs, including work
|
|
417
|
+
// folded into this input. They must not steal its receipt/result owner.
|
|
418
|
+
if ((frame.state === "started" || frame.type === "user") && inFlight.has(uuid)) {
|
|
419
|
+
currentCommandId = uuid;
|
|
420
|
+
startedInputs.add(uuid);
|
|
421
|
+
}
|
|
422
|
+
if (frame.state !== "queued") waitingSteers.delete(uuid);
|
|
423
|
+
if (frame.state === "completed") {
|
|
424
|
+
inFlight.delete(uuid);
|
|
425
|
+
startedInputs.delete(uuid);
|
|
426
|
+
}
|
|
427
|
+
if (frame.state === "queued" || frame.type === "user") {
|
|
428
|
+
const pending = admissions.get(uuid);
|
|
429
|
+
if (pending) {
|
|
430
|
+
// Once acknowledged, storage owns the admission decision. Its write
|
|
431
|
+
// must not race a native-ack timeout and a second submission.
|
|
432
|
+
clearTimeout(pending.timer);
|
|
433
|
+
try {
|
|
434
|
+
await pending.accept();
|
|
435
|
+
if (inputOwner === pending.receiptOwner && !stopped && !cancelling) acceptedInput = pending.receiptOwner;
|
|
436
|
+
admissions.delete(uuid);
|
|
437
|
+
pending.resolve();
|
|
438
|
+
} catch (error) {
|
|
439
|
+
acceptedInput = null;
|
|
440
|
+
clearFinalAssistantResult();
|
|
441
|
+
pending.reject(error);
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
await publish({ type: "admitted", messageId: uuid, userEcho: frame.type === "user" });
|
|
446
|
+
}
|
|
447
|
+
} else if (frame.type === "system") {
|
|
448
|
+
if (active && ["status", "compact_boundary"].includes(frame.subtype)) {
|
|
449
|
+
const nextPhase = frame.subtype === "status" && frame.status === "compacting" ? "compacting" : "";
|
|
450
|
+
// An idle status does not end a wait that only background tasks own.
|
|
451
|
+
if (nextPhase !== phase && (nextPhase || phase !== "background")) {
|
|
452
|
+
phase = nextPhase;
|
|
453
|
+
await publish({ type: "phase", phase });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (frame.subtype === "local_command_output" && frame.content) {
|
|
457
|
+
await publishMessage({ id: `claude_${frame.uuid}`, role: "commentary", text: frame.content, complete: true,
|
|
458
|
+
data: { native: { engineId: "claude", type: "system", subtype: "local_command_output" } } });
|
|
459
|
+
}
|
|
460
|
+
const previousTaskCount = tasks.size;
|
|
461
|
+
updateClaudeBackgroundTasks(tasks, frame);
|
|
462
|
+
// A command Claude started before reading a waiting steer would hold it too.
|
|
463
|
+
if (frame.subtype === "task_started") backgroundForegroundWork();
|
|
464
|
+
// Inventory removal can precede the task notification and the main model's
|
|
465
|
+
// response to it. A foreground result written while that worker was live
|
|
466
|
+
// cannot complete this follow-up or retire its admitted application tools.
|
|
467
|
+
if (tasks.size < previousTaskCount) outcome = null;
|
|
468
|
+
if (phase === "background" && !outcome) {
|
|
469
|
+
phase = "";
|
|
470
|
+
await publish({ type: "phase", phase });
|
|
471
|
+
}
|
|
472
|
+
if (["background_tasks_changed", "task_started", "task_notification", "task_updated"].includes(frame.subtype)) {
|
|
473
|
+
if (tasks.size && turnId && !active && !cancelling) {
|
|
474
|
+
generation++;
|
|
475
|
+
active = true;
|
|
476
|
+
clearFinalAssistantResult();
|
|
477
|
+
await publish({ type: "continued", source: frame.subtype });
|
|
478
|
+
}
|
|
479
|
+
await publish({ type: "diagnostic", stage: "background_tasks", nativeEvent: frame.subtype,
|
|
480
|
+
active, pendingCommands: inFlight.size, backgroundTasks: tasks.size });
|
|
481
|
+
}
|
|
482
|
+
} else if (frame.type === "stream_event") {
|
|
483
|
+
const event = frame.event || {};
|
|
484
|
+
if (event.type === "message_start") messageId = event.message?.id || frame.uuid;
|
|
485
|
+
const id = `claude_${messageId}_${event.index}`;
|
|
486
|
+
if (event.type === "content_block_start" && ["text", "thinking"].includes(event.content_block?.type)) {
|
|
487
|
+
messages.set(id, { id, outputId: id, userId: currentCommandId, complete: false, role: event.content_block.type === "thinking" ? "thinking" : "assistant",
|
|
488
|
+
text: event.content_block.text || event.content_block.thinking || "" });
|
|
489
|
+
} else if (event.type === "content_block_delta") {
|
|
490
|
+
const block = messages.get(id);
|
|
491
|
+
const delta = event.delta?.text ?? event.delta?.thinking;
|
|
492
|
+
if (block && typeof delta === "string") {
|
|
493
|
+
block.text += delta;
|
|
494
|
+
checkOutput(block.text, block.role);
|
|
495
|
+
await publishMessage(block);
|
|
496
|
+
await publish({ type: block.role === "thinking" ? "thinking" : "text", text: delta, messageId: id });
|
|
497
|
+
}
|
|
498
|
+
} else if (event.type === "content_block_stop") {
|
|
499
|
+
if (![...pendingAssistantMessages.values()].some(blocks => blocks.some(block => block.outputId === id))) {
|
|
500
|
+
messages.delete(id);
|
|
501
|
+
await publish({ type: "message-complete", messageId: id });
|
|
502
|
+
}
|
|
503
|
+
} else if (event.type === "message_delta" && event.delta?.stop_reason) {
|
|
504
|
+
await flushAssistantMessages(messageId, event.delta.stop_reason === "tool_use" ? "commentary" : "assistant");
|
|
505
|
+
}
|
|
506
|
+
} else if (frame.type === "assistant") {
|
|
507
|
+
const apiMessageId = frame.message?.id;
|
|
508
|
+
if (apiMessageId && frame.message.content?.some(block => block.type === "tool_use")) {
|
|
509
|
+
await flushAssistantMessages(apiMessageId, "commentary");
|
|
510
|
+
}
|
|
511
|
+
for (const block of claudeMessageBlocks(frame)) {
|
|
512
|
+
block.userId = currentCommandId;
|
|
513
|
+
checkOutput(block.text, block.role);
|
|
514
|
+
const live = liveClaudeBlock(messages, messageId, frame, block.role);
|
|
515
|
+
if (live) {
|
|
516
|
+
block.outputId = live.id;
|
|
517
|
+
block.userId = live.userId || block.userId;
|
|
518
|
+
}
|
|
519
|
+
if (apiMessageId && block.role === "assistant") {
|
|
520
|
+
const role = assistantRoles.get(apiMessageId);
|
|
521
|
+
if (role) await publishMessage({ ...block, role });
|
|
522
|
+
else {
|
|
523
|
+
const pending = pendingAssistantMessages.get(apiMessageId) || [];
|
|
524
|
+
pending.push(block);
|
|
525
|
+
pendingAssistantMessages.set(apiMessageId, pending);
|
|
526
|
+
}
|
|
527
|
+
} else await publishMessage(block);
|
|
528
|
+
}
|
|
529
|
+
if (apiMessageId && frame.message.stop_reason) {
|
|
530
|
+
await flushAssistantMessages(apiMessageId, frame.message.stop_reason === "tool_use" ? "commentary" : "assistant");
|
|
531
|
+
}
|
|
532
|
+
} else if (frame.type === "result") {
|
|
533
|
+
const failed = frame.is_error || frame.subtype !== "success";
|
|
534
|
+
const interrupted = ["aborted_streaming", "aborted_tools"].includes(frame.terminal_reason);
|
|
535
|
+
const role = failed || interrupted ? "commentary" : "assistant";
|
|
536
|
+
for (const id of pendingAssistantMessages.keys()) await flushAssistantMessages(id, role);
|
|
537
|
+
const output = typeof frame.structured_output === "object" ? JSON.stringify(frame.structured_output) : String(frame.result || "");
|
|
538
|
+
checkOutput(output);
|
|
539
|
+
const message = !failed ? "" : claudeResultError(frame, output);
|
|
540
|
+
result = failed ? "" : output;
|
|
541
|
+
if (result && ![...messages.values()].some((block) => block.complete && block.role === "assistant" && block.text === result)) {
|
|
542
|
+
await publishMessage(claudeResultMessage(frame, turnId, result));
|
|
543
|
+
}
|
|
544
|
+
inFlight.delete(currentCommandId);
|
|
545
|
+
for (const uuid of startedInputs) inFlight.delete(uuid);
|
|
546
|
+
startedInputs.clear();
|
|
547
|
+
if (cancelling) return;
|
|
548
|
+
outcome = { status: interrupted ? "interrupted" : failed ? "failed" : "completed", error: message };
|
|
549
|
+
// The reply is final, but native background tasks still own the turn
|
|
550
|
+
// (and may continue it). Keep it active and say what it is waiting for.
|
|
551
|
+
if (active && tasks.size && !inFlight.size && phase !== "background") {
|
|
552
|
+
phase = "background";
|
|
553
|
+
await publish({ type: "phase", phase });
|
|
554
|
+
}
|
|
555
|
+
await publish({ type: "diagnostic", stage: "native_result", status: outcome.status,
|
|
556
|
+
terminalReason: frame.terminal_reason, active, pendingCommands: inFlight.size, backgroundTasks: tasks.size });
|
|
557
|
+
}
|
|
558
|
+
if ((frame.type === "system" || frame.type === "result" || frame.type === "command_lifecycle" && frame.state === "completed") &&
|
|
559
|
+
active && !cancelling && outcome && inFlight.size === 0 && tasks.size === 0) {
|
|
560
|
+
active = false;
|
|
561
|
+
phase = "";
|
|
562
|
+
const candidate = publishedAssistant;
|
|
563
|
+
finalAssistantResult = outcome.status === "completed" && !outcome.error && !stopped && !cancelling &&
|
|
564
|
+
admissions.size === 0 && candidate && currentReceiptOwner(candidate.owner) && candidate.text === result
|
|
565
|
+
? { threadId: conversationId, turnId, inputMessageId: candidate.owner.commandId,
|
|
566
|
+
itemId: candidate.itemId, outputId: candidate.outputId, text: candidate.text,
|
|
567
|
+
conversationTurn: candidate.conversationTurn } : null;
|
|
568
|
+
latestSettlement = { type: "settled", ...read(), generation, source: frame.type };
|
|
569
|
+
try { await publish(latestSettlement); }
|
|
570
|
+
catch (error) { clearFinalAssistantResult(); throw error; }
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function stop(reason = "Claude stopped before acknowledging the prompt.") {
|
|
575
|
+
generation++;
|
|
576
|
+
stopped = true;
|
|
577
|
+
invalidateFinalAssistantResult();
|
|
578
|
+
for (const pending of admissions.values()) {
|
|
579
|
+
pending.reject(claudeTurnFailure(reason));
|
|
580
|
+
}
|
|
581
|
+
admissions.clear();
|
|
582
|
+
inFlight.clear();
|
|
583
|
+
startedInputs.clear();
|
|
584
|
+
waitingSteers.clear();
|
|
585
|
+
steeringClient = null;
|
|
586
|
+
tasks.clear();
|
|
587
|
+
retainStoppedClaudeMessages(messages, pendingAssistantMessages);
|
|
588
|
+
pendingAssistantMessages.clear();
|
|
589
|
+
assistantRoles.clear();
|
|
590
|
+
if (active) outcome = { status: "interrupted", error: reason };
|
|
591
|
+
active = false;
|
|
592
|
+
phase = "";
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function interrupt(client) {
|
|
596
|
+
cancelling = true;
|
|
597
|
+
clearFinalAssistantResult();
|
|
598
|
+
if (client) await client.interrupt();
|
|
599
|
+
// The process host still must drain its owned scope and call stop().
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function begin({ id, maxOutputCharacters = 4 * 1024 * 1024 } = {}) {
|
|
603
|
+
if (active) throw claudeTurnFailure("This Claude turn is still working.");
|
|
604
|
+
if (!id || !Number.isSafeInteger(maxOutputCharacters) || maxOutputCharacters < 1) throw new TypeError("Invalid Claude turn configuration.");
|
|
605
|
+
generation++;
|
|
606
|
+
clearTurnView();
|
|
607
|
+
turnId = id;
|
|
608
|
+
maximumOutput = maxOutputCharacters;
|
|
609
|
+
phase = "";
|
|
610
|
+
messageId = "";
|
|
611
|
+
currentCommandId = "";
|
|
612
|
+
cancelling = false;
|
|
613
|
+
stopped = false;
|
|
614
|
+
active = true;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function reset() {
|
|
618
|
+
if (active || inFlight.size || tasks.size) throw claudeTurnFailure("Stop Claude before resetting its turn view.");
|
|
619
|
+
clearTurnView();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function send(client, message, { messageId: uuid, accept, timeoutMs = 30_000, steer = false } = {}) {
|
|
623
|
+
if (!active || stopped || cancelling) {
|
|
624
|
+
throw Object.assign(steeringRefusal("Claude has no active turn for this message.", {
|
|
625
|
+
messageCode: "assistant.claude.noActiveTurn", finished: !active && !stopped && !cancelling }), { delivery: "not-sent" });
|
|
626
|
+
}
|
|
627
|
+
if (!uuid || typeof accept !== "function" || admissions.has(uuid)) throw new TypeError("A distinct message id and admission writer are required.");
|
|
628
|
+
clearFinalAssistantResult();
|
|
629
|
+
acceptedInput = null;
|
|
630
|
+
inputOwner = entry ? { commandId: uuid, process: entry.process, executionId: entry.executionId,
|
|
631
|
+
accountIdentity: entry.accountIdentity } : null;
|
|
632
|
+
const receiptOwner = inputOwner;
|
|
633
|
+
const admitted = Promise.withResolvers();
|
|
634
|
+
void admitted.promise.catch(() => {});
|
|
635
|
+
inFlight.add(uuid);
|
|
636
|
+
if (steer) {
|
|
637
|
+
waitingSteers.add(uuid);
|
|
638
|
+
steeringClient = client;
|
|
639
|
+
}
|
|
640
|
+
const timer = setTimeout(() => admitted.reject(Object.assign(
|
|
641
|
+
claudeTurnFailure("Claude has not acknowledged this prompt. Its delivery is uncertain.", { code: "assistant_claude_admission_unknown" }),
|
|
642
|
+
{ delivery: "uncertain" }
|
|
643
|
+
)), timeoutMs);
|
|
644
|
+
admissions.set(uuid, { ...admitted, accept, timer, receiptOwner });
|
|
645
|
+
try {
|
|
646
|
+
await Promise.all([client.send(message, { messageId: uuid, sessionId: conversationId })
|
|
647
|
+
.then(() => { if (steer) backgroundForegroundWork(); }), admitted.promise]);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
waitingSteers.delete(uuid);
|
|
650
|
+
if (inputOwner === receiptOwner) invalidateFinalAssistantResult();
|
|
651
|
+
throw error;
|
|
652
|
+
} finally {
|
|
653
|
+
clearTimeout(timer);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function prepareSteering() {
|
|
658
|
+
if (!active || cancelling) {
|
|
659
|
+
const completed = !active && !stopped && !cancelling && outcome?.status === "completed";
|
|
660
|
+
throw Object.assign(steeringRefusal("This Claude turn cannot accept steering now.", {
|
|
661
|
+
messageCode: "assistant.claude.steeringUnavailable", finished: completed }),
|
|
662
|
+
{ delivery: "not-sent" });
|
|
663
|
+
}
|
|
664
|
+
// Claude Code acknowledges streamed input as queued and folds it into the
|
|
665
|
+
// running turn after the current tool call, or starts it once the main
|
|
666
|
+
// loop is idle. Interrupting instead would kill a running foreground tool;
|
|
667
|
+
// send() moves such a tool to the background so the steer is read now.
|
|
668
|
+
// The next input owns completion now: a result or task finishing before
|
|
669
|
+
// it starts must not settle this turn with the previous answer.
|
|
670
|
+
outcome = null;
|
|
671
|
+
acceptedInput = null;
|
|
672
|
+
clearFinalAssistantResult();
|
|
673
|
+
await publish({ type: "diagnostic", stage: "steering_queued", pendingCommands: inFlight.size, backgroundTasks: tasks.size });
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function cancel(client, stopProcess) {
|
|
677
|
+
try {
|
|
678
|
+
await interrupt(client);
|
|
679
|
+
} catch {
|
|
680
|
+
// A verified managed-scope stop also handles an unresponsive control pipe.
|
|
681
|
+
} finally {
|
|
682
|
+
// A control acknowledgement is not a process-exit proof. Draining also
|
|
683
|
+
// cancels queued steering prompts and native background tools.
|
|
684
|
+
await stopProcess();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return Object.freeze({
|
|
689
|
+
receive,
|
|
690
|
+
read,
|
|
691
|
+
stop,
|
|
692
|
+
interrupt,
|
|
693
|
+
readFinalAssistantResult,
|
|
694
|
+
invalidateFinalAssistantResult,
|
|
695
|
+
...(entry ? { readResult, updateState, stopProcess } : {}),
|
|
696
|
+
...(instructions ? {
|
|
697
|
+
prepare: instructions.prepare,
|
|
698
|
+
whenIdle: instructions.whenIdle,
|
|
699
|
+
instructionArguments: instructions.instructionArguments,
|
|
700
|
+
invalidateInstructions: instructions.invalidate
|
|
701
|
+
} : {}),
|
|
702
|
+
begin,
|
|
703
|
+
reset,
|
|
704
|
+
send,
|
|
705
|
+
prepareSteering,
|
|
706
|
+
cancel
|
|
707
|
+
});
|
|
708
|
+
}
|