@jskit-ai/assistant-core 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +13 -10
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Stopped native storage inspection, and retirement of a native family after the host's durable preservation.
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createNativeHistoryExport, readStoppedNativeDatabase, retireNativeConversation } from "./nativeHistoryExport.js";
|
|
5
|
+
import { normalizedMessageRows, openCodeAssistantMessageText } from "./openCodeClient.js";
|
|
6
|
+
|
|
7
|
+
/** Inspect stopped native storage without starting OpenCode or changing its files. */
|
|
8
|
+
async function readOpenCodeConversationDatabase({ databasePath, conversationId, onMessage, signal } = {}) {
|
|
9
|
+
if (!path.isAbsolute(databasePath || "") || !/^ses_[a-zA-Z0-9_]{1,256}$/u.test(conversationId || "") ||
|
|
10
|
+
typeof onMessage !== "function") throw new TypeError("OpenCode inspection requires its exact database, conversation and message reader.");
|
|
11
|
+
const unavailable = () => new Error("OpenCode native storage could not be inspected completely. Keep its writers stopped and inspect its original database before retrying.");
|
|
12
|
+
try {
|
|
13
|
+
return await readStoppedNativeDatabase(databasePath, async database => {
|
|
14
|
+
const session = database.prepare("SELECT * FROM session WHERE id = ?").get(conversationId);
|
|
15
|
+
if (!session || session.id !== conversationId || !path.isAbsolute(session.directory || "")) throw unavailable();
|
|
16
|
+
if (database.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'session_input'").get() &&
|
|
17
|
+
database.prepare("SELECT 1 FROM session_input WHERE session_id = ? AND promoted_seq IS NULL LIMIT 1").get(conversationId)) {
|
|
18
|
+
throw new Error("OpenCode has an unpromoted native request. Inspect it before restoring continuity; no request was repeated.");
|
|
19
|
+
}
|
|
20
|
+
const output = createNativeHistoryExport(async ({ message }) => onMessage(normalizedMessageRows([message])[0]), { signal });
|
|
21
|
+
const parts = database.prepare("SELECT id, message_id, session_id, data FROM part WHERE message_id = ? ORDER BY id");
|
|
22
|
+
for (const row of database.prepare("SELECT id, session_id, data FROM message WHERE session_id = ? ORDER BY time_created, id").iterate(conversationId)) {
|
|
23
|
+
signal?.throwIfAborted();
|
|
24
|
+
if (!/^msg_[a-zA-Z0-9_]{1,256}$/u.test(row.id)) throw unavailable();
|
|
25
|
+
let bytes = Buffer.byteLength(row.data);
|
|
26
|
+
if (bytes > 64 * 1024 * 1024) throw unavailable();
|
|
27
|
+
const data = JSON.parse(row.data);
|
|
28
|
+
if (!data || typeof data !== "object" || Array.isArray(data) || !["user", "assistant"].includes(data.role)) throw unavailable();
|
|
29
|
+
const content = [];
|
|
30
|
+
for (const part of parts.iterate(row.id)) {
|
|
31
|
+
signal?.throwIfAborted();
|
|
32
|
+
bytes += Buffer.byteLength(part.data);
|
|
33
|
+
if (bytes > 64 * 1024 * 1024 || part.session_id !== conversationId || part.message_id !== row.id ||
|
|
34
|
+
!/^prt_[a-zA-Z0-9_]{1,256}$/u.test(part.id)) throw unavailable();
|
|
35
|
+
const value = JSON.parse(part.data);
|
|
36
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw unavailable();
|
|
37
|
+
content.push({ ...value, id: part.id, sessionID: part.session_id, messageID: part.message_id });
|
|
38
|
+
}
|
|
39
|
+
// OpenCode's MessageV2 storage projection: native columns own IDs.
|
|
40
|
+
await output.emit({ type: "message", message: { info: { ...data, id: row.id, sessionID: row.session_id }, parts: content }, text: [] });
|
|
41
|
+
}
|
|
42
|
+
return { session, ...output.complete() };
|
|
43
|
+
}, { signal });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (signal?.aborted) throw signal.reason;
|
|
46
|
+
if (String(error?.message || "").startsWith("OpenCode ") ||
|
|
47
|
+
String(error?.message || "").startsWith("Native storage ")) throw error;
|
|
48
|
+
throw unavailable();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Native preservation uses the storage API independently of the saved worktree.
|
|
53
|
+
// Status still comes from the exact directory-scoped control client.
|
|
54
|
+
async function inspectOpenCodeConversationFamily(storageClient, controlClient, binding, { signal } = {}) {
|
|
55
|
+
const queue = [binding.conversationId];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const records = [];
|
|
58
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
59
|
+
signal?.throwIfAborted();
|
|
60
|
+
const conversationId = queue[index];
|
|
61
|
+
if (seen.has(conversationId) || queue.length > 1000) throw new Error("OpenCode native family is cyclic or exceeds its inventory limit.");
|
|
62
|
+
seen.add(conversationId);
|
|
63
|
+
let native;
|
|
64
|
+
try { native = await storageClient.readConversationStorage(conversationId, { signal }); }
|
|
65
|
+
catch (error) { if (error.statusCode === 404 && index === 0) return []; throw error; }
|
|
66
|
+
if (native?.id !== conversationId || native?.directory !== binding.workdir ||
|
|
67
|
+
(await controlClient.sessionStatus(conversationId, { signal })).type !== "idle") {
|
|
68
|
+
throw new Error("OpenCode retirement requires an idle native family in the exact saved directory.");
|
|
69
|
+
}
|
|
70
|
+
records.push({ conversationId, workdir: native.directory,
|
|
71
|
+
...(Number.isFinite(native.time?.created) ? { createdAt: new Date(native.time.created).toISOString() } : {}),
|
|
72
|
+
...(Number.isFinite(native.time?.updated) ? { updatedAt: new Date(native.time.updated).toISOString() } : {}) });
|
|
73
|
+
const children = await storageClient.listConversationChildren(conversationId, { signal });
|
|
74
|
+
for (const child of children) {
|
|
75
|
+
if (child.directory !== binding.workdir) throw new Error("OpenCode child directory differs from its saved owner.");
|
|
76
|
+
queue.push(child.id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return records.sort((left, right) => left.conversationId.localeCompare(right.conversationId));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function exportOpenCodeNativeHistory(storageClient, controlClient, id, onRecord, { signal: inputSignal } = {}) {
|
|
83
|
+
const deadline = AbortSignal.timeout(300_000);
|
|
84
|
+
const signal = inputSignal ? AbortSignal.any([inputSignal, deadline]) : deadline;
|
|
85
|
+
const output = createNativeHistoryExport(onRecord, { signal });
|
|
86
|
+
const info = await storageClient.readConversationStorage(id, { signal });
|
|
87
|
+
if (info?.id !== id || (await controlClient.sessionStatus(id, { signal })).type !== "idle") {
|
|
88
|
+
throw new Error("OpenCode native export requires the exact idle conversation.");
|
|
89
|
+
}
|
|
90
|
+
await output.emit({ type: "thread", thread: info, text: [] });
|
|
91
|
+
const messageIds = new Set();
|
|
92
|
+
const cursors = new Set();
|
|
93
|
+
let before = "";
|
|
94
|
+
let pages = 0;
|
|
95
|
+
while (true) {
|
|
96
|
+
signal.throwIfAborted();
|
|
97
|
+
if (++pages > 20_000) throw new Error("OpenCode native export exceeded its page limit; history was not retired.");
|
|
98
|
+
const response = await storageClient.readConversationStoragePage(id, { before, signal });
|
|
99
|
+
if (!Array.isArray(response?.data) || response.data.length > 1 ||
|
|
100
|
+
(response.nextCursor !== null && (typeof response.nextCursor !== "string" || !response.nextCursor || response.nextCursor.length > 8192)) ||
|
|
101
|
+
(response.data.length === 0 && response.nextCursor !== null)) {
|
|
102
|
+
throw new Error("OpenCode returned an incomplete or invalid native message page.");
|
|
103
|
+
}
|
|
104
|
+
for (const message of response.data) {
|
|
105
|
+
const info = message?.info;
|
|
106
|
+
if (!/^msg_[a-zA-Z0-9_]{1,256}$/u.test(info?.id) || info.sessionID !== id ||
|
|
107
|
+
!["user", "assistant"].includes(info.role) || !Array.isArray(message.parts) || messageIds.has(info.id)) {
|
|
108
|
+
throw new Error("OpenCode returned an invalid or duplicate native message.");
|
|
109
|
+
}
|
|
110
|
+
messageIds.add(info.id);
|
|
111
|
+
const content = openCodeAssistantMessageText({ content: message.parts });
|
|
112
|
+
await output.emit({ type: "message", message, text: content ? [{ role: info.role, text: content,
|
|
113
|
+
branchId: id, messageId: info.id,
|
|
114
|
+
...(info.parentID ? { parentMessageId: info.parentID } : {}),
|
|
115
|
+
...(Number.isFinite(info.time?.created) ? { createdAt: new Date(info.time.created).toISOString() } : {}),
|
|
116
|
+
...(Number.isFinite(info.time?.completed) ? { completedAt: new Date(info.time.completed).toISOString() } : {}),
|
|
117
|
+
...(info.modelID || info.model?.modelID ? { modelId: info.modelID || info.model.modelID } : {}),
|
|
118
|
+
...(info.providerID || info.model?.providerID ? { modelProviderId: info.providerID || info.model.providerID } : {}),
|
|
119
|
+
...(info.agent ? { agent: info.agent } : {}) }] : [] });
|
|
120
|
+
}
|
|
121
|
+
if (response.nextCursor === null) break;
|
|
122
|
+
if (cursors.has(response.nextCursor)) throw new Error("OpenCode repeated a native history cursor.");
|
|
123
|
+
cursors.add(response.nextCursor);
|
|
124
|
+
before = response.nextCursor;
|
|
125
|
+
}
|
|
126
|
+
return output.complete();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Retire the exact native family after the host's durable preservation. */
|
|
130
|
+
async function retireOpenCodeConversationHistory(storageClient, controlClient, binding, options = {}) {
|
|
131
|
+
const inspect = () => inspectOpenCodeConversationFamily(storageClient, controlClient, binding, { signal: options.signal });
|
|
132
|
+
return retireNativeConversation({ binding, inspect, beforeDelete: options.beforeDelete,
|
|
133
|
+
readConversation: async (id) => ({ info: await storageClient.readConversationStorage(id), messages: (await controlClient.messages(id)).data }),
|
|
134
|
+
exportConversation: (id, onRecord) => exportOpenCodeNativeHistory(storageClient, controlClient, id, onRecord, { signal: options.signal }),
|
|
135
|
+
remove: () => controlClient.deleteSession(binding.conversationId) });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
readOpenCodeConversationDatabase,
|
|
140
|
+
retireOpenCodeConversationHistory
|
|
141
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// One native OpenCode session: its model, its selection, the shared server
|
|
2
|
+
// bound to its directory, and the start that creates or resumes it.
|
|
3
|
+
import { normalizeNullableText } from "./normalize.js";
|
|
4
|
+
|
|
5
|
+
/** The shared server with its client bound to one native directory. */
|
|
6
|
+
export function openCodeServerForDirectory(server, workdir) {
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
...server,
|
|
9
|
+
client: server.client.forDirectory(workdir)
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The native model a selection names, or the one an execution profile overrides it with. */
|
|
14
|
+
export function openCodeModel(selection = {}, executionProfile = null) {
|
|
15
|
+
const modelId = normalizeNullableText(executionProfile?.model) || normalizeNullableText(selection.modelId);
|
|
16
|
+
const variantId = executionProfile
|
|
17
|
+
? normalizeNullableText(executionProfile.thinking)
|
|
18
|
+
: normalizeNullableText(selection.variantId);
|
|
19
|
+
return {
|
|
20
|
+
id: modelId,
|
|
21
|
+
providerID: normalizeNullableText(selection.modelProviderId),
|
|
22
|
+
...(variantId ? { variant: variantId } : {})
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether two selections name the same agent, model, provider and variant. */
|
|
27
|
+
export function sameOpenCodeSelection(left, right) {
|
|
28
|
+
return ["agentId", "modelId", "modelProviderId", "variantId"]
|
|
29
|
+
.every((name) => normalizeNullableText(left?.[name]) === normalizeNullableText(right?.[name]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Native selection and identity publication share the same pending operation.
|
|
34
|
+
* @param {object} target
|
|
35
|
+
* @param {{ selection: object, model: object, workdir?: string, signal?: AbortSignal, invalidIdentity?: () => Error,
|
|
36
|
+
* identity: { write(nativeId: string): unknown, publish(nativeId: string): unknown } }} session
|
|
37
|
+
*/
|
|
38
|
+
export async function ensureOpenCodeSession(target, { selection, model, workdir = target.workdir, identity, signal,
|
|
39
|
+
invalidIdentity = () => new Error("OpenCode did not return a native conversation ID.") }) {
|
|
40
|
+
if (target.upstreamStart) {
|
|
41
|
+
return target.upstreamStart;
|
|
42
|
+
}
|
|
43
|
+
const start = Promise.resolve().then(async () => {
|
|
44
|
+
await target.server.client.prepareDirectory({ signal });
|
|
45
|
+
let upstream = (
|
|
46
|
+
target.upstream &&
|
|
47
|
+
sameOpenCodeSelection(target.upstreamSelection, selection)
|
|
48
|
+
) ? target.upstream : null;
|
|
49
|
+
if (!upstream && target.upstreamSessionId) {
|
|
50
|
+
upstream = await target.server.client.readSession(target.upstreamSessionId, { signal });
|
|
51
|
+
await target.server.client.switchModel(target.upstreamSessionId, model, { signal });
|
|
52
|
+
await target.server.client.switchAgent(target.upstreamSessionId, selection.agentId, { signal });
|
|
53
|
+
} else if (!upstream) {
|
|
54
|
+
upstream = await target.server.client.createSession({
|
|
55
|
+
agent: selection.agentId,
|
|
56
|
+
location: { directory: workdir },
|
|
57
|
+
model
|
|
58
|
+
}, { signal });
|
|
59
|
+
}
|
|
60
|
+
const nativeId = normalizeNullableText(upstream?.id);
|
|
61
|
+
if (!nativeId) {
|
|
62
|
+
throw invalidIdentity();
|
|
63
|
+
}
|
|
64
|
+
await identity.write(nativeId);
|
|
65
|
+
target.upstreamSessionId = nativeId;
|
|
66
|
+
target.upstream = upstream;
|
|
67
|
+
target.upstreamSelection = { ...selection };
|
|
68
|
+
await identity.publish(nativeId);
|
|
69
|
+
return target;
|
|
70
|
+
});
|
|
71
|
+
target.upstreamStart = start;
|
|
72
|
+
try {
|
|
73
|
+
return await start;
|
|
74
|
+
} finally {
|
|
75
|
+
if (target.upstreamStart === start) {
|
|
76
|
+
delete target.upstreamStart;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
1
2
|
import { randomBytes } from "node:crypto";
|
|
2
3
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import net from "node:net";
|
|
@@ -7,9 +8,9 @@ import { createOpenCodeServerClient, readBoundedResponse } from "./openCodeClien
|
|
|
7
8
|
import { createLocalConversationExecution } from "./localExecution.js";
|
|
8
9
|
import { normalizeNullableText } from "./normalize.js";
|
|
9
10
|
import { DISCOVERY_TOOL_DESCRIPTORS } from "../lib/serviceToolCatalog.js";
|
|
10
|
-
export { createOpenCodeSharedRuntime,
|
|
11
|
-
openCodeEnvironmentForSession, assertOpenCodeModelProvider, limitOpenCodeModelOutput
|
|
12
|
-
|
|
11
|
+
export { createOpenCodeSharedRuntime, readOpenCodeEnvironments, openCodeEnvironmentForDirectory,
|
|
12
|
+
openCodeEnvironmentForSession, assertOpenCodeModelProvider, limitOpenCodeModelOutput } from "./openCodeRuntime.js";
|
|
13
|
+
export { openCodeServerForDirectory, ensureOpenCodeSession, openCodeModel, sameOpenCodeSelection } from "./openCodeNativeSession.js";
|
|
13
14
|
export { default as createOpenCodeConversationPlugin } from "./openCodePlugin.js";
|
|
14
15
|
|
|
15
16
|
export const OPENCODE_EXPECTED_VERSION = "1.18.31";
|
|
@@ -26,7 +27,11 @@ const OPENCODE_ZEN_MODELS_URL = "https://opencode.ai/zen/v1/models";
|
|
|
26
27
|
const OPENCODE_ZEN_PROVIDER_ID = "opencode";
|
|
27
28
|
const OPENCODE_ZEN_PUBLIC_API_KEY = "public";
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
/** A server start whose caller records no execution identity. */
|
|
31
|
+
function ignoreServerStart() {}
|
|
32
|
+
|
|
33
|
+
/** A server whose caller keeps no state after an unexpected exit. */
|
|
34
|
+
function ignoreServerFailure() {}
|
|
30
35
|
|
|
31
36
|
export function openCodeProcessEnvironment(baseEnv = {}, {
|
|
32
37
|
cacheRoot = "", dbPath = "", inlineConfig = "", outputTokenMax = 0, packageInstalls = true, password = ""
|
|
@@ -44,7 +49,7 @@ export function openCodeProcessEnvironment(baseEnv = {}, {
|
|
|
44
49
|
npm_config_cache: path.join(cacheRoot, "npm-offline"),
|
|
45
50
|
npm_config_offline: "true"
|
|
46
51
|
}),
|
|
47
|
-
NO_PROXY: [
|
|
52
|
+
NO_PROXY: [normalizeNullableText(baseEnv.NO_PROXY), OPENCODE_HOST, "localhost", "::1"]
|
|
48
53
|
.filter(Boolean)
|
|
49
54
|
.join(","),
|
|
50
55
|
OPENCODE_DB: dbPath,
|
|
@@ -124,9 +129,9 @@ async function waitForOpenCodeReady({
|
|
|
124
129
|
});
|
|
125
130
|
signal?.throwIfAborted();
|
|
126
131
|
if (health?.healthy === true) {
|
|
127
|
-
if (expectedVersion &&
|
|
132
|
+
if (expectedVersion && normalizeNullableText(health.version) !== expectedVersion) {
|
|
128
133
|
const error = new Error(
|
|
129
|
-
`OpenCode ${
|
|
134
|
+
`OpenCode ${normalizeNullableText(health.version) || "(unknown)"} is installed; this host requires ${expectedVersion}.`
|
|
130
135
|
);
|
|
131
136
|
error.code = "assistant_opencode_version_mismatch";
|
|
132
137
|
throw error;
|
|
@@ -161,11 +166,11 @@ export function openCodeConversationAgent({ nativeTools = false, tools = false }
|
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
export function openCodeConversationAgents({ sessionEnvironmentRegistry = "" } = {}) {
|
|
164
|
-
const guarded = Boolean(
|
|
169
|
+
const guarded = Boolean(normalizeNullableText(sessionEnvironmentRegistry));
|
|
165
170
|
return Object.fromEntries([false, true].flatMap(nativeTools => [false, true].map(tools => [
|
|
166
171
|
openCodeConversationAgent({ nativeTools, tools }), {
|
|
167
172
|
hidden: true, mode: "primary",
|
|
168
|
-
// Keep native definitions behind approval
|
|
173
|
+
// Keep native definitions behind approval.
|
|
169
174
|
// The registered plugin rejects every ungranted native call before execution.
|
|
170
175
|
permission: nativeTools || tools || guarded ? agentPermission(!nativeTools && guarded, nativeTools, tools) : "deny"
|
|
171
176
|
}
|
|
@@ -181,12 +186,32 @@ function agentPermission(ask, nativeTools, tools) {
|
|
|
181
186
|
...(tools ? actionPermission("allow") : ask ? actionPermission("deny") : {}), ...(ask ? { doom_loop: "deny" } : {}) };
|
|
182
187
|
}
|
|
183
188
|
|
|
184
|
-
/**
|
|
189
|
+
/** @typedef {import("./localExecution.js").ConversationExecution} ConversationExecution */
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Starts the OpenCode server of one conversation runtime: its private credential home, its configuration for the selected
|
|
193
|
+
* connection, and the environment it passes on from the application's.
|
|
194
|
+
* @param {object} options
|
|
195
|
+
* @param {string} [options.command]
|
|
196
|
+
* @param {ConversationExecution} [options.execution]
|
|
197
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeEnv} options.env
|
|
198
|
+
* @param {string} options.workdir
|
|
199
|
+
* @param {string} options.stateDirectory
|
|
200
|
+
* @param {string} options.databasePath
|
|
201
|
+
* @param {string} options.sessionEnvironmentRegistry
|
|
202
|
+
* @param {Record<string, any>} options.connection
|
|
203
|
+
* @param {number} [options.maxOutputTokens]
|
|
204
|
+
* @param {(executionId: string, processDirectory: string) => unknown} [options.onStarted]
|
|
205
|
+
* @param {(error: Error) => unknown} [options.onFailure]
|
|
206
|
+
* @param {object} [options.executionLimits]
|
|
207
|
+
* @param {AbortSignal} [options.signal]
|
|
208
|
+
*/
|
|
185
209
|
export async function createOpenCodeConversationServer({
|
|
186
|
-
command = "opencode", execution = createLocalConversationExecution(), env
|
|
210
|
+
command = "opencode", execution = createLocalConversationExecution(), env,
|
|
187
211
|
workdir, stateDirectory, databasePath, sessionEnvironmentRegistry, connection, maxOutputTokens,
|
|
188
|
-
onStarted, onFailure, executionLimits, signal
|
|
189
|
-
}
|
|
212
|
+
onStarted = ignoreServerStart, onFailure = ignoreServerFailure, executionLimits, signal
|
|
213
|
+
}) {
|
|
214
|
+
if (!env) throw new TypeError("An OpenCode conversation's host requires the application's environment, such as process.env, to start OpenCode.");
|
|
190
215
|
await mkdir(stateDirectory, { recursive: true, mode: 0o700 });
|
|
191
216
|
const directory = await mkdtemp(path.join(stateDirectory, "process-"));
|
|
192
217
|
const model = `${connection.providerId}/${connection.model}`;
|
|
@@ -221,17 +246,39 @@ export async function createOpenCodeConversationServer({
|
|
|
221
246
|
JSKIT_OPENCODE_TOOL_SCHEMAS: JSON.stringify(openCodeApplicationToolSchemas)
|
|
222
247
|
});
|
|
223
248
|
return createOpenCodeServerProcess({ command, execution, env: environment, workdir, privateRoot: directory,
|
|
224
|
-
directory: workdir, connections: [connection], onStarted: id => onStarted
|
|
249
|
+
directory: workdir, connections: [connection], onStarted: id => onStarted(id, directory),
|
|
225
250
|
onFailure, executionLimits, signal });
|
|
226
251
|
}
|
|
227
252
|
|
|
228
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* Shared server lifecycle for standalone and managed hosts.
|
|
255
|
+
* @param {object} options
|
|
256
|
+
* @param {string} [options.command]
|
|
257
|
+
* @param {ConversationExecution} [options.execution]
|
|
258
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeEnv} options.env
|
|
259
|
+
* @param {string} options.workdir
|
|
260
|
+
* @param {string} options.privateRoot
|
|
261
|
+
* @param {string} [options.directory]
|
|
262
|
+
* @param {Record<string, any>[]} [options.connections]
|
|
263
|
+
* @param {number} [options.port]
|
|
264
|
+
* @param {typeof fetch} [options.fetchImpl]
|
|
265
|
+
* @param {boolean} [options.allowAttachmentDirectories]
|
|
266
|
+
* @param {string} [options.expectedVersion]
|
|
267
|
+
* @param {number} [options.readinessTimeoutMs]
|
|
268
|
+
* @param {(executionId: string) => unknown} [options.onStarted]
|
|
269
|
+
* @param {(error: Error) => unknown} [options.onFailure]
|
|
270
|
+
* @param {object} [options.executionLimits]
|
|
271
|
+
* @param {AbortSignal} [options.signal]
|
|
272
|
+
*/
|
|
229
273
|
export async function createOpenCodeServerProcess({
|
|
230
|
-
command = "opencode", execution = createLocalConversationExecution(), env
|
|
274
|
+
command = "opencode", execution = createLocalConversationExecution(), env,
|
|
231
275
|
workdir, privateRoot, directory = "", connections = [], port = 0, fetchImpl = globalThis.fetch,
|
|
232
276
|
allowAttachmentDirectories = false, expectedVersion = OPENCODE_EXPECTED_VERSION, readinessTimeoutMs = OPENCODE_READY_TIMEOUT_MS,
|
|
233
|
-
onStarted, onFailure, executionLimits, signal
|
|
234
|
-
}
|
|
277
|
+
onStarted = ignoreServerStart, onFailure = ignoreServerFailure, executionLimits, signal
|
|
278
|
+
}) {
|
|
279
|
+
if (!env) {
|
|
280
|
+
throw new TypeError("createOpenCodeServerProcess requires the application's environment, such as process.env.");
|
|
281
|
+
}
|
|
235
282
|
if (!path.isAbsolute(workdir || "") || !path.isAbsolute(privateRoot || "")) {
|
|
236
283
|
throw new TypeError("OpenCode server processes require absolute working and private roots.");
|
|
237
284
|
}
|
|
@@ -250,6 +297,7 @@ export async function createOpenCodeServerProcess({
|
|
|
250
297
|
const logPath = path.join(privateRoot, "opencode-server.log");
|
|
251
298
|
const processHandle = {
|
|
252
299
|
get exited() { return stopped || native?.running === false; },
|
|
300
|
+
// readable-check: allow optional-call - optional operation: an execution host's started process may not keep its logs
|
|
253
301
|
readLogs: () => native?.readLogs?.() || { stderr: "", stdout: "" },
|
|
254
302
|
async stop() {
|
|
255
303
|
const proof = await execution.stop(executionId, {
|
|
@@ -293,17 +341,17 @@ export async function createOpenCodeServerProcess({
|
|
|
293
341
|
native = await execution.start({ command, cwd: workdir, env: environment, limits: executionLimits,
|
|
294
342
|
logPath, args: ["serve", "--hostname", OPENCODE_HOST, "--port", String(selectedPort), "--mdns=false"] });
|
|
295
343
|
executionId = native.id;
|
|
296
|
-
await onStarted
|
|
344
|
+
await onStarted(native.id);
|
|
297
345
|
const fail = error => {
|
|
298
346
|
if (closing) return;
|
|
299
347
|
unexpectedExit.resolve(error);
|
|
300
|
-
Promise.resolve().then(() => onFailure
|
|
348
|
+
Promise.resolve().then(() => onFailure(error)).catch(() => {});
|
|
301
349
|
};
|
|
302
350
|
native.exited.then(() => fail(new Error("The owned OpenCode process exited."))).catch(fail);
|
|
303
351
|
const health = await waitForOpenCodeReady({ client, expectedVersion, processHandle, timeoutMs: readinessTimeoutMs, signal });
|
|
304
352
|
const authenticated = new Set();
|
|
305
353
|
for (const connection of connections) {
|
|
306
|
-
const providerId =
|
|
354
|
+
const providerId = normalizeNullableText(connection?.providerId);
|
|
307
355
|
const key = String(connection?.apiKey || "");
|
|
308
356
|
if (!providerId || !key || authenticated.has(providerId)) {
|
|
309
357
|
continue;
|
|
@@ -311,22 +359,32 @@ export async function createOpenCodeServerProcess({
|
|
|
311
359
|
await client.authenticateApiKey(providerId, key, { signal });
|
|
312
360
|
authenticated.add(providerId);
|
|
313
361
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
362
|
+
/** The native terminal's arguments for one conversation in an absolute directory. */
|
|
363
|
+
function attachArguments({ conversationId, directory }) {
|
|
364
|
+
if (!conversationId || !path.isAbsolute(directory || "")) throw new TypeError("An OpenCode terminal requires a conversation and absolute directory.");
|
|
365
|
+
return ["attach", baseUrl, "--dir", directory, "--session", conversationId, "--pure"];
|
|
366
|
+
}
|
|
367
|
+
return Object.freeze({
|
|
368
|
+
client,
|
|
369
|
+
executionId: native.id,
|
|
370
|
+
privateRoot,
|
|
371
|
+
pid: native.pid,
|
|
372
|
+
port: selectedPort,
|
|
373
|
+
environment,
|
|
374
|
+
health: Object.freeze({ ...health }),
|
|
375
|
+
modelProviderIds: Object.freeze([...authenticated]),
|
|
376
|
+
readLogs: processHandle.readLogs,
|
|
377
|
+
stop,
|
|
317
378
|
// Resolves when the process exits without being stopped.
|
|
318
379
|
unexpectedExit: unexpectedExit.promise,
|
|
319
|
-
attachArguments
|
|
320
|
-
if (!conversationId || !path.isAbsolute(directory || "")) throw new TypeError("An OpenCode terminal requires a conversation and absolute directory.");
|
|
321
|
-
return ["attach", baseUrl, "--dir", directory, "--session", conversationId, "--pure"];
|
|
322
|
-
}
|
|
380
|
+
attachArguments
|
|
323
381
|
});
|
|
324
382
|
} catch (error) {
|
|
325
383
|
error.code ||= "assistant_opencode_start_failed";
|
|
326
384
|
executionId ||= error.executionId;
|
|
327
385
|
error.executionId = executionId;
|
|
328
386
|
if (!native && executionId) {
|
|
329
|
-
try { await onStarted
|
|
387
|
+
try { await onStarted(executionId); }
|
|
330
388
|
catch (bindingError) { error.bindingError = bindingError; }
|
|
331
389
|
}
|
|
332
390
|
try { error.details = processHandle.readLogs(); }
|
|
@@ -342,12 +400,10 @@ export async function createOpenCodeServerProcess({
|
|
|
342
400
|
}
|
|
343
401
|
|
|
344
402
|
function openCodeZenCatalogError(cause = null) {
|
|
345
|
-
const error = new
|
|
346
|
-
cause
|
|
403
|
+
const error = new AppError(503, "OpenCode Zen's current model list could not be read. Try again.", {
|
|
404
|
+
cause, code: "assistant_opencode_zen_catalog_unavailable"
|
|
347
405
|
});
|
|
348
|
-
error.code = "assistant_opencode_zen_catalog_unavailable";
|
|
349
406
|
error.retryable = true;
|
|
350
|
-
error.statusCode = 503;
|
|
351
407
|
return error;
|
|
352
408
|
}
|
|
353
409
|
|
|
@@ -374,7 +430,7 @@ export async function readOpenCodeZenModelIds({
|
|
|
374
430
|
if (!Array.isArray(payload?.data) || payload.data.length === 0) {
|
|
375
431
|
throw new Error("OpenCode Zen returned an empty model list.");
|
|
376
432
|
}
|
|
377
|
-
const ids = payload.data.map((model) =>
|
|
433
|
+
const ids = payload.data.map((model) => normalizeNullableText(model?.id));
|
|
378
434
|
const uniqueIds = new Set(ids);
|
|
379
435
|
if (ids.some((id) => !id) || uniqueIds.size !== ids.length) {
|
|
380
436
|
throw new Error("OpenCode Zen returned an invalid model list.");
|
|
@@ -398,9 +454,9 @@ export async function readOpenCodeCatalog({
|
|
|
398
454
|
privateRoot = "",
|
|
399
455
|
workdir = ""
|
|
400
456
|
} = {}) {
|
|
401
|
-
const normalizedPrivateRoot = path.resolve(
|
|
402
|
-
const normalizedWorkdir = path.resolve(
|
|
403
|
-
if (!
|
|
457
|
+
const normalizedPrivateRoot = path.resolve(normalizeNullableText(privateRoot));
|
|
458
|
+
const normalizedWorkdir = path.resolve(normalizeNullableText(workdir));
|
|
459
|
+
if (!normalizeNullableText(privateRoot) || !normalizeNullableText(workdir)) {
|
|
404
460
|
throw new TypeError("OpenCode catalogue reads require private and working roots.");
|
|
405
461
|
}
|
|
406
462
|
const authRoot = path.join(normalizedPrivateRoot, "data", "opencode");
|
|
@@ -454,11 +510,11 @@ export async function verifyOpenCodeApiKey({
|
|
|
454
510
|
workdir = ""
|
|
455
511
|
} = {}) {
|
|
456
512
|
const key = String(apiKey || "");
|
|
457
|
-
const providerId =
|
|
458
|
-
const selectedModelId =
|
|
459
|
-
const normalizedPrivateRoot = path.resolve(
|
|
460
|
-
const normalizedWorkdir = path.resolve(
|
|
461
|
-
if (!key || !providerId || !selectedModelId || !
|
|
513
|
+
const providerId = normalizeNullableText(modelProviderId);
|
|
514
|
+
const selectedModelId = normalizeNullableText(modelId);
|
|
515
|
+
const normalizedPrivateRoot = path.resolve(normalizeNullableText(privateRoot));
|
|
516
|
+
const normalizedWorkdir = path.resolve(normalizeNullableText(workdir));
|
|
517
|
+
if (!key || !providerId || !selectedModelId || !normalizeNullableText(privateRoot) || !normalizeNullableText(workdir)) {
|
|
462
518
|
throw new TypeError("OpenCode API-key verification requires a key, provider, model, and isolated roots.");
|
|
463
519
|
}
|
|
464
520
|
try {
|
|
@@ -500,26 +556,26 @@ export async function verifyOpenCodeApiKey({
|
|
|
500
556
|
timeout: OPENCODE_VERIFY_TIMEOUT_MS
|
|
501
557
|
});
|
|
502
558
|
} catch {
|
|
503
|
-
const error = new
|
|
504
|
-
|
|
559
|
+
const error = new AppError(503, "OpenCode API-key verification could not run.", {
|
|
560
|
+
code: "assistant_opencode_key_verification_unavailable"
|
|
561
|
+
});
|
|
505
562
|
error.retryable = true;
|
|
506
|
-
error.statusCode = 503;
|
|
507
563
|
throw error;
|
|
508
564
|
}
|
|
509
565
|
if (result?.ok !== true) {
|
|
510
566
|
const exitCode = Number(result?.exitCode);
|
|
511
567
|
const unavailable = result?.timedOut === true ||
|
|
512
568
|
result?.retryable === true ||
|
|
513
|
-
Boolean(
|
|
569
|
+
Boolean(normalizeNullableText(result?.code)) ||
|
|
514
570
|
(Number.isSafeInteger(exitCode) && exitCode !== 1);
|
|
515
|
-
const error =
|
|
516
|
-
? "OpenCode API-key verification could not run."
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
571
|
+
const error = unavailable
|
|
572
|
+
? new AppError(503, "OpenCode API-key verification could not run.", {
|
|
573
|
+
code: "assistant_opencode_key_verification_unavailable"
|
|
574
|
+
})
|
|
575
|
+
: new AppError(422, "OpenCode rejected the API-key verification request.", {
|
|
576
|
+
code: "assistant_opencode_key_verification_failed"
|
|
577
|
+
});
|
|
521
578
|
error.retryable = unavailable;
|
|
522
|
-
error.statusCode = unavailable ? 503 : 422;
|
|
523
579
|
throw error;
|
|
524
580
|
}
|
|
525
581
|
return Object.freeze({ ok: true });
|