@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
|
@@ -1,50 +1,22 @@
|
|
|
1
|
+
import { projectOpenCodeConversationMessages } from "./openCodeTurn.js";
|
|
2
|
+
import { ensureOpenCodeSession } from "./openCodeNativeSession.js";
|
|
3
|
+
import { createOpenCodeProcessOwner } from "./openCodeProcessOwner.js";
|
|
4
|
+
import { createOpenCodeTemporaryConversations } from "./openCodeTemporaryConversations.js";
|
|
1
5
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./
|
|
5
|
-
import { openCodeAssistantMessageText as assistantMessageText } from "./openCodeClient.js";
|
|
6
|
-
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
6
|
+
createOpenCodeTurnMonitor, openCodeProviderApiFailure, openCodeTurnSnapshot, writeConversationProjection
|
|
7
|
+
} from "./openCodeTurnMonitor.js";
|
|
8
|
+
import { createOpenCodeSessionTurns } from "./openCodeSessionTurns.js";
|
|
7
9
|
import { normalizeNullableText } from "./normalize.js";
|
|
8
10
|
import { randomUUID } from "node:crypto";
|
|
9
11
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
10
12
|
import path from "node:path";
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
// One runtime per scope directory: the native server it owns is shared by the
|
|
15
|
+
// whole process.
|
|
13
16
|
const sharedRuntimes = new Map();
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const active = source.active === true;
|
|
18
|
-
return active || text(source.id)
|
|
19
|
-
? {
|
|
20
|
-
active,
|
|
21
|
-
error: text(source.error),
|
|
22
|
-
id: text(source.id),
|
|
23
|
-
phase: active && !source.observationError ? text(source.phase) : "",
|
|
24
|
-
startedAt: text(source.startedAt),
|
|
25
|
-
state: text(source.state) || (active ? "active" : "completed"),
|
|
26
|
-
status: source.observationError ? "observation_lost" : text(source.state) || (active ? "active" : "completed"),
|
|
27
|
-
threadId: text(source.threadId || threadId),
|
|
28
|
-
updatedAt: text(source.updatedAt)
|
|
29
|
-
}
|
|
30
|
-
: null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function openCodeCredentialFailure(value = "") {
|
|
34
|
-
const failure = text(value);
|
|
35
|
-
return (
|
|
36
|
-
/\b(?:401|403)\b/u.test(failure) ||
|
|
37
|
-
(
|
|
38
|
-
/(?:api[-_\s]?key|authentication|authorization|credential|access[-_\s]?token|unauthori[sz]ed|forbidden)/iu.test(failure) &&
|
|
39
|
-
/(?:denied|expired|failed|forbidden|incorrect|invalid|missing|rejected|revoked|unauthori[sz]ed)/iu.test(failure)
|
|
40
|
-
)
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function openCodeProviderApiFailure(error = {}) {
|
|
45
|
-
return text(error?.name) === "APIError";
|
|
46
|
-
}
|
|
47
|
-
|
|
18
|
+
/** A stop of the shared server that no one observes. */
|
|
19
|
+
function ignoreRuntimeStop() {}
|
|
48
20
|
|
|
49
21
|
export function assertOpenCodeModelProvider(selected, input = {}) {
|
|
50
22
|
if (selected?.modelProviderId && input.model?.providerID !== selected.modelProviderId) {
|
|
@@ -74,17 +46,17 @@ function pathContains(root = "", candidate = "") {
|
|
|
74
46
|
}
|
|
75
47
|
|
|
76
48
|
export function openCodeEnvironmentForDirectory(environments, cwd = "") {
|
|
77
|
-
const normalizedCwd = path.resolve(
|
|
49
|
+
const normalizedCwd = path.resolve(normalizeNullableText(cwd) || process.cwd());
|
|
78
50
|
return environments
|
|
79
|
-
.filter(entry =>
|
|
51
|
+
.filter(entry => normalizeNullableText(entry?.workdir) && pathContains(path.resolve(entry.workdir), normalizedCwd))
|
|
80
52
|
.sort((left, right) => path.resolve(right.workdir).length - path.resolve(left.workdir).length)[0] || null;
|
|
81
53
|
}
|
|
82
54
|
|
|
83
55
|
export async function openCodeEnvironmentForSession(environments, sessionId = "", client = null) {
|
|
84
56
|
const visited = new Set();
|
|
85
|
-
let id =
|
|
57
|
+
let id = normalizeNullableText(sessionId);
|
|
86
58
|
while (id && !visited.has(id) && visited.size < 32) {
|
|
87
|
-
const selected = environments.find(entry =>
|
|
59
|
+
const selected = environments.find(entry => normalizeNullableText(entry?.upstreamSessionId) === id);
|
|
88
60
|
if (selected) return selected;
|
|
89
61
|
if (!client) return null;
|
|
90
62
|
visited.add(id);
|
|
@@ -92,84 +64,21 @@ export async function openCodeEnvironmentForSession(environments, sessionId = ""
|
|
|
92
64
|
if (result.error) throw Object.assign(new Error("The subagent's parent session could not be verified."), {
|
|
93
65
|
code: "assistant_opencode_parent_unverified"
|
|
94
66
|
});
|
|
95
|
-
id =
|
|
67
|
+
id = normalizeNullableText(result.data?.parentID);
|
|
96
68
|
}
|
|
97
69
|
return null;
|
|
98
70
|
}
|
|
99
71
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function
|
|
110
|
-
const modelId = text(executionProfile?.model) || text(selection.modelId);
|
|
111
|
-
const variantId = executionProfile
|
|
112
|
-
? text(executionProfile.thinking)
|
|
113
|
-
: text(selection.variantId);
|
|
114
|
-
return {
|
|
115
|
-
id: modelId,
|
|
116
|
-
providerID: text(selection.modelProviderId),
|
|
117
|
-
...(variantId ? { variant: variantId } : {})
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function sameOpenCodeSelection(left = {}, right = {}) {
|
|
122
|
-
return ["agentId", "modelId", "modelProviderId", "variantId"]
|
|
123
|
-
.every((name) => text(left?.[name]) === text(right?.[name]));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Native selection and identity publication share the same pending operation. */
|
|
127
|
-
export async function ensureOpenCodeSession(target, { selection, model, workdir = target.workdir, identity = {}, signal,
|
|
128
|
-
invalidIdentity = () => new Error("OpenCode did not return a native conversation ID.") }) {
|
|
129
|
-
if (target.upstreamStart) {
|
|
130
|
-
return target.upstreamStart;
|
|
131
|
-
}
|
|
132
|
-
const start = Promise.resolve().then(async () => {
|
|
133
|
-
await target.server.client.prepareDirectory({ signal });
|
|
134
|
-
let upstream = (
|
|
135
|
-
target.upstream &&
|
|
136
|
-
sameOpenCodeSelection(target.upstreamSelection, selection)
|
|
137
|
-
) ? target.upstream : null;
|
|
138
|
-
if (!upstream && target.upstreamSessionId) {
|
|
139
|
-
upstream = await target.server.client.readSession(target.upstreamSessionId, { signal });
|
|
140
|
-
await target.server.client.switchModel(target.upstreamSessionId, model, { signal });
|
|
141
|
-
await target.server.client.switchAgent(target.upstreamSessionId, selection.agentId, { signal });
|
|
142
|
-
} else if (!upstream) {
|
|
143
|
-
upstream = await target.server.client.createSession({
|
|
144
|
-
agent: selection.agentId,
|
|
145
|
-
location: { directory: workdir },
|
|
146
|
-
model
|
|
147
|
-
}, { signal });
|
|
148
|
-
}
|
|
149
|
-
const nativeId = text(upstream?.id);
|
|
150
|
-
if (!nativeId) {
|
|
151
|
-
throw invalidIdentity();
|
|
152
|
-
}
|
|
153
|
-
await identity.write?.(nativeId);
|
|
154
|
-
target.upstreamSessionId = nativeId;
|
|
155
|
-
target.upstream = upstream;
|
|
156
|
-
target.upstreamSelection = { ...selection };
|
|
157
|
-
await identity.publish?.(nativeId);
|
|
158
|
-
return target;
|
|
159
|
-
});
|
|
160
|
-
target.upstreamStart = start;
|
|
161
|
-
try {
|
|
162
|
-
return await start;
|
|
163
|
-
} finally {
|
|
164
|
-
if (target.upstreamStart === start) {
|
|
165
|
-
delete target.upstreamStart;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// The production provider's users and pending acquisitions share this owner.
|
|
171
|
-
// Hosts supply authorized startup inputs and their own publication/cleanup work.
|
|
172
|
-
export function createOpenCodeSharedRuntime({ onStop = () => {}, scope, storageIdleMs = 60_000 } = {}) {
|
|
72
|
+
/**
|
|
73
|
+
* The production provider's users and pending acquisitions share this owner.
|
|
74
|
+
* Hosts supply authorized startup inputs and their own publication/cleanup work.
|
|
75
|
+
* @param {{
|
|
76
|
+
* onStop?: (stop: { reason: string, sessions: number, startingSessions: number }) => void,
|
|
77
|
+
* scope?: string,
|
|
78
|
+
* storageIdleMs?: number
|
|
79
|
+
* }} options
|
|
80
|
+
*/
|
|
81
|
+
export function createOpenCodeSharedRuntime({ onStop = ignoreRuntimeStop, scope, storageIdleMs = 60_000 }) {
|
|
173
82
|
if (scope !== undefined && !path.isAbsolute(scope)) throw new TypeError("OpenCode runtime scopes require an absolute directory.");
|
|
174
83
|
if (scope && sharedRuntimes.has(scope)) return sharedRuntimes.get(scope);
|
|
175
84
|
const processes = new Map();
|
|
@@ -177,18 +86,21 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope, storageI
|
|
|
177
86
|
const monitors = new Map();
|
|
178
87
|
const turns = new Map();
|
|
179
88
|
const temporaryConversations = new Map();
|
|
180
|
-
const processExitProofs = new Map();
|
|
181
|
-
let sharedProcess = null;
|
|
182
|
-
let sharedProcessStart = null;
|
|
183
|
-
let sharedProcessStop = null;
|
|
184
|
-
let failedStartup = null;
|
|
185
|
-
let closed = false;
|
|
186
|
-
let connectionUpdate = Promise.resolve();
|
|
187
|
-
let storageReads = 0;
|
|
188
|
-
let storageIdle = null;
|
|
189
89
|
const bindingScopes = new Map();
|
|
190
90
|
let registryPath = "";
|
|
191
91
|
let bindingWrite = Promise.resolve();
|
|
92
|
+
const processOwner = createOpenCodeProcessOwner({
|
|
93
|
+
processes,
|
|
94
|
+
processStarts,
|
|
95
|
+
monitors,
|
|
96
|
+
turns,
|
|
97
|
+
temporaryConversations,
|
|
98
|
+
onStop,
|
|
99
|
+
storageIdleMs
|
|
100
|
+
});
|
|
101
|
+
const conversations = createOpenCodeTemporaryConversations({ processes, temporaryConversations, processOwner });
|
|
102
|
+
const turnMonitor = createOpenCodeTurnMonitor({ processes, monitors, turns, processOwner });
|
|
103
|
+
const sessionTurns = createOpenCodeSessionTurns({ processes, monitors, turns, processOwner, turnMonitor });
|
|
192
104
|
|
|
193
105
|
function writeBindings(filename, scope, entries) {
|
|
194
106
|
if (!path.isAbsolute(filename || "") || registryPath && registryPath !== filename) {
|
|
@@ -209,1674 +121,6 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope, storageI
|
|
|
209
121
|
return writing;
|
|
210
122
|
}
|
|
211
123
|
|
|
212
|
-
async function stop(reason = "opencode-last-session-closed") {
|
|
213
|
-
if (sharedProcessStop) return sharedProcessStop;
|
|
214
|
-
const target = sharedProcess;
|
|
215
|
-
const startup = failedStartup;
|
|
216
|
-
if (!target && !startup) return { exited: true, reason };
|
|
217
|
-
onStop({ reason, sessions: processes.size, startingSessions: processStarts.size });
|
|
218
|
-
const stopping = (async () => {
|
|
219
|
-
const proof = startup ? await startup.retryCleanup() : await target.server.stop();
|
|
220
|
-
if (proof?.exited !== true) {
|
|
221
|
-
throw Object.assign(new Error("OpenCode process exit could not be verified."), {
|
|
222
|
-
code: "assistant_opencode_stop_unverified", details: {}, statusCode: 503
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
if (sharedProcess === target) sharedProcess = null;
|
|
226
|
-
if (failedStartup === startup) failedStartup = null;
|
|
227
|
-
return proof;
|
|
228
|
-
})();
|
|
229
|
-
sharedProcessStop = stopping;
|
|
230
|
-
try {
|
|
231
|
-
return await stopping;
|
|
232
|
-
} finally {
|
|
233
|
-
if (sharedProcessStop === stopping) sharedProcessStop = null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// A credential change reaches a native directory when OpenCode reloads that
|
|
238
|
-
// directory's instance. An idle directory reloads at once; one with a turn in
|
|
239
|
-
// flight reloads when its last turn there ends. New work in a directory waits
|
|
240
|
-
// for its reload, so it never starts on credentials the server no longer holds.
|
|
241
|
-
function directoryState(shared, workdir) {
|
|
242
|
-
const directory = path.resolve(workdir);
|
|
243
|
-
let state = shared.directories.get(directory);
|
|
244
|
-
if (!state) {
|
|
245
|
-
state = { directory, generation: shared.generation, turns: 0, reload: null, idle: null };
|
|
246
|
-
shared.directories.set(directory, state);
|
|
247
|
-
}
|
|
248
|
-
return state;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function directoryCurrent(workdir) {
|
|
252
|
-
const state = sharedProcess && text(workdir) ? sharedProcess.directories.get(path.resolve(workdir)) : null;
|
|
253
|
-
return !state || (!state.reload && state.generation === sharedProcess.generation);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function reloadDirectory(shared, state) {
|
|
257
|
-
if (state.reload || state.generation === shared.generation) return state.reload;
|
|
258
|
-
state.reload = (async () => {
|
|
259
|
-
try {
|
|
260
|
-
while (state.generation < shared.generation) {
|
|
261
|
-
const generation = shared.generation;
|
|
262
|
-
await shared.server.client.forDirectory(state.directory).disposeInstance();
|
|
263
|
-
state.generation = generation;
|
|
264
|
-
}
|
|
265
|
-
} finally {
|
|
266
|
-
state.reload = null;
|
|
267
|
-
}
|
|
268
|
-
})();
|
|
269
|
-
return state.reload;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async function waitForDirectory(workdir) {
|
|
273
|
-
const shared = sharedProcess;
|
|
274
|
-
const state = directoryState(shared, workdir);
|
|
275
|
-
try {
|
|
276
|
-
if (state.reload || !state.turns) await reloadDirectory(shared, state);
|
|
277
|
-
else await (state.idle ||= Promise.withResolvers()).promise;
|
|
278
|
-
} catch (error) {
|
|
279
|
-
if (sharedProcess !== shared) return;
|
|
280
|
-
throw Object.assign(new Error("OpenCode could not load the changed AI connection for this work. Send your message again."), {
|
|
281
|
-
cause: error, code: "assistant_opencode_connection_reload_failed", details: {}, statusCode: 503
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async function currentDirectory(workdir) {
|
|
287
|
-
while (!directoryCurrent(workdir)) await waitForDirectory(workdir);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Holds a directory's credentials for a turn in flight there.
|
|
291
|
-
function holdDirectory(workdir) {
|
|
292
|
-
const shared = sharedProcess;
|
|
293
|
-
if (!shared || !text(workdir)) return () => {};
|
|
294
|
-
const state = directoryState(shared, workdir);
|
|
295
|
-
state.turns += 1;
|
|
296
|
-
let held = true;
|
|
297
|
-
return () => {
|
|
298
|
-
if (!held) return;
|
|
299
|
-
held = false;
|
|
300
|
-
state.turns -= 1;
|
|
301
|
-
if (state.turns) return;
|
|
302
|
-
state.idle?.resolve();
|
|
303
|
-
state.idle = null;
|
|
304
|
-
if (sharedProcess === shared) reloadDirectory(shared, state)?.catch(() => {});
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function admitDirectory(workdir) {
|
|
309
|
-
while (!directoryCurrent(workdir)) await waitForDirectory(workdir);
|
|
310
|
-
return holdDirectory(workdir);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function reloadChangedCredentials(shared) {
|
|
314
|
-
for (const directory of shared.server.client.instanceDirectories?.() || []) directoryState(shared, directory);
|
|
315
|
-
shared.generation += 1;
|
|
316
|
-
for (const state of shared.directories.values()) {
|
|
317
|
-
if (!state.turns) reloadDirectory(shared, state)?.catch(() => {});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function connectionRecord(connection) {
|
|
322
|
-
return { canonicalUrl: connection.canonicalUrl, endpointCode: connection.endpointCode, fingerprint: connection.fingerprint };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function sameConnection(recorded, connection) {
|
|
326
|
-
return recorded?.canonicalUrl === connection.canonicalUrl && recorded.fingerprint === connection.fingerprint &&
|
|
327
|
-
recorded.endpointCode === connection.endpointCode;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// A key changes in place; a provider route belongs to the server's configuration.
|
|
331
|
-
function inPlaceConnection(recorded, connection) {
|
|
332
|
-
return Boolean(connection?.apiKey) && (recorded
|
|
333
|
-
? recorded.canonicalUrl === connection.canonicalUrl && recorded.endpointCode === connection.endpointCode
|
|
334
|
-
: !text(connection.canonicalUrl));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// The running server takes the host's saved connections without stopping its
|
|
338
|
-
// turns. Reading them inside this queue applies the newest saved state last.
|
|
339
|
-
function updateConnections(readConnections) {
|
|
340
|
-
const update = connectionUpdate.then(async () => {
|
|
341
|
-
const shared = sharedProcess;
|
|
342
|
-
if (!shared) return;
|
|
343
|
-
shared.connectionsCurrent = false;
|
|
344
|
-
let changed = false;
|
|
345
|
-
try {
|
|
346
|
-
const saved = new Map((await readConnections()).map(connection => [connection.modelProviderId, connection]));
|
|
347
|
-
for (const [providerId, connection] of saved) {
|
|
348
|
-
const recorded = shared.connections.get(providerId);
|
|
349
|
-
if (sameConnection(recorded, connection) || !inPlaceConnection(recorded, connection)) continue;
|
|
350
|
-
await shared.server.client.authenticateApiKey(providerId, connection.apiKey);
|
|
351
|
-
shared.connections.set(providerId, connectionRecord(connection));
|
|
352
|
-
changed = true;
|
|
353
|
-
}
|
|
354
|
-
for (const providerId of [...shared.connections.keys()]) {
|
|
355
|
-
if (saved.has(providerId)) continue;
|
|
356
|
-
await shared.server.client.removeAuthentication(providerId);
|
|
357
|
-
shared.connections.delete(providerId);
|
|
358
|
-
changed = true;
|
|
359
|
-
}
|
|
360
|
-
shared.connectionsCurrent = true;
|
|
361
|
-
} finally {
|
|
362
|
-
if (changed) reloadChangedCredentials(shared);
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
connectionUpdate = update.catch(() => {});
|
|
366
|
-
return update;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// `readConnections` returns the host's saved connections. Without it, a changed
|
|
370
|
-
// account needs a new server.
|
|
371
|
-
async function ensure(selected, start, readConnections = null) {
|
|
372
|
-
await sharedProcessStop;
|
|
373
|
-
if (failedStartup) await stop("opencode-startup-cleanup");
|
|
374
|
-
if (sharedProcess) {
|
|
375
|
-
const recorded = sharedProcess.connections.get(selected?.modelProviderId);
|
|
376
|
-
const current = !selected || sameConnection(recorded, selected);
|
|
377
|
-
if (current || (readConnections && inPlaceConnection(recorded, selected))) {
|
|
378
|
-
const probe = AbortSignal.timeout(1_000);
|
|
379
|
-
let healthy = false;
|
|
380
|
-
try {
|
|
381
|
-
await sharedProcess.server.client.health({ signal: probe });
|
|
382
|
-
healthy = true;
|
|
383
|
-
} catch (error) {
|
|
384
|
-
// A slow probe is not proof that the shared server stopped working.
|
|
385
|
-
if (probe.aborted) throw error;
|
|
386
|
-
await stop("opencode-health-check-failed");
|
|
387
|
-
}
|
|
388
|
-
if (healthy && sharedProcess) {
|
|
389
|
-
if (readConnections && (!current || !sharedProcess.connectionsCurrent)) await updateConnections(readConnections);
|
|
390
|
-
return sharedProcess;
|
|
391
|
-
}
|
|
392
|
-
} else {
|
|
393
|
-
await stop("opencode-connection-changed");
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
if (sharedProcessStart) return sharedProcessStart;
|
|
397
|
-
sharedProcessStart = Promise.resolve().then(async () => {
|
|
398
|
-
const { connections, server } = await start();
|
|
399
|
-
const started = {
|
|
400
|
-
connections: new Map(connections.map(connection => [connection.modelProviderId, connectionRecord(connection)])),
|
|
401
|
-
connectionsCurrent: true, directories: new Map(), generation: 0, server
|
|
402
|
-
};
|
|
403
|
-
sharedProcess = started;
|
|
404
|
-
// An unexpected exit retires the server's leftovers at once.
|
|
405
|
-
void server.unexpectedExit?.then(() => sharedProcess === started ? stop("opencode-server-exited") : null)
|
|
406
|
-
.catch(() => {});
|
|
407
|
-
for (const target of processes.values()) {
|
|
408
|
-
target.server = openCodeServerForDirectory(server, target.workdir);
|
|
409
|
-
target.upstream = null;
|
|
410
|
-
target.upstreamSelection = null;
|
|
411
|
-
}
|
|
412
|
-
return sharedProcess;
|
|
413
|
-
}).catch(error => {
|
|
414
|
-
if (error.cleanupFailed && typeof error.retryCleanup === "function") failedStartup = error;
|
|
415
|
-
throw error;
|
|
416
|
-
});
|
|
417
|
-
try {
|
|
418
|
-
return await sharedProcessStart;
|
|
419
|
-
} finally {
|
|
420
|
-
sharedProcessStart = null;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Native storage inventory and retirement borrow the shared server; they are
|
|
425
|
-
// not users that keep it running. A short idle after the last read lets one
|
|
426
|
-
// maintenance pass share it, then it stops unless a conversation uses it.
|
|
427
|
-
async function withStorageProcess(start, operation, failure = error => { throw error; }) {
|
|
428
|
-
clearTimeout(storageIdle);
|
|
429
|
-
storageIdle = null;
|
|
430
|
-
storageReads += 1;
|
|
431
|
-
try {
|
|
432
|
-
return await operation(await ensure(null, start).catch(failure));
|
|
433
|
-
} finally {
|
|
434
|
-
storageReads -= 1;
|
|
435
|
-
if (storageReads === 0 && !closed) {
|
|
436
|
-
storageIdle = setTimeout(() => {
|
|
437
|
-
storageIdle = null;
|
|
438
|
-
if (storageReads === 0 && processes.size === 0 && processStarts.size === 0) {
|
|
439
|
-
stop("opencode-storage-read-finished").catch(() => null);
|
|
440
|
-
}
|
|
441
|
-
}, storageIdleMs);
|
|
442
|
-
storageIdle.unref?.();
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function targetFor(key, prepared) {
|
|
448
|
-
const current = processes.get(key);
|
|
449
|
-
const nativeId = prepared.upstreamSessionId;
|
|
450
|
-
if (current && current.upstreamSessionId !== nativeId) {
|
|
451
|
-
current.upstreamSessionId = nativeId;
|
|
452
|
-
current.upstream = null;
|
|
453
|
-
}
|
|
454
|
-
if (
|
|
455
|
-
current && !current.abortController.signal.aborted &&
|
|
456
|
-
current.canonicalUrl === prepared.connection.canonicalUrl &&
|
|
457
|
-
current.connectionFingerprint === prepared.connection.fingerprint &&
|
|
458
|
-
current.endpointCode === prepared.connection.endpointCode &&
|
|
459
|
-
current.modelProviderId === prepared.connection.modelProviderId &&
|
|
460
|
-
current.projectContextRoot === prepared.projectContextRoot &&
|
|
461
|
-
current.workdir === prepared.workdir &&
|
|
462
|
-
text(current.selection?.catalogRevision) === text(prepared.selection.catalogRevision) &&
|
|
463
|
-
sameOpenCodeSelection(current.selection, prepared.selection)
|
|
464
|
-
) {
|
|
465
|
-
current.helperModelId = prepared.helperModelId;
|
|
466
|
-
current.server = openCodeServerForDirectory(prepared.server, prepared.workdir);
|
|
467
|
-
return current;
|
|
468
|
-
}
|
|
469
|
-
const server = openCodeServerForDirectory(prepared.server, prepared.workdir);
|
|
470
|
-
const created = (current && !current.abortController.signal.aborted ? current : null) || {
|
|
471
|
-
abortController: new AbortController(),
|
|
472
|
-
key,
|
|
473
|
-
sessionId: prepared.sessionId,
|
|
474
|
-
upstreamSessionId: nativeId
|
|
475
|
-
};
|
|
476
|
-
Object.assign(created, {
|
|
477
|
-
helperModelId: prepared.helperModelId,
|
|
478
|
-
canonicalUrl: prepared.connection.canonicalUrl,
|
|
479
|
-
connectionFingerprint: prepared.connection.fingerprint,
|
|
480
|
-
endpointCode: prepared.connection.endpointCode,
|
|
481
|
-
modelProviderId: prepared.connection.modelProviderId,
|
|
482
|
-
projectContextRoot: prepared.projectContextRoot,
|
|
483
|
-
selection: prepared.selection,
|
|
484
|
-
server,
|
|
485
|
-
workdir: prepared.workdir
|
|
486
|
-
});
|
|
487
|
-
return created;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
async function acquire(key, prepare, onFailure = async () => {}) {
|
|
491
|
-
const pending = processStarts.get(key);
|
|
492
|
-
if (pending) return pending;
|
|
493
|
-
const start = Promise.resolve().then(async () => {
|
|
494
|
-
const target = await prepare();
|
|
495
|
-
processes.set(key, target);
|
|
496
|
-
return target;
|
|
497
|
-
}).catch(async error => {
|
|
498
|
-
if (processStarts.get(key) === start) processStarts.delete(key);
|
|
499
|
-
if (!processes.has(key)) {
|
|
500
|
-
await onFailure();
|
|
501
|
-
if (processes.size === 0 && processStarts.size === 0 && storageReads === 0 && !failedStartup) {
|
|
502
|
-
await stop("opencode-session-start-failed").catch(() => null);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
throw error;
|
|
506
|
-
});
|
|
507
|
-
processStarts.set(key, start);
|
|
508
|
-
try {
|
|
509
|
-
return await start;
|
|
510
|
-
} finally {
|
|
511
|
-
if (processStarts.get(key) === start) processStarts.delete(key);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
async function release(target, { retainSharedProcess = false, onRemoved } = {}) {
|
|
516
|
-
if (processes.get(target.key) === target) {
|
|
517
|
-
processes.delete(target.key);
|
|
518
|
-
await onRemoved?.();
|
|
519
|
-
}
|
|
520
|
-
return !retainSharedProcess && processes.size === 0 && processStarts.size === 0 && storageReads === 0
|
|
521
|
-
? stop()
|
|
522
|
-
: { exited: true, sharedProcessRetained: true };
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
async function releaseTarget(target, { retainSharedProcess = false, onRemoved } = {}) {
|
|
526
|
-
const activeThreads = new Set();
|
|
527
|
-
if (turns.get(target.key)?.active) activeThreads.add(target.upstreamSessionId);
|
|
528
|
-
for (const entry of temporaryConversations.values()) {
|
|
529
|
-
if (entry.active && entry.target?.abortController === target.abortController) {
|
|
530
|
-
activeThreads.add(entry.conversationId);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
for (const threadId of activeThreads) await stopUnobservedSession(target, threadId);
|
|
534
|
-
target.abortController.abort();
|
|
535
|
-
turns.get(target.key)?.admission?.resolve();
|
|
536
|
-
await monitors.get(target.key)?.catch(() => null);
|
|
537
|
-
await Promise.all([...temporaryConversations.values()]
|
|
538
|
-
.filter((entry) => entry.target?.abortController === target.abortController)
|
|
539
|
-
.map((entry) => entry.completion?.catch(() => null)));
|
|
540
|
-
return release(target, { retainSharedProcess, onRemoved });
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
async function releaseProcessTarget(target = null, { retainSharedProcess = false } = {}, application) {
|
|
544
|
-
if (!target) {
|
|
545
|
-
return { exited: true };
|
|
546
|
-
}
|
|
547
|
-
await application.beforeRelease(target);
|
|
548
|
-
const proof = await releaseTarget(target, {
|
|
549
|
-
retainSharedProcess,
|
|
550
|
-
onRemoved: () => application.onRemoved(target)
|
|
551
|
-
}).catch(application.failure);
|
|
552
|
-
if (target.sessionId) {
|
|
553
|
-
processExitProofs.set(target.sessionId, proof);
|
|
554
|
-
}
|
|
555
|
-
return proof;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
async function closeSession(id, options, application) {
|
|
559
|
-
const terminalClose = await application.closeTerminals();
|
|
560
|
-
const pending = [...processStarts.entries()]
|
|
561
|
-
.filter(([key]) => key.endsWith(`\0${id}`))
|
|
562
|
-
.map(([, start]) => start.catch(() => null));
|
|
563
|
-
await Promise.all(pending);
|
|
564
|
-
const targets = [...processes.values()].filter((target) => target.sessionId === id);
|
|
565
|
-
const proofs = await Promise.all(targets.map((target) => releaseProcessTarget(target, options, application)));
|
|
566
|
-
await application.afterRelease();
|
|
567
|
-
for (const key of [...turns.keys()]) {
|
|
568
|
-
if (key.endsWith(`\0${id}`)) {
|
|
569
|
-
turns.delete(key);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
for (const key of [...temporaryConversations.keys()]) {
|
|
573
|
-
if (temporaryConversations.get(key)?.target?.sessionId === id) {
|
|
574
|
-
temporaryConversations.delete(key);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
const result = {
|
|
578
|
-
closed: targets.length,
|
|
579
|
-
ok: proofs.every((proof) => proof?.exited !== false),
|
|
580
|
-
processExitProof: proofs.at(-1) || processExitProofs.get(id) || { exited: true },
|
|
581
|
-
processExitProofs: proofs
|
|
582
|
-
};
|
|
583
|
-
return {
|
|
584
|
-
...result,
|
|
585
|
-
closed: result.closed + Number(terminalClose?.closed || 0),
|
|
586
|
-
ok: terminalClose?.ok !== false && result.ok
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async function closeProject(input = {}, application) {
|
|
591
|
-
await Promise.all([...processStarts.entries()]
|
|
592
|
-
.filter(([key]) => key.includes("\0"))
|
|
593
|
-
.map(([, start]) => start.catch(() => null)));
|
|
594
|
-
const projectContextRoot = text(input.projectContextRoot);
|
|
595
|
-
const targets = [...processes.values()].filter((target) => (
|
|
596
|
-
application.ownsTarget(target) && (!projectContextRoot || target.projectContextRoot === path.resolve(projectContextRoot))
|
|
597
|
-
));
|
|
598
|
-
const results = await Promise.all(targets.map((target) => releaseProcessTarget(target, {}, application)));
|
|
599
|
-
if (processes.size === 0 && storageReads === 0 && (sharedProcess || failedStartup)) {
|
|
600
|
-
results.push(await stop(text(input.reason) || "opencode-project-close").catch(application.failure));
|
|
601
|
-
}
|
|
602
|
-
return {
|
|
603
|
-
closed: results.length,
|
|
604
|
-
ok: results.every((result) => result?.exited !== false)
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async function invalidateRuntimes(input = {}, preparation) {
|
|
609
|
-
if (text(input.reason) === "server-shutdown") {
|
|
610
|
-
closed = true;
|
|
611
|
-
clearTimeout(storageIdle);
|
|
612
|
-
storageIdle = null;
|
|
613
|
-
}
|
|
614
|
-
const application = preparation.release;
|
|
615
|
-
if (typeof input.publish === "function") {
|
|
616
|
-
if (typeof preparation.connections !== "function") {
|
|
617
|
-
throw new TypeError("OpenCode connection changes require the host's saved connections.");
|
|
618
|
-
}
|
|
619
|
-
// Running turns, retained conversations and the shared server continue.
|
|
620
|
-
// The server takes the saved connections at once, and each directory
|
|
621
|
-
// reloads them as soon as it has no turn in flight.
|
|
622
|
-
await application.assertConnectionChange?.();
|
|
623
|
-
await input.publish();
|
|
624
|
-
const update = () => updateConnections(preparation.connections).catch(() => null);
|
|
625
|
-
void sharedProcessStart?.then(update, () => null);
|
|
626
|
-
await update();
|
|
627
|
-
return { closed: 0, ok: true };
|
|
628
|
-
}
|
|
629
|
-
await Promise.all([...processStarts.entries()]
|
|
630
|
-
.filter(([key]) => key.includes("\0"))
|
|
631
|
-
.map(([, start]) => start.catch(() => null)));
|
|
632
|
-
const targets = [...processes.values()].filter(target => application.ownsTarget(target));
|
|
633
|
-
const results = await Promise.all(targets.map((target) => releaseProcessTarget(target, {}, application)));
|
|
634
|
-
if (processes.size === 0 && processStarts.size === 0 && (sharedProcess || failedStartup)) {
|
|
635
|
-
results.push(await stop(text(input.reason) || "opencode-runtime-invalidation").catch(application.failure));
|
|
636
|
-
}
|
|
637
|
-
return {
|
|
638
|
-
closed: results.length,
|
|
639
|
-
ok: results.every((result) => result?.exited !== false)
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
async function closePreparedSession(prepared) {
|
|
644
|
-
return closeSession(prepared.sessionId, prepared.options, prepared.application);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
async function releaseRenewalPredecessorProcessExitProof(preparation) {
|
|
648
|
-
const closed = await closePreparedSession(preparation.cleanup);
|
|
649
|
-
const released = releaseProcessExitProof(preparation.sessionId);
|
|
650
|
-
return {
|
|
651
|
-
...closed,
|
|
652
|
-
...released,
|
|
653
|
-
ok: closed.ok !== false && released.ok !== false
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function releaseProcessExitProof(id) {
|
|
658
|
-
const proof = processExitProofs.get(id) || null;
|
|
659
|
-
processExitProofs.delete(id);
|
|
660
|
-
return {
|
|
661
|
-
ok: Boolean(proof?.exited),
|
|
662
|
-
processExitProof: proof,
|
|
663
|
-
released: Boolean(proof?.exited)
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
async function startPreparedProcess(prepared) {
|
|
668
|
-
const server = await prepared.create(prepared.options);
|
|
669
|
-
return { connections: prepared.connections, server };
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
async function acquirePrepared(prepared) {
|
|
673
|
-
return acquire(prepared.key, async () => {
|
|
674
|
-
const configured = await prepared.configure();
|
|
675
|
-
const shared = await ensure(prepared.connection,
|
|
676
|
-
async () => startPreparedProcess(await configured.prepareServer()),
|
|
677
|
-
async () => (await configured.prepareServer()).connections).catch(prepared.failure);
|
|
678
|
-
configured.target.server = shared.server;
|
|
679
|
-
return targetFor(prepared.key, configured.target);
|
|
680
|
-
}, prepared.onFailure).catch(prepared.failure);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function scopedTarget(target, prepared) {
|
|
684
|
-
const workdir = prepared.workdir;
|
|
685
|
-
return workdir ? { ...target, server: openCodeServerForDirectory(target.server, workdir), workdir } : target;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
async function runPreparedRenewalTurn(preparation) {
|
|
689
|
-
const acquired = await acquirePrepared(await preparation.process());
|
|
690
|
-
const releaseDirectory = await admitDirectory(acquired.workdir);
|
|
691
|
-
try {
|
|
692
|
-
const target = await ensureOpenCodeSession(acquired, preparation.session());
|
|
693
|
-
const turn = preparation.turn(target);
|
|
694
|
-
const result = await runOpenCodeConversationTurn(target.server.client, target.upstreamSessionId, turn.input, {
|
|
695
|
-
...turn.options,
|
|
696
|
-
...(turn.release ? { close: () => releaseProcessTarget(target, {}, turn.release) } : {})
|
|
697
|
-
});
|
|
698
|
-
return turn.completeResult(result);
|
|
699
|
-
} finally {
|
|
700
|
-
releaseDirectory();
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
async function createPreparedConversation(prepared) {
|
|
705
|
-
const target = scopedTarget(await acquirePrepared(prepared.process), prepared);
|
|
706
|
-
const conversation = await createConversation(prepared.key, target, prepared.selection(), prepared.executionProfile);
|
|
707
|
-
return { conversationId: conversation.id, ephemeral: prepared.ephemeral, ok: true, status: "ready" };
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
async function selectPreparedConversation(prepared) {
|
|
711
|
-
const target = scopedTarget(await acquirePrepared(prepared.process), prepared);
|
|
712
|
-
const selection = prepared.selection();
|
|
713
|
-
const conversationId = selection.conversationId;
|
|
714
|
-
const previous = conversationId ? temporaryConversations.get(`${prepared.key}\0${conversationId}`) : null;
|
|
715
|
-
if (selection.scoped && previous && JSON.stringify(previous.executionProfile || null) !== JSON.stringify(prepared.executionProfile)) {
|
|
716
|
-
throw selection.profileChanged();
|
|
717
|
-
}
|
|
718
|
-
if (!conversationId && !selection.createIfMissing) throw selection.identityRequired();
|
|
719
|
-
if (!previous?.active) await currentDirectory(target.workdir);
|
|
720
|
-
const selected = await selectConversation(prepared.key, target, conversationId, selection.input, selection.registration);
|
|
721
|
-
return { ...selected, executionProfile: prepared.executionProfile, target };
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
async function existingPreparedConversation(prepared) {
|
|
725
|
-
return { conversationId: prepared.conversationId, ...await existingConversation(
|
|
726
|
-
prepared.key, prepared.conversationId, prepared.persistent,
|
|
727
|
-
async () => acquirePrepared(await prepared.process())
|
|
728
|
-
) };
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
async function runPreparedConversationTurn(prepared) {
|
|
732
|
-
const selected = await selectPreparedConversation(prepared);
|
|
733
|
-
const turn = prepared.turn(selected);
|
|
734
|
-
return runConversationTurn(selected.target, selected.conversationId, selected.tracked,
|
|
735
|
-
turn.input, turn.options).catch(prepared.failure);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
async function readPreparedConversation(prepared, input) {
|
|
739
|
-
const { conversationId, target, tracked } = await existingPreparedConversation(prepared);
|
|
740
|
-
if (!target) throw prepared.readUnavailable();
|
|
741
|
-
if (!input.persistent) return prepared.projectResult(
|
|
742
|
-
await readConversation(target, conversationId, tracked, {}, prepared.readError), tracked?.executionProfile);
|
|
743
|
-
return readPersistentConversation(target, conversationId, tracked, prepared.messages).catch(prepared.failure);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
async function waitPreparedConversationTurn(prepared, input) {
|
|
747
|
-
const { conversationId, target, tracked } = await existingPreparedConversation(prepared);
|
|
748
|
-
if (!target) throw prepared.readUnavailable();
|
|
749
|
-
return waitForConversationTurn(target, conversationId, tracked, input, prepared.readError);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
async function stopPreparedConversation(prepared, input) {
|
|
753
|
-
const { conversationId, target, tracked } = await existingPreparedConversation(prepared);
|
|
754
|
-
return stopConversation(target, conversationId, tracked, { persistent: input.persistent }).catch(prepared.failure);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
async function deletePreparedConversation(prepared, input) {
|
|
758
|
-
const { conversationId, key, target, tracked } = await existingPreparedConversation(prepared);
|
|
759
|
-
const result = await deleteConversation(key, target, conversationId, tracked, {
|
|
760
|
-
allowMissing: Boolean(input.persistent || prepared.scoped), onRemoved: prepared.onRemoved
|
|
761
|
-
});
|
|
762
|
-
if (!result.deleted) return result;
|
|
763
|
-
const cleanup = prepared.cleanupAfterDelete;
|
|
764
|
-
let providerExit = null;
|
|
765
|
-
if (cleanup) {
|
|
766
|
-
providerExit = await closeSession(cleanup.sessionId, cleanup.options, cleanup.application);
|
|
767
|
-
// The result carries the deleted scope's exit proof. Only renewal releases
|
|
768
|
-
// a kept proof, and a scope is never renewed, so none is kept.
|
|
769
|
-
processExitProofs.delete(cleanup.sessionId);
|
|
770
|
-
}
|
|
771
|
-
return { conversationId, deleted: true, ok: providerExit?.ok !== false,
|
|
772
|
-
...(providerExit ? { providerExit } : {}) };
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
async function sendPreparedMessage(prepared, options) {
|
|
776
|
-
if (Object.hasOwn(prepared, "value")) return prepared.value;
|
|
777
|
-
return prepared.project(await sendMessage(prepared.key, prepared.input, options, prepared.application));
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
async function inspectPreparedMessage(prepared) {
|
|
781
|
-
const target = await acquirePrepared(prepared.process);
|
|
782
|
-
const observation = await inspectOpenCodeMessageAdmission(target.server.client, prepared.threadId, prepared.inputMessageId);
|
|
783
|
-
return prepared.project(observation);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
async function interruptPreparedTurn(prepared) {
|
|
787
|
-
const target = processes.get(prepared.key) || null;
|
|
788
|
-
return interruptTurn(prepared.key, target, {
|
|
789
|
-
threadId: target ? "" : prepared.threadId, writeRun: prepared.writeRun
|
|
790
|
-
}).catch(prepared.failure);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function observePreparedEvents(target, turn, abortController, signal, projection) {
|
|
794
|
-
const observation = observeOpenCodeEvents(target.server.client, target.upstreamSessionId, {
|
|
795
|
-
abortController, signal,
|
|
796
|
-
eventStartedAt: turn ? turn.eventStartedAt : null,
|
|
797
|
-
readError: projection.readError,
|
|
798
|
-
onEvent: summary => projection.onEvent(summary, target)
|
|
799
|
-
});
|
|
800
|
-
void observation.completion.then(() => {
|
|
801
|
-
projection.failure(observation.readFailure());
|
|
802
|
-
projection.closed();
|
|
803
|
-
});
|
|
804
|
-
return {
|
|
805
|
-
close: observation.close,
|
|
806
|
-
readFailure: observation.readFailure,
|
|
807
|
-
waitUntilReady(options) {
|
|
808
|
-
const ready = observation.waitUntilReady(options);
|
|
809
|
-
void ready.catch(projection.failure);
|
|
810
|
-
return ready;
|
|
811
|
-
}
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
async function stopUnobservedSession(target, threadId) {
|
|
816
|
-
const signal = AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS);
|
|
817
|
-
try {
|
|
818
|
-
const confirmed = await target.server.client.interrupt(threadId, { signal });
|
|
819
|
-
const status = await target.server.client.sessionStatus(threadId, { signal });
|
|
820
|
-
if (confirmed !== true || status?.type !== "idle") {
|
|
821
|
-
throw new Error("OpenCode did not confirm that the session stopped.");
|
|
822
|
-
}
|
|
823
|
-
} catch (error) {
|
|
824
|
-
// The process owns the final stop proof when its control channel cannot.
|
|
825
|
-
// Stop this exact server, even if another connection has since replaced it.
|
|
826
|
-
const server = target.server;
|
|
827
|
-
const proof = sharedProcess?.server.stop === server.stop
|
|
828
|
-
? await stop("opencode-observation-lost")
|
|
829
|
-
: await server.stop();
|
|
830
|
-
if (proof?.exited !== true) {
|
|
831
|
-
throw new Error("OpenCode observation was lost and process exit could not be verified.", { cause: error });
|
|
832
|
-
}
|
|
833
|
-
for (const affected of processes.values()) {
|
|
834
|
-
if (affected.server.stop === server.stop) {
|
|
835
|
-
affected.abortController.abort(new Error("OpenCode's shared service was stopped after observation failed. Send a message to continue."));
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
async function createConversation(key, target, input, executionProfile = null) {
|
|
842
|
-
await target.server.client.prepareDirectory({ signal: target.abortController?.signal });
|
|
843
|
-
const conversation = await target.server.client.createSession({
|
|
844
|
-
agent: input.agent,
|
|
845
|
-
location: { directory: target.workdir },
|
|
846
|
-
model: input.model
|
|
847
|
-
});
|
|
848
|
-
temporaryConversations.set(`${key}\0${conversation.id}`, {
|
|
849
|
-
active: false,
|
|
850
|
-
executionProfile,
|
|
851
|
-
target
|
|
852
|
-
});
|
|
853
|
-
return conversation;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
async function selectConversation(parentKey, target, conversationId, input, registration) {
|
|
857
|
-
await target.server.client.prepareDirectory({ signal: target.abortController?.signal });
|
|
858
|
-
if (!conversationId) {
|
|
859
|
-
const created = await target.server.client.createSession({
|
|
860
|
-
agent: input.agent,
|
|
861
|
-
location: { directory: target.workdir },
|
|
862
|
-
model: input.model
|
|
863
|
-
});
|
|
864
|
-
conversationId = created.id;
|
|
865
|
-
} else {
|
|
866
|
-
await target.server.client.switchModel(conversationId, input.model);
|
|
867
|
-
await target.server.client.switchAgent(conversationId, input.agent);
|
|
868
|
-
}
|
|
869
|
-
const key = `${parentKey}\0${conversationId}`;
|
|
870
|
-
const tracked = temporaryConversations.get(key) || { active: false, target };
|
|
871
|
-
tracked.target = target;
|
|
872
|
-
tracked.executionProfile = registration.executionProfile;
|
|
873
|
-
tracked.conversationId = conversationId;
|
|
874
|
-
tracked.promptContext = registration.promptContext;
|
|
875
|
-
temporaryConversations.set(key, tracked);
|
|
876
|
-
await registration.onSelected();
|
|
877
|
-
return { conversationId, key, tracked };
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
async function existingConversation(parentKey, conversationId, persistent, prepareTarget) {
|
|
881
|
-
const key = `${parentKey}\0${conversationId}`;
|
|
882
|
-
let tracked = temporaryConversations.get(key) || null;
|
|
883
|
-
let target = tracked?.target || processes.get(parentKey) || null;
|
|
884
|
-
if (persistent && (!target || target.abortController.signal.aborted)) {
|
|
885
|
-
temporaryConversations.delete(key);
|
|
886
|
-
tracked = null;
|
|
887
|
-
target = await prepareTarget();
|
|
888
|
-
}
|
|
889
|
-
return { key, tracked, target };
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
async function readConversation(target = {}, conversationId = "", tracked = null, options = {}, readError) {
|
|
893
|
-
const messages = await readOpenCodeMessagesUntil(target.server.client, conversationId,
|
|
894
|
-
rows => rows.some(row => row?.type === "assistant"), options);
|
|
895
|
-
const result = openCodeLastAssistantResult(messages, { readError });
|
|
896
|
-
return {
|
|
897
|
-
conversationId,
|
|
898
|
-
error: text(tracked?.error?.message) || result.error,
|
|
899
|
-
ok: !tracked?.error && !result.error,
|
|
900
|
-
runId: tracked?.inputMessageId || "",
|
|
901
|
-
status: tracked?.active ? "inProgress"
|
|
902
|
-
: tracked?.interrupted ? "interrupted"
|
|
903
|
-
: tracked?.error || result.error ? "failed" : "completed",
|
|
904
|
-
text: result.text
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
async function readPersistentConversation(target, conversationId, tracked, projection) {
|
|
909
|
-
const { readError } = projection;
|
|
910
|
-
let status = await target.server.client.sessionStatus(conversationId);
|
|
911
|
-
if (!tracked && status.type !== "idle") {
|
|
912
|
-
await stopUnobservedSession(target, conversationId);
|
|
913
|
-
status = { type: "idle" };
|
|
914
|
-
}
|
|
915
|
-
const rows = openCodeMessageRows(await target.server.client.messages(conversationId));
|
|
916
|
-
const result = openCodeLastAssistantResult(rows, { readError });
|
|
917
|
-
const messages = openCodePersistentConversationMessages(rows, projection.messageId);
|
|
918
|
-
let conversationStatus = "completed";
|
|
919
|
-
if (status.type !== "idle" || tracked?.active) conversationStatus = "inProgress";
|
|
920
|
-
else if (tracked?.interrupted) conversationStatus = "interrupted";
|
|
921
|
-
else if (tracked?.error || result.error) conversationStatus = "failed";
|
|
922
|
-
return {
|
|
923
|
-
conversationId,
|
|
924
|
-
ok: true,
|
|
925
|
-
messages,
|
|
926
|
-
text: result.text,
|
|
927
|
-
admitted: Boolean(projection.inputMessageId && rows.some((row) => row.id === projection.inputMessageId)),
|
|
928
|
-
error: text(tracked?.error?.message) || result.error,
|
|
929
|
-
runId: tracked?.inputMessageId || [...rows].reverse().find((row) => row.type === "user")?.id || "",
|
|
930
|
-
status: conversationStatus
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
async function waitForConversationTurn(target, conversationId, tracked, input = {}, readError) {
|
|
935
|
-
if (tracked?.completion && (tracked.executionProfile || !input.timeoutMs)) {
|
|
936
|
-
return tracked.completion;
|
|
937
|
-
}
|
|
938
|
-
await waitForOpenCodeMessages(target.server.client, conversationId, tracked?.inputMessageId || "", {
|
|
939
|
-
signal: input.timeoutMs ? AbortSignal.timeout(Number(input.timeoutMs)) : undefined,
|
|
940
|
-
readError
|
|
941
|
-
});
|
|
942
|
-
return readConversation(target, conversationId, null, {}, readError);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
async function runConversationTurn(target, conversationId, tracked, input, options) {
|
|
946
|
-
if (tracked.active && tracked.error) {
|
|
947
|
-
await stopUnobservedSession(tracked.target, conversationId);
|
|
948
|
-
tracked.active = false;
|
|
949
|
-
}
|
|
950
|
-
if (tracked.active && input.steer === true) {
|
|
951
|
-
const messageId = input.steeringId;
|
|
952
|
-
await steerOpenCodeTurn(target.server.client, conversationId, tracked, {
|
|
953
|
-
agent: input.agent,
|
|
954
|
-
id: messageId,
|
|
955
|
-
model: input.model,
|
|
956
|
-
prompt: input.prompt,
|
|
957
|
-
attachments: input.attachments
|
|
958
|
-
}, { signal: AbortSignal.any([target.abortController.signal, tracked.abortController.signal]) });
|
|
959
|
-
return {
|
|
960
|
-
conversationId,
|
|
961
|
-
ok: true,
|
|
962
|
-
runId: tracked.inputMessageId,
|
|
963
|
-
status: "inProgress",
|
|
964
|
-
deliveryMode: "steer"
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
// A new native turn starts only on its directory's current credentials.
|
|
968
|
-
while (!tracked.active && !directoryCurrent(target.workdir)) await waitForDirectory(target.workdir);
|
|
969
|
-
if (tracked.active) {
|
|
970
|
-
throw Object.assign(new Error("This conversation is still working."), {
|
|
971
|
-
code: "assistant_opencode_conversation_busy", details: {}, statusCode: 409
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
tracked.active = true;
|
|
975
|
-
const releaseDirectory = holdDirectory(target.workdir);
|
|
976
|
-
tracked.error = null;
|
|
977
|
-
tracked.interrupted = false;
|
|
978
|
-
const turnAbort = new AbortController();
|
|
979
|
-
tracked.abortController = turnAbort;
|
|
980
|
-
const inputMessageId = input.id;
|
|
981
|
-
const observedTarget = { ...target, upstreamSessionId: conversationId };
|
|
982
|
-
const signal = AbortSignal.any([target.abortController.signal, turnAbort.signal, ...(options.signal ? [options.signal] : [])]);
|
|
983
|
-
const events = options.observation
|
|
984
|
-
? observePreparedEvents(observedTarget, null, turnAbort, signal, options.observation())
|
|
985
|
-
: options.observe(observedTarget, turnAbort, signal);
|
|
986
|
-
let admitted;
|
|
987
|
-
try {
|
|
988
|
-
admitted = await dispatchOpenCodeTurn(tracked, {
|
|
989
|
-
get agent() { return input.agent; },
|
|
990
|
-
id: inputMessageId,
|
|
991
|
-
get model() { return input.model; },
|
|
992
|
-
get prompt() { return input.prompt; },
|
|
993
|
-
get attachments() { return input.attachments; }
|
|
994
|
-
}, {
|
|
995
|
-
get client() { return target.server.client; },
|
|
996
|
-
get conversationId() { return conversationId; },
|
|
997
|
-
events, signal,
|
|
998
|
-
readiness: options.readiness,
|
|
999
|
-
beforeDispatch: options.beforeDispatch
|
|
1000
|
-
});
|
|
1001
|
-
} catch (error) {
|
|
1002
|
-
try {
|
|
1003
|
-
await events.close();
|
|
1004
|
-
tracked.error = error;
|
|
1005
|
-
if (tracked.promptAttempted) await stopUnobservedSession(observedTarget, conversationId);
|
|
1006
|
-
tracked.active = false;
|
|
1007
|
-
} finally {
|
|
1008
|
-
releaseDirectory();
|
|
1009
|
-
}
|
|
1010
|
-
throw error;
|
|
1011
|
-
}
|
|
1012
|
-
tracked.inputMessageId = text(admitted.id);
|
|
1013
|
-
tracked.completion = (async () => {
|
|
1014
|
-
const completion = options.completion;
|
|
1015
|
-
const onMessages = completion.onMessages;
|
|
1016
|
-
const result = await observeOpenCodeTurnCompletion(runtime, observedTarget, conversationId, tracked, {
|
|
1017
|
-
...completion, events, signal,
|
|
1018
|
-
...(onMessages ? {
|
|
1019
|
-
onMessages: (messages, inputMessageId) => onMessages(
|
|
1020
|
-
openCodePersistentConversationMessages(openCodeRowsForInput(messages, inputMessageId), completion.messageId),
|
|
1021
|
-
inputMessageId
|
|
1022
|
-
)
|
|
1023
|
-
} : {}),
|
|
1024
|
-
projectResult: async () => {
|
|
1025
|
-
const result = completion.projectResult(
|
|
1026
|
-
await readConversation(target, conversationId, null, { signal }, completion.readError)
|
|
1027
|
-
);
|
|
1028
|
-
return { ...result, text: input.outputSchema ? openCodeStructuredOutput(result.text) : result.text };
|
|
1029
|
-
}
|
|
1030
|
-
});
|
|
1031
|
-
if (result.interrupted) {
|
|
1032
|
-
return { conversationId, ok: true, runId: tracked.inputMessageId, status: "interrupted", text: "" };
|
|
1033
|
-
}
|
|
1034
|
-
return result;
|
|
1035
|
-
})();
|
|
1036
|
-
void tracked.completion.then(releaseDirectory, releaseDirectory);
|
|
1037
|
-
// Keep a failed admitted turn observable to the client's conversation reads.
|
|
1038
|
-
void tracked.completion.catch((error) => {
|
|
1039
|
-
if (tracked.abortController === turnAbort) tracked.error = error;
|
|
1040
|
-
});
|
|
1041
|
-
options.onCompletion(tracked.completion);
|
|
1042
|
-
return options.waitForCompletion ? tracked.completion : {
|
|
1043
|
-
conversationId,
|
|
1044
|
-
ok: true,
|
|
1045
|
-
runId: tracked.inputMessageId,
|
|
1046
|
-
status: "inProgress",
|
|
1047
|
-
threadId: conversationId,
|
|
1048
|
-
turnId: tracked.inputMessageId
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
async function stopConversation(target, conversationId, tracked, { persistent = false } = {}) {
|
|
1053
|
-
if (!target) {
|
|
1054
|
-
return { conversationId, ok: true, stopped: false };
|
|
1055
|
-
}
|
|
1056
|
-
try {
|
|
1057
|
-
if (tracked?.active && tracked.error) {
|
|
1058
|
-
await stopUnobservedSession(target, conversationId);
|
|
1059
|
-
} else {
|
|
1060
|
-
const confirmed = await target.server.client.interrupt(conversationId, {
|
|
1061
|
-
signal: AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS)
|
|
1062
|
-
});
|
|
1063
|
-
const status = persistent ? await target.server.client.sessionStatus(conversationId, {
|
|
1064
|
-
signal: AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS)
|
|
1065
|
-
}) : null;
|
|
1066
|
-
if (confirmed !== true || status && status.type !== "idle") {
|
|
1067
|
-
throw Object.assign(new Error("OpenCode did not confirm Stop. Try Stop again."), {
|
|
1068
|
-
code: "assistant_opencode_interrupt_unconfirmed", details: {}, statusCode: 502
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
} catch (error) {
|
|
1073
|
-
if (persistent && error.statusCode === 404) return { conversationId, ok: true, stopped: true };
|
|
1074
|
-
if (error?.name === "TimeoutError") {
|
|
1075
|
-
throw Object.assign(new Error("OpenCode did not confirm Stop within 5 seconds. Try Stop again."), {
|
|
1076
|
-
code: "assistant_opencode_interrupt_timeout", details: {}, statusCode: 504
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
throw error;
|
|
1080
|
-
}
|
|
1081
|
-
if (tracked) {
|
|
1082
|
-
tracked.interrupted = true;
|
|
1083
|
-
tracked.abortController?.abort();
|
|
1084
|
-
await tracked.completion?.catch(() => null);
|
|
1085
|
-
tracked.active = false;
|
|
1086
|
-
}
|
|
1087
|
-
return { conversationId, ok: true, stopped: true };
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
async function deleteConversation(key, target, conversationId, tracked, { allowMissing, onRemoved }) {
|
|
1091
|
-
if (!target) {
|
|
1092
|
-
temporaryConversations.delete(key);
|
|
1093
|
-
await onRemoved();
|
|
1094
|
-
return { conversationId, deleted: false, ok: true };
|
|
1095
|
-
}
|
|
1096
|
-
try {
|
|
1097
|
-
await target.server.client.deleteSession(conversationId);
|
|
1098
|
-
} catch (error) {
|
|
1099
|
-
if (!allowMissing || error.statusCode !== 404) throw error;
|
|
1100
|
-
}
|
|
1101
|
-
if (tracked) {
|
|
1102
|
-
tracked.interrupted = true;
|
|
1103
|
-
tracked.abortController?.abort();
|
|
1104
|
-
await tracked.completion?.catch(() => null);
|
|
1105
|
-
}
|
|
1106
|
-
temporaryConversations.delete(key);
|
|
1107
|
-
await onRemoved();
|
|
1108
|
-
return { conversationId, deleted: true, ok: true };
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
async function writeConversationProjection(sessionId, messages = null, {
|
|
1112
|
-
inputMessageId = "",
|
|
1113
|
-
streaming = false,
|
|
1114
|
-
captureFinalAssistantResult = false
|
|
1115
|
-
} = {}, projection) {
|
|
1116
|
-
let failure = "";
|
|
1117
|
-
let finalAssistantResult = null;
|
|
1118
|
-
let providerApiFailure = false;
|
|
1119
|
-
const rows = (inputMessageId
|
|
1120
|
-
? openCodeRowsForInput(messages, inputMessageId)
|
|
1121
|
-
: openCodeMessageRows(messages)).filter((message) => message?.type === "assistant");
|
|
1122
|
-
const finalNativeMessage = captureFinalAssistantResult && !streaming
|
|
1123
|
-
? openCodeLastAssistantResult(rows, { readError: projection.readError }).message : null;
|
|
1124
|
-
for (const [index, message] of rows.entries()) {
|
|
1125
|
-
failure ||= projection.readError(message);
|
|
1126
|
-
providerApiFailure ||= openCodeProviderApiFailure(message.error);
|
|
1127
|
-
if (message.summary === true) continue;
|
|
1128
|
-
const assistantText = assistantMessageText(message);
|
|
1129
|
-
const messageId = assistantText ? projection.messageId(message.id, "assistant") : "";
|
|
1130
|
-
const outputId = assistantText ? projection.outputId?.(message.id, "assistant") : "";
|
|
1131
|
-
const inFlight = streaming && index === rows.length - 1;
|
|
1132
|
-
const reasoning = (message.content || []).filter((part) => part.type === "reasoning" && text(part.text))
|
|
1133
|
-
.map((part) => ({
|
|
1134
|
-
messageId: message.id,
|
|
1135
|
-
partId: part.id,
|
|
1136
|
-
text: part.text,
|
|
1137
|
-
createdAt: message.time?.created,
|
|
1138
|
-
complete: Boolean(part.time?.end)
|
|
1139
|
-
}));
|
|
1140
|
-
await projection.reasoning(reasoning, {
|
|
1141
|
-
complete: !inFlight || Boolean(message.time?.completed || message.finish),
|
|
1142
|
-
flush: !streaming || Boolean(assistantText && !inFlight)
|
|
1143
|
-
});
|
|
1144
|
-
if (streaming && assistantText && !inFlight) {
|
|
1145
|
-
// A completed round persists as soon as the next one starts, so
|
|
1146
|
-
// superseded narration never evaporates from the transcript.
|
|
1147
|
-
const turn = await projection.store.writeConversationAssistantMessage(sessionId, {
|
|
1148
|
-
messageId,
|
|
1149
|
-
...(outputId ? { outputId } : {}),
|
|
1150
|
-
text: assistantText
|
|
1151
|
-
});
|
|
1152
|
-
projection.store.completeConversationStreamMessage(sessionId, messageId);
|
|
1153
|
-
await projection.publishTurn(turn);
|
|
1154
|
-
continue;
|
|
1155
|
-
}
|
|
1156
|
-
if (streaming && assistantText && inFlight) {
|
|
1157
|
-
const conversationStream = projection.store.updateConversationStream(sessionId, {
|
|
1158
|
-
turnId: inputMessageId,
|
|
1159
|
-
messageId,
|
|
1160
|
-
...(outputId ? { outputId } : {}),
|
|
1161
|
-
text: assistantText
|
|
1162
|
-
});
|
|
1163
|
-
if (conversationStream) {
|
|
1164
|
-
await projection.publishStream(conversationStream);
|
|
1165
|
-
}
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1168
|
-
if (assistantText && !streaming) {
|
|
1169
|
-
const turn = await projection.store.writeConversationAssistantMessage(sessionId, {
|
|
1170
|
-
messageId,
|
|
1171
|
-
...(outputId ? { outputId } : {}),
|
|
1172
|
-
text: assistantText
|
|
1173
|
-
});
|
|
1174
|
-
projection.store.completeConversationStreamMessage(sessionId, messageId);
|
|
1175
|
-
await projection.publishTurn(turn);
|
|
1176
|
-
if (message === finalNativeMessage && !projection.readError(message) &&
|
|
1177
|
-
(message.time?.completed || message.finish) && turn) {
|
|
1178
|
-
finalAssistantResult = { inputMessageId: text(inputMessageId), itemId: text(message.id),
|
|
1179
|
-
text: assistantText, conversationTurn: turn };
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
return { failure, providerApiFailure, ...(captureFinalAssistantResult ? { finalAssistantResult } : {}) };
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// Evidence belongs to the exact retained native turn. This read never creates,
|
|
1187
|
-
// observes, resumes or projects a native conversation.
|
|
1188
|
-
function readFinalAssistantResult(key, threadId = "", turnId = "") {
|
|
1189
|
-
const turn = turns.get(key);
|
|
1190
|
-
const result = turn?.finalAssistantResult;
|
|
1191
|
-
const target = turn?.finalAssistantResultTarget;
|
|
1192
|
-
if (!target || processes.get(key) !== target || target.abortController.signal.aborted ||
|
|
1193
|
-
!turn.finalAssistantResultProcess || sharedProcess !== turn.finalAssistantResultProcess ||
|
|
1194
|
-
target.upstreamSessionId !== turn.threadId) return null;
|
|
1195
|
-
if (!text(threadId) || !text(turnId) || !result || turn.active || turn.state !== "completed" ||
|
|
1196
|
-
turn.error || turn.observationError || turn.interruptRequested || turn.interruptAcknowledged ||
|
|
1197
|
-
turn.threadId !== text(threadId) || turn.id !== text(turnId) ||
|
|
1198
|
-
result.threadId !== text(threadId) || result.turnId !== text(turnId) ||
|
|
1199
|
-
result.inputMessageId !== text(turn.inputMessageId) || !result.conversationTurn) return null;
|
|
1200
|
-
return structuredClone(result);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
async function sendMessage(key, input, options, application) {
|
|
1204
|
-
if (input.duplicate) {
|
|
1205
|
-
const currentTurn = openCodeTurnSnapshot(turns.get(key));
|
|
1206
|
-
return {
|
|
1207
|
-
delivered: true,
|
|
1208
|
-
duplicate: true,
|
|
1209
|
-
ok: true,
|
|
1210
|
-
thread: { id: currentTurn?.threadId || input.threadId },
|
|
1211
|
-
turn: currentTurn
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
const currentTurn = turns.get(key);
|
|
1215
|
-
if (currentTurn?.observationError) {
|
|
1216
|
-
await monitors.get(key);
|
|
1217
|
-
}
|
|
1218
|
-
if (currentTurn?.active && currentTurn.observationError) {
|
|
1219
|
-
// An explicit Send may retry stopping uncertain work, but cannot overlap it.
|
|
1220
|
-
try {
|
|
1221
|
-
await stopUnobservedSession(processes.get(key), currentTurn.threadId);
|
|
1222
|
-
} catch (error) {
|
|
1223
|
-
return { failure: { error, recovery: true } };
|
|
1224
|
-
}
|
|
1225
|
-
currentTurn.active = false;
|
|
1226
|
-
currentTurn.state = "interrupted";
|
|
1227
|
-
await application.writeRun(currentTurn, "interrupted", currentTurn.observationError);
|
|
1228
|
-
}
|
|
1229
|
-
let currentMonitor = monitors.get(key);
|
|
1230
|
-
let currentThreadId = input.threadId;
|
|
1231
|
-
const ownershipMatchesTurn = Boolean(
|
|
1232
|
-
currentMonitor &&
|
|
1233
|
-
options.turnOwnership &&
|
|
1234
|
-
text(options.turnOwnership.threadId) === currentThreadId &&
|
|
1235
|
-
text(options.turnOwnership.turnId) === text(currentTurn?.id)
|
|
1236
|
-
);
|
|
1237
|
-
if (ownershipMatchesTurn && options.turnOwnership.reusable !== true) {
|
|
1238
|
-
return { ownerConflict: true, threadId: currentThreadId,
|
|
1239
|
-
turn: openCodeTurnSnapshot(currentTurn, currentThreadId) };
|
|
1240
|
-
}
|
|
1241
|
-
const eventStartedAt = Date.now();
|
|
1242
|
-
const startedAt = new Date(eventStartedAt).toISOString();
|
|
1243
|
-
const providerMessageId = input.id;
|
|
1244
|
-
const startingTurn = currentMonitor
|
|
1245
|
-
? null
|
|
1246
|
-
: {
|
|
1247
|
-
active: true,
|
|
1248
|
-
error: "",
|
|
1249
|
-
eventStartedAt,
|
|
1250
|
-
id: providerMessageId,
|
|
1251
|
-
inputMessageId: providerMessageId,
|
|
1252
|
-
startedAt,
|
|
1253
|
-
state: "starting",
|
|
1254
|
-
threadId: currentThreadId,
|
|
1255
|
-
updatedAt: startedAt
|
|
1256
|
-
};
|
|
1257
|
-
if (startingTurn) {
|
|
1258
|
-
turns.set(key, startingTurn);
|
|
1259
|
-
await application.writeRun(startingTurn, "starting");
|
|
1260
|
-
}
|
|
1261
|
-
let admitted = null;
|
|
1262
|
-
let target = null;
|
|
1263
|
-
let admission = null;
|
|
1264
|
-
let dispatchTurn = null;
|
|
1265
|
-
let dispatchMonitor = null;
|
|
1266
|
-
let previousInputMessageId = null;
|
|
1267
|
-
let previousAdmission;
|
|
1268
|
-
let recoveryReceipt = null;
|
|
1269
|
-
let recoveryObservationError = null;
|
|
1270
|
-
try {
|
|
1271
|
-
const preparation = await application.prepare({ overwrite: !currentMonitor, threadId: currentThreadId });
|
|
1272
|
-
const preparedTarget = preparation.process ? await acquirePrepared(preparation.process) : preparation.target;
|
|
1273
|
-
if (!monitors.get(key)) await currentDirectory(preparedTarget.workdir);
|
|
1274
|
-
target = await ensureOpenCodeSession(preparedTarget, preparation.session);
|
|
1275
|
-
currentThreadId = target.upstreamSessionId;
|
|
1276
|
-
if (startingTurn) {
|
|
1277
|
-
startingTurn.threadId = currentThreadId;
|
|
1278
|
-
}
|
|
1279
|
-
const prompt = await application.prompt();
|
|
1280
|
-
// Preparation can outlive the preceding turn's final projection. Do not
|
|
1281
|
-
// attach a new input to a monitor that has already claimed retirement.
|
|
1282
|
-
currentMonitor = monitors.get(key);
|
|
1283
|
-
if (currentMonitor && !turns.get(key)?.active) {
|
|
1284
|
-
await currentMonitor;
|
|
1285
|
-
currentMonitor = monitors.get(key);
|
|
1286
|
-
}
|
|
1287
|
-
const recoveryTurn = currentMonitor ? turns.get(key) : null;
|
|
1288
|
-
if (recoveryTurn?.recoveryAdmission) {
|
|
1289
|
-
try {
|
|
1290
|
-
recoveryReceipt = await recoveryTurn.recoveryAdmission.promise;
|
|
1291
|
-
currentMonitor = monitors.get(key);
|
|
1292
|
-
if (currentMonitor && !turns.get(key)?.active) {
|
|
1293
|
-
await currentMonitor;
|
|
1294
|
-
currentMonitor = monitors.get(key);
|
|
1295
|
-
}
|
|
1296
|
-
// Stop must not turn a waiting, undispatched input into a fresh turn.
|
|
1297
|
-
recoveryTurn.abortController.signal.throwIfAborted();
|
|
1298
|
-
target.abortController.signal.throwIfAborted();
|
|
1299
|
-
} catch (error) {
|
|
1300
|
-
// This is R's failure, not a rejected or attempted native B input.
|
|
1301
|
-
return { failure: { error, attempted: false, threadId: currentThreadId,
|
|
1302
|
-
turn: openCodeTurnSnapshot(turns.get(key), currentThreadId) } };
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
// A new native turn starts only on its directory's current credentials;
|
|
1306
|
-
// the wait, if any, is for that directory's own running turns.
|
|
1307
|
-
while (!currentMonitor && !directoryCurrent(target.workdir)) {
|
|
1308
|
-
await waitForDirectory(target.workdir);
|
|
1309
|
-
currentMonitor = monitors.get(key);
|
|
1310
|
-
if (currentMonitor && !turns.get(key)?.active) {
|
|
1311
|
-
await currentMonitor;
|
|
1312
|
-
currentMonitor = monitors.get(key);
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
admission = Promise.withResolvers();
|
|
1316
|
-
// The observer may be between polls when dispatch or persistence fails.
|
|
1317
|
-
// Keep the raw promise rejectable without an unhandled rejection.
|
|
1318
|
-
void admission.promise.catch(() => {});
|
|
1319
|
-
let eventReady = null;
|
|
1320
|
-
if (currentMonitor) {
|
|
1321
|
-
dispatchTurn = turns.get(key);
|
|
1322
|
-
previousInputMessageId = dispatchTurn.inputMessageId;
|
|
1323
|
-
previousAdmission = dispatchTurn.admission;
|
|
1324
|
-
dispatchTurn.admission = admission;
|
|
1325
|
-
dispatchTurn.inputMessageId = providerMessageId;
|
|
1326
|
-
dispatchTurn.updatedAt = new Date().toISOString();
|
|
1327
|
-
dispatchMonitor = currentMonitor;
|
|
1328
|
-
} else {
|
|
1329
|
-
eventReady = Promise.withResolvers();
|
|
1330
|
-
dispatchMonitor = beginMessageMonitor(key, target, { id: providerMessageId, eventStartedAt, startedAt }, {
|
|
1331
|
-
...options, admission, eventReady
|
|
1332
|
-
}, application.monitor);
|
|
1333
|
-
dispatchTurn = turns.get(key);
|
|
1334
|
-
}
|
|
1335
|
-
admitted = await dispatchOpenCodeTurn(dispatchTurn, {
|
|
1336
|
-
get agent() { return prompt.agent; },
|
|
1337
|
-
delivery: currentMonitor ? "steer" : "queue",
|
|
1338
|
-
id: providerMessageId,
|
|
1339
|
-
get model() { return prompt.model; },
|
|
1340
|
-
get prompt() { return prompt.prompt; },
|
|
1341
|
-
get attachments() { return prompt.attachments; }
|
|
1342
|
-
}, {
|
|
1343
|
-
get client() { return target.server.client; },
|
|
1344
|
-
get conversationId() { return target.upstreamSessionId; },
|
|
1345
|
-
events: eventReady ? { waitUntilReady: () => eventReady.promise } : null,
|
|
1346
|
-
get signal() {
|
|
1347
|
-
return AbortSignal.any([
|
|
1348
|
-
target.abortController.signal,
|
|
1349
|
-
turns.get(key).abortController.signal
|
|
1350
|
-
]);
|
|
1351
|
-
},
|
|
1352
|
-
beforeDispatch: () => prompt.beforeDispatch(target.upstreamSessionId),
|
|
1353
|
-
promptTimeoutMs: options.promptTimeoutMs,
|
|
1354
|
-
authorizeAttachments: prompt.authorizeAttachments
|
|
1355
|
-
});
|
|
1356
|
-
} catch (error) {
|
|
1357
|
-
const rejected = error?.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 408;
|
|
1358
|
-
const retainPreviousInput = previousInputMessageId !== null &&
|
|
1359
|
-
(dispatchTurn?.promptAttempted !== true || rejected);
|
|
1360
|
-
if (retainPreviousInput) {
|
|
1361
|
-
if (dispatchTurn.admission === admission && dispatchTurn.inputMessageId === providerMessageId) {
|
|
1362
|
-
dispatchTurn.inputMessageId = previousInputMessageId;
|
|
1363
|
-
dispatchTurn.admission = previousAdmission;
|
|
1364
|
-
}
|
|
1365
|
-
admission.resolve();
|
|
1366
|
-
} else {
|
|
1367
|
-
// Attempted unknown admission must retain the new input and use the
|
|
1368
|
-
// same observer's original Stop/error recovery, never roll back to A.
|
|
1369
|
-
admission?.reject(error);
|
|
1370
|
-
dispatchTurn?.abortController?.abort(error);
|
|
1371
|
-
}
|
|
1372
|
-
if (rejected) await input.onPromptRejected?.();
|
|
1373
|
-
if (admission && !retainPreviousInput) await dispatchMonitor;
|
|
1374
|
-
if (startingTurn && !admission && !monitors.has(key)) {
|
|
1375
|
-
startingTurn.active = false;
|
|
1376
|
-
startingTurn.error = text(error?.message) || "OpenCode prompt delivery failed.";
|
|
1377
|
-
startingTurn.state = "failed";
|
|
1378
|
-
startingTurn.updatedAt = new Date().toISOString();
|
|
1379
|
-
await application.writeRun(
|
|
1380
|
-
startingTurn,
|
|
1381
|
-
"failed",
|
|
1382
|
-
startingTurn.error
|
|
1383
|
-
).catch(() => null);
|
|
1384
|
-
}
|
|
1385
|
-
return { failure: { error, attempted: dispatchTurn?.promptAttempted === true, threadId: currentThreadId,
|
|
1386
|
-
turn: openCodeTurnSnapshot(turns.get(key) || startingTurn, currentThreadId) } };
|
|
1387
|
-
}
|
|
1388
|
-
if (dispatchTurn.admission === admission && dispatchTurn.inputMessageId === providerMessageId) {
|
|
1389
|
-
dispatchTurn.inputMessageId = text(admitted.id);
|
|
1390
|
-
dispatchTurn.updatedAt = new Date().toISOString();
|
|
1391
|
-
}
|
|
1392
|
-
let conversationTurn;
|
|
1393
|
-
try {
|
|
1394
|
-
conversationTurn = await application.commit(admitted);
|
|
1395
|
-
} catch (error) {
|
|
1396
|
-
admission.reject(error);
|
|
1397
|
-
dispatchTurn.abortController.abort(error);
|
|
1398
|
-
// Preserve the persistence error while joining the original recovery.
|
|
1399
|
-
await dispatchMonitor.catch(() => null);
|
|
1400
|
-
throw error;
|
|
1401
|
-
}
|
|
1402
|
-
if (recoveryReceipt) {
|
|
1403
|
-
const receiptAbort = new AbortController();
|
|
1404
|
-
// The original monitor can fail and retire without aborting its turn.
|
|
1405
|
-
// Its settlement also ends this read; it never creates a successor turn.
|
|
1406
|
-
void dispatchMonitor.then(() => receiptAbort.abort(new Error(
|
|
1407
|
-
dispatchTurn.observationError || dispatchTurn.error || "OpenCode observation ended before input custody was confirmed."
|
|
1408
|
-
)), error => receiptAbort.abort(error));
|
|
1409
|
-
try {
|
|
1410
|
-
const observation = await inspectOpenCodeMessageAdmission(target.server.client, currentThreadId,
|
|
1411
|
-
text(admitted.id), { wait: true, signal: AbortSignal.any([
|
|
1412
|
-
target.abortController.signal, dispatchTurn.abortController.signal, receiptAbort.signal
|
|
1413
|
-
]) });
|
|
1414
|
-
const message = openCodeMessageRows(observation.messages).find(message =>
|
|
1415
|
-
message.type === "user" && text(message.id) === text(admitted.id));
|
|
1416
|
-
const created = message?.time?.created;
|
|
1417
|
-
const recoveryCreated = recoveryReceipt.time?.created;
|
|
1418
|
-
// Match the native latest-user order, including its ID tie-breaker.
|
|
1419
|
-
if (!Number.isFinite(created) || !Number.isFinite(recoveryCreated) ||
|
|
1420
|
-
!(created > recoveryCreated || (created === recoveryCreated && message.id > recoveryReceipt.id))) {
|
|
1421
|
-
throw Object.assign(new Error(
|
|
1422
|
-
"OpenCode could not confirm that the new input follows its final-answer recovery. The admitted message was not resent."
|
|
1423
|
-
), { code: "assistant_opencode_observation_lost", details: {}, statusCode: 503 });
|
|
1424
|
-
}
|
|
1425
|
-
} catch (error) {
|
|
1426
|
-
recoveryObservationError = error;
|
|
1427
|
-
} finally {
|
|
1428
|
-
receiptAbort.abort();
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
if (recoveryObservationError) {
|
|
1432
|
-
// Native B was dispatched: keep its authored receipt even when observing
|
|
1433
|
-
// its place in native history failed, and use the original cleanup owner.
|
|
1434
|
-
admission.reject(recoveryObservationError);
|
|
1435
|
-
dispatchTurn.abortController.abort(recoveryObservationError);
|
|
1436
|
-
await dispatchMonitor.catch(() => null);
|
|
1437
|
-
} else {
|
|
1438
|
-
admission.resolve();
|
|
1439
|
-
}
|
|
1440
|
-
const turn = openCodeTurnSnapshot(turns.get(key), target.upstreamSessionId);
|
|
1441
|
-
return {
|
|
1442
|
-
conversationTurn,
|
|
1443
|
-
delivered: true,
|
|
1444
|
-
deliveryMode: currentMonitor ? "steer" : "new_turn",
|
|
1445
|
-
ok: true,
|
|
1446
|
-
thread: { id: target.upstreamSessionId },
|
|
1447
|
-
turn,
|
|
1448
|
-
workdir: input.workdir
|
|
1449
|
-
};
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
function hasActiveTemporaryConversation(sessionId) {
|
|
1453
|
-
return [...temporaryConversations.values()].some((entry) => (
|
|
1454
|
-
!entry.reasoningSummary && entry.active === true && entry.target?.sessionId === sessionId
|
|
1455
|
-
));
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
async function reconcileSessions(sessions = [], options = {}, preparation) {
|
|
1459
|
-
const results = [];
|
|
1460
|
-
for (const session of sessions) {
|
|
1461
|
-
const sessionId = text(session?.sessionId || session?.id);
|
|
1462
|
-
try {
|
|
1463
|
-
const prepared = await preparation.session(sessionId, session, options);
|
|
1464
|
-
const resumed = await reconcilePreparedSession(prepared, prepared.activeRun, options);
|
|
1465
|
-
results.push({ ok: true, resumed, sessionId });
|
|
1466
|
-
} catch (error) {
|
|
1467
|
-
results.push({ error: text(error?.message), ok: false, sessionId });
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
return {
|
|
1471
|
-
failed: results.filter((result) => result.ok === false),
|
|
1472
|
-
ok: results.every((result) => result.ok),
|
|
1473
|
-
results,
|
|
1474
|
-
sessionCount: results.length
|
|
1475
|
-
};
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
async function reconcilePreparedSession(prepared, activeRun, options) {
|
|
1479
|
-
const target = await acquirePrepared(await prepared.process());
|
|
1480
|
-
const current = await ensureOpenCodeSession(target, prepared.session());
|
|
1481
|
-
return reconcileSessionTurn(prepared.key, current, activeRun, options, prepared.observation).catch(prepared.failure);
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
async function reconcileSessionTurn(key, target, activeRun, options, application) {
|
|
1485
|
-
if (activeRun?.observationError) {
|
|
1486
|
-
await stopUnobservedSession(target, activeRun.threadId);
|
|
1487
|
-
await application.writeRun({ ...activeRun, id: activeRun.turnId }, "interrupted", activeRun.observationError);
|
|
1488
|
-
} else if (activeRun) {
|
|
1489
|
-
beginMessageMonitor(key, target, {
|
|
1490
|
-
restored: true,
|
|
1491
|
-
eventStartedAt: Date.parse(text(activeRun.startedAt)) || Date.now(),
|
|
1492
|
-
id: text(activeRun.turnId) || application.fallbackTurnId,
|
|
1493
|
-
startedAt: text(activeRun.startedAt)
|
|
1494
|
-
}, options, application.monitor);
|
|
1495
|
-
}
|
|
1496
|
-
return Boolean(activeRun && !activeRun.observationError);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
function beginMessageMonitor(key, target, admitted, options, projection) {
|
|
1500
|
-
const start = () => {
|
|
1501
|
-
target = { ...target };
|
|
1502
|
-
const existing = monitors.get(key);
|
|
1503
|
-
if (existing) {
|
|
1504
|
-
return existing;
|
|
1505
|
-
}
|
|
1506
|
-
const metadata = projection.prepare(admitted, options);
|
|
1507
|
-
const eventStartedAt = Number(admitted.eventStartedAt) || Date.now();
|
|
1508
|
-
const startedAt = text(admitted.startedAt) || new Date(eventStartedAt).toISOString();
|
|
1509
|
-
const turn = {
|
|
1510
|
-
abortController: new AbortController(),
|
|
1511
|
-
admission: options.admission,
|
|
1512
|
-
active: true,
|
|
1513
|
-
error: "",
|
|
1514
|
-
eventStartedAt,
|
|
1515
|
-
id: text(admitted.id),
|
|
1516
|
-
inputMessageId: text(admitted.id),
|
|
1517
|
-
startedAt,
|
|
1518
|
-
state: options.admission ? "starting" : "active",
|
|
1519
|
-
...metadata.fields,
|
|
1520
|
-
threadId: target.upstreamSessionId,
|
|
1521
|
-
updatedAt: startedAt
|
|
1522
|
-
};
|
|
1523
|
-
const monitor = beginMonitor(key, target, turn, projection.create(target, turn, metadata, options));
|
|
1524
|
-
void monitor.catch(error => projection.onError(error));
|
|
1525
|
-
return monitor;
|
|
1526
|
-
};
|
|
1527
|
-
if (typeof projection.runInContext !== "function") return start();
|
|
1528
|
-
let monitor, startFailure;
|
|
1529
|
-
// Context entry invokes this callback synchronously. Do not return the
|
|
1530
|
-
// monitor to an async scope wrapper: retain its identity and error owner.
|
|
1531
|
-
const scope = projection.runInContext(() => {
|
|
1532
|
-
try { monitor = start(); }
|
|
1533
|
-
catch (error) { startFailure = { error }; }
|
|
1534
|
-
});
|
|
1535
|
-
void Promise.resolve(scope).catch(error => projection.onError(error));
|
|
1536
|
-
if (startFailure) throw startFailure.error;
|
|
1537
|
-
return monitor;
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
// A turn's failure and whether it is a credential failure. A removed
|
|
1541
|
-
// connection's models are gone from its reloaded directory.
|
|
1542
|
-
function turnFailure(failure) {
|
|
1543
|
-
const providerId = /\bModel not found: ([^/\s]+)\//u.exec(failure)?.[1];
|
|
1544
|
-
if (providerId && sharedProcess && !sharedProcess.connections.has(providerId)) {
|
|
1545
|
-
return { failure: `${providerId} isn't connected. Connect it again, then send your message again.`, credentialFailure: true };
|
|
1546
|
-
}
|
|
1547
|
-
return { failure, credentialFailure: openCodeCredentialFailure(failure) };
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
function beginMonitor(key, target, turn, {
|
|
1551
|
-
observe, observation, eventReady, readiness, finalResponse,
|
|
1552
|
-
writeRun, projectMessages, completeResult, onRetired
|
|
1553
|
-
}) {
|
|
1554
|
-
delete turn.finalAssistantResult;
|
|
1555
|
-
delete turn.finalAssistantResultTarget;
|
|
1556
|
-
delete turn.finalAssistantResultProcess;
|
|
1557
|
-
const registeredTarget = processes.get(key);
|
|
1558
|
-
const finalAssistantResultTarget = registeredTarget &&
|
|
1559
|
-
registeredTarget.abortController === target.abortController &&
|
|
1560
|
-
registeredTarget.server === target.server &&
|
|
1561
|
-
registeredTarget.upstreamSessionId === target.upstreamSessionId ? registeredTarget : null;
|
|
1562
|
-
const finalAssistantResultProcess = finalAssistantResultTarget ? sharedProcess : null;
|
|
1563
|
-
turns.set(key, turn);
|
|
1564
|
-
const releaseDirectory = holdDirectory(target.workdir);
|
|
1565
|
-
const signal = AbortSignal.any([target.abortController.signal, turn.abortController.signal]);
|
|
1566
|
-
const monitor = Promise.resolve().then(async () => {
|
|
1567
|
-
const events = observation
|
|
1568
|
-
? observePreparedEvents(target, turn, turn.abortController, signal, observation())
|
|
1569
|
-
: observe(turn, signal);
|
|
1570
|
-
if (eventReady) {
|
|
1571
|
-
void events.waitUntilReady(readiness).then(eventReady.resolve, eventReady.reject);
|
|
1572
|
-
}
|
|
1573
|
-
let finalState = "completed";
|
|
1574
|
-
let failure = "";
|
|
1575
|
-
let finalAssistantResult = null;
|
|
1576
|
-
let credentialFailure = false;
|
|
1577
|
-
let providerApiFailure = false;
|
|
1578
|
-
try {
|
|
1579
|
-
await turn.admission?.promise;
|
|
1580
|
-
signal.throwIfAborted();
|
|
1581
|
-
turn.state = "active";
|
|
1582
|
-
await writeRun(turn, "active");
|
|
1583
|
-
for (;;) {
|
|
1584
|
-
const completion = await waitForOpenCodeFinalResponse(
|
|
1585
|
-
target.server.client,
|
|
1586
|
-
target.upstreamSessionId,
|
|
1587
|
-
turn,
|
|
1588
|
-
{
|
|
1589
|
-
...finalResponse,
|
|
1590
|
-
// Preserve the reasoning-only completion before the native recovery
|
|
1591
|
-
// input replaces the projection boundary. Replayed writes are no-ops.
|
|
1592
|
-
beforeRecovery: async completion => {
|
|
1593
|
-
await projectMessages(completion.messages, { inputMessageId: completion.inputMessageId });
|
|
1594
|
-
await finalResponse.beforeRecovery?.(completion);
|
|
1595
|
-
},
|
|
1596
|
-
onMessages: async (messages, inputMessageId) => {
|
|
1597
|
-
const latest = openCodeLastAssistantResult(messages, { readError: finalResponse.readError }).message;
|
|
1598
|
-
const phase = latest?.summary === true && Number(latest.time?.created) >= turn.eventStartedAt &&
|
|
1599
|
-
!latest.time?.completed && !latest.finish && !latest.error ? "compacting" : "";
|
|
1600
|
-
if (turn.active && !signal.aborted && text(turn.phase) !== phase) {
|
|
1601
|
-
turn.phase = phase;
|
|
1602
|
-
turn.updatedAt = new Date().toISOString();
|
|
1603
|
-
await writeRun(turn, turn.state);
|
|
1604
|
-
}
|
|
1605
|
-
return projectMessages(messages, { inputMessageId, streaming: true });
|
|
1606
|
-
},
|
|
1607
|
-
readFailure: events.readFailure,
|
|
1608
|
-
signal
|
|
1609
|
-
}
|
|
1610
|
-
);
|
|
1611
|
-
const admission = turn.admission;
|
|
1612
|
-
await admission?.promise;
|
|
1613
|
-
signal.throwIfAborted();
|
|
1614
|
-
if (admission !== turn.admission || completion.inputMessageId !== text(turn.inputMessageId)) {
|
|
1615
|
-
continue;
|
|
1616
|
-
}
|
|
1617
|
-
const projection = await projectMessages(completion.messages, {
|
|
1618
|
-
inputMessageId: completion.inputMessageId,
|
|
1619
|
-
captureFinalAssistantResult: true
|
|
1620
|
-
});
|
|
1621
|
-
await turn.admission?.promise;
|
|
1622
|
-
signal.throwIfAborted();
|
|
1623
|
-
if (admission !== turn.admission || completion.inputMessageId !== text(turn.inputMessageId)) {
|
|
1624
|
-
continue;
|
|
1625
|
-
}
|
|
1626
|
-
failure = projection.failure || text(events.readFailure()?.message);
|
|
1627
|
-
providerApiFailure = projection.providerApiFailure || openCodeProviderApiFailure(events.readFailure());
|
|
1628
|
-
if (!failure && !turn.interruptRequested && !text(completion.result?.text)) {
|
|
1629
|
-
failure = "OpenCode finished without a user-facing final response. Please send your message again.";
|
|
1630
|
-
finalState = "failed";
|
|
1631
|
-
} else if (failure) {
|
|
1632
|
-
({ failure, credentialFailure } = turnFailure(failure));
|
|
1633
|
-
finalState = "failed";
|
|
1634
|
-
} else if (turn.interruptRequested) {
|
|
1635
|
-
finalState = "interrupted";
|
|
1636
|
-
}
|
|
1637
|
-
const published = projection.finalAssistantResult;
|
|
1638
|
-
if (finalState === "completed" && !failure && published &&
|
|
1639
|
-
published.inputMessageId === completion.inputMessageId &&
|
|
1640
|
-
published.itemId === completion.result?.turnId &&
|
|
1641
|
-
text(published.text) === text(completion.result?.text)) {
|
|
1642
|
-
finalAssistantResult = { ...published, threadId: turn.threadId, turnId: turn.id };
|
|
1643
|
-
}
|
|
1644
|
-
break;
|
|
1645
|
-
}
|
|
1646
|
-
} catch (error) {
|
|
1647
|
-
if (turn.interruptAcknowledged) {
|
|
1648
|
-
finalState = "interrupted";
|
|
1649
|
-
} else {
|
|
1650
|
-
const cause = signal.aborted ? signal.reason : error;
|
|
1651
|
-
({ failure, credentialFailure } = turnFailure(text(cause?.message) || "OpenCode turn failed."));
|
|
1652
|
-
providerApiFailure = openCodeProviderApiFailure(cause);
|
|
1653
|
-
finalState = target.abortController.signal.aborted
|
|
1654
|
-
? "cancelled"
|
|
1655
|
-
: "failed";
|
|
1656
|
-
if (!target.abortController.signal.aborted) {
|
|
1657
|
-
turn.observationError = failure;
|
|
1658
|
-
try {
|
|
1659
|
-
await writeRun(turn, turn.state, failure);
|
|
1660
|
-
} catch {
|
|
1661
|
-
// A failed store must not prevent stopping native work.
|
|
1662
|
-
}
|
|
1663
|
-
try {
|
|
1664
|
-
await stopUnobservedSession(target, turn.threadId);
|
|
1665
|
-
} catch (stopError) {
|
|
1666
|
-
failure = `${failure} ${stopError.message}`;
|
|
1667
|
-
finalState = "active";
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
} finally {
|
|
1672
|
-
// Claim retirement before awaited cleanup/checkpointing. A new Send
|
|
1673
|
-
// joins this monitor's cleanup instead of steering an unobserved turn.
|
|
1674
|
-
turn.active = finalState === "active";
|
|
1675
|
-
await events.close();
|
|
1676
|
-
if (credentialFailure || finalState === "failed") {
|
|
1677
|
-
failure = await completeResult(turn, { credentialFailure, providerApiFailure, finalState, failure });
|
|
1678
|
-
}
|
|
1679
|
-
turn.error = failure;
|
|
1680
|
-
turn.state = finalState;
|
|
1681
|
-
turn.finalAssistantResult = finalState === "completed" && !failure ? finalAssistantResult : null;
|
|
1682
|
-
turn.finalAssistantResultTarget = turn.finalAssistantResult ? finalAssistantResultTarget : null;
|
|
1683
|
-
turn.finalAssistantResultProcess = turn.finalAssistantResult ? finalAssistantResultProcess : null;
|
|
1684
|
-
turn.updatedAt = new Date().toISOString();
|
|
1685
|
-
await writeRun(turn, finalState, failure).catch(() => null);
|
|
1686
|
-
}
|
|
1687
|
-
return openCodeTurnSnapshot(turn, target.upstreamSessionId);
|
|
1688
|
-
}).finally(() => {
|
|
1689
|
-
if (monitors.get(key) === monitor) {
|
|
1690
|
-
monitors.delete(key);
|
|
1691
|
-
}
|
|
1692
|
-
releaseDirectory();
|
|
1693
|
-
onRetired();
|
|
1694
|
-
});
|
|
1695
|
-
monitors.set(key, monitor);
|
|
1696
|
-
return monitor;
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
async function ensurePreparedSessionReadiness(prepared) {
|
|
1700
|
-
if (Object.hasOwn(prepared, "value")) return prepared.value;
|
|
1701
|
-
let target = processes.get(prepared.key);
|
|
1702
|
-
const connection = await prepared.connection();
|
|
1703
|
-
const { ready, unhealthyProcess, missingUpstream } = await inspectSessionReadiness(target, {
|
|
1704
|
-
selection: prepared.selection, workdir: prepared.workdir, connection,
|
|
1705
|
-
changed: prepared.changed, refresh: prepared.refresh
|
|
1706
|
-
});
|
|
1707
|
-
if (!ready) {
|
|
1708
|
-
const { value: recovered } = await prepared.prepare(recovery => recoverSessionReadiness(target, {
|
|
1709
|
-
unhealthyProcess, missingUpstream,
|
|
1710
|
-
get key() { return recovery.key; }
|
|
1711
|
-
}, async () => {
|
|
1712
|
-
const configuration = recovery.configuration();
|
|
1713
|
-
async function ensureProcess() {
|
|
1714
|
-
return acquirePrepared(await configuration.process());
|
|
1715
|
-
}
|
|
1716
|
-
const current = await ensureProcess();
|
|
1717
|
-
return ensureOpenCodeSession(current, configuration.session);
|
|
1718
|
-
}));
|
|
1719
|
-
if (recovered?.ok === false) return recovered;
|
|
1720
|
-
target = recovered;
|
|
1721
|
-
}
|
|
1722
|
-
const savedRun = prepared.observation.run;
|
|
1723
|
-
if (needsObservationRecovery(prepared.key, savedRun)) {
|
|
1724
|
-
await prepared.observation.write(async (run, commit) => {
|
|
1725
|
-
if (!needsObservationRecovery(prepared.key, run)) return;
|
|
1726
|
-
const threadId = text(run.threadId) || target.upstreamSessionId;
|
|
1727
|
-
await recoverObservation(target, threadId, () => commit(writeRun => (
|
|
1728
|
-
recordStoppedObservation(prepared.key, run, threadId, writeRun)
|
|
1729
|
-
)));
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
return {
|
|
1733
|
-
ok: true,
|
|
1734
|
-
thread: { id: target.upstreamSessionId },
|
|
1735
|
-
turn: openCodeTurnSnapshot(turns.get(prepared.key), target.upstreamSessionId),
|
|
1736
|
-
workdir: prepared.workdir
|
|
1737
|
-
};
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
async function inspectSessionReadiness(target, { selection, workdir, connection, refresh, changed }) {
|
|
1741
|
-
let ready = false;
|
|
1742
|
-
let unhealthyProcess = null;
|
|
1743
|
-
let missingUpstream = false;
|
|
1744
|
-
if (
|
|
1745
|
-
target?.upstream &&
|
|
1746
|
-
sameOpenCodeSelection(target.upstreamSelection, selection) &&
|
|
1747
|
-
target.workdir === workdir &&
|
|
1748
|
-
text(target.selection?.catalogRevision) === text(selection.catalogRevision) &&
|
|
1749
|
-
target.connectionFingerprint === connection.fingerprint &&
|
|
1750
|
-
target.canonicalUrl === connection.canonicalUrl &&
|
|
1751
|
-
target.endpointCode === connection.endpointCode
|
|
1752
|
-
) {
|
|
1753
|
-
const observedProcess = sharedProcess;
|
|
1754
|
-
const observedServer = target.server;
|
|
1755
|
-
let healthy = false;
|
|
1756
|
-
const probe = AbortSignal.timeout(1_000);
|
|
1757
|
-
try {
|
|
1758
|
-
await target.server.client.health({ signal: probe });
|
|
1759
|
-
healthy = true;
|
|
1760
|
-
ready = Boolean(await target.server.client.readSession(target.upstreamSessionId));
|
|
1761
|
-
} catch (error) {
|
|
1762
|
-
if ((!healthy && probe.aborted) || (healthy && error?.statusCode !== 404)) {
|
|
1763
|
-
throw error;
|
|
1764
|
-
}
|
|
1765
|
-
if (!healthy) {
|
|
1766
|
-
unhealthyProcess = observedProcess;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
const current = await refresh();
|
|
1770
|
-
if (
|
|
1771
|
-
processes.get(current.key) !== target ||
|
|
1772
|
-
target.abortController.signal.aborted ||
|
|
1773
|
-
target.server !== observedServer ||
|
|
1774
|
-
target.workdir !== current.workdir ||
|
|
1775
|
-
!sameOpenCodeSelection(target.upstreamSelection, current.selection) ||
|
|
1776
|
-
text(target.selection?.catalogRevision) !== text(current.selection.catalogRevision)
|
|
1777
|
-
) {
|
|
1778
|
-
throw changed();
|
|
1779
|
-
}
|
|
1780
|
-
missingUpstream = healthy && !ready;
|
|
1781
|
-
}
|
|
1782
|
-
return { ready, unhealthyProcess, missingUpstream };
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
async function recoverSessionReadiness(target, recovery, prepare) {
|
|
1786
|
-
if (recovery.unhealthyProcess && sharedProcess === recovery.unhealthyProcess) {
|
|
1787
|
-
await stop("opencode-health-check-failed");
|
|
1788
|
-
}
|
|
1789
|
-
if (recovery.missingUpstream && processes.get(recovery.key) === target) {
|
|
1790
|
-
target.upstream = null;
|
|
1791
|
-
}
|
|
1792
|
-
return prepare();
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
function needsObservationRecovery(key, run) {
|
|
1796
|
-
return Boolean(run?.active && run.observationError && !monitors.has(key));
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
async function recordStoppedObservation(key, run, threadId, writeRun) {
|
|
1800
|
-
const stopped = {
|
|
1801
|
-
...turns.get(key),
|
|
1802
|
-
...run,
|
|
1803
|
-
id: text(run.turnId),
|
|
1804
|
-
threadId,
|
|
1805
|
-
active: false,
|
|
1806
|
-
state: "interrupted",
|
|
1807
|
-
updatedAt: new Date().toISOString()
|
|
1808
|
-
};
|
|
1809
|
-
await writeRun(stopped, "interrupted", run.observationError);
|
|
1810
|
-
turns.set(key, stopped);
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
async function recoverObservation(target, threadId, commit) {
|
|
1814
|
-
if (threadId !== target.upstreamSessionId) return;
|
|
1815
|
-
const status = await target.server.client.sessionStatus(threadId, {
|
|
1816
|
-
signal: AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS)
|
|
1817
|
-
});
|
|
1818
|
-
if (status?.type !== "idle") return;
|
|
1819
|
-
await commit();
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
async function interruptTurn(key, target, { threadId = "", writeRun } = {}) {
|
|
1823
|
-
const turn = turns.get(key);
|
|
1824
|
-
// Helpers can acquire the shared process before a main conversation exists.
|
|
1825
|
-
// Their cleanup is owned by closeSession, not a native main-turn interrupt.
|
|
1826
|
-
if (!target || (!text(target.upstreamSessionId) && !turn?.active)) {
|
|
1827
|
-
return {
|
|
1828
|
-
interrupted: false,
|
|
1829
|
-
ok: true,
|
|
1830
|
-
thread: { id: threadId },
|
|
1831
|
-
turn: openCodeTurnSnapshot(turn)
|
|
1832
|
-
};
|
|
1833
|
-
}
|
|
1834
|
-
if (turn?.observationError) {
|
|
1835
|
-
await monitors.get(key);
|
|
1836
|
-
if (turn.active) {
|
|
1837
|
-
await stopUnobservedSession(target, turn.threadId);
|
|
1838
|
-
turn.active = false;
|
|
1839
|
-
turn.state = "interrupted";
|
|
1840
|
-
await writeRun(turn, turn.state, turn.observationError);
|
|
1841
|
-
}
|
|
1842
|
-
return { ok: true, interrupted: true, thread: { id: turn.threadId }, turn: openCodeTurnSnapshot(turn) };
|
|
1843
|
-
}
|
|
1844
|
-
if (turn) {
|
|
1845
|
-
turn.interruptRequested = true;
|
|
1846
|
-
}
|
|
1847
|
-
const interrupted = Boolean(turn?.active);
|
|
1848
|
-
try {
|
|
1849
|
-
const confirmed = await target.server.client.interrupt(target.upstreamSessionId, {
|
|
1850
|
-
signal: AbortSignal.timeout(OPENCODE_INTERRUPT_TIMEOUT_MS)
|
|
1851
|
-
});
|
|
1852
|
-
if (confirmed !== true) {
|
|
1853
|
-
throw Object.assign(new Error("OpenCode did not confirm Stop. Try Stop again."), {
|
|
1854
|
-
code: "assistant_opencode_interrupt_unconfirmed", details: {}, statusCode: 502
|
|
1855
|
-
});
|
|
1856
|
-
}
|
|
1857
|
-
} catch (error) {
|
|
1858
|
-
if (turn) turn.interruptRequested = false;
|
|
1859
|
-
if (error?.name === "TimeoutError") {
|
|
1860
|
-
throw Object.assign(new Error("OpenCode did not confirm Stop within 5 seconds. Try Stop again."), {
|
|
1861
|
-
code: "assistant_opencode_interrupt_timeout", details: {}, statusCode: 504
|
|
1862
|
-
});
|
|
1863
|
-
}
|
|
1864
|
-
throw error;
|
|
1865
|
-
}
|
|
1866
|
-
if (turn?.active && turn.abortController) {
|
|
1867
|
-
turn.interruptAcknowledged = true;
|
|
1868
|
-
turn.abortController.abort();
|
|
1869
|
-
turn.admission?.resolve();
|
|
1870
|
-
await monitors.get(key);
|
|
1871
|
-
}
|
|
1872
|
-
return {
|
|
1873
|
-
interrupted,
|
|
1874
|
-
ok: true,
|
|
1875
|
-
thread: { id: target.upstreamSessionId },
|
|
1876
|
-
turn: openCodeTurnSnapshot(turn, target.upstreamSessionId)
|
|
1877
|
-
};
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
124
|
async function readSessionState(current, application) {
|
|
1881
125
|
const context = await application.context(current);
|
|
1882
126
|
const target = processes.get(context.key);
|
|
@@ -1892,13 +136,13 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope, storageI
|
|
|
1892
136
|
}
|
|
1893
137
|
|
|
1894
138
|
async function acquirePreparedTerminal(preparation) {
|
|
1895
|
-
return acquirePrepared(await preparation.process());
|
|
139
|
+
return processOwner.acquirePrepared(await preparation.process());
|
|
1896
140
|
}
|
|
1897
141
|
|
|
1898
142
|
async function ensurePreparedTerminal(preparation) {
|
|
1899
143
|
const target = await acquirePreparedTerminal(preparation);
|
|
1900
144
|
// The native terminal starts its own turns there.
|
|
1901
|
-
await currentDirectory(target.workdir);
|
|
145
|
+
await processOwner.currentDirectory(target.workdir);
|
|
1902
146
|
return ensureOpenCodeSession(target, preparation.session());
|
|
1903
147
|
}
|
|
1904
148
|
|
|
@@ -1922,14 +166,14 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope, storageI
|
|
|
1922
166
|
upstreamSessionId: target.upstreamSessionId,
|
|
1923
167
|
workdir: context.workdir
|
|
1924
168
|
});
|
|
1925
|
-
if (terminal?.ok === true &&
|
|
1926
|
-
target.terminalSessionId =
|
|
169
|
+
if (terminal?.ok === true && normalizeNullableText(terminal.id)) {
|
|
170
|
+
target.terminalSessionId = normalizeNullableText(terminal.id);
|
|
1927
171
|
}
|
|
1928
172
|
return terminal;
|
|
1929
173
|
}
|
|
1930
174
|
|
|
1931
175
|
async function closeTerminal(sessionId, terminalSessionId, terminalHost) {
|
|
1932
|
-
const id =
|
|
176
|
+
const id = normalizeNullableText(terminalSessionId);
|
|
1933
177
|
const result = await terminalHost.close(id, {
|
|
1934
178
|
namespace: terminalHost.namespace(sessionId)
|
|
1935
179
|
});
|
|
@@ -1949,22 +193,75 @@ export function createOpenCodeSharedRuntime({ onStop = () => {}, scope, storageI
|
|
|
1949
193
|
}
|
|
1950
194
|
|
|
1951
195
|
const runtime = Object.freeze({
|
|
1952
|
-
processes,
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
196
|
+
processes,
|
|
197
|
+
processStarts,
|
|
198
|
+
monitors,
|
|
199
|
+
turns,
|
|
200
|
+
temporaryConversations,
|
|
201
|
+
get current() { return processOwner.current; },
|
|
202
|
+
get closed() { return processOwner.closed; },
|
|
203
|
+
get cleanupPending() { return processOwner.cleanupPending; },
|
|
204
|
+
acquire: processOwner.acquire,
|
|
205
|
+
targetFor: processOwner.targetFor,
|
|
206
|
+
ensure: processOwner.ensure,
|
|
207
|
+
withStorageProcess: processOwner.withStorageProcess,
|
|
208
|
+
release: processOwner.release,
|
|
209
|
+
releaseTarget: processOwner.releaseTarget,
|
|
210
|
+
releaseProcessTarget: processOwner.releaseProcessTarget,
|
|
211
|
+
closeSession: processOwner.closeSession,
|
|
212
|
+
closeProject: processOwner.closeProject,
|
|
213
|
+
startPreparedProcess: processOwner.startPreparedProcess,
|
|
214
|
+
acquirePrepared: processOwner.acquirePrepared,
|
|
215
|
+
createPreparedConversation: conversations.createPreparedConversation,
|
|
216
|
+
runPreparedRenewalTurn: sessionTurns.runPreparedRenewalTurn,
|
|
217
|
+
selectPreparedConversation: conversations.selectPreparedConversation,
|
|
218
|
+
existingPreparedConversation: conversations.existingPreparedConversation,
|
|
219
|
+
runPreparedConversationTurn: conversations.runPreparedConversationTurn,
|
|
220
|
+
readPreparedConversation: conversations.readPreparedConversation,
|
|
221
|
+
waitPreparedConversationTurn: conversations.waitPreparedConversationTurn,
|
|
222
|
+
stopPreparedConversation: conversations.stopPreparedConversation,
|
|
223
|
+
deletePreparedConversation: conversations.deletePreparedConversation,
|
|
224
|
+
sendPreparedMessage: sessionTurns.sendPreparedMessage,
|
|
225
|
+
inspectPreparedMessage: sessionTurns.inspectPreparedMessage,
|
|
226
|
+
interruptPreparedTurn: sessionTurns.interruptPreparedTurn,
|
|
227
|
+
invalidateRuntimes: processOwner.invalidateRuntimes,
|
|
228
|
+
releaseRenewalPredecessorProcessExitProof: processOwner.releaseRenewalPredecessorProcessExitProof,
|
|
229
|
+
releaseProcessExitProof: processOwner.releaseProcessExitProof,
|
|
230
|
+
stop: processOwner.stop,
|
|
231
|
+
stopUnobservedSession: processOwner.stopUnobservedSession,
|
|
232
|
+
interruptTurn: sessionTurns.interruptTurn,
|
|
233
|
+
ensurePreparedSessionReadiness: sessionTurns.ensurePreparedSessionReadiness,
|
|
234
|
+
inspectSessionReadiness: sessionTurns.inspectSessionReadiness,
|
|
235
|
+
recoverSessionReadiness: sessionTurns.recoverSessionReadiness,
|
|
236
|
+
needsObservationRecovery: sessionTurns.needsObservationRecovery,
|
|
237
|
+
recoverObservation: sessionTurns.recoverObservation,
|
|
238
|
+
createConversation: conversations.createConversation,
|
|
239
|
+
selectConversation: conversations.selectConversation,
|
|
240
|
+
existingConversation: conversations.existingConversation,
|
|
241
|
+
runConversationTurn: conversations.runConversationTurn,
|
|
242
|
+
readConversation: conversations.readConversation,
|
|
243
|
+
readPersistentConversation: conversations.readPersistentConversation,
|
|
244
|
+
waitForConversationTurn: conversations.waitForConversationTurn,
|
|
245
|
+
stopConversation: conversations.stopConversation,
|
|
246
|
+
deleteConversation: conversations.deleteConversation,
|
|
247
|
+
sendMessage: sessionTurns.sendMessage,
|
|
248
|
+
hasActiveTemporaryConversation: conversations.hasActiveTemporaryConversation,
|
|
249
|
+
reconcileSessions: sessionTurns.reconcileSessions,
|
|
250
|
+
reconcilePreparedSession: sessionTurns.reconcilePreparedSession,
|
|
251
|
+
recordStoppedObservation: sessionTurns.recordStoppedObservation,
|
|
252
|
+
beginMessageMonitor: turnMonitor.beginMessageMonitor,
|
|
253
|
+
beginMonitor: turnMonitor.beginMonitor,
|
|
254
|
+
writeBindings,
|
|
255
|
+
writeConversationProjection,
|
|
256
|
+
readFinalAssistantResult: turnMonitor.readFinalAssistantResult,
|
|
1965
257
|
projectConversationMessages: projectOpenCodeConversationMessages,
|
|
1966
|
-
readSessionState,
|
|
1967
|
-
|
|
258
|
+
readSessionState,
|
|
259
|
+
startPreparedTerminal,
|
|
260
|
+
attachTerminal,
|
|
261
|
+
closeTerminal,
|
|
262
|
+
allowTerminalAttachments,
|
|
263
|
+
turnSnapshot: openCodeTurnSnapshot,
|
|
264
|
+
providerApiFailure: openCodeProviderApiFailure
|
|
1968
265
|
});
|
|
1969
266
|
if (scope) sharedRuntimes.set(scope, runtime);
|
|
1970
267
|
return runtime;
|