@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,183 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { claudeNativeMessageId } from "./claudeHistory.js";
|
|
3
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
4
|
+
import { ignorePromptSending, ignoreReceipt } from "./nativeCommandCallbacks.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Delivers an authored input to an entry's Claude process: duplicate checks,
|
|
8
|
+
* preparation, steering, the new turn, and the native and persisted receipts.
|
|
9
|
+
* @param {{
|
|
10
|
+
* store: import("./claudeTurn.js").ClaudeConversationStore,
|
|
11
|
+
* preparation: import("./claudeTurn.js").ClaudeConversationPreparation,
|
|
12
|
+
* createError: import("./claudeTurn.js").ClaudeConversationErrorFactory,
|
|
13
|
+
* onEvent: import("./claudeTurn.js").ClaudeConversationEventHandler,
|
|
14
|
+
* readHistory: (entry: import("./claudeTurn.js").ClaudeConversationEntry) => Promise<Record<string, any>>,
|
|
15
|
+
* bindAccount: (entry: import("./claudeTurn.js").ClaudeConversationEntry) => Promise<void>,
|
|
16
|
+
* ensureProcess: (entry: import("./claudeTurn.js").ClaudeConversationEntry, input?: Record<string, any>) => Promise<Record<string, any>>,
|
|
17
|
+
* snapshot: (entry: import("./claudeTurn.js").ClaudeConversationEntry) => Record<string, any> | null
|
|
18
|
+
* }} options
|
|
19
|
+
*/
|
|
20
|
+
export function createClaudePromptDelivery({
|
|
21
|
+
store,
|
|
22
|
+
preparation,
|
|
23
|
+
createError,
|
|
24
|
+
onEvent,
|
|
25
|
+
readHistory,
|
|
26
|
+
bindAccount,
|
|
27
|
+
ensureProcess,
|
|
28
|
+
snapshot
|
|
29
|
+
}) {
|
|
30
|
+
async function send(entry, ...args) {
|
|
31
|
+
entry.sending = (entry.sending || 0) + 1;
|
|
32
|
+
try { return await dispatchPrompt(entry, ...args); }
|
|
33
|
+
finally { entry.sending -= 1; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function dispatchPrompt(entry, input = {}, { renewal = false, command, applicationCommand } = {}) {
|
|
37
|
+
const message = command ? input.text : normalizeNullableText(input.message || input.prompt);
|
|
38
|
+
if (!command && !message) throw createError("Enter a message for Claude.", { code: "conversation_claude_message_required" });
|
|
39
|
+
const context = entry.context;
|
|
40
|
+
const messageId = command ? input.messageId : normalizeNullableText(input.messageId) || randomUUID();
|
|
41
|
+
const uuid = claudeNativeMessageId(messageId);
|
|
42
|
+
const deliveryTiming = (command || input).deliveryTiming;
|
|
43
|
+
const { onNativeReceipt = ignoreReceipt, onReceiptPersisted = ignoreReceipt } = command || input;
|
|
44
|
+
const { onPromptSending = ignorePromptSending } = input;
|
|
45
|
+
if (!command) {
|
|
46
|
+
if (entry.main && await store.hasMessage(context, messageId)) {
|
|
47
|
+
return { ok: true, delivered: true, duplicate: true, thread: { id: entry.id }, turn: snapshot(entry) };
|
|
48
|
+
}
|
|
49
|
+
deliveryTiming?.("duplicate-checked");
|
|
50
|
+
preparation.check(entry, context);
|
|
51
|
+
const history = await readHistory(entry);
|
|
52
|
+
deliveryTiming?.("native-history-read");
|
|
53
|
+
if (history.userIds.includes(uuid)) {
|
|
54
|
+
return { ok: true, delivered: true, duplicate: true, thread: { id: entry.id }, turn: snapshot(entry) };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let native;
|
|
58
|
+
try {
|
|
59
|
+
if (!command && entry.main && entry.turn?.active && entry.process) {
|
|
60
|
+
// Main steering keeps the active instruction installation, just like
|
|
61
|
+
// command steering. Refresh guidance on the next idle admission.
|
|
62
|
+
// Account replacement must still confirm cleanup before any new send.
|
|
63
|
+
if (preparation.account) await bindAccount(entry);
|
|
64
|
+
native = entry.process || await ensureProcess(entry, input);
|
|
65
|
+
} else {
|
|
66
|
+
native = command ? command.steering ? entry.process : await entry.nativeTurn.prepare(command.prepared) : await ensureProcess(entry, input);
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Preparation may start or resume the process, but has not written this
|
|
70
|
+
// authored input. Keep its failure distinct from a lost native receipt.
|
|
71
|
+
throw Object.assign(error, { delivery: "not-sent" });
|
|
72
|
+
}
|
|
73
|
+
deliveryTiming?.("process-ready");
|
|
74
|
+
if (command && !command.steering) command.signal.throwIfAborted();
|
|
75
|
+
const steering = command ? command.steering === true : Boolean(entry.turn?.active);
|
|
76
|
+
if (input.signal?.aborted) {
|
|
77
|
+
throw Object.assign(createError("The goal command was stopped before delivery.", { code: "conversation_claude_command_stopped" }), { delivery: "not-sent" });
|
|
78
|
+
}
|
|
79
|
+
if (steering) {
|
|
80
|
+
if (command) {
|
|
81
|
+
await command.beforeDispatch({ threadId: entry.id });
|
|
82
|
+
command.signal.throwIfAborted();
|
|
83
|
+
}
|
|
84
|
+
// A mid-turn message is never an interrupt: Claude Code queues it and
|
|
85
|
+
// reads it after the running tool call. Only explicit Stop interrupts.
|
|
86
|
+
try { await entry.nativeTurn.prepareSteering(); }
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!command) await entry.nativeTurn.stopProcess(error.message);
|
|
89
|
+
// This precedes the input's pipe write: a refused steer is definitely not sent.
|
|
90
|
+
throw Object.assign(error, { delivery: "not-sent" });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const prompt = command ? undefined : await preparation.message(entry, input, { context, message, steering, renewal, deliveryTiming });
|
|
94
|
+
if (!command) deliveryTiming?.("prompt-rendered");
|
|
95
|
+
if (command) {
|
|
96
|
+
await command.beforeDispatch({ threadId: entry.id });
|
|
97
|
+
command.signal.throwIfAborted();
|
|
98
|
+
}
|
|
99
|
+
if (!steering) {
|
|
100
|
+
entry.renewal = renewal;
|
|
101
|
+
entry.nativeTurn.begin({ id: uuid, maxOutputCharacters: command ? command.maximumOutput : entry.profile?.limits.maxOutputCharacters });
|
|
102
|
+
entry.observationError = "";
|
|
103
|
+
entry.turn = { id: uuid, active: true, state: "starting", startedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), error: "" };
|
|
104
|
+
if (!command) entry.completion = Promise.withResolvers();
|
|
105
|
+
if (!command) await entry.nativeTurn.updateState("starting");
|
|
106
|
+
}
|
|
107
|
+
entry.onEvent = context.onEvent;
|
|
108
|
+
entry.lastMessageId = uuid;
|
|
109
|
+
const actorMetadata = command ? undefined : await onEvent(entry, { type: "message-metadata", input, context });
|
|
110
|
+
if (!command) deliveryTiming?.("metadata-ready");
|
|
111
|
+
const nativeOwner = applicationCommand ? { process: native, executionId: entry.executionId,
|
|
112
|
+
accountIdentity: entry.accountIdentity, threadId: entry.id, turnId: entry.turn.id } : null;
|
|
113
|
+
let conversationTurn;
|
|
114
|
+
function deliveryResult() {
|
|
115
|
+
return { ok: true, delivered: true, deliveryMode: steering ? "steer" : "new_turn",
|
|
116
|
+
thread: { id: entry.id }, turn: snapshot(entry), workdir: context.workdir, conversationTurn };
|
|
117
|
+
}
|
|
118
|
+
async function accept() {
|
|
119
|
+
onNativeReceipt();
|
|
120
|
+
// The new native ACK retires old effect custody while its canonical
|
|
121
|
+
// admission/commit is awaited; it cannot retag a delayed old failure.
|
|
122
|
+
if (applicationCommand) applicationCommand.active.nativeOwner = null;
|
|
123
|
+
if (command) {
|
|
124
|
+
if (!steering) await onEvent(entry, { type: "binding-admitted" });
|
|
125
|
+
await command.accept();
|
|
126
|
+
if (steering) {
|
|
127
|
+
command.active.messageId = input.messageId;
|
|
128
|
+
command.active.nativeToolCount = 0;
|
|
129
|
+
} else command.active.admitted = true;
|
|
130
|
+
command.admitted = true;
|
|
131
|
+
} else {
|
|
132
|
+
const admission = onEvent(entry, { type: "admit-message", input, context, renewal, message, messageId, uuid, actorMetadata });
|
|
133
|
+
if (admission) conversationTurn = await admission;
|
|
134
|
+
deliveryTiming?.("receipt-written");
|
|
135
|
+
}
|
|
136
|
+
if (!command) {
|
|
137
|
+
await entry.nativeTurn.updateState("active");
|
|
138
|
+
deliveryTiming?.("state-active");
|
|
139
|
+
}
|
|
140
|
+
onReceiptPersisted();
|
|
141
|
+
if (applicationCommand && conversationTurn) {
|
|
142
|
+
const value = deliveryResult();
|
|
143
|
+
const active = applicationCommand.active;
|
|
144
|
+
const previousCompletion = active.completion;
|
|
145
|
+
if (active.finishIfCurrent) {
|
|
146
|
+
active.completion = Promise.withResolvers();
|
|
147
|
+
active.completion.promise.catch(() => {});
|
|
148
|
+
}
|
|
149
|
+
active.delivery = { value, completion: active.finishIfCurrent ? active.completion.promise : entry.completion?.promise };
|
|
150
|
+
if (active.finishIfCurrent) previousCompletion.resolve();
|
|
151
|
+
// The original canonical admission and publication precede Core's same
|
|
152
|
+
// accepted-request gate; native tools cannot race an unaccepted user.
|
|
153
|
+
await applicationCommand.accept({ nativeResult: { value }, conversationTurn, nativeTurnId: entry.turn?.id });
|
|
154
|
+
if (entry.process !== nativeOwner.process || entry.executionId !== nativeOwner.executionId ||
|
|
155
|
+
entry.accountIdentity !== nativeOwner.accountIdentity || entry.id !== nativeOwner.threadId ||
|
|
156
|
+
entry.turn?.id !== nativeOwner.turnId || entry.disposed || entry.stopping || entry.stopPending) {
|
|
157
|
+
throw new Error("Claude's admitted tools no longer belong to this native process.");
|
|
158
|
+
}
|
|
159
|
+
applicationCommand.active.nativeOwner = nativeOwner;
|
|
160
|
+
applicationCommand.active.messageId = messageId;
|
|
161
|
+
if (steering) applicationCommand.active.nativeToolCount = 0;
|
|
162
|
+
applicationCommand.active.admitted = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
if (command) command.attempted = true;
|
|
167
|
+
else await onPromptSending({ threadId: entry.id, displayAttachments: input.displayAttachments, turnMetadata: actorMetadata });
|
|
168
|
+
if (input.signal?.aborted) {
|
|
169
|
+
throw Object.assign(createError("The goal command was stopped before delivery.", { code: "conversation_claude_command_stopped" }), { delivery: "not-sent" });
|
|
170
|
+
}
|
|
171
|
+
await entry.nativeTurn.send(command ? entry.process.client : native.client, command ? preparation.message(entry, input) : prompt,
|
|
172
|
+
{ messageId: uuid, accept, steer: steering, ...(command ? { timeoutMs: command.timeoutMs } : {}) });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (!command) await entry.nativeTurn.stopProcess(error.message);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
return deliveryResult();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
send
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { deleteClaudeConversationHistory } from "./claudeHistory.js";
|
|
3
|
+
import { hasPendingClaudeStartup } from "./claudeProcessStart.js";
|
|
4
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
5
|
+
|
|
6
|
+
const ACTIVE_TURN_STATES = ["starting", "active", "finalizing"];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Closes what an owner runs: sessions, projects, a connection change's work
|
|
10
|
+
* and the whole owner at shutdown. It knows which sessions are closing and
|
|
11
|
+
* whether the owner itself is.
|
|
12
|
+
* @param {{
|
|
13
|
+
* entries: Map<string, import("./claudeTurn.js").ClaudeConversationEntry>,
|
|
14
|
+
* store: import("./claudeTurn.js").ClaudeConversationStore,
|
|
15
|
+
* configRoot: string,
|
|
16
|
+
* acquire: (context: Record<string, any>, conversationId?: string, options?: Record<string, any>) => Promise<import("./claudeTurn.js").ClaudeConversationEntry>,
|
|
17
|
+
* savedState: (saved: string | null, conversationId: string) => Record<string, any>,
|
|
18
|
+
* holdsNativeWork: (entry: import("./claudeTurn.js").ClaudeConversationEntry) => boolean,
|
|
19
|
+
* disconnectedMessage: string
|
|
20
|
+
* }} options
|
|
21
|
+
*/
|
|
22
|
+
export function createClaudeSessionCleanup({
|
|
23
|
+
entries,
|
|
24
|
+
store,
|
|
25
|
+
configRoot,
|
|
26
|
+
acquire,
|
|
27
|
+
savedState,
|
|
28
|
+
holdsNativeWork,
|
|
29
|
+
disconnectedMessage
|
|
30
|
+
}) {
|
|
31
|
+
const closingSessions = new Set();
|
|
32
|
+
let closing = false;
|
|
33
|
+
|
|
34
|
+
// The owner is closing for shutdown, or the session this context key names is.
|
|
35
|
+
function isClosing(contextKey) {
|
|
36
|
+
return closing || closingSessions.has(contextKey);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function reconcileSessions(sessions, options = {}, application) {
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const session of sessions) {
|
|
42
|
+
const ctx = await application.readContext({ ...options, session, sessionId: session.sessionId });
|
|
43
|
+
await recover(await restoreSessionEntries(ctx, { acquire }));
|
|
44
|
+
results.push({ ok: true, sessionId: session.sessionId, resumed: false });
|
|
45
|
+
}
|
|
46
|
+
return { ok: true, results, failed: [], sessionCount: results.length };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function restoreSessionEntries(context, { acquire }) {
|
|
50
|
+
let saved = store.readSessionConversations(context);
|
|
51
|
+
// readable-check: allow method-probe - data: a store lists a session's conversations synchronously or with a promise
|
|
52
|
+
if (saved && typeof saved.then === "function") saved = await saved;
|
|
53
|
+
for (const conversationId of saved.ids) await acquire(saved.context, conversationId);
|
|
54
|
+
return [...entries.values()].filter((entry) => entry.context.key === context.key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A session's close stops what this server runs for it, then reopens only
|
|
58
|
+
// saved custody an earlier server left. Once the owner is closing for an
|
|
59
|
+
// account change or shutdown it reopens nothing.
|
|
60
|
+
async function closeSession(context, application) {
|
|
61
|
+
return closeRetained(await application.readContext(context), application,
|
|
62
|
+
{ recover: !closing, forget: context.forgetConversationBinding === true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Renewal proof release ends a session this server no longer uses. Its own
|
|
66
|
+
// close already recovered saved custody while its record could be written.
|
|
67
|
+
async function releaseSession(context, application) {
|
|
68
|
+
return closeRetained(await application.readContext(context), application, { recover: false, forget: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function closeRetained(ctx, application, { recover: recoverCustody, forget: forgetEntries }) {
|
|
72
|
+
closingSessions.add(ctx.key);
|
|
73
|
+
try {
|
|
74
|
+
if (recoverCustody) await recoverSavedCustody(ctx);
|
|
75
|
+
const proofs = await drain(ctx.key);
|
|
76
|
+
const terminal = await application.terminals.close(ctx.sessionId);
|
|
77
|
+
// A forgotten entry is drained: everything it held is in its saved record.
|
|
78
|
+
if (terminal?.ok !== false && forgetEntries) forget(ctx.key);
|
|
79
|
+
return { ok: terminal?.ok !== false, closed: proofs.length + Number(terminal?.closed || 0),
|
|
80
|
+
processExitProof: proofs.at(-1) || { exited: true, scopeEmpty: true }, processExitProofs: proofs };
|
|
81
|
+
} finally {
|
|
82
|
+
closingSessions.delete(ctx.key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Saved custody an earlier server left: an execution whose exit is still
|
|
87
|
+
// unproven, or a turn saved as active. Only those conversations are reopened,
|
|
88
|
+
// so a verified stop can retire them; idle saved conversations stay closed.
|
|
89
|
+
async function recoverSavedCustody(ctx) {
|
|
90
|
+
let saved = store.readSessionConversations(ctx);
|
|
91
|
+
// readable-check: allow method-probe - data: a store lists a session's conversations synchronously or with a promise
|
|
92
|
+
if (saved && typeof saved.then === "function") saved = await saved;
|
|
93
|
+
for (const conversationId of saved.ids) {
|
|
94
|
+
if (entries.has(`${ctx.key}\0${conversationId}`)) continue;
|
|
95
|
+
let record = store.read(saved.context, conversationId);
|
|
96
|
+
// readable-check: allow method-probe - data: a store reads a saved record synchronously or with a promise
|
|
97
|
+
if (record && typeof record.then === "function") record = await record;
|
|
98
|
+
const state = savedState(record, conversationId);
|
|
99
|
+
if (state.executionId || ACTIVE_TURN_STATES.includes(state.state)) await acquire(saved.context, conversationId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Project close releases what this server runs for the project's sessions,
|
|
104
|
+
// from their retained entries alone, and forgets them: nothing uses them.
|
|
105
|
+
function closeProject(input = {}, application) {
|
|
106
|
+
const root = normalizeNullableText(input.projectContextRoot);
|
|
107
|
+
return eachSession(entry => !root || path.resolve(store.projectContextRoot(entry)) === path.resolve(root),
|
|
108
|
+
ctx => closeRetained(ctx, application, { recover: false, forget: true }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function invalidateRuntimes(input = {}, application, account) {
|
|
112
|
+
if (input.provider && input.provider !== "claude") return { ok: true, closed: 0 };
|
|
113
|
+
if (input.modelProviderId) {
|
|
114
|
+
return stopSelected(entry => store.modelProviderId(entry) === input.modelProviderId,
|
|
115
|
+
"The provider connection changed.");
|
|
116
|
+
}
|
|
117
|
+
if (input.reason !== "server-shutdown") {
|
|
118
|
+
// An account change stops and waits on nothing. Claude Code reads its
|
|
119
|
+
// credentials for every request, so running work continues on the newly
|
|
120
|
+
// connected account or ends at once as signed out; each conversation
|
|
121
|
+
// rebinds its account at its next idle admission. Only the account's
|
|
122
|
+
// cached reads are dropped. A cancelled query's process stop that fails
|
|
123
|
+
// stays owned and is retried before the next query.
|
|
124
|
+
account.queries.invalidate().catch(() => {});
|
|
125
|
+
return { ok: true, closed: 0 };
|
|
126
|
+
}
|
|
127
|
+
closing = true;
|
|
128
|
+
// Neither waits for the other: a failed account query cleanup must not
|
|
129
|
+
// leave the sessions' processes running.
|
|
130
|
+
const [queries, retained] = await Promise.allSettled([account.queries.invalidate(), stopRetained(application)]);
|
|
131
|
+
if (queries.status === "rejected") await account.failure(queries.reason);
|
|
132
|
+
if (retained.status === "rejected") throw retained.reason;
|
|
133
|
+
return retained.value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Shutdown stops only what this server runs: a Claude process cannot outlive
|
|
137
|
+
// the server that started it, so retained entries hold every one. Saved
|
|
138
|
+
// conversations are not reopened just to be stopped.
|
|
139
|
+
function stopRetained(application) {
|
|
140
|
+
return eachSession(() => true, ctx => closeRetained(ctx, application, { recover: false, forget: false }));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Sessions close independently and together: one that fails is reported by
|
|
144
|
+
// its ID while every other session still stops.
|
|
145
|
+
async function eachSession(select, close) {
|
|
146
|
+
const contexts = new Map([...entries.values()].filter(select).map(entry => [entry.context.key, entry.context]));
|
|
147
|
+
const settled = await Promise.allSettled([...contexts.values()].map(ctx => close(ctx)));
|
|
148
|
+
let closed = 0;
|
|
149
|
+
const failed = [];
|
|
150
|
+
[...contexts.values()].forEach((ctx, index) => {
|
|
151
|
+
const result = settled[index];
|
|
152
|
+
if (result.status === "fulfilled") closed += result.value.closed;
|
|
153
|
+
const error = result.status === "rejected" ? result.reason?.message || String(result.reason)
|
|
154
|
+
: result.value.ok === false ? "Its Claude Code terminal could not be closed." : "";
|
|
155
|
+
if (error) failed.push({ sessionId: ctx.sessionId, error });
|
|
156
|
+
});
|
|
157
|
+
return { ok: failed.length === 0, closed, failed,
|
|
158
|
+
...(failed.length ? { error: failed.map(({ sessionId, error }) => `Claude session ${sessionId}: ${error}`).join(" ") } : {}) };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function drain(key) {
|
|
162
|
+
const selected = [...entries.values()].filter(entry => entry.context.key === key);
|
|
163
|
+
// Fence configuration still in progress: it must not start a process later.
|
|
164
|
+
for (const entry of selected) entry.preparationRevision++;
|
|
165
|
+
const proofs = [];
|
|
166
|
+
let failure;
|
|
167
|
+
async function stop() {
|
|
168
|
+
for (const entry of selected) if (holdsNativeWork(entry)) {
|
|
169
|
+
try { proofs.push(await entry.nativeTurn.stopProcess()); }
|
|
170
|
+
catch (error) { failure ||= error; }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Startup already lends cleanup before initialization completes. Stop it
|
|
174
|
+
// first; waiting for the preparation queue first can exhaust host shutdown.
|
|
175
|
+
await stop();
|
|
176
|
+
await Promise.allSettled(selected.map(entry => entry.nativeTurn.whenIdle()));
|
|
177
|
+
await stop();
|
|
178
|
+
// Every other entry still stopped; the first failure keeps the close retryable.
|
|
179
|
+
if (failure) throw failure;
|
|
180
|
+
return proofs;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function forget(contextKey) {
|
|
184
|
+
for (const [key, entry] of entries) if (entry.context.key === contextKey) {
|
|
185
|
+
// Nothing would ever stop a process a forgotten entry started.
|
|
186
|
+
entry.disposed = true;
|
|
187
|
+
entries.delete(key);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function close(entry) {
|
|
192
|
+
await entry.nativeTurn.stopProcess("Conversation closed.");
|
|
193
|
+
entry.disposed = true;
|
|
194
|
+
await entry.nativeTurn.whenIdle().catch(() => {});
|
|
195
|
+
await entry.nativeTurn.stopProcess("Conversation closed.");
|
|
196
|
+
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function deleteConversation(entry) {
|
|
200
|
+
await entry.nativeTurn.stopProcess();
|
|
201
|
+
await deleteClaudeConversationHistory({ configRoot, workdir: entry.nativeWorkdir, conversationId: entry.id });
|
|
202
|
+
await store.remove(entry);
|
|
203
|
+
entries.delete(entry.key);
|
|
204
|
+
return { ok: true, deleted: true, conversationId: entry.id };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function recover(restoredEntries) {
|
|
208
|
+
for (const entry of restoredEntries) {
|
|
209
|
+
if (!entry.process && !hasPendingClaudeStartup(entry) && (entry.executionId || entry.turn?.active)) await entry.nativeTurn.stopProcess(disconnectedMessage);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// A provider connection change stops the matching work this server runs.
|
|
214
|
+
// Each entry stops on its own, so one failure is reported by ID.
|
|
215
|
+
async function stopSelected(select, reason) {
|
|
216
|
+
let closed = 0;
|
|
217
|
+
const failed = [];
|
|
218
|
+
for (const entry of [...entries.values()].filter(select)) {
|
|
219
|
+
entry.preparationRevision++;
|
|
220
|
+
if (!holdsNativeWork(entry)) continue;
|
|
221
|
+
try {
|
|
222
|
+
await entry.nativeTurn.stopProcess(reason);
|
|
223
|
+
closed++;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
failed.push({ sessionId: entry.context.sessionId, conversationId: entry.id, error: error.message });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { ok: failed.length === 0, closed, failed };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Object.freeze({
|
|
232
|
+
get closing() { return closing; },
|
|
233
|
+
closingSessions,
|
|
234
|
+
isClosing,
|
|
235
|
+
reconcileSessions,
|
|
236
|
+
restoreSessionEntries,
|
|
237
|
+
closeSession,
|
|
238
|
+
releaseSession,
|
|
239
|
+
closeProject,
|
|
240
|
+
invalidateRuntimes,
|
|
241
|
+
drain,
|
|
242
|
+
forget,
|
|
243
|
+
close,
|
|
244
|
+
deleteConversation,
|
|
245
|
+
recover,
|
|
246
|
+
stopSelected
|
|
247
|
+
});
|
|
248
|
+
}
|
|
@@ -2,9 +2,21 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
|
|
3
3
|
export const CLAUDE_APPLICATION_TOOL_SERVER = "application";
|
|
4
4
|
|
|
5
|
+
/** A call from tools the conversation owns needs no supplied owner's check. */
|
|
6
|
+
function acceptNativeCall() {}
|
|
7
|
+
|
|
8
|
+
/** A call from tools the conversation owns fails only its own exchange. */
|
|
9
|
+
function ignoreExecutionFailure() {}
|
|
10
|
+
|
|
5
11
|
// The CLI carries this SDK MCP exchange over its existing control pipe. There
|
|
6
12
|
// is no listening socket or separate tool process. The shared executor owns effects.
|
|
7
|
-
export async function claudeApplicationToolResponse(request, {
|
|
13
|
+
export async function claudeApplicationToolResponse(request, {
|
|
14
|
+
schemas,
|
|
15
|
+
turn,
|
|
16
|
+
signal,
|
|
17
|
+
assertCurrent = acceptNativeCall,
|
|
18
|
+
onExecutionFailure = ignoreExecutionFailure
|
|
19
|
+
}) {
|
|
8
20
|
if (request?.subtype !== "mcp_message" || request.server_name !== CLAUDE_APPLICATION_TOOL_SERVER ||
|
|
9
21
|
request.message?.jsonrpc !== "2.0") throw new Error("This Claude control request is not an application tool exchange.");
|
|
10
22
|
const message = request.message;
|
|
@@ -33,12 +45,12 @@ export async function claudeApplicationToolResponse(request, { schemas, turn, si
|
|
|
33
45
|
}
|
|
34
46
|
// Optional supplied-owner checks run after the unchanged native-use
|
|
35
47
|
// validation. Protocol/stale refusals are not owned executor failures.
|
|
36
|
-
assertCurrent
|
|
48
|
+
assertCurrent(native);
|
|
37
49
|
let value;
|
|
38
50
|
try {
|
|
39
51
|
value = await turn.tools.execute({ id, name: params.name, arguments: JSON.stringify(params.arguments || {}) }, { signal, messageId: native.messageId });
|
|
40
52
|
} catch (error) {
|
|
41
|
-
onExecutionFailure
|
|
53
|
+
onExecutionFailure(error, native);
|
|
42
54
|
throw error;
|
|
43
55
|
}
|
|
44
56
|
result = { content: [{ type: "text", text: JSON.stringify(value) }], isError: value.ok === false };
|