@jskit-ai/assistant-core 0.2.7 → 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 +13 -10
- 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,547 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import {
|
|
3
|
+
OPENCODE_INTERRUPT_TIMEOUT_MS, inspectOpenCodeMessageAdmission, openCodeMessageRows, dispatchOpenCodeTurn, runOpenCodeConversationTurn
|
|
4
|
+
} from "./openCodeTurn.js";
|
|
5
|
+
import { ensureOpenCodeSession, sameOpenCodeSelection } from "./openCodeNativeSession.js";
|
|
6
|
+
import { openCodeTurnSnapshot } from "./openCodeTurnMonitor.js";
|
|
7
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
8
|
+
import { ignorePromptRejection } from "./nativeCommandCallbacks.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A session's main native conversation: its readiness, the messages sent to
|
|
12
|
+
* it, its renewal turn, its interrupt, and the reconciliation of saved runs
|
|
13
|
+
* after a restart.
|
|
14
|
+
* @param {{
|
|
15
|
+
* processes: Map<string, object>,
|
|
16
|
+
* monitors: Map<string, Promise<object>>,
|
|
17
|
+
* turns: Map<string, object>,
|
|
18
|
+
* processOwner: import("./openCodeProcessOwner.js").OpenCodeProcessOwner,
|
|
19
|
+
* turnMonitor: import("./openCodeTurnMonitor.js").OpenCodeTurnMonitor
|
|
20
|
+
* }} options
|
|
21
|
+
*/
|
|
22
|
+
export function createOpenCodeSessionTurns({ processes, monitors, turns, processOwner, turnMonitor }) {
|
|
23
|
+
async function runPreparedRenewalTurn(preparation) {
|
|
24
|
+
const acquired = await processOwner.acquirePrepared(await preparation.process());
|
|
25
|
+
const releaseDirectory = await processOwner.admitDirectory(acquired.workdir);
|
|
26
|
+
try {
|
|
27
|
+
const target = await ensureOpenCodeSession(acquired, preparation.session());
|
|
28
|
+
const turn = preparation.turn(target);
|
|
29
|
+
const result = await runOpenCodeConversationTurn(target.server.client, target.upstreamSessionId, turn.input, {
|
|
30
|
+
...turn.options,
|
|
31
|
+
...(turn.release ? { close: () => processOwner.releaseProcessTarget(target, {}, turn.release) } : {})
|
|
32
|
+
});
|
|
33
|
+
return turn.completeResult(result);
|
|
34
|
+
} finally {
|
|
35
|
+
releaseDirectory();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function sendPreparedMessage(prepared, options) {
|
|
40
|
+
if (Object.hasOwn(prepared, "value")) return prepared.value;
|
|
41
|
+
return prepared.project(await sendMessage(prepared.key, prepared.input, options, prepared.application));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function inspectPreparedMessage(prepared) {
|
|
45
|
+
const target = await processOwner.acquirePrepared(prepared.process);
|
|
46
|
+
const observation = await inspectOpenCodeMessageAdmission(target.server.client, prepared.threadId, prepared.inputMessageId);
|
|
47
|
+
return prepared.project(observation);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function interruptPreparedTurn(prepared) {
|
|
51
|
+
const target = processes.get(prepared.key) || null;
|
|
52
|
+
return interruptTurn(prepared.key, target, {
|
|
53
|
+
threadId: target ? "" : prepared.threadId, writeRun: prepared.writeRun
|
|
54
|
+
}).catch(prepared.failure);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function sendMessage(key, input, options, application) {
|
|
58
|
+
if (input.duplicate) {
|
|
59
|
+
const currentTurn = openCodeTurnSnapshot(turns.get(key));
|
|
60
|
+
return {
|
|
61
|
+
delivered: true,
|
|
62
|
+
duplicate: true,
|
|
63
|
+
ok: true,
|
|
64
|
+
thread: { id: currentTurn?.threadId || input.threadId },
|
|
65
|
+
turn: currentTurn
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const currentTurn = turns.get(key);
|
|
69
|
+
if (currentTurn?.observationError) {
|
|
70
|
+
await monitors.get(key);
|
|
71
|
+
}
|
|
72
|
+
if (currentTurn?.active && currentTurn.observationError) {
|
|
73
|
+
// An explicit Send may retry stopping uncertain work, but cannot overlap it.
|
|
74
|
+
try {
|
|
75
|
+
await processOwner.stopUnobservedSession(processes.get(key), currentTurn.threadId);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return { failure: { error, recovery: true } };
|
|
78
|
+
}
|
|
79
|
+
currentTurn.active = false;
|
|
80
|
+
currentTurn.state = "interrupted";
|
|
81
|
+
await application.writeRun(currentTurn, "interrupted", currentTurn.observationError);
|
|
82
|
+
}
|
|
83
|
+
let currentMonitor = monitors.get(key);
|
|
84
|
+
let currentThreadId = input.threadId;
|
|
85
|
+
const ownershipMatchesTurn = Boolean(
|
|
86
|
+
currentMonitor &&
|
|
87
|
+
options.turnOwnership &&
|
|
88
|
+
normalizeNullableText(options.turnOwnership.threadId) === currentThreadId &&
|
|
89
|
+
normalizeNullableText(options.turnOwnership.turnId) === normalizeNullableText(currentTurn?.id)
|
|
90
|
+
);
|
|
91
|
+
if (ownershipMatchesTurn && options.turnOwnership.reusable !== true) {
|
|
92
|
+
return { ownerConflict: true, threadId: currentThreadId,
|
|
93
|
+
turn: openCodeTurnSnapshot(currentTurn, currentThreadId) };
|
|
94
|
+
}
|
|
95
|
+
const eventStartedAt = Date.now();
|
|
96
|
+
const startedAt = new Date(eventStartedAt).toISOString();
|
|
97
|
+
const providerMessageId = input.id;
|
|
98
|
+
const startingTurn = currentMonitor
|
|
99
|
+
? null
|
|
100
|
+
: {
|
|
101
|
+
active: true,
|
|
102
|
+
error: "",
|
|
103
|
+
eventStartedAt,
|
|
104
|
+
id: providerMessageId,
|
|
105
|
+
inputMessageId: providerMessageId,
|
|
106
|
+
startedAt,
|
|
107
|
+
state: "starting",
|
|
108
|
+
threadId: currentThreadId,
|
|
109
|
+
updatedAt: startedAt
|
|
110
|
+
};
|
|
111
|
+
if (startingTurn) {
|
|
112
|
+
turns.set(key, startingTurn);
|
|
113
|
+
await application.writeRun(startingTurn, "starting");
|
|
114
|
+
}
|
|
115
|
+
let admitted = null;
|
|
116
|
+
let target = null;
|
|
117
|
+
let admission = null;
|
|
118
|
+
let dispatchTurn = null;
|
|
119
|
+
let dispatchMonitor = null;
|
|
120
|
+
let previousInputMessageId = null;
|
|
121
|
+
let previousAdmission;
|
|
122
|
+
let recoveryReceipt = null;
|
|
123
|
+
let recoveryObservationError = null;
|
|
124
|
+
try {
|
|
125
|
+
const preparation = await application.prepare({ overwrite: !currentMonitor, threadId: currentThreadId });
|
|
126
|
+
const preparedTarget = preparation.process ? await processOwner.acquirePrepared(preparation.process) : preparation.target;
|
|
127
|
+
if (!monitors.get(key)) await processOwner.currentDirectory(preparedTarget.workdir);
|
|
128
|
+
target = await ensureOpenCodeSession(preparedTarget, preparation.session);
|
|
129
|
+
currentThreadId = target.upstreamSessionId;
|
|
130
|
+
if (startingTurn) {
|
|
131
|
+
startingTurn.threadId = currentThreadId;
|
|
132
|
+
}
|
|
133
|
+
const prompt = await application.prompt();
|
|
134
|
+
// Preparation can outlive the preceding turn's final projection. Do not
|
|
135
|
+
// attach a new input to a monitor that has already claimed retirement.
|
|
136
|
+
currentMonitor = monitors.get(key);
|
|
137
|
+
if (currentMonitor && !turns.get(key)?.active) {
|
|
138
|
+
await currentMonitor;
|
|
139
|
+
currentMonitor = monitors.get(key);
|
|
140
|
+
}
|
|
141
|
+
const recoveryTurn = currentMonitor ? turns.get(key) : null;
|
|
142
|
+
if (recoveryTurn?.recoveryAdmission) {
|
|
143
|
+
try {
|
|
144
|
+
recoveryReceipt = await recoveryTurn.recoveryAdmission.promise;
|
|
145
|
+
currentMonitor = monitors.get(key);
|
|
146
|
+
if (currentMonitor && !turns.get(key)?.active) {
|
|
147
|
+
await currentMonitor;
|
|
148
|
+
currentMonitor = monitors.get(key);
|
|
149
|
+
}
|
|
150
|
+
// Stop must not turn a waiting, undispatched input into a fresh turn.
|
|
151
|
+
recoveryTurn.abortController.signal.throwIfAborted();
|
|
152
|
+
target.abortController.signal.throwIfAborted();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// This is R's failure, not a rejected or attempted native B input.
|
|
155
|
+
return { failure: { error, attempted: false, threadId: currentThreadId,
|
|
156
|
+
turn: openCodeTurnSnapshot(turns.get(key), currentThreadId) } };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// A new native turn starts only on its directory's current credentials;
|
|
160
|
+
// the wait, if any, is for that directory's own running turns.
|
|
161
|
+
while (!currentMonitor && !processOwner.directoryCurrent(target.workdir)) {
|
|
162
|
+
await processOwner.waitForDirectory(target.workdir);
|
|
163
|
+
currentMonitor = monitors.get(key);
|
|
164
|
+
if (currentMonitor && !turns.get(key)?.active) {
|
|
165
|
+
await currentMonitor;
|
|
166
|
+
currentMonitor = monitors.get(key);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
admission = Promise.withResolvers();
|
|
170
|
+
// The observer may be between polls when dispatch or persistence fails.
|
|
171
|
+
// Keep the raw promise rejectable without an unhandled rejection.
|
|
172
|
+
void admission.promise.catch(() => {});
|
|
173
|
+
let eventReady = null;
|
|
174
|
+
if (currentMonitor) {
|
|
175
|
+
dispatchTurn = turns.get(key);
|
|
176
|
+
previousInputMessageId = dispatchTurn.inputMessageId;
|
|
177
|
+
previousAdmission = dispatchTurn.admission;
|
|
178
|
+
dispatchTurn.admission = admission;
|
|
179
|
+
dispatchTurn.inputMessageId = providerMessageId;
|
|
180
|
+
dispatchTurn.updatedAt = new Date().toISOString();
|
|
181
|
+
dispatchMonitor = currentMonitor;
|
|
182
|
+
} else {
|
|
183
|
+
eventReady = Promise.withResolvers();
|
|
184
|
+
dispatchMonitor = turnMonitor.beginMessageMonitor(key, target, { id: providerMessageId, eventStartedAt, startedAt }, {
|
|
185
|
+
...options, admission, eventReady
|
|
186
|
+
}, application.monitor);
|
|
187
|
+
dispatchTurn = turns.get(key);
|
|
188
|
+
}
|
|
189
|
+
admitted = await dispatchOpenCodeTurn(dispatchTurn, {
|
|
190
|
+
get agent() { return prompt.agent; },
|
|
191
|
+
delivery: currentMonitor ? "steer" : "queue",
|
|
192
|
+
id: providerMessageId,
|
|
193
|
+
get model() { return prompt.model; },
|
|
194
|
+
get prompt() { return prompt.prompt; },
|
|
195
|
+
get attachments() { return prompt.attachments; }
|
|
196
|
+
}, {
|
|
197
|
+
get client() { return target.server.client; },
|
|
198
|
+
get conversationId() { return target.upstreamSessionId; },
|
|
199
|
+
events: eventReady ? { waitUntilReady: () => eventReady.promise } : null,
|
|
200
|
+
get signal() {
|
|
201
|
+
return AbortSignal.any([
|
|
202
|
+
target.abortController.signal,
|
|
203
|
+
turns.get(key).abortController.signal
|
|
204
|
+
]);
|
|
205
|
+
},
|
|
206
|
+
beforeDispatch: () => prompt.beforeDispatch(target.upstreamSessionId),
|
|
207
|
+
promptTimeoutMs: options.promptTimeoutMs,
|
|
208
|
+
authorizeAttachments: prompt.authorizeAttachments
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const rejected = error?.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 408;
|
|
212
|
+
const retainPreviousInput = previousInputMessageId !== null &&
|
|
213
|
+
(dispatchTurn?.promptAttempted !== true || rejected);
|
|
214
|
+
if (retainPreviousInput) {
|
|
215
|
+
if (dispatchTurn.admission === admission && dispatchTurn.inputMessageId === providerMessageId) {
|
|
216
|
+
dispatchTurn.inputMessageId = previousInputMessageId;
|
|
217
|
+
dispatchTurn.admission = previousAdmission;
|
|
218
|
+
}
|
|
219
|
+
admission.resolve();
|
|
220
|
+
} else {
|
|
221
|
+
// Attempted unknown admission must retain the new input and use the
|
|
222
|
+
// same observer's original Stop/error recovery, never roll back to A.
|
|
223
|
+
admission?.reject(error);
|
|
224
|
+
dispatchTurn?.abortController?.abort(error);
|
|
225
|
+
}
|
|
226
|
+
if (rejected) {
|
|
227
|
+
const { onPromptRejected = ignorePromptRejection } = input;
|
|
228
|
+
await onPromptRejected();
|
|
229
|
+
}
|
|
230
|
+
if (admission && !retainPreviousInput) await dispatchMonitor;
|
|
231
|
+
if (startingTurn && !admission && !monitors.has(key)) {
|
|
232
|
+
startingTurn.active = false;
|
|
233
|
+
startingTurn.error = normalizeNullableText(error?.message) || "OpenCode prompt delivery failed.";
|
|
234
|
+
startingTurn.state = "failed";
|
|
235
|
+
startingTurn.updatedAt = new Date().toISOString();
|
|
236
|
+
await application.writeRun(
|
|
237
|
+
startingTurn,
|
|
238
|
+
"failed",
|
|
239
|
+
startingTurn.error
|
|
240
|
+
).catch(() => null);
|
|
241
|
+
}
|
|
242
|
+
return { failure: { error, attempted: dispatchTurn?.promptAttempted === true, threadId: currentThreadId,
|
|
243
|
+
turn: openCodeTurnSnapshot(turns.get(key) || startingTurn, currentThreadId) } };
|
|
244
|
+
}
|
|
245
|
+
if (dispatchTurn.admission === admission && dispatchTurn.inputMessageId === providerMessageId) {
|
|
246
|
+
dispatchTurn.inputMessageId = normalizeNullableText(admitted.id);
|
|
247
|
+
dispatchTurn.updatedAt = new Date().toISOString();
|
|
248
|
+
}
|
|
249
|
+
let conversationTurn;
|
|
250
|
+
try {
|
|
251
|
+
conversationTurn = await application.commit(admitted);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
admission.reject(error);
|
|
254
|
+
dispatchTurn.abortController.abort(error);
|
|
255
|
+
// Preserve the persistence error while joining the original recovery.
|
|
256
|
+
await dispatchMonitor.catch(() => null);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
if (recoveryReceipt) {
|
|
260
|
+
const receiptAbort = new AbortController();
|
|
261
|
+
// The original monitor can fail and retire without aborting its turn.
|
|
262
|
+
// Its settlement also ends this read; it never creates a successor turn.
|
|
263
|
+
void dispatchMonitor.then(() => receiptAbort.abort(new Error(
|
|
264
|
+
dispatchTurn.observationError || dispatchTurn.error || "OpenCode observation ended before input custody was confirmed."
|
|
265
|
+
)), error => receiptAbort.abort(error));
|
|
266
|
+
try {
|
|
267
|
+
const observation = await inspectOpenCodeMessageAdmission(target.server.client, currentThreadId,
|
|
268
|
+
normalizeNullableText(admitted.id), { wait: true, signal: AbortSignal.any([
|
|
269
|
+
target.abortController.signal, dispatchTurn.abortController.signal, receiptAbort.signal
|
|
270
|
+
]) });
|
|
271
|
+
const message = openCodeMessageRows(observation.messages).find(message =>
|
|
272
|
+
message.type === "user" && normalizeNullableText(message.id) === normalizeNullableText(admitted.id));
|
|
273
|
+
const created = message?.time?.created;
|
|
274
|
+
const recoveryCreated = recoveryReceipt.time?.created;
|
|
275
|
+
// Match the native latest-user order, including its ID tie-breaker.
|
|
276
|
+
if (!Number.isFinite(created) || !Number.isFinite(recoveryCreated) ||
|
|
277
|
+
!(created > recoveryCreated || (created === recoveryCreated && message.id > recoveryReceipt.id))) {
|
|
278
|
+
throw new AppError(503,
|
|
279
|
+
"OpenCode could not confirm that the new input follows its final-answer recovery. The admitted message was not resent.",
|
|
280
|
+
{ code: "assistant_opencode_recovery_order_unconfirmed", details: {} });
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
recoveryObservationError = error;
|
|
284
|
+
} finally {
|
|
285
|
+
receiptAbort.abort();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (recoveryObservationError) {
|
|
289
|
+
// Native B was dispatched: keep its authored receipt even when observing
|
|
290
|
+
// its place in native history failed, and use the original cleanup owner.
|
|
291
|
+
admission.reject(recoveryObservationError);
|
|
292
|
+
dispatchTurn.abortController.abort(recoveryObservationError);
|
|
293
|
+
await dispatchMonitor.catch(() => null);
|
|
294
|
+
} else {
|
|
295
|
+
admission.resolve();
|
|
296
|
+
}
|
|
297
|
+
const turn = openCodeTurnSnapshot(turns.get(key), target.upstreamSessionId);
|
|
298
|
+
return {
|
|
299
|
+
conversationTurn,
|
|
300
|
+
delivered: true,
|
|
301
|
+
deliveryMode: currentMonitor ? "steer" : "new_turn",
|
|
302
|
+
ok: true,
|
|
303
|
+
thread: { id: target.upstreamSessionId },
|
|
304
|
+
turn,
|
|
305
|
+
workdir: input.workdir
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function reconcileSessions(sessions = [], options = {}, preparation) {
|
|
310
|
+
const results = [];
|
|
311
|
+
for (const session of sessions) {
|
|
312
|
+
const sessionId = normalizeNullableText(session?.sessionId || session?.id);
|
|
313
|
+
try {
|
|
314
|
+
const prepared = await preparation.session(sessionId, session, options);
|
|
315
|
+
const resumed = await reconcilePreparedSession(prepared, prepared.activeRun, options);
|
|
316
|
+
results.push({ ok: true, resumed, sessionId });
|
|
317
|
+
} catch (error) {
|
|
318
|
+
results.push({ error: normalizeNullableText(error?.message), ok: false, sessionId });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
failed: results.filter((result) => result.ok === false),
|
|
323
|
+
ok: results.every((result) => result.ok),
|
|
324
|
+
results,
|
|
325
|
+
sessionCount: results.length
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function reconcilePreparedSession(prepared, activeRun, options) {
|
|
330
|
+
const target = await processOwner.acquirePrepared(await prepared.process());
|
|
331
|
+
const current = await ensureOpenCodeSession(target, prepared.session());
|
|
332
|
+
return reconcileSessionTurn(prepared.key, current, activeRun, options, prepared.observation).catch(prepared.failure);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function reconcileSessionTurn(key, target, activeRun, options, application) {
|
|
336
|
+
if (activeRun?.observationError) {
|
|
337
|
+
await processOwner.stopUnobservedSession(target, activeRun.threadId);
|
|
338
|
+
await application.writeRun({ ...activeRun, id: activeRun.turnId }, "interrupted", activeRun.observationError);
|
|
339
|
+
} else if (activeRun) {
|
|
340
|
+
turnMonitor.beginMessageMonitor(key, target, {
|
|
341
|
+
restored: true,
|
|
342
|
+
eventStartedAt: Date.parse(normalizeNullableText(activeRun.startedAt)) || Date.now(),
|
|
343
|
+
id: normalizeNullableText(activeRun.turnId) || application.fallbackTurnId,
|
|
344
|
+
startedAt: normalizeNullableText(activeRun.startedAt)
|
|
345
|
+
}, options, application.monitor);
|
|
346
|
+
}
|
|
347
|
+
return Boolean(activeRun && !activeRun.observationError);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function ensurePreparedSessionReadiness(prepared) {
|
|
351
|
+
if (Object.hasOwn(prepared, "value")) return prepared.value;
|
|
352
|
+
let target = processes.get(prepared.key);
|
|
353
|
+
const connection = await prepared.connection();
|
|
354
|
+
const { ready, unhealthyProcess, missingUpstream } = await inspectSessionReadiness(target, {
|
|
355
|
+
selection: prepared.selection, workdir: prepared.workdir, connection,
|
|
356
|
+
changed: prepared.changed, refresh: prepared.refresh
|
|
357
|
+
});
|
|
358
|
+
if (!ready) {
|
|
359
|
+
const { value: recovered } = await prepared.prepare(recovery => recoverSessionReadiness(target, {
|
|
360
|
+
unhealthyProcess, missingUpstream,
|
|
361
|
+
get key() { return recovery.key; }
|
|
362
|
+
}, async () => {
|
|
363
|
+
const configuration = recovery.configuration();
|
|
364
|
+
async function ensureProcess() {
|
|
365
|
+
return processOwner.acquirePrepared(await configuration.process());
|
|
366
|
+
}
|
|
367
|
+
const current = await ensureProcess();
|
|
368
|
+
return ensureOpenCodeSession(current, configuration.session);
|
|
369
|
+
}));
|
|
370
|
+
if (recovered?.ok === false) return recovered;
|
|
371
|
+
target = recovered;
|
|
372
|
+
}
|
|
373
|
+
const savedRun = prepared.observation.run;
|
|
374
|
+
if (needsObservationRecovery(prepared.key, savedRun)) {
|
|
375
|
+
await prepared.observation.write(async (run, commit) => {
|
|
376
|
+
if (!needsObservationRecovery(prepared.key, run)) return;
|
|
377
|
+
const threadId = normalizeNullableText(run.threadId) || target.upstreamSessionId;
|
|
378
|
+
await recoverObservation(target, threadId, () => commit(writeRun => (
|
|
379
|
+
recordStoppedObservation(prepared.key, run, threadId, writeRun)
|
|
380
|
+
)));
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
ok: true,
|
|
385
|
+
thread: { id: target.upstreamSessionId },
|
|
386
|
+
turn: openCodeTurnSnapshot(turns.get(prepared.key), target.upstreamSessionId),
|
|
387
|
+
workdir: prepared.workdir
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function inspectSessionReadiness(target, { selection, workdir, connection, refresh, changed }) {
|
|
392
|
+
let ready = false;
|
|
393
|
+
let unhealthyProcess = null;
|
|
394
|
+
let missingUpstream = false;
|
|
395
|
+
if (
|
|
396
|
+
target?.upstream &&
|
|
397
|
+
sameOpenCodeSelection(target.upstreamSelection, selection) &&
|
|
398
|
+
target.workdir === workdir &&
|
|
399
|
+
normalizeNullableText(target.selection?.catalogRevision) === normalizeNullableText(selection.catalogRevision) &&
|
|
400
|
+
target.connectionFingerprint === connection.fingerprint &&
|
|
401
|
+
target.canonicalUrl === connection.canonicalUrl &&
|
|
402
|
+
target.endpointCode === connection.endpointCode
|
|
403
|
+
) {
|
|
404
|
+
const observedProcess = processOwner.current;
|
|
405
|
+
const observedServer = target.server;
|
|
406
|
+
let healthy = false;
|
|
407
|
+
const probe = AbortSignal.timeout(1_000);
|
|
408
|
+
try {
|
|
409
|
+
await target.server.client.health({ signal: probe });
|
|
410
|
+
healthy = true;
|
|
411
|
+
ready = Boolean(await target.server.client.readSession(target.upstreamSessionId));
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if ((!healthy && probe.aborted) || (healthy && error?.statusCode !== 404)) {
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
if (!healthy) {
|
|
417
|
+
unhealthyProcess = observedProcess;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const current = await refresh();
|
|
421
|
+
if (
|
|
422
|
+
processes.get(current.key) !== target ||
|
|
423
|
+
target.abortController.signal.aborted ||
|
|
424
|
+
target.server !== observedServer ||
|
|
425
|
+
target.workdir !== current.workdir ||
|
|
426
|
+
!sameOpenCodeSelection(target.upstreamSelection, current.selection) ||
|
|
427
|
+
normalizeNullableText(target.selection?.catalogRevision) !== normalizeNullableText(current.selection.catalogRevision)
|
|
428
|
+
) {
|
|
429
|
+
throw changed();
|
|
430
|
+
}
|
|
431
|
+
missingUpstream = healthy && !ready;
|
|
432
|
+
}
|
|
433
|
+
return { ready, unhealthyProcess, missingUpstream };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function recoverSessionReadiness(target, recovery, prepare) {
|
|
437
|
+
if (recovery.unhealthyProcess && processOwner.current === recovery.unhealthyProcess) {
|
|
438
|
+
await processOwner.stop("opencode-health-check-failed");
|
|
439
|
+
}
|
|
440
|
+
if (recovery.missingUpstream && processes.get(recovery.key) === target) {
|
|
441
|
+
target.upstream = null;
|
|
442
|
+
}
|
|
443
|
+
return prepare();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function needsObservationRecovery(key, run) {
|
|
447
|
+
return Boolean(run?.active && run.observationError && !monitors.has(key));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function recordStoppedObservation(key, run, threadId, writeRun) {
|
|
451
|
+
const stopped = {
|
|
452
|
+
...turns.get(key),
|
|
453
|
+
...run,
|
|
454
|
+
id: normalizeNullableText(run.turnId),
|
|
455
|
+
threadId,
|
|
456
|
+
active: false,
|
|
457
|
+
state: "interrupted",
|
|
458
|
+
updatedAt: new Date().toISOString()
|
|
459
|
+
};
|
|
460
|
+
await writeRun(stopped, "interrupted", run.observationError);
|
|
461
|
+
turns.set(key, stopped);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function recoverObservation(target, threadId, commit) {
|
|
465
|
+
if (threadId !== target.upstreamSessionId) return;
|
|
466
|
+
const status = await target.server.client.sessionStatus(threadId, {
|
|
467
|
+
signal: AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS)
|
|
468
|
+
});
|
|
469
|
+
if (status?.type !== "idle") return;
|
|
470
|
+
await commit();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function interruptTurn(key, target, { threadId = "", writeRun } = {}) {
|
|
474
|
+
const turn = turns.get(key);
|
|
475
|
+
// Helpers can acquire the shared process before a main conversation exists.
|
|
476
|
+
// Their cleanup is owned by closeSession, not a native main-turn interrupt.
|
|
477
|
+
if (!target || (!normalizeNullableText(target.upstreamSessionId) && !turn?.active)) {
|
|
478
|
+
return {
|
|
479
|
+
interrupted: false,
|
|
480
|
+
ok: true,
|
|
481
|
+
thread: { id: threadId },
|
|
482
|
+
turn: openCodeTurnSnapshot(turn)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (turn?.observationError) {
|
|
486
|
+
await monitors.get(key);
|
|
487
|
+
if (turn.active) {
|
|
488
|
+
await processOwner.stopUnobservedSession(target, turn.threadId);
|
|
489
|
+
turn.active = false;
|
|
490
|
+
turn.state = "interrupted";
|
|
491
|
+
await writeRun(turn, turn.state, turn.observationError);
|
|
492
|
+
}
|
|
493
|
+
return { ok: true, interrupted: true, thread: { id: turn.threadId }, turn: openCodeTurnSnapshot(turn) };
|
|
494
|
+
}
|
|
495
|
+
if (turn) {
|
|
496
|
+
turn.interruptRequested = true;
|
|
497
|
+
}
|
|
498
|
+
const interrupted = Boolean(turn?.active);
|
|
499
|
+
try {
|
|
500
|
+
const confirmed = await target.server.client.interrupt(target.upstreamSessionId, {
|
|
501
|
+
signal: AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS)
|
|
502
|
+
});
|
|
503
|
+
if (confirmed !== true) {
|
|
504
|
+
throw new AppError(502, "OpenCode did not confirm Stop. Try Stop again.", {
|
|
505
|
+
code: "assistant_opencode_interrupt_unconfirmed", details: {}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (turn) turn.interruptRequested = false;
|
|
510
|
+
if (error?.name === "TimeoutError") {
|
|
511
|
+
throw new AppError(504, "OpenCode did not confirm Stop within 5 seconds. Try Stop again.", {
|
|
512
|
+
code: "assistant_opencode_interrupt_timeout", details: {}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
if (turn?.active && turn.abortController) {
|
|
518
|
+
turn.interruptAcknowledged = true;
|
|
519
|
+
turn.abortController.abort();
|
|
520
|
+
turn.admission?.resolve();
|
|
521
|
+
await monitors.get(key);
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
interrupted,
|
|
525
|
+
ok: true,
|
|
526
|
+
thread: { id: target.upstreamSessionId },
|
|
527
|
+
turn: openCodeTurnSnapshot(turn, target.upstreamSessionId)
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return Object.freeze({
|
|
532
|
+
runPreparedRenewalTurn,
|
|
533
|
+
sendPreparedMessage,
|
|
534
|
+
inspectPreparedMessage,
|
|
535
|
+
interruptPreparedTurn,
|
|
536
|
+
sendMessage,
|
|
537
|
+
reconcileSessions,
|
|
538
|
+
reconcilePreparedSession,
|
|
539
|
+
ensurePreparedSessionReadiness,
|
|
540
|
+
inspectSessionReadiness,
|
|
541
|
+
recoverSessionReadiness,
|
|
542
|
+
needsObservationRecovery,
|
|
543
|
+
recordStoppedObservation,
|
|
544
|
+
recoverObservation,
|
|
545
|
+
interruptTurn
|
|
546
|
+
});
|
|
547
|
+
}
|