@jskit-ai/assistant-core 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +13 -10
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -1,92 +1,55 @@
|
|
|
1
1
|
import { createConversationProviderFactory } from "./provider.js";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
inspectPersistentConversationAdmission, preparePersistentConversationChangeover,
|
|
6
|
-
inspectPersistentConversationDelivery, notSentDeliveryReceipt, stopPersistentConversation, deletePersistentConversation } from "./providers/scoped.js";
|
|
7
|
-
import { isDeepStrictEqual } from "node:util";
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
-
import { createConversationTranscript, isCompletedEnvelopeTurn } from "./transcript.js";
|
|
2
|
+
import { normalizeConversationDeliveryHostStages } from "./deliveryTiming.js";
|
|
3
|
+
import * as scopedConversations from "./providers/scoped.js";
|
|
4
|
+
import { createConversationTranscript } from "./transcript.js";
|
|
10
5
|
import { createConversationStreams } from "./streams.js";
|
|
11
6
|
import { createReentrantConversationStorage } from "./storage.js";
|
|
12
|
-
import { createCodexConversationStore } from "./codexConversationStore.js";
|
|
13
|
-
import { codexAppServerTurnStateFromAgentRun } from "./codexTurn.js";
|
|
14
|
-
import { hasUnfinishedConversationRewind, createConversationRuntimeReplacement,
|
|
15
|
-
releaseConversationRuntimeBinding, finishConversationRuntimeReplacement,
|
|
16
|
-
conversationNativeIdentity, conversationNativeBinding } from "./runtimeStateUpgrade.js";
|
|
17
7
|
import { createServiceToolCatalog } from "../lib/serviceToolCatalog.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{ message: "Copied into a stopped development environment. This request was not replayed.",
|
|
64
|
-
messageCode: "assistant.conversation.copiedInterrupted" },
|
|
65
|
-
{ message: "The application operation result could not be saved. Restore storage access and retry saving.",
|
|
66
|
-
messageCode: "assistant.conversation.operationResultUnsaved" },
|
|
67
|
-
{ message: "Conversation progress could not be saved. Restore access to its storage before continuing.",
|
|
68
|
-
messageCode: "assistant.conversation.progressUnsaved" },
|
|
69
|
-
{ message: "The conversation request exceeded its time limit.", messageCode: "assistant.conversation.requestTimedOut" },
|
|
70
|
-
{ message: "Work stopped.", messageCode: "assistant.conversation.workStopped" },
|
|
71
|
-
{ message: "Stopped by user.", messageCode: "assistant.conversation.stoppedByUser" }
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
|
-
function stateErrorFields(error) {
|
|
75
|
-
const known = STATE_ERROR_MESSAGES.find(entry => entry.message === error);
|
|
76
|
-
return known ? { errorMessageCode: known.messageCode, errorMessageParams: {} } : {};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const submittedMessage = turn => turn.user || turn.system;
|
|
80
|
-
const retainedSegments = state => [...new Map([...state.predecessors, state].map(segment => [segment.segmentId, segment])).values()];
|
|
81
|
-
const pendingSuccessor = state => state.engine !== "api" && state.predecessors.findLast(segment => segment.successorId === state.segmentId &&
|
|
82
|
-
segment.segmentId !== state.segmentId && segment.replacement?.operation === "replace" && !segment.acceptedAt);
|
|
83
|
-
const sameRequest = (left, right) => left.text === right.text && (left.origin || "user") === right.origin &&
|
|
84
|
-
isDeepStrictEqual(left.goal, right.goal) && isDeepStrictEqual(left.data, right.data) &&
|
|
85
|
-
isDeepStrictEqual((left.attachments || []).map(file => file.attachmentId), right.attachments.map(file => file.attachmentId));
|
|
86
|
-
|
|
87
|
-
/** One application conversation API. Storage and authorization remain host facilities. */
|
|
88
|
-
export function createConversationRuntime({ engine: defaultEngine = "api", defaultIntegrationId, storage, authorize, connections, apiClientFactory, apiHistory,
|
|
89
|
-
actions, toolPolicy, toolCatalog, attachments, fetch, host: defaultHost, onDeliveryTiming, hostDeliveryStages, persistCommentary = true, completedEnvelope = false, limits = {} } = {}) {
|
|
8
|
+
import { NO_ATTACHMENTS } from "./attachments.js";
|
|
9
|
+
import { failure } from "./conversationRefusals.js";
|
|
10
|
+
import { createConversationEvents, serial } from "./conversationEvents.js";
|
|
11
|
+
import { createConversationReads } from "./conversationReads.js";
|
|
12
|
+
import { createConversationEntries } from "./conversationEntries.js";
|
|
13
|
+
import { createConversationDelivery } from "./conversationDelivery.js";
|
|
14
|
+
import { createCompletedResponses } from "./completedResponses.js";
|
|
15
|
+
import { createConversationTurnDelivery } from "./conversationTurnDelivery.js";
|
|
16
|
+
import { createConversationTurns } from "./conversationTurns.js";
|
|
17
|
+
import { createConversationSending } from "./conversationSending.js";
|
|
18
|
+
import { createConversationStopping } from "./conversationStopping.js";
|
|
19
|
+
import { createConversationGoals } from "./conversationGoals.js";
|
|
20
|
+
import { createConversationReplacement } from "./conversationReplacement.js";
|
|
21
|
+
import { createNativeConversationLifecycle } from "./nativeConversationLifecycle.js";
|
|
22
|
+
import { createConversationHandles } from "./conversationHandles.js";
|
|
23
|
+
|
|
24
|
+
/** The limits of an application that sets none: each takes its default. */
|
|
25
|
+
const DEFAULT_LIMITS = Object.freeze({});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One application conversation API. Storage and authorization remain host facilities.
|
|
29
|
+
* @param {object} options
|
|
30
|
+
* @param {string} [options.engine] The engine of a new conversation; "api" when not given.
|
|
31
|
+
* @param {string} [options.defaultIntegrationId] The AI connection a new API conversation uses.
|
|
32
|
+
* @param {import("../types.js").ConversationStorage} [options.storage] Required unless the host supplies its conversations.
|
|
33
|
+
* @param {import("../types.js").ConversationAuthorize} options.authorize
|
|
34
|
+
* @param {import("../types.js").ConversationConnections} [options.connections]
|
|
35
|
+
* @param {import("../types.js").ConversationApiClientFactory} [options.apiClientFactory]
|
|
36
|
+
* @param {import("../types.js").ConversationApiHistory} [options.apiHistory]
|
|
37
|
+
* @param {object} [options.actions] Application actions offered as tools, with toolPolicy.
|
|
38
|
+
* @param {(action: object, context: object) => boolean} [options.toolPolicy]
|
|
39
|
+
* @param {import("../types.js").ServiceToolCatalog} [options.toolCatalog] An existing tool catalogue, instead of actions.
|
|
40
|
+
* @param {import("../types.js").ConversationAttachments} [options.attachments]
|
|
41
|
+
* @param {typeof fetch} [options.fetch]
|
|
42
|
+
* @param {import("../types.js").ConversationHost} [options.host]
|
|
43
|
+
* @param {import("../types.js").ConversationDeliveryTimingObserver} [options.onDeliveryTiming]
|
|
44
|
+
* @param {readonly string[]} [options.hostDeliveryStages]
|
|
45
|
+
* @param {boolean} [options.persistCommentary]
|
|
46
|
+
* @param {boolean} [options.completedEnvelope]
|
|
47
|
+
* @param {import("../types.js").ConversationLimits} [options.limits]
|
|
48
|
+
*/
|
|
49
|
+
// readable-check: allow method-probe - input validation: the facilities an application hands the runtime are checked before it is built
|
|
50
|
+
export function createConversationRuntime({ engine: defaultEngine = "api", defaultIntegrationId, storage: suppliedStorage, authorize, connections,
|
|
51
|
+
apiClientFactory, apiHistory, actions, toolPolicy, toolCatalog, attachments: suppliedAttachments, fetch, host: defaultHost, onDeliveryTiming,
|
|
52
|
+
hostDeliveryStages, persistCommentary = true, completedEnvelope = false, limits = DEFAULT_LIMITS }) {
|
|
90
53
|
if (typeof authorize !== "function") throw new TypeError("Conversation access requires an authorize function.");
|
|
91
54
|
if (onDeliveryTiming !== undefined && typeof onDeliveryTiming !== "function") throw new TypeError("Delivery timing requires a server-owned observer.");
|
|
92
55
|
const deliveryHostStages = normalizeConversationDeliveryHostStages(hostDeliveryStages);
|
|
@@ -94,13 +57,14 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
94
57
|
if (typeof persistCommentary !== "boolean") throw new TypeError("Conversation commentary persistence requires an explicit server-side boolean.");
|
|
95
58
|
if (apiHistory !== undefined && typeof apiHistory !== "function") throw new TypeError("API request history requires a server-owned selector.");
|
|
96
59
|
if (toolCatalog && (actions || toolPolicy)) throw new TypeError("Supply the existing tool catalog or actions and toolPolicy, not both.");
|
|
97
|
-
|
|
60
|
+
// Whether the configured host prepares its own conversations, which then need no runtime storage.
|
|
61
|
+
const hostPrepares = Boolean(defaultHost?.conversation);
|
|
62
|
+
const storage = (suppliedStorage || !hostPrepares) ? createReentrantConversationStorage(suppliedStorage) : null;
|
|
98
63
|
const transcript = storage ? createConversationTranscript({ storage, applicationTurns: true }) : null;
|
|
99
64
|
const streams = createConversationStreams();
|
|
100
|
-
const entries = new Map();
|
|
101
65
|
let closed = false;
|
|
66
|
+
const isClosed = () => closed;
|
|
102
67
|
const providers = createConversationProviderFactory({ connections, apiClientFactory, fetch, limits, actions, toolCatalog });
|
|
103
|
-
const { createDriver } = providers;
|
|
104
68
|
const catalog = toolCatalog || (actions ? createServiceToolCatalog(actions, { ...limits, isActionAvailable: toolPolicy }) : null);
|
|
105
69
|
if (completedEnvelope && !catalog) throw new TypeError("Completed envelopes require an application tool catalogue.");
|
|
106
70
|
const maximumToolCalls = limits.maxToolCalls ?? 32;
|
|
@@ -119,2816 +83,160 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
119
83
|
if (!Number.isSafeInteger(maximumContinuity) || maximumContinuity < 1) throw new TypeError("Invalid conversation continuity limit.");
|
|
120
84
|
const maximumAttachmentBytes = limits.maxAttachmentBytes ?? 8 * 1024 * 1024;
|
|
121
85
|
if (!Number.isSafeInteger(maximumAttachmentBytes) || maximumAttachmentBytes < 1) throw new TypeError("Invalid conversation attachment byte limit.");
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
attachments: Boolean((attachments || entry.conversation?.prepareInput || entry.conversation?.commands) && entry.driver.capabilities.attachments) };
|
|
126
|
-
if (entry.conversation?.commands) {
|
|
127
|
-
delete value.goalCommands;
|
|
128
|
-
Object.assign(value, { goals: false, configuration: false, wake: false, replacement: false });
|
|
129
|
-
}
|
|
130
|
-
return value;
|
|
131
|
-
};
|
|
86
|
+
// readable-check: allow method-probe - input validation: an application's attachments must bring their resolve function
|
|
87
|
+
if (suppliedAttachments !== undefined && typeof suppliedAttachments?.resolve !== "function") throw new TypeError("Conversation attachments require an authorized resolve function.");
|
|
88
|
+
const attachments = suppliedAttachments ?? NO_ATTACHMENTS;
|
|
132
89
|
if (limits.timeoutMs !== undefined && (!Number.isSafeInteger(limits.timeoutMs) || limits.timeoutMs < 1)) {
|
|
133
90
|
throw new TypeError("Invalid conversation request time limit.");
|
|
134
91
|
}
|
|
135
92
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
async function access(context, id, operation, entry, grant) {
|
|
139
|
-
if (closed || entry?.disposed) throw failure("This conversation handle is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleClosed" });
|
|
140
|
-
if (!grant?.has(operation)) {
|
|
141
|
-
if (!await authorize({ context, conversationId: id, operation })) {
|
|
142
|
-
throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
|
|
143
|
-
}
|
|
144
|
-
grant?.add(operation);
|
|
145
|
-
}
|
|
146
|
-
if (closed || entry?.disposed) throw failure("This conversation handle is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleClosed" });
|
|
93
|
+
function assertRuntimeOpen() {
|
|
94
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_runtime_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
147
95
|
}
|
|
148
96
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
97
|
+
const events = createConversationEvents({ authorize, isClosed, storage, streams, completedEnvelope });
|
|
98
|
+
const reads = createConversationReads({ storage, transcript, streams, attachmentsOffered: suppliedAttachments !== undefined, completedEnvelope });
|
|
99
|
+
const entries = createConversationEntries({ storage, transcript, streams, providers, events, catalog, defaultEngine, defaultIntegrationId,
|
|
100
|
+
persistCommentary, completedEnvelope });
|
|
101
|
+
const delivery = createConversationDelivery({ storage, transcript, events, entries, reads, limits, persistCommentary, completedEnvelope,
|
|
102
|
+
maximumFinalReply, maximumCompletedOutput });
|
|
103
|
+
const completedResponses = createCompletedResponses({ storage, transcript, events, catalog, isClosed, completedEnvelope, maximumToolCalls });
|
|
104
|
+
const turnDelivery = createConversationTurnDelivery({ storage, streams, events, reads, delivery, attachments, persistCommentary, maximumContinuity,
|
|
105
|
+
maximumAttachmentBytes });
|
|
106
|
+
const turns = createConversationTurns({ storage, streams, events, entries, reads, delivery, turnDelivery, catalog, apiHistory, isClosed,
|
|
107
|
+
completedEnvelope, maximumToolCalls, maximumFinalReply, maximumCompletedOutput, timeoutMs: limits.timeoutMs });
|
|
108
|
+
const sending = createConversationSending({ storage, events, entries, reads, turns, onDeliveryTiming, deliveryHostStages, completedEnvelope,
|
|
109
|
+
maximumInput });
|
|
110
|
+
const stopping = createConversationStopping({ storage, entries, events, delivery });
|
|
111
|
+
const goals = createConversationGoals({ events, entries, reads, sending, stopping, catalog });
|
|
112
|
+
const replacement = createConversationReplacement({ storage, providers, events, entries, reads, delivery, completedEnvelope, defaultIntegrationId,
|
|
113
|
+
maximumContinuity });
|
|
114
|
+
const lifecycle = createNativeConversationLifecycle({ authorize, defaultHost, hostPrepares, providers, entries, events, stopping, assertRuntimeOpen });
|
|
115
|
+
const handles = createConversationHandles({ entries, storage, defaultHost, events, reads, sending, goals, delivery, stopping, replacement,
|
|
116
|
+
completedResponses, completedEnvelope, persistCommentary, defaultIntegrationId });
|
|
154
117
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (payload.type === "message" && ["assistant", "commentary", "thinking"].includes(payload.role) && payload.text?.trim()) {
|
|
158
|
-
// Bound live streams deliberately omit private native turn identities.
|
|
159
|
-
// Their current admitted request owns them; historical rows retain IDs.
|
|
160
|
-
let request;
|
|
161
|
-
if (payload.turnId) {
|
|
162
|
-
request = [...(entry.active?.requests.values() || [])].find(request =>
|
|
163
|
-
!request.sealed && (request.turnId === payload.turnId || request.nativeTurnId === payload.turnId));
|
|
164
|
-
} else if (entry.conversation) {
|
|
165
|
-
const current = entry.active?.request;
|
|
166
|
-
if (current?.accepted && !current.sealed) request = current;
|
|
167
|
-
}
|
|
168
|
-
request?.deliveryTiming?.("first-response");
|
|
169
|
-
}
|
|
170
|
-
if (completedEnvelope && !entry.conversation) {
|
|
171
|
-
const request = payload.messageId && entry.active?.requests.get(payload.messageId) ||
|
|
172
|
-
payload.turnId && [...(entry.active?.requests.values() || [])].find(request => request.turnId === payload.turnId);
|
|
173
|
-
let marked = isCompletedEnvelopeTurn(payload.patch?.turn) ||
|
|
174
|
-
request?.completedEnvelope === true && request.input.origin === "application";
|
|
175
|
-
if (!marked && payload.turnId) {
|
|
176
|
-
try { marked = await storage.read(entry.id, async transaction => isCompletedEnvelopeTurn(await transaction.readTurn(payload.turnId))); }
|
|
177
|
-
catch { /* An unavailable receipt cannot establish presentation custody. */ }
|
|
178
|
-
}
|
|
179
|
-
if (!marked && payload.type === "message") marked =
|
|
180
|
-
[...(payload.streaming?.messages || []), ...(payload.streaming?.completedMessages || [])]
|
|
181
|
-
.some(message => message.messageId === payload.messageId && message.completedEnvelope === true && message.origin === "application");
|
|
182
|
-
if (marked) payload.completedEnvelope = true;
|
|
183
|
-
}
|
|
184
|
-
await Promise.all([...entry.listeners].map(async subscription => {
|
|
185
|
-
try { await access(subscription.context, entry.id, "subscribe", entry); }
|
|
186
|
-
catch { entry.listeners.delete(subscription); return; }
|
|
187
|
-
if (isCurrent && !isCurrent()) return;
|
|
188
|
-
// Presentation failures cannot interrupt persistence or provider work.
|
|
189
|
-
try { Promise.resolve(subscription.listener(structuredClone({ conversationId: entry.id, ...payload }))).catch(() => {}); }
|
|
190
|
-
catch { /* A failed observer does not own the conversation. */ }
|
|
191
|
-
}));
|
|
192
|
-
}
|
|
118
|
+
// Persistent and scoped conversations run under the host's own admission and write lease. These
|
|
119
|
+
// operations share the scoped owner and open no entry or queue here.
|
|
193
120
|
|
|
194
|
-
async function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
entry.phase = phase;
|
|
198
|
-
await emit(entry, { type: "phase", phase, turnId });
|
|
121
|
+
async function projectStoredPersistentConversation(options) {
|
|
122
|
+
assertRuntimeOpen();
|
|
123
|
+
return scopedConversations.projectStoredPersistentConversation(options);
|
|
199
124
|
}
|
|
200
125
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// Identity lookups avoid reading the whole log when the transcript supports
|
|
205
|
-
// them. A bound host may supply findSubmittedTurn(messageId) and readTurn(turnId).
|
|
206
|
-
async function findSubmittedTurn(entry, messageId) {
|
|
207
|
-
const find = entry.conversation ? entry.conversation.transcript.findSubmittedTurn : transcript.findSubmittedTurn &&
|
|
208
|
-
(id => transcript.findSubmittedTurn(entry.id, id));
|
|
209
|
-
if (typeof find === "function") return (await find(messageId)) || null;
|
|
210
|
-
return (await readTranscript(entry)).find(turn => submittedMessage(turn)?.messageId === messageId) || null;
|
|
126
|
+
async function readPersistentConversation(options) {
|
|
127
|
+
assertRuntimeOpen();
|
|
128
|
+
return scopedConversations.readPersistentConversation(options);
|
|
211
129
|
}
|
|
212
130
|
|
|
213
|
-
async function
|
|
214
|
-
|
|
215
|
-
return
|
|
131
|
+
async function startPersistentConversationTurn(options) {
|
|
132
|
+
assertRuntimeOpen();
|
|
133
|
+
return scopedConversations.startPersistentConversationTurn(options);
|
|
216
134
|
}
|
|
217
135
|
|
|
218
|
-
async function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
: await transcript.readConversationLogPage(entry.id, { limit: 1 });
|
|
222
|
-
return page?.conversationLog.at(-1) || (await readTranscript(entry)).at(-1);
|
|
136
|
+
async function inspectPersistentConversationAdmission(options) {
|
|
137
|
+
assertRuntimeOpen();
|
|
138
|
+
return scopedConversations.inspectPersistentConversationAdmission(options);
|
|
223
139
|
}
|
|
224
140
|
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
throw failure("This conversation does not supply paged history.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.pagedHistoryUnsupported" });
|
|
229
|
-
}
|
|
230
|
-
return entry.conversation.transcript.readConversationLogPage(query);
|
|
141
|
+
async function preparePersistentConversationChangeover(options) {
|
|
142
|
+
assertRuntimeOpen();
|
|
143
|
+
return scopedConversations.preparePersistentConversationChangeover(options);
|
|
231
144
|
}
|
|
232
145
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (!entry.conversation) return (await storage.read(entry.id, transaction => transaction.readMetadata())).runtime;
|
|
237
|
-
const value = entry.conversation.native && entry.driver.readState
|
|
238
|
-
? await entry.driver.readState({ conversation: entry.conversation, context })
|
|
239
|
-
: await entry.conversation.read(context);
|
|
240
|
-
entry.segmentId = value.threadId ? `${entry.engine}:${value.threadId}` : null;
|
|
241
|
-
const saved = value.delivery?.engines[entry.engine]?.pending;
|
|
242
|
-
// The saved receipt is the delivery proof, as in changeover recovery. A
|
|
243
|
-
// restart after native admission leaves only a stale attempt marker, which
|
|
244
|
-
// must not leave the conversation unconfirmed until another message is sent.
|
|
245
|
-
const pending = (saved?.attempted && !saved.inspectionOnly &&
|
|
246
|
-
await entry.conversation.transcript.hasMessage(saved.messageId)) ? null : saved;
|
|
247
|
-
return { engine: entry.engine, configuration: value.configuration, segmentId: entry.segmentId,
|
|
248
|
-
binding: { ...conversationNativeBinding(entry.engine, value.threadId),
|
|
249
|
-
...(entry.engine === "codex" ? { codexAppServerRun: value.run } : {}) },
|
|
250
|
-
nativeTurn: value.turn, nativeResult: value.nativeResult,
|
|
251
|
-
lastEngine: value.delivery?.lastEngine ?? entry.engine,
|
|
252
|
-
replacement: ["preparing", "ready"].includes(value.delivery?.replacement?.status) ? value.delivery.replacement : null,
|
|
253
|
-
request: pending && { ...pending, text: pending.displayMessage, attachments: pending.displayAttachments || [] } };
|
|
146
|
+
async function inspectPersistentConversationDelivery(options) {
|
|
147
|
+
assertRuntimeOpen();
|
|
148
|
+
return scopedConversations.inspectPersistentConversationDelivery(options);
|
|
254
149
|
}
|
|
255
150
|
|
|
256
|
-
async function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const value = entry.driver.readState
|
|
260
|
-
? await entry.driver.readState({ conversation: entry.conversation, context, representation })
|
|
261
|
-
: await entry.conversation.read(context, representation);
|
|
262
|
-
return value.nativeResult;
|
|
263
|
-
}
|
|
264
|
-
const logical = entry.conversation?.commands ? await entry.conversation.read(context, query) : null;
|
|
265
|
-
if (logical && logical.engine !== entry.engine) {
|
|
266
|
-
entry.engine = logical.engine;
|
|
267
|
-
entry.driver = createDriver(entry.engine, entry.host);
|
|
268
|
-
}
|
|
269
|
-
const metadata = { runtime: logical ? { configuration: logical.configuration,
|
|
270
|
-
segmentId: logical.threadId ? `${logical.engine}:${logical.threadId}` : null } : await readState(entry, context) };
|
|
271
|
-
if (representation === "native") return metadata.runtime.nativeResult;
|
|
272
|
-
const page = logical ? logical.pagination ? { conversationLog: logical.conversationLog, pagination: logical.pagination } : null
|
|
273
|
-
: query ? await readTranscriptPage(entry, query) : null;
|
|
274
|
-
const conversationLog = logical ? logical.conversationLog : page ? page.conversationLog
|
|
275
|
-
: completedEnvelope ? await transcript.readConversationLog(entry.id, { presentation: true }) : await readTranscript(entry);
|
|
276
|
-
// Browsing an older page does not change the current turn's control/error state.
|
|
277
|
-
const latest = !logical && (completedEnvelope || query?.beforeTurnId)
|
|
278
|
-
? entry.conversation ? (await readTranscriptPage(entry, { limit: 1 })).conversationLog
|
|
279
|
-
: (await transcript.readConversationLogPage(entry.id, { limit: 1 })).conversationLog : conversationLog;
|
|
280
|
-
const last = latest.at(-1)?.metadata?.runtime;
|
|
281
|
-
const savedRequest = metadata.runtime.request;
|
|
282
|
-
const nativeTurn = metadata.runtime.nativeTurn || (entry.driver.canonicalTranscript && metadata.runtime.binding?.codexAppServerRun
|
|
283
|
-
? codexAppServerTurnStateFromAgentRun(metadata.runtime.binding.codexAppServerRun) : null);
|
|
284
|
-
const request = savedRequest && (entry.engine === "api" || savedRequest.attempted || savedRequest.inspectionOnly ||
|
|
285
|
-
entry.active?.requests.has(savedRequest.messageId)) ? savedRequest : null;
|
|
286
|
-
const streaming = entry.conversation ? await entry.conversation.readStream() : streams.read(entry.id);
|
|
287
|
-
const error = logical ? logical.error || "" : entry.error || nativeTurn?.error || request?.error || last?.error || "";
|
|
288
|
-
const pendingRequest = request && !(completedEnvelope && request.origin === "application" && request.completedEnvelope === true) ? Object.fromEntries([
|
|
289
|
-
"messageId", "text", "origin", "data", "attachments", "at", "goal", "steering", "error"
|
|
290
|
-
].filter(name => Object.hasOwn(request, name)).map(name => [name, request[name]])) : null;
|
|
291
|
-
if (pendingRequest?.error) Object.assign(pendingRequest, stateErrorFields(pendingRequest.error));
|
|
292
|
-
return {
|
|
293
|
-
id: entry.id, engine: entry.engine, capabilities: capabilities(entry), configuration: metadata.runtime.configuration,
|
|
294
|
-
segmentId: metadata.runtime.segmentId,
|
|
295
|
-
replacement: metadata.runtime.replacement ? { operationId: metadata.runtime.replacement.request?.operationId || metadata.runtime.replacement.operationId,
|
|
296
|
-
reason: metadata.runtime.replacement.reason, segmentId: metadata.runtime.replacement.segmentId } : null,
|
|
297
|
-
status: logical ? logical.status : entry.storageFailure || entry.executionFailure ? "unavailable" : entry.active || nativeTurn?.active ? "working" : metadata.runtime.replacement ? "replacement-pending"
|
|
298
|
-
: metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly) ? "unconfirmed" : "ready",
|
|
299
|
-
phase: logical ? logical.phase || "" : entry.phase || (nativeTurn?.active && nativeTurn.status !== "observation_lost" ? ["compacting", "background"].includes(nativeTurn.phase) ? nativeTurn.phase : "working" : ""),
|
|
300
|
-
error,
|
|
301
|
-
...stateErrorFields(error),
|
|
302
|
-
goal: entry.goal || null,
|
|
303
|
-
pendingRequest: logical ? logical.pendingRequest || null : pendingRequest,
|
|
304
|
-
...(logical?.presentation ? { presentation: logical.presentation } : {}),
|
|
305
|
-
conversationLog: conversationLog.filter(turn => !turn.metadata?.runtime?.supersededBy),
|
|
306
|
-
...(page ? { pagination: page.pagination } : {}),
|
|
307
|
-
streaming: completedEnvelope && !entry.conversation ? { ...streaming,
|
|
308
|
-
messages: streaming.messages.filter(message => !(message.origin === "application" && message.completedEnvelope === true)) } : streaming
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function publish(entry, event) {
|
|
313
|
-
if (entry.disposed) return;
|
|
314
|
-
const publicationOwner = entry.bindingOwner;
|
|
315
|
-
const publicationSegment = entry.segmentId;
|
|
316
|
-
const publicationActive = entry.active;
|
|
317
|
-
// A native-backed bound conversation also uses configuration as a browser
|
|
318
|
-
// invalidation. It has no command facade, but owns the same subscriptions.
|
|
319
|
-
if (entry.conversation && (event.type === "configuration" || entry.conversation.commands && event.type === "phase")) {
|
|
320
|
-
return emit(entry, event);
|
|
321
|
-
}
|
|
322
|
-
const payload = event.payload || {};
|
|
323
|
-
if (Object.hasOwn(payload, "goal")) {
|
|
324
|
-
entry.goal = payload.goal;
|
|
325
|
-
await emit(entry, { type: "goal", goal: entry.goal });
|
|
326
|
-
} else if (event.goalChanged) {
|
|
327
|
-
await emit(entry, { type: "goal" });
|
|
328
|
-
}
|
|
329
|
-
const turn = payload.conversationLogPatch?.turn;
|
|
330
|
-
if (turn) {
|
|
331
|
-
await emit(entry, { type: "transcript", patch: payload.conversationLogPatch,
|
|
332
|
-
...(payload.conversationStream ? { streaming: payload.conversationStream } : {}) });
|
|
333
|
-
const authored = submittedMessage(turn);
|
|
334
|
-
const request = authored && entry.active?.requests.get(authored.messageId);
|
|
335
|
-
// The original receipt already owns this row. Common admission is
|
|
336
|
-
// still resolved by the dispatch continuation outside its queue.
|
|
337
|
-
if (request) request.turnId = turn.turnId;
|
|
338
|
-
for (const message of turn.messages || []) {
|
|
339
|
-
if (!["assistant", "commentary", "thinking"].includes(message.role)) continue;
|
|
340
|
-
await emit(entry, { type: "message", turnId: turn.turnId, messageId: message.messageId,
|
|
341
|
-
...(message.outputId ? { outputId: message.outputId } : {}),
|
|
342
|
-
...(["user", "application"].includes(turn.metadata?.runtime?.origin) ? { origin: turn.metadata.runtime.origin } : {}),
|
|
343
|
-
role: message.role, text: message.text, at: message.at, status: "complete", persisted: true,
|
|
344
|
-
streaming: payload.conversationStream || streams.read(entry.id) });
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
if (payload.conversationStream) {
|
|
348
|
-
const snapshot = payload.conversationStream;
|
|
349
|
-
for (const message of snapshot.completedMessages || []) {
|
|
350
|
-
if (turn?.messages?.some(saved => saved.messageId === message.messageId ||
|
|
351
|
-
(message.outputId && saved.outputId === message.outputId))) continue;
|
|
352
|
-
const request = publicationActive && [...publicationActive.requests.values()].find(request => request.turnId === message.turnId);
|
|
353
|
-
const isCurrent = () => !entry.disposed && !request?.sealed && entry.active === publicationActive &&
|
|
354
|
-
entry.bindingOwner === publicationOwner && entry.segmentId === publicationSegment;
|
|
355
|
-
if (!request || !isCurrent()) continue;
|
|
356
|
-
if (!request.admissionPublished) {
|
|
357
|
-
request.progress.set(message.messageId, message);
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
await emit(entry, { type: "message", ...message, streaming: snapshot }, isCurrent);
|
|
361
|
-
}
|
|
362
|
-
for (const message of snapshot.messages) {
|
|
363
|
-
await emit(entry, { type: "message", ...message, streaming: snapshot });
|
|
364
|
-
}
|
|
365
|
-
if (!snapshot.messages.length) await emit(entry, { type: "message-complete", streaming: snapshot });
|
|
366
|
-
}
|
|
367
|
-
const nativeTurn = payload.agentSession?.turn;
|
|
368
|
-
if (nativeTurn) {
|
|
369
|
-
entry.nativePhase = nativeTurn.active
|
|
370
|
-
? ["preparing", "compacting", "retrying", "background"].includes(nativeTurn.phase) ? nativeTurn.phase : "working"
|
|
371
|
-
: "";
|
|
372
|
-
const request = entry.active?.request;
|
|
373
|
-
if (!entry.active || request.accepted) {
|
|
374
|
-
if (request?.deliveryCommitted) {
|
|
375
|
-
try { await request.deliveryCommitted; }
|
|
376
|
-
catch { return; } // Admission owns this failure; native cleanup must still settle.
|
|
377
|
-
}
|
|
378
|
-
await setPhase(entry, entry.nativePhase, request?.turnId);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async function openProvider(entry, driver, binding, segmentId, owner = (entry.bindingOwner = {})) {
|
|
384
|
-
const isCurrent = () => entry.bindingOwner === owner && (entry.conversation || entry.segmentId === segmentId);
|
|
385
|
-
let conversation;
|
|
386
|
-
if (entry.conversation || driver.canonicalTranscript) {
|
|
387
|
-
const store = entry.conversation ? null : createCodexConversationStore({ storage, scope: entry.id, segmentId, isCurrent, transcript, streams });
|
|
388
|
-
conversation = {
|
|
389
|
-
...entry.conversation,
|
|
390
|
-
sessionId: entry.id,
|
|
391
|
-
persistCommentary,
|
|
392
|
-
runtime: entry.conversation?.runtime || { store, getSession: store.getSession },
|
|
393
|
-
publish: event => isCurrent() ? publish(entry, event) : undefined,
|
|
394
|
-
checkpoint: entry.conversation?.checkpoint || (input => isCurrent() ? entry.active?.nativeCheckpoint?.(input) : null)
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
return providers.open(driver, {
|
|
398
|
-
binding,
|
|
399
|
-
conversation,
|
|
400
|
-
async onFailure(error, { recovered = false } = {}) {
|
|
401
|
-
if (!isCurrent()) return;
|
|
402
|
-
if (recovered && entry.executionFailure !== error) return;
|
|
403
|
-
entry.executionFailure = recovered ? null : error;
|
|
404
|
-
entry.error = error.message;
|
|
405
|
-
await emit(entry, { type: "error", error: error.message, ...stateErrorFields(error.message) });
|
|
406
|
-
},
|
|
407
|
-
async writeBinding(value) {
|
|
408
|
-
if (entry.conversation) throw new Error("A bound native conversation writes identity through its original preparation owner.");
|
|
409
|
-
if (entry.bindingOwner !== owner) throw new Error("A retired provider cannot change a native binding.");
|
|
410
|
-
await storage.write(entry.id, async transaction => {
|
|
411
|
-
const metadata = await transaction.readMetadata();
|
|
412
|
-
if (entry.bindingOwner !== owner || metadata.runtime.segmentId !== segmentId) throw new Error("A retired native conversation cannot change the current binding.");
|
|
413
|
-
if (metadata.runtime.engine === "codex") {
|
|
414
|
-
const { codexAppServerRun: _staleRun, ...next } = value;
|
|
415
|
-
if (Object.hasOwn(metadata.runtime.binding, "codexAppServerRun")) {
|
|
416
|
-
next.codexAppServerRun = metadata.runtime.binding.codexAppServerRun;
|
|
417
|
-
}
|
|
418
|
-
metadata.runtime.binding = next;
|
|
419
|
-
} else metadata.runtime.binding = value;
|
|
420
|
-
await transaction.writeMetadata(metadata);
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
});
|
|
151
|
+
async function stopPersistentConversation(options) {
|
|
152
|
+
assertRuntimeOpen();
|
|
153
|
+
return scopedConversations.stopPersistentConversation(options);
|
|
424
154
|
}
|
|
425
155
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
function changeoverState(entry, request, transaction) {
|
|
430
|
-
const readMetadata = () => transaction ? transaction.readMetadata()
|
|
431
|
-
: storage.read(entry.id, current => current.readMetadata());
|
|
432
|
-
const write = callback => transaction ? callback(transaction) : storage.write(entry.id, callback);
|
|
433
|
-
return {
|
|
434
|
-
async read() {
|
|
435
|
-
const { runtime } = await readMetadata();
|
|
436
|
-
const engines = Object.fromEntries(retainedSegments(runtime).filter(segment => segment.engine !== "api").map(segment => {
|
|
437
|
-
const pending = segment.request;
|
|
438
|
-
return [segment.engine, { seen: structuredClone(segment.seen),
|
|
439
|
-
...(typeof pending?.message === "string" || pending?.inspectionOnly ? { pending: structuredClone({ ...pending,
|
|
440
|
-
...(pending.inspectionOnly ? { attempted: true, displayMessage: pending.text, displayAttachments: pending.attachments } : {}) }) } : {}) }];
|
|
441
|
-
}));
|
|
442
|
-
const predecessor = pendingSuccessor(runtime);
|
|
443
|
-
const preparing = runtime.replacement?.request?.operation === "replace" ? runtime.replacement : null;
|
|
444
|
-
return { lastEngine: runtime.lastEngine, engines,
|
|
445
|
-
...(preparing ? { replacement: { engineId: preparing.engine, status: "preparing",
|
|
446
|
-
operationId: preparing.request.operationId, handover: preparing.request.briefing || "",
|
|
447
|
-
bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: conversationNativeIdentity(runtime) },
|
|
448
|
-
...(preparing.preparedAt ? { preparedAt: preparing.preparedAt } : {}) } }
|
|
449
|
-
: predecessor ? { replacement: { engineId: runtime.engine, status: "ready",
|
|
450
|
-
operationId: predecessor.replacement.operationId, handover: predecessor.replacement.briefing || "",
|
|
451
|
-
previous: { engineId: predecessor.engine, conversationId: conversationNativeIdentity(predecessor) },
|
|
452
|
-
...(predecessor.preparedAt ? { preparedAt: predecessor.preparedAt } : {}) } } : {}) };
|
|
453
|
-
},
|
|
454
|
-
write(value) {
|
|
455
|
-
return write(async current => {
|
|
456
|
-
const metadata = await current.readMetadata();
|
|
457
|
-
const runtime = metadata.runtime;
|
|
458
|
-
if (runtime.segmentId !== entry.segmentId || runtime.engine !== entry.engine) throw new Error("The native conversation changed before its delivery was saved.");
|
|
459
|
-
const binding = value.engines[entry.engine];
|
|
460
|
-
if (binding.seen !== undefined) runtime.seen = binding.seen;
|
|
461
|
-
else if (!runtime.request?.inspectionOnly) throw new Error("Native delivery has no saved history cursor.");
|
|
462
|
-
// A pre-preparation v2 reservation has no snapshot. Its confirmed
|
|
463
|
-
// receipt settles only that request; it cannot acknowledge extra history.
|
|
464
|
-
runtime.lastEngine = value.lastEngine;
|
|
465
|
-
if (binding.pending) {
|
|
466
|
-
const authored = runtime.request?.messageId === binding.pending.messageId ? runtime.request
|
|
467
|
-
: request?.input.messageId === binding.pending.messageId ? { ...request.input, at: request.at } : null;
|
|
468
|
-
if (!authored) throw new Error("A native prompt has no matching authored request.");
|
|
469
|
-
// Inspection-only predecessors retain exactly their old authored
|
|
470
|
-
// record. Their absent prompt must never be manufactured on read.
|
|
471
|
-
runtime.request = authored.inspectionOnly ? authored : { ...authored, ...binding.pending };
|
|
472
|
-
} else if (runtime.request && (typeof runtime.request.message === "string" || runtime.request.inspectionOnly ||
|
|
473
|
-
runtime.request.messageId === request?.input.messageId)) {
|
|
474
|
-
delete runtime.request;
|
|
475
|
-
}
|
|
476
|
-
const predecessor = pendingSuccessor(runtime);
|
|
477
|
-
if (predecessor && value.replacement?.status === "accepted") {
|
|
478
|
-
predecessor.acceptedAt = value.replacement.acceptedAt;
|
|
479
|
-
predecessor.successorConversationId = value.replacement.successorConversationId;
|
|
480
|
-
delete runtime.continuity;
|
|
481
|
-
delete runtime.continuityAttachments;
|
|
482
|
-
}
|
|
483
|
-
await current.writeMetadata(metadata);
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
};
|
|
156
|
+
async function deletePersistentConversation(options) {
|
|
157
|
+
assertRuntimeOpen();
|
|
158
|
+
return scopedConversations.deletePersistentConversation(options);
|
|
487
159
|
}
|
|
488
160
|
|
|
489
|
-
function
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
throw new TypeError("
|
|
161
|
+
async function runScopedTurn(options) {
|
|
162
|
+
assertRuntimeOpen();
|
|
163
|
+
if (["createEphemeralConversation", "startEphemeralConversationTurn", "waitForEphemeralConversationTurn", "stopEphemeralConversation"]
|
|
164
|
+
// readable-check: allow method-probe - input validation: a scoped turn's caller passes its admitted conversation operations
|
|
165
|
+
.some(name => typeof options.operations?.[name] !== "function")) {
|
|
166
|
+
throw new TypeError("A scoped turn requires its existing admitted conversation operations.");
|
|
495
167
|
}
|
|
496
|
-
return
|
|
497
|
-
state: entry.conversation?.state || changeoverState(entry, request),
|
|
498
|
-
captureContext: !entry.conversation, applicationMessages: !entry.conversation,
|
|
499
|
-
maximumInitialMessages: initialWindow === undefined ? (budget ? Infinity : undefined)
|
|
500
|
-
: Math.max(0, initialWindow - (request?.input.origin === "user" ? 1 : 0)),
|
|
501
|
-
maximumInitialMessageCharacters: limits.maxInitialNativeHistoryMessageCharacters,
|
|
502
|
-
...(budget ? { maximumHistoryBytes: async options => {
|
|
503
|
-
try {
|
|
504
|
-
await access(context, entry.id, request.operation, entry);
|
|
505
|
-
signal.throwIfAborted();
|
|
506
|
-
// A host may free native context first; show that as compacting.
|
|
507
|
-
const bytes = await budget({ ...options, context, signal,
|
|
508
|
-
setPhase: phase => setPhase(entry, phase === "compacting" ? "compacting" : "preparing") });
|
|
509
|
-
signal.throwIfAborted();
|
|
510
|
-
return bytes;
|
|
511
|
-
} catch (error) {
|
|
512
|
-
// Sizing missed history precedes native dispatch, so this message
|
|
513
|
-
// was definitely not sent. Hosts and clients report it as such.
|
|
514
|
-
throw Object.assign(error, { delivery: "not-sent" });
|
|
515
|
-
}
|
|
516
|
-
} } : {}),
|
|
517
|
-
presentation: entry.conversation?.presentation || { unconfirmedCode: "conversation_delivery_uncertain",
|
|
518
|
-
unconfirmedMessage: "The previous submission has uncertain delivery. Retry this message to check its native receipt; it will not be resent automatically." },
|
|
519
|
-
transcript: entry.conversation?.transcript || {
|
|
520
|
-
hasMessage: messageId => transcript.conversationMessageIdExists(entry.id, messageId),
|
|
521
|
-
async writeUserMessage({ messageId }) {
|
|
522
|
-
if (observation) await recoverDelivery(entry, context, messageId, { observation, allowActive: true });
|
|
523
|
-
}
|
|
524
|
-
},
|
|
525
|
-
agent: {
|
|
526
|
-
sendMessage,
|
|
527
|
-
async inspectMessageAdmission({ messageId, threadId }) {
|
|
528
|
-
if (entry.conversation) {
|
|
529
|
-
observation = await entry.provider.inspectAdmission({ messageId, threadId, context });
|
|
530
|
-
await access(context, entry.id, request?.operation || "inspectDelivery", entry);
|
|
531
|
-
return { admission: observation.accepted ? "accepted" : "unknown" };
|
|
532
|
-
}
|
|
533
|
-
const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
|
|
534
|
-
if (!threadId || threadId !== conversationNativeIdentity(runtime) || runtime.request?.messageId !== messageId) return null;
|
|
535
|
-
observation = await entry.provider.inspectAdmission({ ...runtime.request, context, configuration: runtime.configuration });
|
|
536
|
-
await access(context, entry.id, request?.operation || "inspectDelivery", entry);
|
|
537
|
-
return { admission: observation.accepted ? "accepted" : "unknown" };
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
});
|
|
168
|
+
return scopedConversations.runScopedConversationTurn(options);
|
|
541
169
|
}
|
|
542
170
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (entry.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (requestedEngine && requestedEngine !== entry.engine) throw failure("The selected native engine changed.", "conversation_engine_mismatch", 409, { messageCode: "assistant.conversation.engineChanged" });
|
|
553
|
-
entry.driver = createDriver(entry.engine, entry.host);
|
|
554
|
-
if (entry.conversation.commands) return;
|
|
555
|
-
if (entry.conversation.native?.scoped) {
|
|
556
|
-
entry.provider = await providers.open(entry.driver, { conversation: entry.conversation });
|
|
557
|
-
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleDisposed" });
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
const state = await readState(entry);
|
|
561
|
-
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleDisposed" });
|
|
562
|
-
entry.provider = await openProvider(entry, entry.driver, state.binding, state.segmentId);
|
|
563
|
-
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleDisposed" });
|
|
564
|
-
return;
|
|
171
|
+
// Trusted server lifecycle: the application deleted or archived a logical
|
|
172
|
+
// conversation that it binds through its own commands. Its open entry, if
|
|
173
|
+
// any, leaves memory and its handles close. Nothing is opened; native and
|
|
174
|
+
// storage-owned conversations keep their own disposal.
|
|
175
|
+
async function retireOpenConversation({ namespace, sessionId }) {
|
|
176
|
+
const entry = entries.find(namespace);
|
|
177
|
+
if (!entry || entry.id !== sessionId) return { retired: false };
|
|
178
|
+
if (!entry.conversation?.commands) {
|
|
179
|
+
throw new TypeError("Only a logical conversation bound through its commands retires without disposal.");
|
|
565
180
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
entry.engine = metadata.runtime?.engine || requestedEngine || defaultEngine;
|
|
572
|
-
if (completedEnvelope && !["codex", "claude", "opencode"].includes(entry.engine)) {
|
|
573
|
-
throw failure("Completed envelopes require a runtime-owned native conversation.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.completedEnvelopesNeedNativeOwner" });
|
|
574
|
-
}
|
|
575
|
-
if (configuration) configuration = normalizeConversationConfiguration(configuration, { engine: entry.engine, defaultIntegrationId });
|
|
576
|
-
const driver = entry.driver = createDriver(entry.engine, entry.host);
|
|
577
|
-
if (metadata.runtime) {
|
|
578
|
-
if (metadata.runtime.version !== 3 || typeof metadata.runtime.lastEngine !== "string" || typeof metadata.runtime.segmentId !== "string" || !metadata.runtime.segmentId || !Array.isArray(metadata.runtime.predecessors)) {
|
|
579
|
-
throw failure("This conversation needs its offline runtime-state upgrade before it can be opened. Its saved requests were not replayed or changed.", "conversation_storage_version");
|
|
580
|
-
}
|
|
581
|
-
if (hasUnfinishedConversationRewind(metadata.runtime)) {
|
|
582
|
-
throw failure("This conversation has an unfinished Undo operation from an earlier runtime. Inspect its saved native and local history offline before opening it; no history was changed.", "conversation_rewind_inspection_required", 409, { messageCode: "assistant.conversation.rewindInspectionBeforeOpen" });
|
|
583
|
-
}
|
|
584
|
-
if (requestedEngine && requestedEngine !== entry.engine) throw failure("Use replace() to change this conversation's engine.", "conversation_engine_mismatch", 409, { messageCode: "assistant.conversation.useReplaceForEngine" });
|
|
585
|
-
driver.validateConfiguration(metadata.runtime.configuration);
|
|
586
|
-
if (configuration && !isDeepStrictEqual(configuration, metadata.runtime.configuration)) {
|
|
587
|
-
throw failure("Use configure() to change a saved conversation's settings.", "conversation_configuration_mismatch");
|
|
588
|
-
}
|
|
589
|
-
// Stored work is never permission to replay it after application restart.
|
|
590
|
-
if (metadata.runtime.request && driver.admissionBeforeDispatch) {
|
|
591
|
-
// API requests are dispatched only after the accepted user message and
|
|
592
|
-
// receipt are committed together. A remaining reservation was not sent.
|
|
593
|
-
delete metadata.runtime.request;
|
|
594
|
-
} else if (metadata.runtime.request) {
|
|
595
|
-
metadata.runtime.request.error = "The previous submission was interrupted. Check delivery before submitting it again.";
|
|
596
|
-
}
|
|
597
|
-
for (const id of await transaction.listTurnIds()) {
|
|
598
|
-
const turn = await transaction.readTurn(id);
|
|
599
|
-
if (turn.metadata?.runtime?.status === "running") {
|
|
600
|
-
await transaction.updateTurnMetadata(id, { runtime: { ...turn.metadata.runtime,
|
|
601
|
-
status: "interrupted", error: "The application restarted before this answer completed. The request was not replayed." } });
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
} else {
|
|
605
|
-
driver.validateConfiguration(configuration);
|
|
606
|
-
metadata.runtime = { version: 3, segmentId: randomUUID(), engine: entry.engine, configuration: structuredClone(configuration), predecessors: [], seen: {},
|
|
607
|
-
lastEngine: (await transaction.listTurnIds()).length ? "" : entry.engine };
|
|
608
|
-
if (driver.createBinding) metadata.runtime.binding = await driver.createBinding();
|
|
609
|
-
}
|
|
610
|
-
await transaction.writeMetadata(metadata);
|
|
611
|
-
entry.segmentId = metadata.runtime.segmentId;
|
|
612
|
-
return metadata.runtime.binding;
|
|
181
|
+
await entry.initializing.catch(() => {});
|
|
182
|
+
return serial(entry, async () => {
|
|
183
|
+
if (entry.disposed || entries.find(namespace) !== entry) return { retired: false };
|
|
184
|
+
entries.retire(entry);
|
|
185
|
+
return { retired: true };
|
|
613
186
|
});
|
|
614
|
-
entry.provider = await openProvider(entry, entry.driver, binding, entry.segmentId);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function boundApplicationTools(conversation) {
|
|
618
|
-
const facilities = conversation.applicationTools;
|
|
619
|
-
if (facilities === undefined) return null;
|
|
620
|
-
if (!catalog || conversation.commands || conversation.native?.scoped || typeof facilities?.prepareContext !== "function") {
|
|
621
|
-
throw new TypeError("Bound application tools require a catalogue, an admitted conversation and a server-owned context mapper.");
|
|
622
|
-
}
|
|
623
|
-
return { storage: createReentrantConversationStorage(facilities.storage), prepareContext: facilities.prepareContext };
|
|
624
187
|
}
|
|
625
188
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
await transaction.updateTurnMetadata(request.turnId, { applicationTools: calls });
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
async function prepareCompletedResponse(entry, context, { messageId, turnId } = {}, { signal, toolSet } = {}) {
|
|
638
|
-
if (!completedEnvelope || entry.conversation || !["codex", "claude", "opencode"].includes(entry.engine)) {
|
|
639
|
-
throw failure("This conversation does not own completed envelopes.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.completedEnvelopesNotOwned" });
|
|
640
|
-
}
|
|
641
|
-
if (typeof messageId !== "string" || !messageId || typeof turnId !== "string" || !turnId || !(signal instanceof AbortSignal)) {
|
|
642
|
-
throw new TypeError("Completed responses require their exact receipt and the application worker signal.");
|
|
643
|
-
}
|
|
644
|
-
signal.throwIfAborted();
|
|
645
|
-
await access(context, entry.id, "tool", entry);
|
|
646
|
-
const owner = entry.bindingOwner;
|
|
647
|
-
const segmentId = entry.segmentId;
|
|
648
|
-
const engine = entry.engine;
|
|
649
|
-
const toolCallId = `${messageId}:operation`;
|
|
650
|
-
if (toolCallId.length > 256) throw new TypeError("The completed response call identity exceeds its limit.");
|
|
651
|
-
let captured;
|
|
652
|
-
|
|
653
|
-
async function exact(transaction, { retry = false } = {}) {
|
|
654
|
-
if (closed || entry.disposed || entry.bindingOwner !== owner || entry.segmentId !== segmentId || entry.engine !== engine) {
|
|
655
|
-
throw failure("The completed response owner changed.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.responseOwnerChanged" });
|
|
656
|
-
}
|
|
657
|
-
if (entry.active) throw failure("Wait for the native response to finish.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForNativeResponse" });
|
|
658
|
-
if (entry.executionFailure) throw entry.executionFailure;
|
|
659
|
-
if (entry.storageFailure && !retry) throw entry.storageFailure;
|
|
660
|
-
const { runtime } = await transaction.readMetadata();
|
|
661
|
-
const turn = await transaction.readTurn(turnId);
|
|
662
|
-
const authored = submittedMessage(turn || {});
|
|
663
|
-
const saved = turn?.metadata?.runtime;
|
|
664
|
-
if (runtime?.version !== 3 || runtime.engine !== engine || runtime.segmentId !== segmentId ||
|
|
665
|
-
runtime.replacement || runtime.request || !conversationNativeIdentity(runtime) || authored?.messageId !== messageId ||
|
|
666
|
-
authored.origin !== "application" || saved?.completedEnvelope !== true || saved.engine !== engine ||
|
|
667
|
-
saved.segmentId !== segmentId || saved.status !== "complete" || saved.supersededBy ||
|
|
668
|
-
typeof turn.assistant?.text !== "string" || !turn.assistant.text || !turn.assistant.messageId) {
|
|
669
|
-
throw failure("The completed response has no current verified receipt.", "conversation_completed_response_unavailable", 409, { messageCode: "assistant.conversation.completedResponseNoReceipt" });
|
|
670
|
-
}
|
|
671
|
-
const turnIds = await transaction.listTurnIds();
|
|
672
|
-
for (const id of turnIds.slice(turnIds.indexOf(turnId) + 1)) {
|
|
673
|
-
const later = await transaction.readTurn(id);
|
|
674
|
-
if (later.metadata?.runtime?.segmentId === segmentId && later.metadata.runtime.completedEnvelope === true) {
|
|
675
|
-
throw failure("A newer completed response owns this worker.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.newerResponseOwnsWorker" });
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
// These verified process references may refresh on cold inspection;
|
|
679
|
-
// all native conversation, path and account custody remains exact.
|
|
680
|
-
const { codexAppServerRun: run, executionId, processDirectory, ...binding } = runtime.binding;
|
|
681
|
-
if (engine === "codex" && (!saved.nativeTurnId || run?.outerTurnId !== messageId ||
|
|
682
|
-
run.providerThreadId !== binding.threadId || run.providerTurnId !== saved.nativeTurnId || run.active || run.state !== "completed")) {
|
|
683
|
-
throw failure("The completed response has no exact native turn receipt.", "conversation_completed_response_unavailable", 409, { messageCode: "assistant.conversation.completedResponseNoTurnReceipt" });
|
|
684
|
-
}
|
|
685
|
-
if (engine === "claude" && typeof saved.nativeToolAttempt !== "boolean") {
|
|
686
|
-
throw failure("The completed Claude response has no native tool-attempt evidence.", "conversation_completed_response_unavailable", 409, { messageCode: "assistant.conversation.completedClaudeNoToolEvidence" });
|
|
687
|
-
}
|
|
688
|
-
const calls = turn.metadata.applicationTools || [];
|
|
689
|
-
if (!Array.isArray(calls) || calls.length > 1 || calls.some(call => call.id !== toolCallId)) {
|
|
690
|
-
throw failure("This response already belongs to a different application operation.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.responseOtherOperation" });
|
|
691
|
-
}
|
|
692
|
-
const identity = { configuration: runtime.configuration, binding, assistant: turn.assistant,
|
|
693
|
-
nativeTurnId: saved.nativeTurnId, ...(engine === "claude" ? { nativeToolAttempt: saved.nativeToolAttempt } : {}) };
|
|
694
|
-
if (captured && !isDeepStrictEqual(captured, identity)) {
|
|
695
|
-
throw failure("The completed response identity changed.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.responseIdentityChanged" });
|
|
696
|
-
}
|
|
697
|
-
return { runtime, turn, calls, identity };
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
async function inspect() {
|
|
701
|
-
signal.throwIfAborted();
|
|
702
|
-
await access(context, entry.id, "tool", entry);
|
|
703
|
-
const saved = await storage.read(entry.id, transaction => exact(transaction));
|
|
704
|
-
const observation = await entry.provider.inspectAdmission({ messageId,
|
|
705
|
-
threadId: conversationNativeIdentity(saved.runtime), nativeTurnId: saved.turn.metadata.runtime.nativeTurnId,
|
|
706
|
-
configuration: saved.runtime.configuration, receiptBinding: saved.runtime.binding, context, verifyAccount: true });
|
|
707
|
-
signal.throwIfAborted();
|
|
708
|
-
await access(context, entry.id, "tool", entry);
|
|
709
|
-
if (!observation?.accepted || observation.recoveryLimitation || observation.conversationTurn &&
|
|
710
|
-
observation.conversationTurn.turnId !== turnId || engine === "codex" && observation.nativeTurnId !== saved.turn.metadata.runtime.nativeTurnId) {
|
|
711
|
-
throw failure("The native owner could not verify this response association.", "conversation_completed_response_unavailable", 409, { messageCode: "assistant.conversation.responseAssociationUnverified" });
|
|
712
|
-
}
|
|
713
|
-
// Canonical successful completion owns the exact final, including a
|
|
714
|
-
// StructuredOutput carrier whose native history may contain only commentary.
|
|
715
|
-
captured ||= structuredClone(saved.identity);
|
|
716
|
-
return storage.read(entry.id, transaction => exact(transaction));
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const response = await inspect();
|
|
720
|
-
return { text: response.turn.assistant.text, toolCallId,
|
|
721
|
-
...(engine === "claude" ? { nativeToolAttempt: response.turn.metadata.runtime.nativeToolAttempt } : {}),
|
|
722
|
-
publishReply(input) {
|
|
723
|
-
return serial(entry, async () => {
|
|
724
|
-
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some(key => key !== "turnId") ||
|
|
725
|
-
typeof input.turnId !== "string" || !input.turnId || input.turnId !== input.turnId.trim() || input.turnId.length > 128) {
|
|
726
|
-
throw new TypeError("Reply publication requires only its exact existing plain turn id.");
|
|
727
|
-
}
|
|
728
|
-
await inspect();
|
|
729
|
-
return storage.write(entry.id, async transaction => {
|
|
730
|
-
const saved = await exact(transaction);
|
|
731
|
-
signal.throwIfAborted();
|
|
732
|
-
const decoded = readAssistantResponseEnvelope(saved.turn.assistant.text);
|
|
733
|
-
if (decoded.kind !== "reply" || saved.turn.metadata.runtime.nativeToolAttempt === true || saved.calls.length) {
|
|
734
|
-
throw failure("This completed response does not own a plain reply.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.completedResponseNoPlainReply" });
|
|
735
|
-
}
|
|
736
|
-
const text = decoded.text.trim();
|
|
737
|
-
const target = await transaction.readTurn(input.turnId);
|
|
738
|
-
if (!text || input.turnId === turnId || !target || isCompletedEnvelopeTurn(target) ||
|
|
739
|
-
target.metadata?.runtime?.engine || !(target.user || target.system) || saved.runtime.lastEngine !== engine ||
|
|
740
|
-
!saved.runtime.seen || typeof saved.runtime.seen !== "object" || Array.isArray(saved.runtime.seen)) {
|
|
741
|
-
throw failure("The completed reply has no current plain authored target.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.completedReplyNoTarget" });
|
|
742
|
-
}
|
|
743
|
-
const assistantId = conversationMessageIdentity(input.turnId, { role: "assistant" });
|
|
744
|
-
const published = saved.turn.metadata.runtime.publishedReplyTurnId;
|
|
745
|
-
if (published !== undefined) {
|
|
746
|
-
if (published !== input.turnId || !target.assistant || target.assistant.text !== text ||
|
|
747
|
-
saved.runtime.seen[assistantId] !== conversationMessageVersion(target.assistant, { includeData: true })) {
|
|
748
|
-
throw failure("The published reply target or text changed.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.publishedReplyChanged" });
|
|
749
|
-
}
|
|
750
|
-
signal.throwIfAborted();
|
|
751
|
-
return target;
|
|
752
|
-
}
|
|
753
|
-
if (target.assistant || Object.hasOwn(saved.runtime.seen, assistantId)) {
|
|
754
|
-
throw failure("This plain reply target already has unassociated content or consumption.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.replyTargetUsed" });
|
|
755
|
-
}
|
|
756
|
-
// The runtime transcript uses this same reentrant wrapper: its
|
|
757
|
-
// original upsert joins this draft rather than starting a second write.
|
|
758
|
-
const plain = await transcript.upsertConversationAssistantMessage(entry.id, { turnId: input.turnId, text });
|
|
759
|
-
saved.runtime.seen[assistantId] = conversationMessageVersion(plain.assistant, { includeData: true });
|
|
760
|
-
await transaction.updateTurnMetadata(turnId, { runtime: { ...saved.turn.metadata.runtime, publishedReplyTurnId: input.turnId } });
|
|
761
|
-
const metadata = await transaction.readMetadata();
|
|
762
|
-
await transaction.writeMetadata({ ...metadata, runtime: saved.runtime });
|
|
763
|
-
await exact(transaction);
|
|
764
|
-
signal.throwIfAborted();
|
|
765
|
-
return plain;
|
|
766
|
-
});
|
|
767
|
-
});
|
|
768
|
-
},
|
|
769
|
-
tools: { execute(input, options = {}) {
|
|
770
|
-
return serial(entry, async () => {
|
|
771
|
-
if (input?.id !== toolCallId) throw failure("The operation has a different completed response identity.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.operationResponseIdentity" });
|
|
772
|
-
const saved = await inspect();
|
|
773
|
-
if (saved.turn.metadata.runtime.publishedReplyTurnId !== undefined) {
|
|
774
|
-
throw failure("This completed response already published its plain reply.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.plainReplyPublished" });
|
|
775
|
-
}
|
|
776
|
-
const workerSignal = options.signal && options.signal !== signal ? AbortSignal.any([signal, options.signal]) : signal;
|
|
777
|
-
workerSignal.throwIfAborted();
|
|
778
|
-
const request = { turnId, input: { messageId } };
|
|
779
|
-
async function persist(calls, retry = false) {
|
|
780
|
-
await storage.write(entry.id, async transaction => {
|
|
781
|
-
const current = await exact(transaction, { retry });
|
|
782
|
-
const previous = current.calls[0];
|
|
783
|
-
const next = calls[0];
|
|
784
|
-
if (calls.length !== 1 || next?.id !== toolCallId || previous &&
|
|
785
|
-
(previous.name !== next.name || previous.arguments !== next.arguments ||
|
|
786
|
-
previous.status !== "running" && next.status === "running")) {
|
|
787
|
-
throw failure("The saved response operation changed.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.savedOperationChanged" });
|
|
788
|
-
}
|
|
789
|
-
await saveTools(entry, transaction, request, calls);
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
const tools = createConversationTools({ catalog, toolSet, context, signal: workerSignal, previousCalls: saved.calls,
|
|
793
|
-
maximumCalls: maximumToolCalls, discoveryOnly: false,
|
|
794
|
-
authorize: () => access(context, entry.id, "tool", entry),
|
|
795
|
-
prepareContext: async () => {
|
|
796
|
-
await storage.read(entry.id, transaction => exact(transaction));
|
|
797
|
-
return context;
|
|
798
|
-
},
|
|
799
|
-
async save(calls) {
|
|
800
|
-
try { await persist(calls); }
|
|
801
|
-
catch (error) {
|
|
802
|
-
if (calls[0]?.status !== "running") {
|
|
803
|
-
const snapshot = structuredClone(calls);
|
|
804
|
-
entry.error = "The application operation result could not be saved. Restore storage access and retry saving.";
|
|
805
|
-
entry.storageFailure = failure(entry.error, "conversation_storage_unavailable", 503);
|
|
806
|
-
entry.storageFailure.cause = error;
|
|
807
|
-
entry.saveProgress = async () => {
|
|
808
|
-
await persist(snapshot, true);
|
|
809
|
-
entry.storageFailure = null;
|
|
810
|
-
entry.saveProgress = null;
|
|
811
|
-
entry.error = "";
|
|
812
|
-
return { type: "tool", turnId, call: snapshot[0] };
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
throw error;
|
|
816
|
-
}
|
|
817
|
-
},
|
|
818
|
-
emit: event => emit(entry, { ...event, turnId }) });
|
|
819
|
-
return tools.execute(input, { signal: workerSignal });
|
|
820
|
-
});
|
|
821
|
-
} } };
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
async function perform(entry, active, configuration) {
|
|
825
|
-
const { driver, engine, segmentId } = entry;
|
|
826
|
-
const bindingOwner = entry.bindingOwner;
|
|
827
|
-
const initial = active.request;
|
|
828
|
-
let current = initial;
|
|
829
|
-
let goalOwner = initial.input.goal ? initial : null;
|
|
830
|
-
let problem;
|
|
831
|
-
const messageOwners = new Map();
|
|
832
|
-
const toolOwners = new Map();
|
|
833
|
-
const rejectionSaves = new Map();
|
|
834
|
-
let toolWork = Promise.resolve();
|
|
835
|
-
let toolFailure;
|
|
836
|
-
const applicationTools = entry.applicationTools;
|
|
837
|
-
const toolStorage = applicationTools?.storage || storage;
|
|
838
|
-
// Every saved turn and pending request records its attribution identically.
|
|
839
|
-
const turnAttribution = (request, extra = {}) => ({ engine, segmentId, origin: request.input.origin,
|
|
840
|
-
...(request.completedEnvelope ? { completedEnvelope: true } : {}),
|
|
841
|
-
...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}), ...extra });
|
|
842
|
-
const authoredRequestRecord = request => ({ ...request.input, at: request.at,
|
|
843
|
-
...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}),
|
|
844
|
-
...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}) });
|
|
845
|
-
|
|
846
|
-
function prepareTools(request) {
|
|
847
|
-
if (request.completedEnvelope || !catalog || entry.conversation && !applicationTools) return;
|
|
848
|
-
request.tools = createConversationTools({ catalog, context: request.context, signal: active.controller.signal,
|
|
849
|
-
...(applicationTools ? { prepareContext() {
|
|
850
|
-
if (!request.accepted) throw new Error("The application tool does not belong to an admitted message.");
|
|
851
|
-
return applicationTools.prepareContext(request.context, Object.freeze({ conversationId: entry.id,
|
|
852
|
-
turnId: request.turnId, messageId: request.input.messageId, nativeTurnId: request.nativeTurnId,
|
|
853
|
-
nativeThreadId: request.nativeThreadId, origin: request.input.origin,
|
|
854
|
-
assertCurrent() {
|
|
855
|
-
active.controller.signal.throwIfAborted();
|
|
856
|
-
if (entry.active !== active || current !== request || !request.accepted || request.finished) {
|
|
857
|
-
throw failure("This application tool's admitted message is no longer current.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.applicationToolMessageStale" });
|
|
858
|
-
}
|
|
859
|
-
},
|
|
860
|
-
...(entry.conversation ? { assertCompletionCurrent() {
|
|
861
|
-
active.controller.signal.throwIfAborted();
|
|
862
|
-
if (closed || entry.disposed || entry.bindingOwner !== bindingOwner || entry.segmentId !== segmentId ||
|
|
863
|
-
!active.finishing || active.timedOut || problem || toolFailure || entry.executionFailure || entry.storageFailure ||
|
|
864
|
-
current !== request || !request.accepted || entry.lastAcceptedRequest !== request ||
|
|
865
|
-
entry.active && entry.active !== active) {
|
|
866
|
-
throw failure("This native completion's admitted message is no longer current.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.nativeCompletionMessageStale" });
|
|
867
|
-
}
|
|
868
|
-
} } : {}) }));
|
|
869
|
-
} } : {}),
|
|
870
|
-
maximumCalls: maximumToolCalls, discoveryOnly: driver.toolDiscovery === true,
|
|
871
|
-
authorize: () => access(request.context, entry.id, "tool", entry),
|
|
872
|
-
save: calls => {
|
|
873
|
-
if (!request.accepted) throw new Error("The engine requested an application tool before admitting the message.");
|
|
874
|
-
return toolStorage.write(entry.id, transaction => saveTools(entry, transaction, request, calls));
|
|
875
|
-
},
|
|
876
|
-
emit: event => emit(entry, { ...event, turnId: request.turnId })
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
async function writeProgress(transaction, request, status, error = "", extra = {}) {
|
|
881
|
-
if (request.tools) await transaction.updateTurnMetadata(request.turnId, { applicationTools: request.tools.records() });
|
|
882
|
-
if (!driver.canonicalTranscript) {
|
|
883
|
-
for (const message of request.progress.values()) {
|
|
884
|
-
if (!persistCommentary && message.role === "commentary") continue;
|
|
885
|
-
if (!await transaction.hasMessage(message.messageId)) await transaction.appendMessage(request.turnId, message);
|
|
886
|
-
}
|
|
887
|
-
if (request.answer?.text.trim()) await transaction.replaceAssistant(request.turnId, request.answer);
|
|
888
|
-
}
|
|
889
|
-
const saved = driver.canonicalTranscript ? await transaction.readTurn(request.turnId) : null;
|
|
890
|
-
await transaction.updateTurnMetadata(request.turnId, { runtime: { ...saved?.metadata?.runtime, status, ...turnAttribution(request, {
|
|
891
|
-
...(request.nativeTurnId ? { nativeTurnId: request.nativeTurnId } : {}),
|
|
892
|
-
...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
|
|
893
|
-
...(request.continuedBy ? { continuedBy: request.continuedBy } : {}) }), ...extra, ...(error ? { error } : {}) } });
|
|
894
|
-
return conversationHistoryVersions([{ turnId: request.turnId,
|
|
895
|
-
messages: [...request.progress.values()].filter(message => !(message.turnId === request.turnId && message.status === "complete"))
|
|
896
|
-
.concat(request.answer?.text.trim() ? [request.answer] : []) }]);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
async function publishAdmission(request, nativeResult) {
|
|
900
|
-
request.deliveryTiming?.("accepted");
|
|
901
|
-
const receipt = { status: "accepted", messageId: request.input.messageId, turnId: request.turnId, origin: request.input.origin };
|
|
902
|
-
request.admission.resolve(entry.conversation ? { receipt, nativeResult } : receipt);
|
|
903
|
-
request.admissionSettled = true;
|
|
904
|
-
await emit(entry, { type: "accepted", ...receipt });
|
|
905
|
-
if (driver.canonicalTranscript && !entry.conversation) {
|
|
906
|
-
// Native items can complete before common admission finishes. Their
|
|
907
|
-
// exact request already owns them; publish before tools join the
|
|
908
|
-
// existing delivery commit, without blocking the native event queue.
|
|
909
|
-
for (const [id, message] of request.progress) {
|
|
910
|
-
if (message.turnId !== request.turnId || message.status !== "complete") continue;
|
|
911
|
-
request.progress.delete(id);
|
|
912
|
-
if (request.sealed || entry.disposed || entry.active !== active ||
|
|
913
|
-
entry.bindingOwner !== bindingOwner || entry.segmentId !== segmentId) continue;
|
|
914
|
-
await emit(entry, { type: "message", ...message, streaming: streams.read(entry.id) },
|
|
915
|
-
() => !entry.disposed && !request.sealed && entry.active === active && entry.bindingOwner === bindingOwner && entry.segmentId === segmentId);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
request.admissionPublished = true;
|
|
919
|
-
await setPhase(entry, "working", request.turnId);
|
|
920
|
-
if ((entry.conversation || driver.canonicalTranscript) && entry.nativePhase && entry.nativePhase !== "working") await setPhase(entry, entry.nativePhase, request.turnId);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
async function admit(request, seen, { nativeDelivery = false, conversationTurn } = {}) {
|
|
924
|
-
await access(request.context, entry.id, request.operation, entry);
|
|
925
|
-
request.deliveryTiming?.("admission-authorized");
|
|
926
|
-
if (!conversationTurn) active.controller.signal.throwIfAborted();
|
|
927
|
-
if (request.input.goal) request.goalMessageId = request.input.messageId;
|
|
928
|
-
else if (goalOwner) request.goalMessageId = goalOwner.input.messageId;
|
|
929
|
-
const nativeTurnId = active.nativeTurnId;
|
|
930
|
-
const previous = current;
|
|
931
|
-
if (entry.conversation) {
|
|
932
|
-
const saved = conversationTurn?.turnId ? await readBoundTurn(entry, conversationTurn.turnId) : null;
|
|
933
|
-
request.deliveryTiming?.("turn-located");
|
|
934
|
-
if (submittedMessage(saved || {})?.messageId !== request.input.messageId) {
|
|
935
|
-
throw new Error("The native receipt does not belong to this authored request.");
|
|
936
|
-
}
|
|
937
|
-
request.turnId = saved.turnId;
|
|
938
|
-
} else request.turnId = await storage.write(entry.id, async transaction => {
|
|
939
|
-
const metadata = await transaction.readMetadata();
|
|
940
|
-
if (metadata.runtime.request?.messageId !== request.input.messageId) throw new Error("The pending conversation message changed before admission.");
|
|
941
|
-
if (previous !== request && !previous.finished) {
|
|
942
|
-
const versions = await writeProgress(transaction, previous, "running", "", { continuedBy: request.input.messageId });
|
|
943
|
-
if (engine === "api") Object.assign(metadata.runtime.seen, versions);
|
|
944
|
-
}
|
|
945
|
-
let id;
|
|
946
|
-
if (conversationTurn) {
|
|
947
|
-
const saved = await transaction.readTurn(conversationTurn.turnId);
|
|
948
|
-
if (submittedMessage(saved || {})?.messageId !== request.input.messageId || saved.metadata?.runtime?.segmentId !== segmentId) {
|
|
949
|
-
throw new Error("The native receipt does not belong to this authored request.");
|
|
950
|
-
}
|
|
951
|
-
if (request.completedEnvelope && saved.metadata.runtime.completedEnvelope !== true) {
|
|
952
|
-
throw new Error("The native receipt lost its completed-response marker.");
|
|
953
|
-
}
|
|
954
|
-
id = saved.turnId;
|
|
955
|
-
await transaction.updateTurnMetadata(id, { runtime: { ...saved.metadata.runtime,
|
|
956
|
-
...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
|
|
957
|
-
...(nativeTurnId ? { nativeTurnId } : {}) } });
|
|
958
|
-
} else {
|
|
959
|
-
id = await transaction.nextTurnId();
|
|
960
|
-
await transaction.appendMessage(id, { role: request.input.origin === "application" ? "system" : "user", ...request.input, at: new Date().toISOString(),
|
|
961
|
-
turnMetadata: { runtime: { status: "running", ...turnAttribution(request, {
|
|
962
|
-
...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
|
|
963
|
-
...(nativeTurnId ? { nativeTurnId } : {}) }) } } });
|
|
964
|
-
}
|
|
965
|
-
if (engine === "api") {
|
|
966
|
-
metadata.runtime.seen = { ...(seen || metadata.runtime.seen), ...conversationHistoryVersions([await transaction.readTurn(id)]) };
|
|
967
|
-
metadata.runtime.lastEngine = engine;
|
|
968
|
-
delete metadata.runtime.request;
|
|
969
|
-
}
|
|
970
|
-
await transaction.writeMetadata(metadata);
|
|
971
|
-
if (engine !== "api" && !nativeDelivery) await rememberNative(entry, transaction, [await transaction.readTurn(id)]);
|
|
972
|
-
return id;
|
|
973
|
-
});
|
|
974
|
-
if (previous !== request) {
|
|
975
|
-
previous.sealed = true;
|
|
976
|
-
previous.continuedBy = request.input.messageId;
|
|
977
|
-
if (!driver.canonicalTranscript) streams.clear(entry.id);
|
|
978
|
-
}
|
|
979
|
-
request.accepted = true;
|
|
980
|
-
if (!entry.conversation) request.deliveryTiming?.("receipt-persisted");
|
|
981
|
-
request.nativeTurnId = nativeTurnId;
|
|
982
|
-
if (request.input.goal) goalOwner = request;
|
|
983
|
-
current = active.request = request;
|
|
984
|
-
if (entry.conversation) entry.lastAcceptedRequest = request;
|
|
985
|
-
if (!nativeDelivery) await publishAdmission(request);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
async function onNativeTurn({ turnId, nativeOwner = false }) {
|
|
989
|
-
if (active.nativeTurnId === turnId) return;
|
|
990
|
-
if (!nativeOwner || !driver.canonicalTranscript) throw new Error("The native continuation has no original conversation owner.");
|
|
991
|
-
await access(current.context, entry.id, current.operation, entry);
|
|
992
|
-
active.controller.signal.throwIfAborted();
|
|
993
|
-
// Original Codex successors retain the admitted request, actor and budget.
|
|
994
|
-
active.nativeTurnId = turnId;
|
|
995
|
-
if (!current.nativeTurnId && !entry.conversation) {
|
|
996
|
-
current.nativeTurnId = turnId;
|
|
997
|
-
await storage.write(entry.id, async transaction => {
|
|
998
|
-
const turn = await transaction.readTurn(current.turnId);
|
|
999
|
-
await transaction.updateTurnMetadata(current.turnId, { runtime: { ...turn.metadata.runtime, nativeTurnId: turnId } });
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
async function completeNativeTurn({ turnId, status = "completed", nativeOwner = false }) {
|
|
1005
|
-
if (!nativeOwner && active.nativeTurnId !== turnId) throw new Error("The completed native goal turn has a different identity.");
|
|
1006
|
-
// The original owner checkpoints from its notification queue. Steering's
|
|
1007
|
-
// exact receipt can still be behind that task, so only the outside run
|
|
1008
|
-
// continuation joins active.steering; this checkpoint saves admitted work.
|
|
1009
|
-
if (!nativeOwner) await active.steering;
|
|
1010
|
-
await toolWork;
|
|
1011
|
-
if (toolFailure) throw toolFailure;
|
|
1012
|
-
const completedStatus = nativeOwner && active.timedOut ? "failed"
|
|
1013
|
-
: nativeOwner && active.controller.signal.aborted ? "cancelled" : status === "completed" ? "complete" : status;
|
|
1014
|
-
const completedError = nativeOwner && active.timedOut ? "The conversation request exceeded its time limit."
|
|
1015
|
-
: nativeOwner && active.controller.signal.aborted ? "Work stopped." : "";
|
|
1016
|
-
await storage.write(entry.id, async transaction => {
|
|
1017
|
-
const metadata = await transaction.readMetadata();
|
|
1018
|
-
const written = [];
|
|
1019
|
-
for (const request of active.requests.values()) {
|
|
1020
|
-
if (request.turnId && request.accepted && !request.finished &&
|
|
1021
|
-
(nativeOwner && driver.canonicalTranscript || request.nativeTurnId === turnId)) {
|
|
1022
|
-
const versions = await writeProgress(transaction, request, completedStatus, completedError);
|
|
1023
|
-
if (engine === "api") Object.assign(metadata.runtime.seen, versions);
|
|
1024
|
-
else written.push(await transaction.readTurn(request.turnId));
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
if (nativeOwner) for (const id of await transaction.listTurnIds()) {
|
|
1028
|
-
const turn = await transaction.readTurn(id);
|
|
1029
|
-
const saved = turn.metadata?.runtime;
|
|
1030
|
-
if (saved?.segmentId !== segmentId || saved.nativeTurnId !== turnId || written.some(row => row.turnId === id)) continue;
|
|
1031
|
-
if (["cancelled", "failed"].includes(saved.status)) continue;
|
|
1032
|
-
await transaction.updateTurnMetadata(id, { runtime: { ...saved, status: completedStatus } });
|
|
1033
|
-
written.push(await transaction.readTurn(id));
|
|
1034
|
-
}
|
|
1035
|
-
await transaction.writeMetadata(metadata);
|
|
1036
|
-
if (written.length) await rememberNative(entry, transaction, written);
|
|
1037
|
-
});
|
|
1038
|
-
for (const request of active.requests.values()) {
|
|
1039
|
-
if (request.accepted && (nativeOwner && driver.canonicalTranscript || request.nativeTurnId === turnId)) {
|
|
1040
|
-
request.finished = request.sealed = true;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
streams.clear(entry.id);
|
|
1044
|
-
}
|
|
1045
|
-
if (driver.canonicalTranscript && !entry.conversation) active.nativeCheckpoint = input => completeNativeTurn({ ...input, nativeOwner: true });
|
|
1046
|
-
|
|
1047
|
-
async function onMessage(message) {
|
|
1048
|
-
const request = messageOwners.get(message.id) || current;
|
|
1049
|
-
if (!request.accepted) throw new Error("The engine produced output before admitting the message.");
|
|
1050
|
-
messageOwners.set(message.id, request);
|
|
1051
|
-
// An already visible native item keeps its original request identity. Late
|
|
1052
|
-
// snapshots cannot replay that answer after a newer instruction is admitted.
|
|
1053
|
-
if (request.sealed) return;
|
|
1054
|
-
await setPhase(entry, "working", request.turnId);
|
|
1055
|
-
if (request.sealed) return;
|
|
1056
|
-
const messageId = `${request.turnId}:${message.id}`;
|
|
1057
|
-
const finalReplyLimit = completedEnvelope && request.completedEnvelope && request.input.origin === "application" && !request.input.goal
|
|
1058
|
-
? maximumCompletedOutput : maximumFinalReply;
|
|
1059
|
-
if (message.role === "assistant" && message.complete && message.text.length > finalReplyLimit) {
|
|
1060
|
-
if (request.answer?.messageId === messageId) request.answer = null;
|
|
1061
|
-
throw new Error("The assistant exceeded the configured final reply limit.");
|
|
1062
|
-
}
|
|
1063
|
-
const previous = request.answer?.messageId === messageId ? request.answer : request.progress.get(messageId);
|
|
1064
|
-
const outputOwner = message.outputId && messageOwners.get(message.outputId) || request;
|
|
1065
|
-
const outputId = previous?.outputId || (message.outputId ? `${outputOwner.turnId}:${message.outputId}` : "");
|
|
1066
|
-
const value = { messageId, ...(outputId ? { outputId } : {}),
|
|
1067
|
-
role: message.role, text: message.text, at: previous?.at || new Date().toISOString() };
|
|
1068
|
-
const authorship = request.completedEnvelope ? { authorship: { messageId: request.input.messageId,
|
|
1069
|
-
turnId: request.turnId, origin: request.input.origin, completedEnvelope: true } } : {};
|
|
1070
|
-
// Some native streams have temporary block identities. Only their final
|
|
1071
|
-
// history snapshots belong in the transcript, as in the original owner.
|
|
1072
|
-
if (message.transient) {
|
|
1073
|
-
streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value, ...authorship });
|
|
1074
|
-
} else if (message.role === "assistant") {
|
|
1075
|
-
if (request.answer && request.answer.messageId !== messageId) {
|
|
1076
|
-
request.progress.set(request.answer.messageId, { ...request.answer, role: "commentary" });
|
|
1077
|
-
streams.complete(entry.id, request.answer.messageId);
|
|
1078
|
-
}
|
|
1079
|
-
request.progress.delete(messageId);
|
|
1080
|
-
request.answer = value;
|
|
1081
|
-
streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value, ...authorship });
|
|
1082
|
-
} else {
|
|
1083
|
-
if (request.answer?.messageId === messageId) request.answer = null;
|
|
1084
|
-
if (message.complete) request.progress.set(messageId, value);
|
|
1085
|
-
if (message.complete) streams.complete(entry.id, messageId);
|
|
1086
|
-
else streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value, ...authorship });
|
|
1087
|
-
}
|
|
1088
|
-
await emit(entry, { type: "message", turnId: request.turnId, messageId, origin: request.input.origin, role: message.role,
|
|
1089
|
-
...(outputId ? { outputId } : {}),
|
|
1090
|
-
text: message.text, status: message.complete ? "complete" : "inProgress", streaming: streams.read(entry.id) });
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
function attachmentReader(request) {
|
|
1094
|
-
return createConversationAttachmentReader({ attachments, context: request.context, conversationId: entry.id,
|
|
1095
|
-
signal: active.controller.signal, authorize: () => access(request.context, entry.id, request.operation, entry),
|
|
1096
|
-
types: driver.attachmentTypes, maximumBytes: maximumAttachmentBytes });
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
async function beforeDispatch(request) {
|
|
1100
|
-
await access(request.context, entry.id, request.operation, entry);
|
|
1101
|
-
active.controller.signal.throwIfAborted();
|
|
1102
|
-
if (request.input.goal && request.context?.signal?.aborted) {
|
|
1103
|
-
throw Object.assign(failure("The goal command was stopped before delivery.", "conversation_goal_stopped"), { delivery: "not-sent" });
|
|
1104
|
-
}
|
|
1105
|
-
if (active.finishing) throw Object.assign(failure("The native turn finished before this instruction was sent. Send it as a new message.", "conversation_steer_turn_finished", 409, { messageCode: "assistant.conversation.turnFinishedBeforeSend" }), { delivery: "not-sent" });
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
async function deliverNative(request, dispatch) {
|
|
1109
|
-
// A bound conversation supplies its own native history below.
|
|
1110
|
-
const history = entry.conversation ? [] : (await readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy);
|
|
1111
|
-
let completion;
|
|
1112
|
-
let committed;
|
|
1113
|
-
let rendered;
|
|
1114
|
-
let threadId;
|
|
1115
|
-
let attachmentManifest;
|
|
1116
|
-
let nativeResult;
|
|
1117
|
-
const dispatchPrepared = async input => {
|
|
1118
|
-
rendered = input;
|
|
1119
|
-
rejectionSaves.delete(request.input.messageId);
|
|
1120
|
-
if (request.operation === "steer" && !request.steering) throw Object.assign(failure("There is no active native turn to steer. Send a new message.", "conversation_steer_turn_finished", 409, { messageCode: "assistant.conversation.noActiveTurnToSteer" }), { delivery: "not-sent" });
|
|
1121
|
-
if ((input.contextText || "").length > maximumContinuity) throw new Error("Conversation updates exceed the configured history budget. Renew the context with a continuity briefing before continuing.");
|
|
1122
|
-
if (request.input.goal && input.contextText) {
|
|
1123
|
-
throw failure("Send a chat message to deliver the saved continuity briefing before starting a native terminal or goal.", "conversation_replacement_briefing_pending", 409, { messageCode: "assistant.conversation.briefingBeforeTerminal" });
|
|
1124
|
-
}
|
|
1125
|
-
const readAttachments = attachmentReader(request);
|
|
1126
|
-
// The bound host has already resolved its original attachment records;
|
|
1127
|
-
// the production preparation owner keeps their manifest/native mapping.
|
|
1128
|
-
const boundInput = entry.conversation
|
|
1129
|
-
? await entry.conversation.prepareInput(input, request.context, { deliveryTiming: request.deliveryTiming }) : null;
|
|
1130
|
-
const files = boundInput ? { attachments: boundInput.displayAttachments || [], localFiles: [], content: [] }
|
|
1131
|
-
: await readAttachments((input.attachmentIds || []).map(attachmentId => ({ attachmentId })));
|
|
1132
|
-
const contextFiles = boundInput ? { localFiles: [], content: [] } : await readAttachments(input.contextAttachments || []);
|
|
1133
|
-
request.deliveryTiming?.("input-prepared");
|
|
1134
|
-
const localFiles = [...contextFiles.localFiles, ...files.localFiles];
|
|
1135
|
-
if (request.input.goal && localFiles.length) {
|
|
1136
|
-
throw failure("Native goals require extracted text or supported image bytes for attachments.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.goalAttachmentContentUnsupported" });
|
|
1137
|
-
}
|
|
1138
|
-
attachmentManifest = conversationAttachmentManifest(localFiles);
|
|
1139
|
-
request.input.attachments = files.attachments;
|
|
1140
|
-
if (!entry.conversation) await storage.write(entry.id, async transaction => {
|
|
1141
|
-
const metadata = await transaction.readMetadata();
|
|
1142
|
-
const pending = metadata.runtime.request;
|
|
1143
|
-
if (pending && pending.messageId !== request.input.messageId && (pending.attempted || pending.inspectionOnly)) throw new Error("The pending conversation instruction changed.");
|
|
1144
|
-
if (pending?.messageId === request.input.messageId && pending.attachmentManifest !== undefined &&
|
|
1145
|
-
pending.attachmentManifest !== attachmentManifest) {
|
|
1146
|
-
throw failure("Authorized attachment paths changed after this instruction was prepared. Restore the original files or send a new message.", "conversation_attachment_manifest_changed");
|
|
1147
|
-
}
|
|
1148
|
-
metadata.runtime.request = { ...(pending?.messageId === request.input.messageId ? pending
|
|
1149
|
-
: { ...authoredRequestRecord(request), attempted: false }), attachmentManifest };
|
|
1150
|
-
await transaction.writeMetadata(metadata);
|
|
1151
|
-
});
|
|
1152
|
-
const admitted = Promise.withResolvers();
|
|
1153
|
-
committed = Promise.withResolvers();
|
|
1154
|
-
committed.promise.catch(() => {});
|
|
1155
|
-
request.deliveryCommitted = committed.promise;
|
|
1156
|
-
request.deliveryTiming?.("provider-started");
|
|
1157
|
-
completion = Promise.resolve().then(() => dispatch({
|
|
1158
|
-
context: request.context,
|
|
1159
|
-
input: { ...request.input, authoredInput: { ...request.input, at: request.at },
|
|
1160
|
-
...(request.input.goal ? { signal: request.context?.signal } : {}),
|
|
1161
|
-
...(boundInput ? { nativeMessage: boundInput } : {}),
|
|
1162
|
-
text: input.message, contextText: input.contextText || "",
|
|
1163
|
-
content: [...contextFiles.content, ...files.content], localFiles, attachmentManifest },
|
|
1164
|
-
signal: active.controller.signal,
|
|
1165
|
-
deliveryTiming: request.deliveryTiming,
|
|
1166
|
-
onNativeReceipt: () => request.deliveryTiming?.("native-acknowledged"),
|
|
1167
|
-
onReceiptPersisted: () => request.deliveryTiming?.("receipt-persisted"),
|
|
1168
|
-
...(engine === "opencode" ? { deliveryCommitted: committed.promise } : {}),
|
|
1169
|
-
async beforeDispatch(native) {
|
|
1170
|
-
request.deliveryTiming?.("runtime-ready");
|
|
1171
|
-
await beforeDispatch(request);
|
|
1172
|
-
threadId = native?.threadId;
|
|
1173
|
-
if (!threadId) throw new Error("Native admission requires the saved conversation identity.");
|
|
1174
|
-
await input.onPromptSending(entry.conversation ? native : { threadId, displayAttachments: files.attachments,
|
|
1175
|
-
turnMetadata: { runtime: turnAttribution(request) } });
|
|
1176
|
-
if (request.input.goal && request.context?.signal?.aborted) {
|
|
1177
|
-
throw Object.assign(failure("The goal command was stopped before delivery.", "conversation_goal_stopped"), { delivery: "not-sent" });
|
|
1178
|
-
}
|
|
1179
|
-
request.deliveryTiming?.("dispatch-started");
|
|
1180
|
-
},
|
|
1181
|
-
async accept(native) {
|
|
1182
|
-
if (entry.conversation) nativeResult = native.nativeResult;
|
|
1183
|
-
if (native?.nativeTurnId) active.nativeTurnId = native.nativeTurnId;
|
|
1184
|
-
request.nativeThreadId = threadId;
|
|
1185
|
-
await admit(request, undefined, { nativeDelivery: true, conversationTurn: native?.conversationTurn });
|
|
1186
|
-
admitted.resolve(entry.conversation ? nativeResult.value
|
|
1187
|
-
: { ok: true, delivered: true, messageId: request.input.messageId, threadId });
|
|
1188
|
-
// Drivers may stream or continue a goal as soon as accept returns.
|
|
1189
|
-
// The original delivery marker must be durable before that happens.
|
|
1190
|
-
await committed.promise;
|
|
1191
|
-
}
|
|
1192
|
-
})).then(result => {
|
|
1193
|
-
if (entry.conversation && !request.accepted && result) {
|
|
1194
|
-
nativeResult = result;
|
|
1195
|
-
admitted.resolve(result.value);
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
if (!request.accepted) throw new Error("The engine did not acknowledge the message.");
|
|
1199
|
-
}).catch(async error => {
|
|
1200
|
-
if (!request.accepted && (error.delivery === "not-sent" || !entry.conversation && error.delivery !== "uncertain")) {
|
|
1201
|
-
try { await input.onPromptRejected(); }
|
|
1202
|
-
catch (saveError) {
|
|
1203
|
-
rejectionSaves.set(request.input.messageId, input.onPromptRejected);
|
|
1204
|
-
error = saveError;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
admitted.reject(error);
|
|
1208
|
-
throw error;
|
|
1209
|
-
});
|
|
1210
|
-
completion.catch(() => {});
|
|
1211
|
-
return admitted.promise;
|
|
1212
|
-
};
|
|
1213
|
-
try {
|
|
1214
|
-
const input = {
|
|
1215
|
-
...request.originalInput,
|
|
1216
|
-
messageId: request.input.messageId,
|
|
1217
|
-
message: request.originalInput ? request.originalInput.message ?? request.originalInput.text ?? "" : conversationRequestText(request.input),
|
|
1218
|
-
displayMessage: request.input.text,
|
|
1219
|
-
displayAttachments: request.originalInput?.displayAttachments || request.input.attachments,
|
|
1220
|
-
attachmentIds: request.input.attachments.map(file => file.attachmentId),
|
|
1221
|
-
async onPromptSending({ threadId, displayAttachments, turnMetadata }) {
|
|
1222
|
-
if (entry.conversation) return request.originalInput?.onPromptSending?.({ threadId, displayAttachments, turnMetadata });
|
|
1223
|
-
await storage.write(entry.id, async transaction => {
|
|
1224
|
-
const metadata = await transaction.readMetadata();
|
|
1225
|
-
const pending = metadata.runtime.request;
|
|
1226
|
-
if (pending && pending.messageId !== request.input.messageId && (pending.attempted || pending.inspectionOnly)) throw new Error("The pending conversation instruction changed.");
|
|
1227
|
-
metadata.runtime.request = { ...authoredRequestRecord(request),
|
|
1228
|
-
message: rendered.message, displayMessage: rendered.displayMessage,
|
|
1229
|
-
displayAttachments, attachmentIds: rendered.attachmentIds,
|
|
1230
|
-
attachmentManifest,
|
|
1231
|
-
...(rendered.contextText !== undefined ? { contextText: rendered.contextText, contextAttachments: rendered.contextAttachments } : {}),
|
|
1232
|
-
seen: pending?.seen || conversationHistoryVersions(history), attempted: true, threadId, turnMetadata,
|
|
1233
|
-
...(request.operation === "steer" ? { steering: true } : {}) };
|
|
1234
|
-
await transaction.writeMetadata(metadata);
|
|
1235
|
-
});
|
|
1236
|
-
},
|
|
1237
|
-
async onPromptRejected() {
|
|
1238
|
-
if (entry.conversation) return request.originalInput?.onPromptRejected?.();
|
|
1239
|
-
await storage.write(entry.id, async transaction => {
|
|
1240
|
-
const metadata = await transaction.readMetadata();
|
|
1241
|
-
if (metadata.runtime.request?.messageId === request.input.messageId) {
|
|
1242
|
-
metadata.runtime.request.attempted = false;
|
|
1243
|
-
await transaction.writeMetadata(metadata);
|
|
1244
|
-
}
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
// Original goal commands call the same native sender directly. Chat
|
|
1249
|
-
// context rendering and its delivery cursor must not change /goal input.
|
|
1250
|
-
const delivered = entry.conversation && request.input.goal
|
|
1251
|
-
? await dispatchPrepared(input)
|
|
1252
|
-
: await nativeChangeover(entry, request.context, request, dispatchPrepared, active.controller.signal).send({ engineId: engine,
|
|
1253
|
-
messages: entry.conversation ? await entry.conversation.transcript.history() : conversationNativeMessages(history),
|
|
1254
|
-
...(entry.conversation ? { turnMetadata: { engineId: engine } } : {}),
|
|
1255
|
-
...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}), input });
|
|
1256
|
-
if (!delivered?.delivered) {
|
|
1257
|
-
const error = deliveryFailure(delivered);
|
|
1258
|
-
if (entry.conversation) {
|
|
1259
|
-
request.admission.resolve({ nativeResult: nativeResult || { value: delivered }, error });
|
|
1260
|
-
request.admissionSettled = true;
|
|
1261
|
-
}
|
|
1262
|
-
throw error;
|
|
1263
|
-
}
|
|
1264
|
-
if (request.accepted) await publishAdmission(request, nativeResult);
|
|
1265
|
-
else {
|
|
1266
|
-
const saved = await findSubmittedTurn(entry, request.input.messageId);
|
|
1267
|
-
if (!saved && entry.conversation) {
|
|
1268
|
-
// An original native-history duplicate need not have an authored
|
|
1269
|
-
// application row. Preserve its exact result without inventing a
|
|
1270
|
-
// canonical receipt or changing native success into a failure.
|
|
1271
|
-
request.admission.resolve({ nativeResult: nativeResult || { value: delivered },
|
|
1272
|
-
error: failure("The native receipt has no stored authored message.", "conversation_receipt_unavailable") });
|
|
1273
|
-
request.admissionSettled = request.finished = true;
|
|
1274
|
-
committed?.resolve();
|
|
1275
|
-
await completion;
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
|
-
if (!saved) throw new Error("The native receipt has no stored authored message.");
|
|
1279
|
-
request.accepted = request.finished = true;
|
|
1280
|
-
if (entry.conversation) entry.lastAcceptedRequest = request;
|
|
1281
|
-
request.turnId = saved.turnId;
|
|
1282
|
-
request.recovered = saved.metadata?.runtime;
|
|
1283
|
-
const receipt = { status: "accepted", messageId: request.input.messageId, turnId: saved.turnId,
|
|
1284
|
-
origin: request.input.origin, ...(delivered.duplicate ? { duplicate: true } : { recovered: true }) };
|
|
1285
|
-
request.admission.resolve(entry.conversation ? { receipt, nativeResult: { value: delivered } } : receipt);
|
|
1286
|
-
request.admissionSettled = true;
|
|
1287
|
-
}
|
|
1288
|
-
committed?.resolve();
|
|
1289
|
-
await completion;
|
|
1290
|
-
} catch (error) {
|
|
1291
|
-
committed?.reject(error);
|
|
1292
|
-
await completion?.catch(() => {});
|
|
1293
|
-
throw error;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
active.submit = async request => {
|
|
1298
|
-
try {
|
|
1299
|
-
prepareTools(request);
|
|
1300
|
-
await deliverNative(request, entry.provider.steer);
|
|
1301
|
-
} catch (error) {
|
|
1302
|
-
let reported = error;
|
|
1303
|
-
if (!request.admissionSettled) {
|
|
1304
|
-
try {
|
|
1305
|
-
if (!entry.conversation) await storage.write(entry.id, async transaction => {
|
|
1306
|
-
const metadata = await transaction.readMetadata();
|
|
1307
|
-
if (metadata.runtime.request?.messageId === request.input.messageId) {
|
|
1308
|
-
metadata.runtime.request.error = error.message;
|
|
1309
|
-
await transaction.writeMetadata(metadata);
|
|
1310
|
-
}
|
|
1311
|
-
});
|
|
1312
|
-
} catch (saveError) { reported = saveError; }
|
|
1313
|
-
request.admission.reject(reported);
|
|
1314
|
-
if (!request.accepted) active.requests.delete(request.input.messageId);
|
|
1315
|
-
}
|
|
1316
|
-
if (error.delivery === "uncertain") active.steerError = error.message;
|
|
1317
|
-
}
|
|
1318
|
-
};
|
|
1319
|
-
|
|
1320
|
-
try {
|
|
1321
|
-
await setPhase(entry, "preparing");
|
|
1322
|
-
// Only the API engine sends the application transcript as history.
|
|
1323
|
-
const recordedHistory = engine === "api" ? await readTranscript(entry) : [];
|
|
1324
|
-
const activeHistory = recordedHistory.filter(turn => !turn.metadata?.runtime?.supersededBy);
|
|
1325
|
-
const metadata = { runtime: await readState(entry) };
|
|
1326
|
-
const continuity = metadata.runtime.continuity;
|
|
1327
|
-
// Existing application transcripts precede the first runtime segment.
|
|
1328
|
-
// After replacement, their context is already carried by the briefing.
|
|
1329
|
-
const history = activeHistory.filter(turn => turn.metadata?.runtime?.segmentId === segmentId ||
|
|
1330
|
-
!turn.metadata?.runtime?.segmentId && !continuity);
|
|
1331
|
-
prepareTools(initial);
|
|
1332
|
-
const tools = initial.tools && { descriptors: initial.tools.descriptors, schemas: initial.tools.schemas, maximumArgumentBytes: initial.tools.maximumArgumentBytes,
|
|
1333
|
-
execute(input, options) {
|
|
1334
|
-
const request = toolOwners.get(input.id) || (options?.messageId ? active.requests.get(options.messageId) : current);
|
|
1335
|
-
if (!request?.accepted) throw new Error("The application tool does not belong to an admitted message.");
|
|
1336
|
-
toolOwners.set(input.id, request);
|
|
1337
|
-
// Calls already in flight retain their actor and turn. Keep application
|
|
1338
|
-
// mutations ordered across steering as well as within each request.
|
|
1339
|
-
const work = toolWork.then(() => request.tools.execute(input, options));
|
|
1340
|
-
toolWork = work.catch(error => {
|
|
1341
|
-
// Claude accepts a separate successor query; its predecessor's
|
|
1342
|
-
// effect cannot fail that query. Other engines retain the original
|
|
1343
|
-
// active-turn failure guard across steering.
|
|
1344
|
-
if (engine !== "claude" || current === request) toolFailure ||= error;
|
|
1345
|
-
});
|
|
1346
|
-
return work;
|
|
1347
|
-
}
|
|
1348
|
-
};
|
|
1349
|
-
const completedToolSet = engine === "claude" && initial.completedEnvelope && typeof initial.nativeToolAttempt === "boolean"
|
|
1350
|
-
? catalog.resolveToolSet(initial.context) : null;
|
|
1351
|
-
const providerInput = { configuration, context: initial.context, tools,
|
|
1352
|
-
...(completedToolSet ? {
|
|
1353
|
-
async onNativeToolUse({ messageId, threadId, name }) {
|
|
1354
|
-
await initial.deliveryCommitted;
|
|
1355
|
-
active.controller.signal.throwIfAborted();
|
|
1356
|
-
if (entry.disposed || entry.active !== active || current !== initial || !initial.accepted || initial.finished || initial.sealed ||
|
|
1357
|
-
entry.bindingOwner !== bindingOwner || entry.segmentId !== segmentId ||
|
|
1358
|
-
messageId !== initial.input.messageId || threadId !== initial.nativeThreadId) {
|
|
1359
|
-
throw failure("The native tool observation belongs to a retired response.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.toolObservationRetired" });
|
|
1360
|
-
}
|
|
1361
|
-
if (!completedToolSet.byName.has(name)) return;
|
|
1362
|
-
await storage.write(entry.id, async transaction => {
|
|
1363
|
-
const metadata = await transaction.readMetadata();
|
|
1364
|
-
const turn = await transaction.readTurn(initial.turnId);
|
|
1365
|
-
const saved = turn?.metadata?.runtime;
|
|
1366
|
-
if (metadata.runtime.engine !== engine || metadata.runtime.segmentId !== segmentId ||
|
|
1367
|
-
conversationNativeIdentity(metadata.runtime) !== threadId || submittedMessage(turn || {})?.messageId !== messageId ||
|
|
1368
|
-
saved?.engine !== engine || saved.segmentId !== segmentId || saved.completedEnvelope !== true ||
|
|
1369
|
-
typeof saved.nativeToolAttempt !== "boolean" || saved.status !== "running" || saved.supersededBy ||
|
|
1370
|
-
entry.active !== active || current !== initial || initial.finished || initial.sealed || active.controller.signal.aborted) {
|
|
1371
|
-
throw failure("The native tool observation lost its exact admitted receipt.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.toolObservationReceiptLost" });
|
|
1372
|
-
}
|
|
1373
|
-
await transaction.updateTurnMetadata(initial.turnId, { runtime: { ...saved, nativeToolAttempt: true } });
|
|
1374
|
-
});
|
|
1375
|
-
initial.nativeToolAttempt = true;
|
|
1376
|
-
}
|
|
1377
|
-
} : {}),
|
|
1378
|
-
...(engine === "opencode" || entry.conversation && ["codex", "claude"].includes(engine) ? {
|
|
1379
|
-
async finishIfCurrent(isCurrent) {
|
|
1380
|
-
// A reserved Send may still be preparing when the native turn ends.
|
|
1381
|
-
// Join that same admission outside the native notification queue.
|
|
1382
|
-
for (;;) {
|
|
1383
|
-
const steering = active.steering;
|
|
1384
|
-
await steering;
|
|
1385
|
-
if (active.steering && active.steering !== steering) continue;
|
|
1386
|
-
if (!isCurrent()) return false;
|
|
1387
|
-
active.finishing = true;
|
|
1388
|
-
return true;
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
} : {}),
|
|
1392
|
-
onMessage, onNativeTurn,
|
|
1393
|
-
onEvent: async event => {
|
|
1394
|
-
if (event.type === "message-complete") {
|
|
1395
|
-
const request = messageOwners.get(event.messageId);
|
|
1396
|
-
if (!request) return;
|
|
1397
|
-
const messageId = `${request.turnId}:${event.messageId}`;
|
|
1398
|
-
streams.complete(entry.id, messageId);
|
|
1399
|
-
return emit(entry, { ...event, turnId: request.turnId, messageId, streaming: streams.read(entry.id) });
|
|
1400
|
-
}
|
|
1401
|
-
if (event.type === "goal") {
|
|
1402
|
-
if (!driver.canonicalTranscript && event.goal?.status === "active" && !goalOwner) {
|
|
1403
|
-
await access(current.context, entry.id, "goal", entry);
|
|
1404
|
-
await storage.write(entry.id, async transaction => {
|
|
1405
|
-
const turn = await transaction.readTurn(current.turnId);
|
|
1406
|
-
await transaction.updateTurnMetadata(current.turnId, { runtime: { ...turn.metadata.runtime, goalMessageId: current.input.messageId } });
|
|
1407
|
-
});
|
|
1408
|
-
goalOwner = current;
|
|
1409
|
-
current.goalMessageId = current.input.messageId;
|
|
1410
|
-
}
|
|
1411
|
-
entry.goal = event.goal;
|
|
1412
|
-
}
|
|
1413
|
-
return event.type === "phase" ? setPhase(entry, event.phase, current.turnId) : emit(entry, { ...event, turnId: current.turnId });
|
|
1414
|
-
}
|
|
1415
|
-
};
|
|
1416
|
-
if (engine === "api") {
|
|
1417
|
-
const readAttachments = attachmentReader(initial);
|
|
1418
|
-
const files = await readAttachments(initial.input.attachments);
|
|
1419
|
-
initial.input.attachments = files.attachments;
|
|
1420
|
-
const selectedHistory = apiHistory ? structuredClone(await apiHistory({ context: initial.context })) : undefined;
|
|
1421
|
-
let requestHistory;
|
|
1422
|
-
if (selectedHistory !== undefined) {
|
|
1423
|
-
if (!Array.isArray(selectedHistory)) throw new TypeError("API request history must be an array of authored user/assistant context rows.");
|
|
1424
|
-
requestHistory = [];
|
|
1425
|
-
for (const message of selectedHistory) {
|
|
1426
|
-
if (!["user", "assistant"].includes(message?.role) || typeof message.content !== "string") {
|
|
1427
|
-
throw new TypeError("API request history requires user/assistant roles and text content.");
|
|
1428
|
-
}
|
|
1429
|
-
const attachmentIds = conversationAttachmentIds(message.attachmentIds);
|
|
1430
|
-
if (message.role !== "user" && attachmentIds.length) throw new TypeError("Only authored user context may reference attachments.");
|
|
1431
|
-
const previousFiles = await readAttachments(attachmentIds.map(attachmentId => ({ attachmentId })));
|
|
1432
|
-
requestHistory.push({ role: message.role, content: previousFiles.content.length
|
|
1433
|
-
? [{ type: "text", text: message.content }, ...previousFiles.content] : message.content });
|
|
1434
|
-
}
|
|
1435
|
-
} else {
|
|
1436
|
-
for (const turn of history) {
|
|
1437
|
-
const message = submittedMessage(turn);
|
|
1438
|
-
if (message?.attachments?.length) message.content = (await readAttachments(message.attachments)).content;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
const seen = conversationHistoryVersions(activeHistory);
|
|
1442
|
-
const priorFiles = await readAttachments(metadata.runtime.continuityAttachments || []);
|
|
1443
|
-
const input = { ...initial.input, text: conversationRequestText(initial.input), contextText: "",
|
|
1444
|
-
content: [...priorFiles.content, ...files.content] };
|
|
1445
|
-
await storage.write(entry.id, async transaction => {
|
|
1446
|
-
const metadata = await transaction.readMetadata();
|
|
1447
|
-
metadata.runtime.request.seen = seen;
|
|
1448
|
-
metadata.runtime.request.attachments = files.attachments;
|
|
1449
|
-
await transaction.writeMetadata(metadata);
|
|
1450
|
-
});
|
|
1451
|
-
const completion = await entry.provider.run({ ...providerInput, input, signal: active.controller.signal, continuity, history, requestHistory,
|
|
1452
|
-
beforeDispatch: () => beforeDispatch(initial), accept: () => admit(initial, seen) });
|
|
1453
|
-
if (completion?.metadata && Object.keys(completion.metadata).length) {
|
|
1454
|
-
await storage.write(entry.id, transaction => transaction.updateTurnMetadata(initial.turnId, { completion: completion.metadata }));
|
|
1455
|
-
}
|
|
1456
|
-
} else {
|
|
1457
|
-
await deliverNative(initial, input => entry.provider.run({ ...providerInput, ...input }));
|
|
1458
|
-
}
|
|
1459
|
-
active.controller.signal.throwIfAborted();
|
|
1460
|
-
} catch (error) { problem = error; }
|
|
1461
|
-
active.finishing = true;
|
|
1462
|
-
await active.steering;
|
|
1463
|
-
await toolWork;
|
|
1464
|
-
problem ||= toolFailure;
|
|
1465
|
-
clearTimeout(active.timer);
|
|
1466
|
-
if (problem?.cleanupFailed) {
|
|
1467
|
-
entry.executionFailure = problem;
|
|
1468
|
-
entry.cleanupTurnId = current.turnId;
|
|
1469
|
-
}
|
|
1470
|
-
const status = entry.executionFailure ? "interrupted" : active.timedOut ? "failed" : active.controller.signal.aborted ? "cancelled"
|
|
1471
|
-
: problem?.status === "interrupted" ? "interrupted" : problem ? "failed" : initial.recovered?.status || "complete";
|
|
1472
|
-
const error = entry.executionFailure?.message || (active.timedOut ? "The conversation request exceeded its time limit."
|
|
1473
|
-
: status === "cancelled" ? "Work stopped." : problem?.message || active.steerError || initial.recovered?.error || "");
|
|
1474
|
-
entry.saveProgress = async () => {
|
|
1475
|
-
if (entry.conversation) {
|
|
1476
|
-
if (applicationTools) await toolStorage.write(entry.id, async transaction => {
|
|
1477
|
-
for (const request of active.requests.values()) {
|
|
1478
|
-
if (request.accepted && request.tools?.records().length) await saveTools(entry, transaction, request, request.tools.records());
|
|
1479
|
-
}
|
|
1480
|
-
});
|
|
1481
|
-
// Only opted-in application receipts are saved above. Native output,
|
|
1482
|
-
// checkpoints and status remain with the original run owner and sender.
|
|
1483
|
-
entry.error = error;
|
|
1484
|
-
entry.storageFailure = null;
|
|
1485
|
-
entry.saveProgress = null;
|
|
1486
|
-
return { type: "settled", turnId: current.turnId, status, error, ...stateErrorFields(error) };
|
|
1487
|
-
}
|
|
1488
|
-
// Preserve the common storage-retry facility by retrying the original
|
|
1489
|
-
// failed rejection save, never by deleting or rebuilding its frozen draft.
|
|
1490
|
-
for (const [messageId, save] of rejectionSaves) {
|
|
1491
|
-
const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
|
|
1492
|
-
if (runtime.request?.messageId === messageId) await save();
|
|
1493
|
-
rejectionSaves.delete(messageId);
|
|
1494
|
-
}
|
|
1495
|
-
await storage.write(entry.id, async transaction => {
|
|
1496
|
-
const metadata = await transaction.readMetadata();
|
|
1497
|
-
const written = [];
|
|
1498
|
-
for (const request of active.requests.values()) {
|
|
1499
|
-
if (request.accepted && !request.finished) {
|
|
1500
|
-
const versions = await writeProgress(transaction, request, status, error);
|
|
1501
|
-
if (engine === "api") Object.assign(metadata.runtime.seen, versions);
|
|
1502
|
-
else written.push(await transaction.readTurn(request.turnId));
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
if (!initial.accepted && metadata.runtime.request?.messageId === initial.input.messageId) {
|
|
1506
|
-
if (engine !== "api" || problem?.delivery === "uncertain") metadata.runtime.request.error = error;
|
|
1507
|
-
else delete metadata.runtime.request;
|
|
1508
|
-
}
|
|
1509
|
-
await transaction.writeMetadata(metadata);
|
|
1510
|
-
if (written.length) await rememberNative(entry, transaction, written);
|
|
1511
|
-
});
|
|
1512
|
-
streams.clear(entry.id);
|
|
1513
|
-
entry.error = error;
|
|
1514
|
-
entry.storageFailure = null;
|
|
1515
|
-
entry.saveProgress = null;
|
|
1516
|
-
return { type: "settled", turnId: current.turnId, status, error: entry.error, ...stateErrorFields(entry.error) };
|
|
1517
|
-
};
|
|
1518
|
-
try { await entry.saveProgress(); }
|
|
1519
|
-
catch (saveError) {
|
|
1520
|
-
entry.error = "Conversation progress could not be saved. Restore access to its storage before continuing.";
|
|
1521
|
-
entry.storageFailure = failure(entry.error, "conversation_storage_unavailable", 503);
|
|
1522
|
-
entry.storageFailure.cause = saveError;
|
|
1523
|
-
problem ||= saveError;
|
|
1524
|
-
}
|
|
1525
|
-
if (!initial.admissionSettled) initial.admission.reject(problem || new Error("The engine did not acknowledge the message."));
|
|
1526
|
-
entry.active = null;
|
|
1527
|
-
await setPhase(entry, "", current.turnId);
|
|
1528
|
-
await emit(entry, { type: "settled", turnId: current.turnId, status: entry.storageFailure || entry.executionFailure ? "failed" : status,
|
|
1529
|
-
error: entry.error, ...stateErrorFields(entry.error) });
|
|
189
|
+
// A host publication from the native owner of an open entry. Nothing
|
|
190
|
+
// subscribes to a provider or keeps another observer registry.
|
|
191
|
+
async function publishNative({ namespace, sessionId, event }) {
|
|
192
|
+
const entry = entries.find(namespace);
|
|
193
|
+
if (!entry || entry.id !== sessionId || entry.disposed || !entry.conversation) return;
|
|
194
|
+
await entry.initializing;
|
|
195
|
+
return entry.conversation.commands ? events.publish(entry, event) : entry.provider.publishNative(event);
|
|
1530
196
|
}
|
|
1531
197
|
|
|
1532
|
-
async function
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
return entry.conversation.commands.send(input, context);
|
|
1542
|
-
}
|
|
1543
|
-
if (entry.conversation?.native?.scoped) return entry.provider.sendNative({ input, context });
|
|
1544
|
-
const originalInput = representation === "native" ? input : undefined;
|
|
1545
|
-
const message = originalInput ? { ...input, text: input.displayMessage ?? input.message ?? input.text ?? "" } : input;
|
|
1546
|
-
const admission = await serial(entry, () => admitSend(entry, context, message, origin, goal, originalInput, wakeOptions, deliveryTiming, grant));
|
|
1547
|
-
const result = await (admission.receipt || admission.promise);
|
|
1548
|
-
if (!entry.conversation) return result;
|
|
1549
|
-
if (representation === "native") return result.nativeResult;
|
|
1550
|
-
if (result.error) throw result.error;
|
|
1551
|
-
return result.receipt;
|
|
1552
|
-
} catch (error) {
|
|
1553
|
-
deliveryTiming("failed");
|
|
1554
|
-
throw error;
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
// Called while holding the conversation queue, including Claude's existing
|
|
1559
|
-
// stop-then-Send goal commands. All commands retain the ordinary receipt path.
|
|
1560
|
-
async function admitSend(entry, context, input, origin = "user", goal, originalInput, wakeOptions, deliveryTiming, grant) {
|
|
1561
|
-
deliveryTiming?.("preparation-started");
|
|
1562
|
-
const operation = goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send";
|
|
1563
|
-
await access(context, entry.id, operation, entry, grant);
|
|
1564
|
-
if (entry.cancelledMessages.has(input?.messageId)) {
|
|
1565
|
-
throw Object.assign(failure("This message was cancelled. Send it again as a new message.", "conversation_message_cancelled", 409,
|
|
1566
|
-
{ messageCode: "assistant.conversation.messageCancelled" }), { delivery: "not-sent" });
|
|
1567
|
-
}
|
|
1568
|
-
await entry.conversation?.admission(context, operation);
|
|
1569
|
-
deliveryTiming?.("authorized");
|
|
1570
|
-
if (goal) context?.signal?.throwIfAborted();
|
|
1571
|
-
let suppliedExclusions;
|
|
1572
|
-
if (wakeOptions !== undefined) {
|
|
1573
|
-
if (!completedEnvelope || entry.conversation || origin !== "application" || goal ||
|
|
1574
|
-
!wakeOptions || typeof wakeOptions !== "object" || Array.isArray(wakeOptions) ||
|
|
1575
|
-
Object.keys(wakeOptions).some(name => name !== "excludedMessageIds") ||
|
|
1576
|
-
!Array.isArray(wakeOptions.excludedMessageIds) ||
|
|
1577
|
-
wakeOptions.excludedMessageIds.some(id => typeof id !== "string" || !id.trim() || id.length > 128) ||
|
|
1578
|
-
new Set(wakeOptions.excludedMessageIds).size !== wakeOptions.excludedMessageIds.length) {
|
|
1579
|
-
throw failure("Native history exclusions require a server-owned completed application wake and exact message IDs.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.historyExclusionsInvalid" });
|
|
1580
|
-
}
|
|
1581
|
-
suppliedExclusions = [...wakeOptions.excludedMessageIds];
|
|
1582
|
-
}
|
|
1583
|
-
const attachmentIds = conversationAttachmentIds(input?.attachmentIds);
|
|
1584
|
-
if (!input || typeof input.messageId !== "string" || !input.messageId.trim() || input.messageId.length > 128 ||
|
|
1585
|
-
entry.engine !== "api" && !/^[\w-]{1,128}$/u.test(input.messageId) ||
|
|
1586
|
-
typeof input.text !== "string" || !input.text.trim() && !attachmentIds.length || input.text.length > maximumInput) {
|
|
1587
|
-
throw failure("Bounded text or attachment IDs and a stable messageId are required.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.messageContentRequired" });
|
|
1588
|
-
}
|
|
1589
|
-
if (!originalInput && input.attachments?.length) throw failure("Supply attachmentIds; only the application's resolver can supply file content.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.attachmentIdsOnly" });
|
|
1590
|
-
if (attachmentIds.length && !capabilities(entry).attachments) throw failure("File attachments are not configured for this conversation.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.attachmentsNotConfigured" });
|
|
1591
|
-
if (input.steer !== undefined && typeof input.steer !== "boolean") throw failure("steer must be a boolean.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.steerNotBoolean" });
|
|
1592
|
-
if (input.steer && !entry.driver.capabilities.steering) throw failure("This connection does not support steering.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.steeringUnsupported" });
|
|
1593
|
-
let data;
|
|
1594
|
-
if (input.data !== undefined) {
|
|
1595
|
-
try {
|
|
1596
|
-
if (!input.data || typeof input.data !== "object" || Array.isArray(input.data)) throw new TypeError();
|
|
1597
|
-
const encoded = JSON.stringify(input.data);
|
|
1598
|
-
if (input.text.length + encoded.length > maximumInput) throw new TypeError();
|
|
1599
|
-
data = JSON.parse(encoded);
|
|
1600
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) throw new TypeError();
|
|
1601
|
-
} catch {
|
|
1602
|
-
throw failure("Application data must be a JSON object within the conversation input limit.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.applicationDataInvalid" });
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
const message = { messageId: input.messageId, text: input.text.trim(), origin,
|
|
1606
|
-
attachments: attachmentIds.map(attachmentId => ({ attachmentId })), ...(data !== undefined ? { data } : {}), ...(goal ? { goal } : {}) };
|
|
1607
|
-
if (entry.storageFailure) throw entry.storageFailure;
|
|
1608
|
-
if (entry.executionFailure) throw entry.executionFailure;
|
|
1609
|
-
if (goal && goal.expectedSegmentId !== entry.segmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalConversationChanged" });
|
|
1610
|
-
const pending = entry.active?.requests.get(message.messageId);
|
|
1611
|
-
if (pending && entry.engine !== "api" && !pending.admissionSettled) {
|
|
1612
|
-
if (suppliedExclusions && !isDeepStrictEqual(pending.excludedMessageIds || [], suppliedExclusions)) throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdExclusionsConflict" });
|
|
1613
|
-
if (!entry.conversation && !sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
1614
|
-
return { promise: pending.admission.promise };
|
|
1615
|
-
}
|
|
1616
|
-
const existing = await findSubmittedTurn(entry, message.messageId);
|
|
1617
|
-
deliveryTiming?.("history-read");
|
|
1618
|
-
const nativeState = entry.engine !== "api" && (await readState(entry));
|
|
1619
|
-
deliveryTiming?.("state-read");
|
|
1620
|
-
const nativePending = nativeState?.request;
|
|
1621
|
-
if (existing) {
|
|
1622
|
-
const previous = submittedMessage(existing);
|
|
1623
|
-
if (!entry.conversation && !sameRequest(previous, message)) {
|
|
1624
|
-
throw failure("This messageId already belongs to different content.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
1625
|
-
}
|
|
1626
|
-
if (!entry.conversation && !nativePending) return { receipt: { status: "accepted", messageId: message.messageId, turnId: existing.turnId, origin, duplicate: true } };
|
|
1627
|
-
}
|
|
1628
|
-
if (pending && !entry.conversation) {
|
|
1629
|
-
if (!sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
1630
|
-
if (suppliedExclusions && !isDeepStrictEqual(pending.excludedMessageIds || [], suppliedExclusions)) throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdExclusionsConflict" });
|
|
1631
|
-
return { promise: pending.admission.promise };
|
|
1632
|
-
}
|
|
1633
|
-
const working = entry.active;
|
|
1634
|
-
if (entry.conversation && ["codex", "opencode", "claude"].includes(entry.engine) && ["send", "goal"].includes(operation) && working?.finishing) {
|
|
1635
|
-
// No native dispatch has occurred for this request. Finish the previous
|
|
1636
|
-
// waiter before reserving the same authored message or goal request.
|
|
1637
|
-
await working.done;
|
|
1638
|
-
return admitSend(entry, context, input, origin, goal, originalInput, wakeOptions, deliveryTiming, grant);
|
|
1639
|
-
}
|
|
1640
|
-
const nativeSteering = !working && (nativeState.nativeTurn?.active || entry.driver.canonicalTranscript && nativeState.binding?.codexAppServerRun &&
|
|
1641
|
-
codexAppServerTurnStateFromAgentRun(nativeState.binding.codexAppServerRun).active);
|
|
1642
|
-
if (completedEnvelope && entry.engine === "claude" &&
|
|
1643
|
-
(working?.request.completedEnvelope || nativePending?.completedEnvelope === true || origin === "application" && !goal) &&
|
|
1644
|
-
(working || input.steer || nativeSteering)) {
|
|
1645
|
-
throw failure("Completed Claude responses cannot share a native steering turn. Wait for the response or stop it.", "conversation_not_steerable", 409, { messageCode: "assistant.conversation.completedClaudeNotSteerable" });
|
|
1646
|
-
}
|
|
1647
|
-
if (working && !input.steer && !goal && !entry.conversation) throw failure("Wait for this answer or use steer to send another instruction.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForAnswer" });
|
|
1648
|
-
if (input.steer && (!working && !nativeSteering || working?.finishing) && !(nativePending?.attempted || nativePending?.inspectionOnly)) {
|
|
1649
|
-
const previousState = nativeState?.nativeTurn?.runState || nativeState?.nativeTurn?.state ||
|
|
1650
|
-
(await lastTranscriptTurn(entry))?.metadata?.runtime?.status;
|
|
1651
|
-
const completed = working ? !working.controller.signal.aborted && !working.timedOut : ["complete", "completed"].includes(previousState);
|
|
1652
|
-
throw Object.assign(failure("There is no active native turn to steer. Send a new message.",
|
|
1653
|
-
completed ? "conversation_steer_turn_finished" : "conversation_not_steerable", 409, { messageCode: "assistant.conversation.noActiveTurnToSteer" }), { delivery: "not-sent" });
|
|
1654
|
-
}
|
|
1655
|
-
if (goal && working?.finishing) throw failure("Wait for the current turn to finish saving before starting the goal.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForTurnSaving" });
|
|
1656
|
-
if (working && (!working.request.admissionSettled || working.steering)) throw failure("Wait for the pending instruction to be admitted before steering again.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForInstructionAdmission" });
|
|
1657
|
-
const barrier = working ? Promise.withResolvers() : null;
|
|
1658
|
-
if (barrier) working.steering = barrier.promise;
|
|
1659
|
-
const release = () => {
|
|
1660
|
-
barrier?.resolve();
|
|
1661
|
-
if (working && working.steering === barrier?.promise) working.steering = null;
|
|
1662
|
-
};
|
|
1663
|
-
let configuration;
|
|
1664
|
-
let markedResponse = false;
|
|
1665
|
-
let nativeToolAttempt;
|
|
1666
|
-
let excludedMessageIds;
|
|
1667
|
-
let requestAt = new Date().toISOString();
|
|
1668
|
-
// The bound state was read above unless this is an API conversation.
|
|
1669
|
-
try { configuration = entry.conversation ? (nativeState || await readState(entry)).configuration : await storage.write(entry.id, async transaction => {
|
|
1670
|
-
const metadata = await transaction.readMetadata();
|
|
1671
|
-
if (metadata.runtime.replacement) throw failure("Finish the pending native replacement before sending another message.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementPendingBeforeSend" });
|
|
1672
|
-
if (retainedSegments(metadata.runtime).some(segment => segment.segmentId !== entry.segmentId && segment.request?.messageId === message.messageId)) {
|
|
1673
|
-
throw failure("This message has uncertain delivery in a retained conversation. Select that engine and check its receipt before submitting it again.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.retainedDeliveryUncertain" });
|
|
1674
|
-
}
|
|
1675
|
-
if (metadata.runtime.request) {
|
|
1676
|
-
if (suppliedExclusions && metadata.runtime.request.messageId !== message.messageId) {
|
|
1677
|
-
throw failure("Check delivery of the previous instruction before preparing another completed response.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.previousInstructionUnchecked" });
|
|
1678
|
-
}
|
|
1679
|
-
if (entry.engine === "api") throw failure("The previous submission has uncertain delivery. It will not be resent automatically.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.previousSubmissionUncertain" });
|
|
1680
|
-
if (metadata.runtime.request.messageId === message.messageId) {
|
|
1681
|
-
if (!sameRequest(metadata.runtime.request, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
1682
|
-
if (suppliedExclusions && !isDeepStrictEqual(metadata.runtime.request.excludedMessageIds || [], suppliedExclusions)) {
|
|
1683
|
-
throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdExclusionsConflict" });
|
|
1684
|
-
}
|
|
1685
|
-
requestAt = metadata.runtime.request.at;
|
|
1686
|
-
}
|
|
1687
|
-
} else metadata.runtime.request = { ...message, at: requestAt,
|
|
1688
|
-
...(completedEnvelope && origin === "application" && !goal ? { completedEnvelope: true, excludedMessageIds: suppliedExclusions || [],
|
|
1689
|
-
...(entry.engine === "claude" ? { nativeToolAttempt: false } : {}) } : {}),
|
|
1690
|
-
...(entry.engine !== "api" ? { attempted: false } : {}), ...(input.steer ? { steering: true } : {}) };
|
|
1691
|
-
markedResponse = origin === "application" && !goal && metadata.runtime.request?.messageId === message.messageId &&
|
|
1692
|
-
metadata.runtime.request.completedEnvelope === true;
|
|
1693
|
-
if (markedResponse) excludedMessageIds = structuredClone(metadata.runtime.request.excludedMessageIds || []);
|
|
1694
|
-
if (entry.engine === "claude" && markedResponse && typeof metadata.runtime.request.nativeToolAttempt === "boolean") nativeToolAttempt = metadata.runtime.request.nativeToolAttempt;
|
|
1695
|
-
await transaction.writeMetadata(metadata);
|
|
1696
|
-
return metadata.runtime.configuration;
|
|
1697
|
-
}); } catch (error) { release(); throw error; }
|
|
1698
|
-
deliveryTiming?.("request-saved");
|
|
1699
|
-
const request = { input: message, originalInput, operation, context, at: requestAt, deliveryTiming,
|
|
1700
|
-
...(markedResponse ? { completedEnvelope: true, excludedMessageIds } : {}),
|
|
1701
|
-
...(typeof nativeToolAttempt === "boolean" ? { nativeToolAttempt } : {}), steering: Boolean(working || input.steer && nativeSteering),
|
|
1702
|
-
admission: Promise.withResolvers(), progress: new Map() };
|
|
1703
|
-
request.admission.promise.catch(() => {});
|
|
1704
|
-
if (working) {
|
|
1705
|
-
working.requests.set(message.messageId, request);
|
|
1706
|
-
working.submit(request).catch(error => request.admission.reject(error)).finally(release);
|
|
1707
|
-
return { promise: request.admission.promise };
|
|
1708
|
-
}
|
|
1709
|
-
const active = { request, requests: new Map([[message.messageId, request]]), controller: new AbortController() };
|
|
1710
|
-
entry.active = active;
|
|
1711
|
-
entry.error = "";
|
|
1712
|
-
if (limits.timeoutMs !== undefined) active.timer = setTimeout(() => {
|
|
1713
|
-
active.timedOut = true;
|
|
1714
|
-
active.controller.abort(new Error("The conversation request exceeded its time limit."));
|
|
1715
|
-
}, limits.timeoutMs);
|
|
1716
|
-
active.done = perform(entry, active, configuration);
|
|
1717
|
-
return { promise: request.admission.promise };
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
async function stop(entry, context, input, representation) {
|
|
1721
|
-
if (representation === "native") return entry.provider.cancel({ context, input, representation });
|
|
1722
|
-
const active = entry.active;
|
|
1723
|
-
if (!active && entry.executionFailure) {
|
|
1724
|
-
await entry.provider.cancel();
|
|
1725
|
-
await retrySave(entry);
|
|
1726
|
-
if (entry.cleanupTurnId && !entry.conversation) await storage.write(entry.id, async transaction => {
|
|
1727
|
-
const turn = await transaction.readTurn(entry.cleanupTurnId);
|
|
1728
|
-
await transaction.updateTurnMetadata(entry.cleanupTurnId,
|
|
1729
|
-
{ runtime: { ...turn.metadata.runtime, status: "cancelled", error: "Work stopped." } });
|
|
1730
|
-
});
|
|
1731
|
-
entry.executionFailure = null;
|
|
1732
|
-
entry.cleanupTurnId = null;
|
|
1733
|
-
entry.error = "Work stopped.";
|
|
1734
|
-
return { stopped: true };
|
|
1735
|
-
}
|
|
1736
|
-
if (!active) {
|
|
1737
|
-
if (!entry.conversation && !entry.driver.canonicalTranscript) return { stopped: false };
|
|
1738
|
-
const metadata = { runtime: await readState(entry) };
|
|
1739
|
-
const result = await entry.provider.cancel({ configuration: metadata.runtime.configuration, context });
|
|
1740
|
-
return { stopped: result?.interrupted === true || result?.operationOutcome === "interrupted" };
|
|
1741
|
-
}
|
|
1742
|
-
active.controller.abort(new Error("Work stopped."));
|
|
1743
|
-
await active.done;
|
|
1744
|
-
if (entry.executionFailure) throw entry.executionFailure;
|
|
1745
|
-
return { stopped: true };
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
// Cancel of one message in a runtime-owned conversation never waits for the
|
|
1749
|
-
// conversation queue. The message is recorded as cancelled at once, so a send
|
|
1750
|
-
// of it still queued, or arriving later, is refused as not sent; its own
|
|
1751
|
-
// submission stops, ending any turn it started. Its unconfirmed request or
|
|
1752
|
-
// pending native attempt is discarded afterwards, in queue order. A message
|
|
1753
|
-
// already accepted stays history.
|
|
1754
|
-
async function cancelDelivery(entry, context, messageId, representation) {
|
|
1755
|
-
entry.cancelledMessages.delete(messageId);
|
|
1756
|
-
entry.cancelledMessages.add(messageId);
|
|
1757
|
-
if (entry.cancelledMessages.size > MAX_CANCELLED_MESSAGES) entry.cancelledMessages.delete(entry.cancelledMessages.values().next().value);
|
|
1758
|
-
const interrupted = Boolean(entry.active?.requests.has(messageId));
|
|
1759
|
-
if (interrupted) await stop(entry, context, undefined, representation);
|
|
1760
|
-
void serial(entry, async () => {
|
|
1761
|
-
await access(context, entry.id, "cancel", entry);
|
|
1762
|
-
if (entry.conversation) {
|
|
1763
|
-
await nativeChangeover(entry, context, null, () => { throw new Error("Cancel never sends a message."); }).discard({ messageId });
|
|
1764
|
-
return;
|
|
1765
|
-
}
|
|
1766
|
-
await storage.write(entry.id, async transaction => {
|
|
1767
|
-
const metadata = await transaction.readMetadata();
|
|
1768
|
-
if (metadata.runtime.request?.messageId !== messageId) return;
|
|
1769
|
-
delete metadata.runtime.request;
|
|
1770
|
-
await transaction.writeMetadata(metadata);
|
|
1771
|
-
});
|
|
1772
|
-
}).catch(() => {});
|
|
1773
|
-
return { ok: true, cancelled: true, messageId, interrupted };
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
async function readGoal(entry, context, representation) {
|
|
1777
|
-
await access(context, entry.id, "readGoal", entry);
|
|
1778
|
-
if (!capabilities(entry).goals) return null;
|
|
1779
|
-
const configuration = representation === "native" ? undefined : (await readState(entry)).configuration;
|
|
1780
|
-
const result = await entry.provider.readGoal({ context, configuration });
|
|
1781
|
-
if (entry.conversation && representation !== "native" && result.nativeResult.status === "unavailable") {
|
|
1782
|
-
throw failure("The native goal is unavailable. Try reading its status again.", "conversation_goal_unavailable", 503);
|
|
1783
|
-
}
|
|
1784
|
-
entry.goal = entry.conversation ? result.goal : result;
|
|
1785
|
-
return representation === "native" ? result.nativeResult : structuredClone(entry.goal);
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
async function updateGoal(entry, context, input, representation, retryFinishedTurn = true) {
|
|
1789
|
-
await access(context, entry.id, "goal", entry);
|
|
1790
|
-
context?.signal?.throwIfAborted();
|
|
1791
|
-
if (!capabilities(entry).goals) throw failure("This connection does not support native goals.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.goalsUnsupported" });
|
|
1792
|
-
if (representation === "native") {
|
|
1793
|
-
const { operation } = await serial(entry, async () => {
|
|
1794
|
-
await access(context, entry.id, "goal", entry);
|
|
1795
|
-
// The original goal owner has its own admission. Its native message
|
|
1796
|
-
// acknowledgement must not hold the common queue ahead of Stop.
|
|
1797
|
-
return { operation: entry.provider.updateGoal({ context, input, representation }) };
|
|
1798
|
-
});
|
|
1799
|
-
const result = await operation;
|
|
1800
|
-
if (result.nativeResult.ok !== false) {
|
|
1801
|
-
entry.goal = result.goal;
|
|
1802
|
-
await emit(entry, { type: "goal", goal: entry.goal });
|
|
1803
|
-
}
|
|
1804
|
-
return result.nativeResult;
|
|
1805
|
-
}
|
|
1806
|
-
if (!input || !["set", "resume", "pause", "cancel"].includes(input.action) ||
|
|
1807
|
-
(typeof input.expectedSegmentId !== "string" && !(entry.conversation && input.expectedSegmentId === null)) ||
|
|
1808
|
-
input.expectedGoalId != null && (typeof input.expectedGoalId !== "string" || !input.expectedGoalId)) {
|
|
1809
|
-
throw failure("A goal action and current conversation segment are required.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalActionRequired" });
|
|
1810
|
-
}
|
|
1811
|
-
const starting = input.action === "set" || input.action === "resume";
|
|
1812
|
-
const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId, expectedGoalId: input.expectedGoalId || null };
|
|
1813
|
-
if (starting) {
|
|
1814
|
-
if (input.action === "set") {
|
|
1815
|
-
if (input.tokenBudget != null && !entry.driver.capabilities.goalBudgets) throw failure("This connection does not support goal token budgets.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.goalTokenBudgetsUnsupported" });
|
|
1816
|
-
if (typeof input.objective !== "string" || !input.objective.trim() || input.objective.length > 4000 ||
|
|
1817
|
-
input.tokenBudget != null && (!Number.isSafeInteger(input.tokenBudget) || input.tokenBudget < 1)) {
|
|
1818
|
-
throw failure("A bounded objective and optional positive token budget are required.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalObjectiveInvalid" });
|
|
1819
|
-
}
|
|
1820
|
-
goal.objective = input.objective.trim();
|
|
1821
|
-
if (input.tokenBudget != null) goal.tokenBudget = input.tokenBudget;
|
|
1822
|
-
} else if (!goal.expectedGoalId) throw failure("Select the goal to resume.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalToResumeRequired" });
|
|
1823
|
-
}
|
|
1824
|
-
if (!starting && !input.expectedGoalId) throw failure("Select the goal to stop.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalToStopRequired" });
|
|
1825
|
-
const delivery = entry.driver.capabilities.goalCommands?.[input.action]?.delivery;
|
|
1826
|
-
if (!["message", "control"].includes(delivery)) throw failure("This connection does not support that goal command.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.goalCommandUnsupported" });
|
|
1827
|
-
const messageCommand = delivery === "message";
|
|
1828
|
-
if (messageCommand && (typeof input.messageId !== "string" || !/^[\w-]{1,128}$/u.test(input.messageId))) {
|
|
1829
|
-
throw failure("A stable messageId is required for this goal command.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.goalMessageIdRequired" });
|
|
1830
|
-
}
|
|
1831
|
-
if (conversationAttachmentIds(input.attachmentIds).length || input.attachments?.length) {
|
|
1832
|
-
throw failure("This engine's native goal commands do not accept attachments.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.goalAttachmentsUnsupported" });
|
|
1833
|
-
}
|
|
1834
|
-
const result = await serial(entry, async () => {
|
|
1835
|
-
await access(context, entry.id, "goal", entry);
|
|
1836
|
-
if (entry.conversation) await readState(entry);
|
|
1837
|
-
if (entry.segmentId !== input.expectedSegmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalConversationChanged" });
|
|
1838
|
-
if (entry.conversation && input.expectedSegmentId === null && input.action !== "set") {
|
|
1839
|
-
throw failure("This command requires an existing native conversation.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalNeedsNativeConversation" });
|
|
1840
|
-
}
|
|
1841
|
-
if (entry.storageFailure) throw entry.storageFailure;
|
|
1842
|
-
if (entry.executionFailure) throw entry.executionFailure;
|
|
1843
|
-
if (messageCommand) {
|
|
1844
|
-
const history = await readTranscript(entry);
|
|
1845
|
-
const existing = history.find(turn => submittedMessage(turn)?.messageId === input.messageId);
|
|
1846
|
-
if (existing) {
|
|
1847
|
-
if (!entry.conversation && !isDeepStrictEqual(submittedMessage(existing).goal, goal)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
1848
|
-
return { receipt: { status: "accepted", messageId: input.messageId, turnId: existing.turnId, origin: "user", duplicate: true } };
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
if (entry.active && !entry.active.request.accepted) throw failure("Wait for the goal command to be admitted.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForGoalCommand" });
|
|
1852
|
-
const metadata = { runtime: await readState(entry) };
|
|
1853
|
-
if (metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly)) {
|
|
1854
|
-
throw failure("Check the uncertain command's delivery before changing the goal.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.goalCommandDeliveryUncertain" });
|
|
1855
|
-
}
|
|
1856
|
-
if (entry.driver.canonicalTranscript && metadata.runtime.binding?.goalRequest) {
|
|
1857
|
-
throw failure("A goal command from an earlier runtime has unresolved delivery. Inspect its saved state before changing the goal.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.earlierGoalCommandUnresolved" });
|
|
1858
|
-
}
|
|
1859
|
-
if (starting && (metadata.runtime.replacement || !entry.conversation && pendingSuccessor(metadata.runtime) || metadata.runtime.lastEngine !== entry.engine)) {
|
|
1860
|
-
throw failure("Send a message to catch this AI up before starting or resuming its goal.", "conversation_replacement_briefing_pending", 409, { messageCode: "assistant.conversation.briefingBeforeGoal" });
|
|
1861
|
-
}
|
|
1862
|
-
// Definitions keep native thread discovery stable. A control has no
|
|
1863
|
-
// admitted request, application-tool actor or executor of its own.
|
|
1864
|
-
const tools = catalog && entry.driver.toolDiscovery
|
|
1865
|
-
? { schemas: catalog.resolveToolSet(context, { discoveryOnly: true }).tools.map(catalog.toOpenAiToolSchema) } : undefined;
|
|
1866
|
-
if (entry.conversation) {
|
|
1867
|
-
let receipt;
|
|
1868
|
-
// Preserve the original goal owner's acknowledgement wait outside the
|
|
1869
|
-
// invocation queue so the same Stop command can still interrupt it.
|
|
1870
|
-
const operation = entry.provider.updateGoal({ context, configuration: metadata.runtime.configuration,
|
|
1871
|
-
input: { ...input, ...goal }, representation, tools, interrupt: () => stop(entry, context),
|
|
1872
|
-
async send({ message }, { threadId }) {
|
|
1873
|
-
const admission = await serial(entry, async () => {
|
|
1874
|
-
const state = await readState(entry);
|
|
1875
|
-
const selectedSegment = `${entry.engine}:${threadId}`;
|
|
1876
|
-
if (!threadId || state.segmentId !== selectedSegment ||
|
|
1877
|
-
input.expectedSegmentId !== null && input.expectedSegmentId !== selectedSegment) {
|
|
1878
|
-
throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalConversationChanged" });
|
|
1879
|
-
}
|
|
1880
|
-
return admitSend(entry, context, { messageId: input.messageId, text: message }, "user",
|
|
1881
|
-
{ ...goal, expectedSegmentId: selectedSegment });
|
|
1882
|
-
});
|
|
1883
|
-
const accepted = await (admission.receipt || admission.promise);
|
|
1884
|
-
if (accepted.error) throw accepted.error;
|
|
1885
|
-
receipt = accepted.receipt;
|
|
1886
|
-
}
|
|
1887
|
-
}).then(async updated => {
|
|
1888
|
-
if (updated.nativeResult.ok === false) throw Object.assign(new Error(updated.nativeResult.error), updated.nativeResult);
|
|
1889
|
-
entry.goal = updated.goal;
|
|
1890
|
-
await emit(entry, { type: "goal", goal: entry.goal });
|
|
1891
|
-
return receipt ? { receipt } : { goal: structuredClone(entry.goal) };
|
|
1892
|
-
});
|
|
1893
|
-
return { operation };
|
|
1894
|
-
}
|
|
1895
|
-
let delivery;
|
|
1896
|
-
const updated = await entry.provider.updateGoal({ context, configuration: metadata.runtime.configuration, input: { ...input, ...goal }, tools,
|
|
1897
|
-
interrupt: () => stop(entry, context),
|
|
1898
|
-
async send({ message }) {
|
|
1899
|
-
delivery = await admitSend(entry, context, { messageId: input.messageId, text: message }, "user", goal);
|
|
1900
|
-
}
|
|
1901
|
-
});
|
|
1902
|
-
// Release the queue once Send is reserved, just as an ordinary message
|
|
1903
|
-
// does. Stop remains available while native acknowledgement is pending.
|
|
1904
|
-
if (delivery) return delivery;
|
|
1905
|
-
entry.goal = updated;
|
|
1906
|
-
await emit(entry, { type: "goal", goal: entry.goal });
|
|
1907
|
-
return { goal: structuredClone(entry.goal) };
|
|
1908
|
-
});
|
|
1909
|
-
try {
|
|
1910
|
-
const completed = result.operation ? await result.operation : result;
|
|
1911
|
-
return await (completed.receipt || completed.promise || completed.goal);
|
|
1912
|
-
} catch (error) {
|
|
1913
|
-
if (!retryFinishedTurn || !entry.conversation || !starting || !messageCommand ||
|
|
1914
|
-
error.code !== "conversation_steer_turn_finished" || error.delivery !== "not-sent") throw error;
|
|
1915
|
-
// Completion can retire Claude's generation during asynchronous native
|
|
1916
|
-
// preparation. Its exact pre-write rejection permits one re-admission of
|
|
1917
|
-
// this same goal/message ID, after the original cleanup and save finish.
|
|
1918
|
-
await entry.active?.done;
|
|
1919
|
-
return updateGoal(entry, context, input, representation, false);
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
async function retrySave(entry) {
|
|
1924
|
-
if (!entry.storageFailure || !entry.saveProgress) return { saved: false };
|
|
1925
|
-
const event = await entry.saveProgress();
|
|
1926
|
-
await emit(entry, event);
|
|
1927
|
-
return { saved: true };
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
async function recoverDelivery(entry, context, messageId, { observation: suppliedObservation, allowActive = false } = {}) {
|
|
1931
|
-
await access(context, entry.id, "inspectDelivery", entry);
|
|
1932
|
-
if (entry.active && !allowActive) throw failure("Wait for the current submission before checking delivery.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForSubmission" });
|
|
1933
|
-
if (entry.storageFailure) throw entry.storageFailure;
|
|
1934
|
-
if (entry.executionFailure) throw entry.executionFailure;
|
|
1935
|
-
const history = await readTranscript(entry);
|
|
1936
|
-
const existing = history.find(turn => submittedMessage(turn)?.messageId === messageId);
|
|
1937
|
-
const receipt = existing && { status: "accepted", messageId, turnId: existing.turnId, duplicate: true };
|
|
1938
|
-
// Turns written before the first runtime segment carry no runtime metadata.
|
|
1939
|
-
const existingRuntime = existing?.metadata?.runtime;
|
|
1940
|
-
const goalMessageId = existingRuntime?.goalMessageId;
|
|
1941
|
-
const goalRequest = Boolean(submittedMessage(existing || {})?.goal || goalMessageId === messageId);
|
|
1942
|
-
if (existing && (existingRuntime?.segmentId !== entry.segmentId || existingRuntime.supersededBy ||
|
|
1943
|
-
existingRuntime.status === "complete" && !goalRequest)) return receipt;
|
|
1944
|
-
if (goalMessageId && goalMessageId !== messageId) {
|
|
1945
|
-
const owner = history.find(turn => submittedMessage(turn)?.messageId === goalMessageId);
|
|
1946
|
-
const ownerRuntime = owner?.metadata?.runtime;
|
|
1947
|
-
if (ownerRuntime?.goalMessageId !== goalMessageId || ownerRuntime.segmentId !== entry.segmentId) throw new Error("The goal command for this native continuation is missing.");
|
|
1948
|
-
const result = await inspectDelivery(entry, context, goalMessageId);
|
|
1949
|
-
return { ...receipt, recovered: result.recovered === true };
|
|
1950
|
-
}
|
|
1951
|
-
const metadata = { runtime: await readState(entry) };
|
|
1952
|
-
if (metadata.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
|
|
1953
|
-
const savedRequests = [...metadata.runtime.predecessors, metadata.runtime].filter(segment => segment.request?.messageId === messageId);
|
|
1954
|
-
if (!existing && savedRequests.length && savedRequests.every(segment =>
|
|
1955
|
-
["codex", "claude", "opencode"].includes(segment.engine) && segment.request.attempted === false && !segment.request.inspectionOnly &&
|
|
1956
|
-
isDeepStrictEqual(segment.request, savedRequests[0].request))) {
|
|
1957
|
-
await access(context, entry.id, "inspectDelivery", entry);
|
|
1958
|
-
return notSentDeliveryReceipt(messageId);
|
|
1959
|
-
}
|
|
1960
|
-
const request = existing ? submittedMessage(existing) : metadata.runtime.request;
|
|
1961
|
-
if (!request || request.messageId !== messageId || !entry.provider.inspectAdmission) return receipt || { status: "unknown", messageId };
|
|
1962
|
-
const pending = metadata.runtime.request?.messageId === messageId ? metadata.runtime.request : null;
|
|
1963
|
-
if (pending?.threadId && pending.threadId !== conversationNativeIdentity(metadata.runtime)) return receipt || { status: "unknown", messageId };
|
|
1964
|
-
const observation = suppliedObservation || await entry.provider.inspectAdmission({ ...request, nativeTurnId: existing?.metadata?.runtime?.nativeTurnId,
|
|
1965
|
-
context, configuration: metadata.runtime.configuration });
|
|
1966
|
-
await access(context, entry.id, "inspectDelivery", entry);
|
|
1967
|
-
// The engine proved it never admitted this saved request. Record that it
|
|
1968
|
-
// was not sent, as a definite rejection would, so Retry may send it.
|
|
1969
|
-
if (!existing && observation.notAdmitted && pending && !pending.inspectionOnly && !entry.active) {
|
|
1970
|
-
await storage.write(entry.id, async transaction => {
|
|
1971
|
-
const current = await transaction.readMetadata();
|
|
1972
|
-
if (current.runtime.request?.messageId !== messageId) return;
|
|
1973
|
-
current.runtime.request.attempted = false;
|
|
1974
|
-
await transaction.writeMetadata(current);
|
|
1975
|
-
});
|
|
1976
|
-
return notSentDeliveryReceipt(messageId);
|
|
1977
|
-
}
|
|
1978
|
-
if (!observation.accepted) return { ...(receipt || { status: "unknown", messageId }),
|
|
1979
|
-
...(observation.recoveryLimitation ? { recoveryLimitation: observation.recoveryLimitation } : {}) };
|
|
1980
|
-
if (entry.driver.canonicalTranscript) {
|
|
1981
|
-
const latest = await readTranscript(entry);
|
|
1982
|
-
const saved = latest.find(turn => submittedMessage(turn)?.messageId === messageId);
|
|
1983
|
-
if (!saved) throw new Error("The native receipt did not persist its authored message.");
|
|
1984
|
-
const state = saved.metadata?.runtime;
|
|
1985
|
-
if (state?.segmentId !== entry.segmentId) throw new Error("The native receipt belongs to another conversation segment.");
|
|
1986
|
-
const unknownTool = saved.metadata.applicationTools?.some(call => !call.result || call.status === "unknown");
|
|
1987
|
-
const status = ["cancelled", "failed"].includes(state.status) ? state.status : unknownTool ? "interrupted"
|
|
1988
|
-
: observation.recoveryLimitation ? "interrupted" : ["completed", "idle"].includes(observation.status) ? "complete"
|
|
1989
|
-
: observation.status === "failed" ? "failed" : "interrupted";
|
|
1990
|
-
const error = unknownTool ? "Native output was recovered, but an application tool has no verified result. Inspect its target before requesting another execution."
|
|
1991
|
-
: observation.recoveryLimitation || (status === "complete" ? "" : state.error || "Delivery was confirmed. Completion could not be verified; this request was not replayed.");
|
|
1992
|
-
await storage.write(entry.id, async transaction => {
|
|
1993
|
-
const current = await transaction.readMetadata();
|
|
1994
|
-
if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) {
|
|
1995
|
-
throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
|
|
1996
|
-
}
|
|
1997
|
-
const exact = await transaction.readTurn(saved.turnId);
|
|
1998
|
-
const { error: _previousError, ...runtime } = exact.metadata.runtime;
|
|
1999
|
-
await transaction.updateTurnMetadata(saved.turnId, { runtime: { ...runtime, status, ...(error ? { error } : {}) } });
|
|
2000
|
-
});
|
|
2001
|
-
if (Object.hasOwn(observation, "goal")) entry.goal = observation.goal;
|
|
2002
|
-
entry.error = error;
|
|
2003
|
-
const result = { ...(receipt || { status: "accepted", messageId, turnId: saved.turnId }),
|
|
2004
|
-
recovered: observation.recovered === true, ...(observation.recoveryLimitation ? { recoveryLimitation: observation.recoveryLimitation } : {}) };
|
|
2005
|
-
if (!existing) await emit(entry, { type: "accepted", ...result });
|
|
2006
|
-
if (result.recovered) await emit(entry, { type: "settled", turnId: saved.turnId, messageId, status, error, ...stateErrorFields(error) });
|
|
2007
|
-
return result;
|
|
2008
|
-
}
|
|
2009
|
-
const recovered = await storage.write(entry.id, async transaction => {
|
|
2010
|
-
const current = await transaction.readMetadata();
|
|
2011
|
-
if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
|
|
2012
|
-
const native = { ...observation, messageId };
|
|
2013
|
-
const results = [];
|
|
2014
|
-
const id = messageId;
|
|
2015
|
-
let turn = history.find(turn => submittedMessage(turn)?.messageId === id);
|
|
2016
|
-
if (turn?.metadata?.runtime?.supersededBy || turn && turn.metadata?.runtime?.segmentId !== entry.segmentId) {
|
|
2017
|
-
throw new Error("Native history refers to a turn outside the active conversation segment.");
|
|
2018
|
-
}
|
|
2019
|
-
if (turn?.metadata?.runtime?.status !== "complete") {
|
|
2020
|
-
if (!turn) {
|
|
2021
|
-
const input = request;
|
|
2022
|
-
const turnId = await transaction.nextTurnId();
|
|
2023
|
-
await transaction.appendMessage(turnId, { role: input.origin === "application" ? "system" : "user", origin: input.origin || "user",
|
|
2024
|
-
messageId: id, text: input.text, attachments: input.attachments, at: input.at,
|
|
2025
|
-
...(input.data !== undefined ? { data: input.data } : {}), ...(input.goal ? { goal: input.goal } : {}),
|
|
2026
|
-
turnMetadata: { runtime: { engine: entry.engine, segmentId: entry.segmentId, origin: input.origin || "user",
|
|
2027
|
-
...(input.completedEnvelope === true ? { completedEnvelope: true } : {}),
|
|
2028
|
-
...(typeof input.nativeToolAttempt === "boolean" ? { nativeToolAttempt: input.nativeToolAttempt } : {}) } } });
|
|
2029
|
-
turn = await transaction.readTurn(turnId);
|
|
2030
|
-
history.push(turn);
|
|
2031
|
-
}
|
|
2032
|
-
const runtime = turn.metadata.runtime;
|
|
2033
|
-
if (runtime.nativeTurnId && native.nativeTurnId && runtime.nativeTurnId !== native.nativeTurnId) throw new Error("Native history changed the accepted turn's identity.");
|
|
2034
|
-
const replies = [];
|
|
2035
|
-
for (const message of native.messages || []) {
|
|
2036
|
-
if (!persistCommentary && message.role === "commentary") continue;
|
|
2037
|
-
if (message.role === "assistant") replies.push(message.text);
|
|
2038
|
-
else {
|
|
2039
|
-
const messageId = `${turn.turnId}:${message.id}`;
|
|
2040
|
-
if (!await transaction.hasMessage(messageId)) await transaction.appendMessage(turn.turnId,
|
|
2041
|
-
{ messageId, ...(message.outputId ? { outputId: `${turn.turnId}:${message.outputId}` } : {}),
|
|
2042
|
-
role: message.role, text: message.text, at: submittedMessage(turn).at });
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
const nativeReplies = (native.messages || []).filter(message => message.role === "assistant");
|
|
2046
|
-
const outputId = nativeReplies.length === 1 && nativeReplies[0].outputId;
|
|
2047
|
-
const replyText = replies.join("\n\n");
|
|
2048
|
-
const finalReplyLimit = completedEnvelope && isCompletedEnvelopeTurn(turn) && !submittedMessage(turn)?.goal
|
|
2049
|
-
? maximumCompletedOutput : maximumFinalReply;
|
|
2050
|
-
const oversizedReply = replyText.length > finalReplyLimit;
|
|
2051
|
-
if (replies.length && !oversizedReply) await transaction.replaceAssistant(turn.turnId, { messageId: turn.assistant?.messageId || `${turn.turnId}:assistant`,
|
|
2052
|
-
...(!turn.assistant?.outputId && outputId ? { outputId: `${turn.turnId}:${outputId}` } : {}),
|
|
2053
|
-
role: "assistant", text: replyText, at: submittedMessage(turn).at });
|
|
2054
|
-
const unknownTool = turn.metadata.applicationTools?.some(call => !call.result || call.status === "unknown");
|
|
2055
|
-
const retainedFailure = ["cancelled", "failed"].includes(runtime.status);
|
|
2056
|
-
const status = retainedFailure ? runtime.status : oversizedReply ? "failed" : unknownTool ? "interrupted" : native.status || "interrupted";
|
|
2057
|
-
const { error: previousError, ...saved } = runtime;
|
|
2058
|
-
const error = unknownTool ? "Native output was recovered, but an application tool has no verified result. Inspect its target before requesting another execution."
|
|
2059
|
-
: oversizedReply ? retainedFailure && (previousError || native.error) || "The assistant exceeded the configured final reply limit."
|
|
2060
|
-
: status === "complete" ? "" : previousError || native.error || "Delivery was confirmed from native history. Completion was not confirmed; this request was not replayed.";
|
|
2061
|
-
await transaction.updateTurnMetadata(turn.turnId, { runtime: { ...saved, status,
|
|
2062
|
-
...(request.goal || goalRequest ? { goalMessageId: messageId } : {}),
|
|
2063
|
-
...(native.nativeTurnId ? { nativeTurnId: native.nativeTurnId } : {}), ...(error ? { error } : {}) } });
|
|
2064
|
-
results.push({ turnId: turn.turnId, messageId: id, status, error });
|
|
2065
|
-
}
|
|
2066
|
-
await transaction.writeMetadata(current);
|
|
2067
|
-
return results;
|
|
2068
|
-
});
|
|
2069
|
-
if (Object.hasOwn(observation, "goal")) entry.goal = observation.goal;
|
|
2070
|
-
entry.error = recovered.find(turn => turn.error)?.error || "";
|
|
2071
|
-
const result = { ...(receipt || { status: "accepted", messageId, turnId: recovered[0].turnId }), recovered: recovered.length > 0 };
|
|
2072
|
-
if (!existing) await emit(entry, { type: "accepted", ...result });
|
|
2073
|
-
for (const turn of recovered) await emit(entry, { type: "settled", ...turn, ...stateErrorFields(turn.error) });
|
|
2074
|
-
return result;
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
async function inspectDelivery(entry, context, messageId) {
|
|
2078
|
-
if (entry.conversation?.commands) {
|
|
2079
|
-
await access(context, entry.id, "inspectDelivery", entry);
|
|
2080
|
-
const result = await entry.conversation.commands.inspectDelivery({ messageId }, context);
|
|
2081
|
-
await access(context, entry.id, "inspectDelivery", entry);
|
|
2082
|
-
return result;
|
|
2083
|
-
}
|
|
2084
|
-
if (entry.conversation) {
|
|
2085
|
-
await access(context, entry.id, "inspectDelivery", entry);
|
|
2086
|
-
const state = await readState(entry);
|
|
2087
|
-
// A running owner completes its own receipt. Only an idle owner recovers
|
|
2088
|
-
// the saved changeover, preserving write-receipt-before-clear ordering.
|
|
2089
|
-
if (!entry.active && state.request?.messageId === messageId && state.request.attempted) {
|
|
2090
|
-
const sender = nativeChangeover(entry, context, null, () => {
|
|
2091
|
-
throw new Error("Receipt inspection cannot submit another native prompt.");
|
|
2092
|
-
});
|
|
2093
|
-
const delivered = await sender.send({ engineId: entry.engine,
|
|
2094
|
-
messages: await entry.conversation.transcript.history(), input: { messageId }, inspectOnly: true });
|
|
2095
|
-
if (delivered?.notSent) return notSentDeliveryReceipt(messageId);
|
|
2096
|
-
if (!delivered?.delivered) throw deliveryFailure(delivered);
|
|
2097
|
-
}
|
|
2098
|
-
const row = (await readTranscript(entry)).find(turn => submittedMessage(turn)?.messageId === messageId);
|
|
2099
|
-
if (row) return { status: "accepted", messageId, turnId: row.turnId, duplicate: true };
|
|
2100
|
-
const threadId = state.request?.messageId === messageId ? state.request.threadId : conversationNativeIdentity(state);
|
|
2101
|
-
if (!threadId) return { status: "unknown", messageId };
|
|
2102
|
-
const observation = await entry.provider.inspectAdmission({ messageId, threadId, context });
|
|
2103
|
-
await access(context, entry.id, "inspectDelivery", entry);
|
|
2104
|
-
// Native-only acceptance is evidence of delivery, not permission to
|
|
2105
|
-
// manufacture an authored row or use a native turn ID as its receipt.
|
|
2106
|
-
if (observation.accepted) return { status: "accepted", messageId, duplicate: true };
|
|
2107
|
-
return observation.notAdmitted && !entry.active ? notSentDeliveryReceipt(messageId) : { status: "unknown", messageId };
|
|
2108
|
-
}
|
|
2109
|
-
const result = await recoverDelivery(entry, context, messageId);
|
|
2110
|
-
if (result.status !== "accepted" || entry.engine === "api") return result;
|
|
2111
|
-
if (entry.driver.canonicalTranscript && result.recoveryLimitation) {
|
|
2112
|
-
const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
|
|
2113
|
-
if (runtime.request?.goal || runtime.binding?.goalRequest?.messageId === messageId ||
|
|
2114
|
-
runtime.binding?.goalReceipt?.messageId === messageId) return result;
|
|
2115
|
-
}
|
|
2116
|
-
const messages = conversationNativeMessages((await readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy));
|
|
2117
|
-
const sender = nativeChangeover(entry, context, null, () => {
|
|
2118
|
-
throw new Error("Receipt inspection cannot submit another native prompt.");
|
|
2119
|
-
});
|
|
2120
|
-
const pending = (await readState(entry)).request;
|
|
2121
|
-
if (pending?.messageId === messageId) {
|
|
2122
|
-
const delivered = await sender.send({ engineId: entry.engine, messages, input: { messageId } });
|
|
2123
|
-
if (!delivered?.delivered) throw deliveryFailure(delivered);
|
|
2124
|
-
}
|
|
2125
|
-
await sender.remember({ engineId: entry.engine, messages });
|
|
2126
|
-
return result;
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
async function replace(entry, context, input, selection = false, representation) {
|
|
2130
|
-
const operation = selection ? "select" : "replace";
|
|
2131
|
-
if (completedEnvelope && input?.engine === "api") throw failure("Completed envelopes require a native engine.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.completedEnvelopesNeedNativeEngine" });
|
|
2132
|
-
await access(context, entry.id, operation, entry);
|
|
2133
|
-
if (entry.conversation?.commands) {
|
|
2134
|
-
if (!selection) throw failure("This conversation retains its original replacement policy.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.replacementPolicyRetained" });
|
|
2135
|
-
return entry.conversation.commands.select(input, context);
|
|
2136
|
-
}
|
|
2137
|
-
if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original selected profile.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.selectedProfileRetained" });
|
|
2138
|
-
const keys = selection ? ["operationId", "expectedSegmentId", "engine", "configuration", "retireNative"]
|
|
2139
|
-
: ["operationId", "expectedSegmentId", "reason", "engine", "configuration", "briefing"];
|
|
2140
|
-
if (!input || !/^[\w-]{1,128}$/u.test(input.operationId || "") || typeof input.expectedSegmentId !== "string" ||
|
|
2141
|
-
!input.expectedSegmentId || (selection ? typeof input.engine !== "string" : !["engine-change", "model-change", "renewal"].includes(input.reason)) ||
|
|
2142
|
-
input.briefing !== undefined && typeof input.briefing !== "string" ||
|
|
2143
|
-
input.retireNative !== undefined && typeof input.retireNative !== "boolean" ||
|
|
2144
|
-
Object.keys(input).some(key => !keys.includes(key))) {
|
|
2145
|
-
throw failure("Replacement needs an operationId, exact predecessor segment, and an engine-change, model-change or renewal reason.", "conversation_invalid_replacement", 400, { messageCode: "assistant.conversation.replacementRequestInvalid" });
|
|
2146
|
-
}
|
|
2147
|
-
if (entry.active) throw failure("Stop the current turn before replacing its native conversation.", "conversation_busy", 409, { messageCode: "assistant.conversation.stopBeforeReplace" });
|
|
2148
|
-
if (entry.storageFailure) throw entry.storageFailure;
|
|
2149
|
-
if (entry.conversation) {
|
|
2150
|
-
const state = await readState(entry);
|
|
2151
|
-
const saved = await entry.conversation.state.read();
|
|
2152
|
-
const retry = saved?.replacement?.operationId === input.operationId;
|
|
2153
|
-
let nativeResult;
|
|
2154
|
-
if (state.segmentId !== input.expectedSegmentId && !retry) throw failure("The native predecessor has changed. Read the conversation before replacing it.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.predecessorChanged" });
|
|
2155
|
-
if (selection) {
|
|
2156
|
-
if (!entry.conversation.selection?.write) throw failure("This host requires its authorized assistant selection operation.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.hostSelectionRequired" });
|
|
2157
|
-
await entry.conversation.selection.write(input, context);
|
|
2158
|
-
} else {
|
|
2159
|
-
if (!input.briefing?.trim()) throw failure("Native replacement requires a continuity briefing.", "conversation_invalid_replacement", 400, { messageCode: "assistant.conversation.replacementBriefingRequired" });
|
|
2160
|
-
const replacement = createConversationChangeover({ state: entry.conversation.state,
|
|
2161
|
-
identity: entry.conversation.identity, presentation: entry.conversation.presentation,
|
|
2162
|
-
agent: { async closeSession() { await entry.provider.dispose({ changeover: true, forgetConversationBinding: true }); return { ok: true }; } } });
|
|
2163
|
-
nativeResult = await replacement.replace({ engineId: entry.engine, messages: await entry.conversation.transcript.history(),
|
|
2164
|
-
operationId: input.operationId, expectedId: input.expectedSegmentId.startsWith(`${entry.engine}:`)
|
|
2165
|
-
? input.expectedSegmentId.slice(entry.engine.length + 1) : "", handover: input.briefing || "" });
|
|
2166
|
-
if (retry && saved.replacement.status === "accepted") {
|
|
2167
|
-
return representation === "native" ? nativeResult : { operationId: input.operationId, segmentId: entry.segmentId, duplicate: true };
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
const next = await entry.host.conversation({ id: entry.id, context });
|
|
2171
|
-
entry.conversation = next;
|
|
2172
|
-
entry.applicationTools = boundApplicationTools(next);
|
|
2173
|
-
entry.engine = next.engine;
|
|
2174
|
-
entry.driver = createDriver(entry.engine, entry.host);
|
|
2175
|
-
const current = await readState(entry);
|
|
2176
|
-
entry.provider = await openProvider(entry, entry.driver, current.binding, current.segmentId);
|
|
2177
|
-
entry.goal = null;
|
|
2178
|
-
const receipt = { operationId: input.operationId, segmentId: entry.segmentId, ...(retry ? { duplicate: true } : {}) };
|
|
2179
|
-
await emit(entry, { type: "replaced", reason: input.reason || "model-change", engine: entry.engine, ...receipt });
|
|
2180
|
-
return representation === "native" ? nativeResult : receipt;
|
|
2181
|
-
}
|
|
2182
|
-
const request = { ...structuredClone(input), operation };
|
|
2183
|
-
// Receipt identity must survive JSON storage without picking up a later
|
|
2184
|
-
// runtime's connection default when the same operation is retried.
|
|
2185
|
-
if (request.engine === undefined) delete request.engine;
|
|
2186
|
-
if (request.briefing === undefined) delete request.briefing;
|
|
2187
|
-
if (request.configuration === undefined) delete request.configuration;
|
|
2188
|
-
else if (request.configuration) request.configuration = normalizeConversationConfiguration(request.configuration);
|
|
2189
|
-
let state = (await readState(entry));
|
|
2190
|
-
if (hasUnfinishedConversationRewind(state)) {
|
|
2191
|
-
throw failure("This conversation has an unfinished Undo operation from an earlier runtime. Inspect its saved native and local history offline before replacing it; no history was changed.", "conversation_rewind_inspection_required", 409, { messageCode: "assistant.conversation.rewindInspectionBeforeReplace" });
|
|
2192
|
-
}
|
|
2193
|
-
const completed = state.predecessors.find(item => item.replacement?.operationId === input.operationId);
|
|
2194
|
-
if (completed) {
|
|
2195
|
-
if (!isDeepStrictEqual(completed.replacement, request)) throw failure("This replacement operationId already belongs to another request.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.replacementOperationConflict" });
|
|
2196
|
-
return { operationId: input.operationId, segmentId: completed.successorId, duplicate: true };
|
|
2197
|
-
}
|
|
2198
|
-
let pending = state.replacement;
|
|
2199
|
-
if (pending && !isDeepStrictEqual(pending.request, request)) throw failure("Retry the unfinished native replacement before starting another one.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementUnfinished" });
|
|
2200
|
-
if (state.segmentId !== input.expectedSegmentId) throw failure("The native predecessor has changed. Read the conversation before replacing it.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.predecessorChanged" });
|
|
2201
|
-
if (!selection && retainedSegments(state).some(segment => segment.request)) throw failure("Resolve pending delivery before replacing native history. Another engine may be selected while its receipt stays pending.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.pendingDeliveryBeforeReplace" });
|
|
2202
|
-
if (!selection && !pending && pendingSuccessor(state)) throw failure("Deliver the previous replacement's briefing before replacing native history again.", "conversation_replacement_briefing_pending", 409, { messageCode: "assistant.conversation.briefingBeforeReplacement" });
|
|
2203
|
-
if (entry.executionFailure && !pending) throw entry.executionFailure;
|
|
2204
|
-
const engine = pending?.engine || input.engine || state.engine;
|
|
2205
|
-
const configuration = pending?.configuration || (input.configuration
|
|
2206
|
-
? normalizeConversationConfiguration(input.configuration, { engine, defaultIntegrationId }) : state.configuration);
|
|
2207
|
-
const driver = createDriver(engine, entry.host);
|
|
2208
|
-
driver.validateConfiguration(configuration);
|
|
2209
|
-
const predecessorId = conversationNativeIdentity(state);
|
|
2210
|
-
const nativeReplacement = !selection && state.engine !== "api" && predecessorId &&
|
|
2211
|
-
(state.engine !== "claude" || state.binding.sent);
|
|
2212
|
-
const retainNativeBinding = input.retireNative !== true && engine === state.engine &&
|
|
2213
|
-
(engine !== "opencode" || configuration.integrationId === state.configuration.integrationId);
|
|
2214
|
-
if (selection && engine !== "api" && retainNativeBinding && !pending) {
|
|
2215
|
-
// The production native owners apply compatible settings to the running
|
|
2216
|
-
// conversation themselves. Selecting a model is not process retirement.
|
|
2217
|
-
await storage.write(entry.id, async transaction => {
|
|
2218
|
-
const metadata = await transaction.readMetadata();
|
|
2219
|
-
const current = metadata.runtime;
|
|
2220
|
-
current.predecessors.push({ segmentId: current.segmentId, engine: current.engine,
|
|
2221
|
-
configuration: current.configuration, binding: current.binding, seen: current.seen,
|
|
2222
|
-
request: current.request, successorId: current.segmentId, replacement: request });
|
|
2223
|
-
current.configuration = structuredClone(configuration);
|
|
2224
|
-
await transaction.writeMetadata(metadata);
|
|
2225
|
-
});
|
|
2226
|
-
const receipt = { operationId: input.operationId, segmentId: state.segmentId };
|
|
2227
|
-
await emit(entry, { type: "replaced", reason: "model-change", engine, ...receipt });
|
|
2228
|
-
return receipt;
|
|
2229
|
-
}
|
|
2230
|
-
if (!pending) {
|
|
2231
|
-
const history = (await readTranscript(entry)).filter(turn => !turn.metadata?.runtime?.supersededBy);
|
|
2232
|
-
let destination = null;
|
|
2233
|
-
if (selection) {
|
|
2234
|
-
if (retainNativeBinding) destination = state;
|
|
2235
|
-
else if (input.retireNative !== true && engine !== state.engine && driver.createBinding) {
|
|
2236
|
-
destination = retainedSegments(state).findLast(segment => segment.engine === engine &&
|
|
2237
|
-
(engine !== "opencode" || segment.configuration.integrationId === configuration.integrationId));
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
if (engine !== "api" && (input.briefing || "").length > maximumContinuity) throw new Error("The continuity briefing exceeds the configured history budget.");
|
|
2241
|
-
const continuity = destination ? { text: destination.continuity, attachments: destination.continuityAttachments || [] }
|
|
2242
|
-
: engine === "api" ? conversationContinuity({ history, briefing: input.briefing, maximumCharacters: maximumContinuity })
|
|
2243
|
-
: { text: undefined, attachments: [] };
|
|
2244
|
-
pending = createConversationRuntimeReplacement({ request, engine, configuration: structuredClone(configuration),
|
|
2245
|
-
reason: selection ? state.engine === engine ? "model-change" : "engine-change" : input.reason,
|
|
2246
|
-
segmentId: destination?.segmentId || randomUUID(), continuity,
|
|
2247
|
-
seen: destination ? destination.seen : engine === "api" ? conversationHistoryVersions(history) : {}, destination });
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
function saveReplacement() {
|
|
2251
|
-
return storage.write(entry.id, async transaction => {
|
|
2252
|
-
const metadata = await transaction.readMetadata();
|
|
2253
|
-
if (metadata.runtime.segmentId !== input.expectedSegmentId || metadata.runtime.replacement &&
|
|
2254
|
-
!isDeepStrictEqual(metadata.runtime.replacement.request, request)) {
|
|
2255
|
-
throw failure("The saved replacement changed before it could be written.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.replacementChangedBeforeWrite" });
|
|
2256
|
-
}
|
|
2257
|
-
metadata.runtime.replacement = pending;
|
|
2258
|
-
await transaction.writeMetadata(metadata);
|
|
2259
|
-
});
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
async function closeSession() {
|
|
2263
|
-
try { await entry.provider.dispose(); }
|
|
2264
|
-
catch (error) { entry.executionFailure = error; entry.error = error.message; throw error; }
|
|
2265
|
-
return { ok: true };
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
let provider;
|
|
2269
|
-
const bindingOwner = {};
|
|
2270
|
-
async function replacementTransaction(callback) {
|
|
2271
|
-
// These common storage/host facilities allocate an inert successor handle.
|
|
2272
|
-
// Native process/thread creation stays in ordinary Send, after this commit.
|
|
2273
|
-
await access(context, entry.id, operation, entry);
|
|
2274
|
-
if (pending.segmentId === state.segmentId) {
|
|
2275
|
-
pending.binding = (await readState(entry)).binding || null;
|
|
2276
|
-
}
|
|
2277
|
-
if (!Object.hasOwn(pending, "binding")) {
|
|
2278
|
-
pending.binding = driver.createBinding ? await driver.createBinding() : null;
|
|
2279
|
-
await saveReplacement();
|
|
2280
|
-
}
|
|
2281
|
-
provider = await openProvider(entry, driver, pending.binding, pending.segmentId, bindingOwner);
|
|
2282
|
-
try {
|
|
2283
|
-
await access(context, entry.id, operation, entry);
|
|
2284
|
-
await storage.write(entry.id, async transaction => {
|
|
2285
|
-
const metadata = await transaction.readMetadata();
|
|
2286
|
-
state = metadata.runtime;
|
|
2287
|
-
if (state.segmentId !== input.expectedSegmentId || !isDeepStrictEqual(state.replacement?.request, request)) {
|
|
2288
|
-
throw failure("The saved replacement changed before its binding was released.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.replacementChangedBeforeRelease" });
|
|
2289
|
-
}
|
|
2290
|
-
await callback({
|
|
2291
|
-
async releaseBinding(replacement) {
|
|
2292
|
-
if (replacement && conversationNativeIdentity(state) !== replacement.previous.conversationId) {
|
|
2293
|
-
throw failure("The native predecessor identity changed before its binding was released.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.predecessorChangedBeforeRelease" });
|
|
2294
|
-
}
|
|
2295
|
-
releaseConversationRuntimeBinding(state, pending, request, engine);
|
|
2296
|
-
},
|
|
2297
|
-
async write(value) {
|
|
2298
|
-
finishConversationRuntimeReplacement(state, { engine, selection, retireNative: input.retireNative, pending, value });
|
|
2299
|
-
await transaction.writeMetadata(metadata);
|
|
2300
|
-
}
|
|
2301
|
-
});
|
|
2302
|
-
});
|
|
2303
|
-
} catch (error) { await provider.dispose(); throw error; }
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
if (nativeReplacement) {
|
|
2307
|
-
const replacement = createConversationChangeover({
|
|
2308
|
-
state: {
|
|
2309
|
-
read: changeoverState(entry).read,
|
|
2310
|
-
async write(value) {
|
|
2311
|
-
pending.preparedAt = value.replacement.preparedAt;
|
|
2312
|
-
await saveReplacement();
|
|
2313
|
-
},
|
|
2314
|
-
transaction: replacementTransaction
|
|
2315
|
-
},
|
|
2316
|
-
identity: {
|
|
2317
|
-
async inspect(expectedId) {
|
|
2318
|
-
const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
|
|
2319
|
-
if (runtime.segmentId !== input.expectedSegmentId || conversationNativeIdentity(runtime) !== expectedId) {
|
|
2320
|
-
throw failure("The native predecessor identity changed or is incomplete.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.predecessorIncomplete" });
|
|
2321
|
-
}
|
|
2322
|
-
return { bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: expectedId } };
|
|
2323
|
-
}
|
|
2324
|
-
},
|
|
2325
|
-
agent: { closeSession }
|
|
2326
|
-
});
|
|
2327
|
-
await replacement.replace({ engineId: engine, messages: [], operationId: input.operationId,
|
|
2328
|
-
expectedId: predecessorId, handover: input.briefing || "" });
|
|
2329
|
-
} else {
|
|
2330
|
-
// API conversations, retained-engine selection and unstarted native handles
|
|
2331
|
-
// have no native predecessor replacement. Keep their common facilities.
|
|
2332
|
-
if (!state.replacement) await saveReplacement();
|
|
2333
|
-
await closeSession();
|
|
2334
|
-
await replacementTransaction(async transaction => {
|
|
2335
|
-
await transaction.releaseBinding();
|
|
2336
|
-
await transaction.write();
|
|
2337
|
-
});
|
|
2338
|
-
}
|
|
2339
|
-
Object.assign(entry, { engine, driver, provider, bindingOwner, segmentId: pending.segmentId,
|
|
2340
|
-
goal: null, executionFailure: null, error: pending.submission?.error || "" });
|
|
2341
|
-
const receipt = { operationId: input.operationId, segmentId: pending.segmentId };
|
|
2342
|
-
await emit(entry, { type: "replaced", reason: pending.reason, engine, ...receipt });
|
|
2343
|
-
return receipt;
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
function retireEntry(entry) {
|
|
2347
|
-
entry.disposed = true;
|
|
2348
|
-
entry.listeners.clear();
|
|
2349
|
-
// Live streams are kept per conversation id. A logical conversation streams
|
|
2350
|
-
// through its own owner, so the stream under its id belongs to another entry
|
|
2351
|
-
// with the same id, such as its session's Main conversation.
|
|
2352
|
-
if (!entry.conversation?.commands) streams.clear(entry.id);
|
|
2353
|
-
if (entries.get(entry.key) === entry) entries.delete(entry.key);
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
async function disposeEntry(entry, context, options, representation) {
|
|
2357
|
-
if (entry.conversation?.commands) { retireEntry(entry); return; }
|
|
2358
|
-
if (!entry.conversation) await stop(entry, context);
|
|
2359
|
-
await retrySave(entry);
|
|
2360
|
-
const result = await entry.provider.dispose(entry.conversation?.native?.scoped && representation === "native"
|
|
2361
|
-
? { input: options, context } : representation === "native" ? options : undefined);
|
|
2362
|
-
if (result?.ok === false) return result;
|
|
2363
|
-
if (entry.conversation) await entry.active?.done;
|
|
2364
|
-
retireEntry(entry);
|
|
2365
|
-
return result;
|
|
198
|
+
async function close() {
|
|
199
|
+
closed = true;
|
|
200
|
+
const results = await Promise.allSettled(entries.list().map(entry => serial(entry, async () => {
|
|
201
|
+
await entry.initializing;
|
|
202
|
+
const result = await stopping.disposeEntry(entry);
|
|
203
|
+
if (result?.ok === false) throw Object.assign(new Error(result.error || "Native cleanup could not be confirmed."), result);
|
|
204
|
+
})));
|
|
205
|
+
const errors = results.filter(result => result.status === "rejected").map(result => result.reason);
|
|
206
|
+
if (errors.length) throw new AggregateError(errors, "Some conversations could not be safely closed. Restore their storage or execution host and retry close().");
|
|
2366
207
|
}
|
|
2367
208
|
|
|
2368
209
|
return Object.freeze({
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2401
|
-
return deletePersistentConversation(options);
|
|
2402
|
-
},
|
|
2403
|
-
async runScopedTurn(options = {}) {
|
|
2404
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2405
|
-
if (["createEphemeralConversation", "startEphemeralConversationTurn", "waitForEphemeralConversationTurn", "stopEphemeralConversation"]
|
|
2406
|
-
.some(name => typeof options.operations?.[name] !== "function")) {
|
|
2407
|
-
throw new TypeError("A scoped turn requires its existing admitted conversation operations.");
|
|
2408
|
-
}
|
|
2409
|
-
return runScopedConversationTurn(options);
|
|
2410
|
-
},
|
|
2411
|
-
// Activity can still be inspected while cleanup retries after runtime closure.
|
|
2412
|
-
// Use the configured owner without retaining or creating a Main conversation.
|
|
2413
|
-
async inspectNativeTemporaryActivity({ id, context } = {}) {
|
|
2414
|
-
if (!await authorize({ context, conversationId: id, operation: "inspectTemporaryActivity" })) {
|
|
2415
|
-
throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
|
|
2416
|
-
}
|
|
2417
|
-
if (typeof defaultHost?.conversation !== "function") throw new TypeError("Native activity requires a configured conversation host.");
|
|
2418
|
-
const prepared = await defaultHost.conversation({ id, context, operation: "inspectTemporaryActivity" });
|
|
2419
|
-
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) ||
|
|
2420
|
-
!prepared.native || !Object.hasOwn(prepared, "context")) {
|
|
2421
|
-
throw new TypeError("Native activity requires its authorized host preparation.");
|
|
2422
|
-
}
|
|
2423
|
-
return createDriver(prepared.engine, defaultHost).inspectTemporaryActivity(prepared);
|
|
2424
|
-
},
|
|
2425
|
-
// Renewal cleanup keeps its existing trusted server authority after closure.
|
|
2426
|
-
// Neither proof release opens a retained conversation or admits inference.
|
|
2427
|
-
async releaseNativeRenewalPredecessorProcessExitProof({ id, context, input = {} } = {}) {
|
|
2428
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2429
|
-
throw new TypeError("Native renewal proof release requires a configured conversation host.");
|
|
2430
|
-
}
|
|
2431
|
-
const prepared = defaultHost.conversation({ id, context, input, operation: "releaseRenewalPredecessorProcessExitProof" });
|
|
2432
|
-
if (!prepared || typeof prepared.then === "function" || prepared.sessionId !== id ||
|
|
2433
|
-
!["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2434
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2435
|
-
throw new TypeError("Native renewal proof release requires synchronous configured host preparation.");
|
|
2436
|
-
}
|
|
2437
|
-
const driver = createDriver(prepared.engine, defaultHost);
|
|
2438
|
-
if (typeof driver.releaseRenewalPredecessorProcessExitProof !== "function") {
|
|
2439
|
-
throw new TypeError("The selected conversation driver does not implement native renewal proof release.");
|
|
2440
|
-
}
|
|
2441
|
-
return driver.releaseRenewalPredecessorProcessExitProof(prepared);
|
|
2442
|
-
},
|
|
2443
|
-
async releaseNativeRenewalSuccessorProcessExitProof({ id, context, input = {} } = {}) {
|
|
2444
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2445
|
-
throw new TypeError("Native renewal proof release requires a configured conversation host.");
|
|
2446
|
-
}
|
|
2447
|
-
const prepared = defaultHost.conversation({ id, context, input, operation: "releaseRenewalSuccessorProcessExitProof" });
|
|
2448
|
-
if (!prepared || typeof prepared.then === "function" || prepared.sessionId !== id ||
|
|
2449
|
-
!["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2450
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2451
|
-
throw new TypeError("Native renewal proof release requires synchronous configured host preparation.");
|
|
2452
|
-
}
|
|
2453
|
-
const driver = createDriver(prepared.engine, defaultHost);
|
|
2454
|
-
if (typeof driver.releaseRenewalSuccessorProcessExitProof !== "function") {
|
|
2455
|
-
throw new TypeError("The selected conversation driver does not implement native renewal proof release.");
|
|
2456
|
-
}
|
|
2457
|
-
return driver.releaseRenewalSuccessorProcessExitProof(prepared);
|
|
2458
|
-
},
|
|
2459
|
-
// Renewal retains the application's existing session and private successor transaction.
|
|
2460
|
-
async generateNativeRenewalHandover({ id, context, input = {} } = {}) {
|
|
2461
|
-
if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
|
|
2462
|
-
await access(context, id, "generateRenewalHandover");
|
|
2463
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2464
|
-
throw new TypeError("Native renewal requires a configured conversation host.");
|
|
2465
|
-
}
|
|
2466
|
-
const prepared = await defaultHost.conversation({ id, context, input, operation: "generateRenewalHandover" });
|
|
2467
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2468
|
-
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2469
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2470
|
-
throw new TypeError("Native renewal requires its authorized host preparation.");
|
|
2471
|
-
}
|
|
2472
|
-
return createDriver(prepared.engine, defaultHost).generateRenewalHandover(prepared);
|
|
2473
|
-
},
|
|
2474
|
-
// Renewal retains the application's existing session and private successor transaction.
|
|
2475
|
-
async seedNativeRenewalHandover({ id, context, input = {} } = {}) {
|
|
2476
|
-
if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
|
|
2477
|
-
await access(context, id, "seedRenewalHandover");
|
|
2478
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2479
|
-
throw new TypeError("Native renewal requires a configured conversation host.");
|
|
2480
|
-
}
|
|
2481
|
-
const prepared = await defaultHost.conversation({ id, context, input, operation: "seedRenewalHandover" });
|
|
2482
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2483
|
-
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2484
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2485
|
-
throw new TypeError("Native renewal requires its authorized host preparation.");
|
|
2486
|
-
}
|
|
2487
|
-
return createDriver(prepared.engine, defaultHost).seedRenewalHandover(prepared);
|
|
2488
|
-
},
|
|
2489
|
-
async updateNativeConversationSettings({ id, context, input = {} } = {}) {
|
|
2490
|
-
if (typeof id !== "string" || !id.trim()) throw new TypeError("A conversation id is required.");
|
|
2491
|
-
await access(context, id, "configure");
|
|
2492
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2493
|
-
const prepared = await defaultHost?.conversation?.({ id, context, input, operation: "updateSettings" });
|
|
2494
|
-
if (prepared?.sessionId !== id || !prepared.native || !Object.hasOwn(prepared, "context")) {
|
|
2495
|
-
throw new TypeError("Native settings require their authorized host preparation.");
|
|
2496
|
-
}
|
|
2497
|
-
const driver = createDriver(prepared.engine, defaultHost);
|
|
2498
|
-
if (typeof driver.updateConversationSettings !== "function") {
|
|
2499
|
-
throw failure("This provider does not support live native settings.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.liveSettingsUnsupported" });
|
|
2500
|
-
}
|
|
2501
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2502
|
-
return driver.updateConversationSettings({ ...prepared, input });
|
|
2503
|
-
},
|
|
2504
|
-
// Readiness uses the same authorized host and original native owner without
|
|
2505
|
-
// opening a retained handle or starting a conversational turn.
|
|
2506
|
-
async ensureNativeConversation({ id, context } = {}) {
|
|
2507
|
-
if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
|
|
2508
|
-
await access(context, id, "ensure");
|
|
2509
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2510
|
-
throw new TypeError("Native conversation readiness requires a configured conversation host.");
|
|
2511
|
-
}
|
|
2512
|
-
const prepared = await defaultHost.conversation({ id, context, operation: "ensure" });
|
|
2513
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2514
|
-
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) ||
|
|
2515
|
-
!prepared.native || !Object.hasOwn(prepared, "context")) {
|
|
2516
|
-
throw new TypeError("Native conversation readiness requires its authorized host preparation.");
|
|
2517
|
-
}
|
|
2518
|
-
const driver = createDriver(prepared.engine, defaultHost);
|
|
2519
|
-
if (typeof driver.ensureConversation !== "function") {
|
|
2520
|
-
throw new TypeError("The selected conversation driver does not implement native readiness.");
|
|
2521
|
-
}
|
|
2522
|
-
return driver.ensureConversation(prepared);
|
|
2523
|
-
},
|
|
2524
|
-
// Advanced hosts create a native identity before a retained handle exists.
|
|
2525
|
-
// Only the configured host supplies native facilities; caller input is data.
|
|
2526
|
-
async createNativeConversation({ id, context, input = {} } = {}) {
|
|
2527
|
-
if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
|
|
2528
|
-
await access(context, id, "create");
|
|
2529
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2530
|
-
throw new TypeError("Native conversation creation requires a configured conversation host.");
|
|
2531
|
-
}
|
|
2532
|
-
const prepared = await defaultHost.conversation({ id, context, input, operation: "create" });
|
|
2533
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2534
|
-
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2535
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2536
|
-
throw new TypeError("Native conversation creation requires its authorized host preparation.");
|
|
2537
|
-
}
|
|
2538
|
-
return createDriver(prepared.engine, defaultHost).createConversation(prepared);
|
|
2539
|
-
},
|
|
2540
|
-
async runNativeDetachedConversation({ id, context, input = {}, options } = {}) {
|
|
2541
|
-
if (typeof id !== "string" || !id.trim()) throw new TypeError("A parent conversation or scope id is required.");
|
|
2542
|
-
await access(context, id, "runDetachedConversation");
|
|
2543
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2544
|
-
throw new TypeError("Native detached execution requires a configured conversation host.");
|
|
2545
|
-
}
|
|
2546
|
-
const prepared = await defaultHost.conversation({ id, context, input,
|
|
2547
|
-
...(options === undefined ? {} : { options }), operation: "runDetachedConversation" });
|
|
2548
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2549
|
-
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2550
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context") || !Object.hasOwn(prepared, "options")) {
|
|
2551
|
-
throw new TypeError("Native detached execution requires its authorized host preparation.");
|
|
2552
|
-
}
|
|
2553
|
-
return createDriver(prepared.engine, defaultHost).runDetachedConversation(prepared);
|
|
2554
|
-
},
|
|
2555
|
-
async open({ id, context, configuration, engine, host, representation = "canonical" } = {}) {
|
|
2556
|
-
if (typeof id !== "string" || !id.trim()) throw new TypeError("A conversation id is required.");
|
|
2557
|
-
await access(context, id, "open");
|
|
2558
|
-
const selectedHost = host || defaultHost;
|
|
2559
|
-
const conversation = selectedHost?.conversation ? await selectedHost.conversation({ id, context }) : null;
|
|
2560
|
-
if (completedEnvelope && (conversation || representation !== "canonical")) {
|
|
2561
|
-
throw failure("Completed envelopes require runtime-owned canonical storage.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.completedEnvelopesNeedStorage" });
|
|
2562
|
-
}
|
|
2563
|
-
if (conversation && !persistCommentary) {
|
|
2564
|
-
throw new TypeError("Transient commentary requires a runtime-owned transcript; supplied conversations retain their host's policy.");
|
|
2565
|
-
}
|
|
2566
|
-
const scoped = conversation?.native?.scoped;
|
|
2567
|
-
const commands = conversation?.commands;
|
|
2568
|
-
const invalidClaudeOwner = conversation?.engine === "claude" && (!conversation.native?.owner ||
|
|
2569
|
-
typeof conversation.native.owner !== "object" || typeof conversation.native.owner.acquire !== "function" ||
|
|
2570
|
-
typeof conversation.native.preparation?.cleanup !== "function");
|
|
2571
|
-
const invalidOpenCodeOwner = conversation?.engine === "opencode" && (!conversation.native?.owner ||
|
|
2572
|
-
typeof conversation.native.owner !== "object" || typeof conversation.native.preparation?.cleanup !== "function" ||
|
|
2573
|
-
(scoped ? ["turn", "existing"] : ["message", "inspection", "interruption"])
|
|
2574
|
-
.some(name => typeof conversation.native.preparation?.[name] !== "function") ||
|
|
2575
|
-
!scoped && typeof conversation.native.acquire !== "function");
|
|
2576
|
-
if (commands && (representation !== "canonical" || configuration !== undefined ||
|
|
2577
|
-
["send", "cancel", "select", "inspectDelivery"].some(name => typeof commands[name] !== "function") ||
|
|
2578
|
-
typeof conversation.read !== "function" || typeof conversation.readStream !== "function")) {
|
|
2579
|
-
throw new TypeError("A logical conversation requires its complete original command, read and stream owners.");
|
|
2580
|
-
}
|
|
2581
|
-
if (scoped && (representation !== "native" || configuration !== undefined || !["codex", "claude", "opencode"].includes(conversation.engine) ||
|
|
2582
|
-
typeof scoped.conversationId !== "string" || !scoped.conversationId ||
|
|
2583
|
-
(conversation.engine === "claude" ? invalidClaudeOwner : conversation.engine === "opencode" ? invalidOpenCodeOwner
|
|
2584
|
-
: typeof conversation.native.preparation?.cleanup !== "function" ||
|
|
2585
|
-
["startConversationTurn", "readConversation", "waitForConversationTurn", "stopConversation", "deleteConversation"]
|
|
2586
|
-
.some(name => typeof conversation.native.runOwner?.[name] !== "function")))) {
|
|
2587
|
-
throw new TypeError("An original scoped conversation requires its native representation, identity and existing operations.");
|
|
2588
|
-
}
|
|
2589
|
-
if (conversation && (conversation.sessionId !== id || typeof conversation.namespace !== "string" || !conversation.namespace ||
|
|
2590
|
-
!["codex", "claude", "opencode"].includes(conversation.engine) || !scoped && !commands && (
|
|
2591
|
-
(conversation.engine === "codex" ? !conversation.native?.runOwner || !conversation.native?.providerOwner
|
|
2592
|
-
: conversation.engine === "claude" ? invalidClaudeOwner
|
|
2593
|
-
: invalidOpenCodeOwner) ||
|
|
2594
|
-
typeof conversation.prepareInput !== "function" || typeof conversation.read !== "function"))) {
|
|
2595
|
-
throw new TypeError("A bound native conversation requires its original identity, store, preparation and native owners.");
|
|
2596
|
-
}
|
|
2597
|
-
if (conversation?.maximumHistoryBytes !== undefined && typeof conversation.maximumHistoryBytes !== "function") {
|
|
2598
|
-
throw new TypeError("A bound native history budget requires a server-owned resolver.");
|
|
2599
|
-
}
|
|
2600
|
-
if (!["canonical", "native"].includes(representation) || representation === "native" && !conversation) {
|
|
2601
|
-
throw new TypeError("Native result representation requires an original bound conversation.");
|
|
2602
|
-
}
|
|
2603
|
-
const key = conversation?.namespace || id;
|
|
2604
|
-
let entry = entries.get(key);
|
|
2605
|
-
if (!entry) {
|
|
2606
|
-
entry = { id, key, conversation, host: selectedHost, pending: Promise.resolve(), active: null, listeners: new Set(), error: "",
|
|
2607
|
-
cancelledMessages: new Set() };
|
|
2608
|
-
entries.set(key, entry);
|
|
2609
|
-
entry.initializing = initialize(entry, configuration, engine).catch(error => {
|
|
2610
|
-
if (entries.get(key) === entry) entries.delete(key);
|
|
2611
|
-
throw error;
|
|
2612
|
-
});
|
|
2613
|
-
}
|
|
2614
|
-
await entry.initializing;
|
|
2615
|
-
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleDisposed" });
|
|
2616
|
-
if (host && !isDeepStrictEqual(host, entry.host)) throw failure("This open conversation already belongs to another host scope. Dispose its handle before reopening it on the original host.", "conversation_host_mismatch", 409, { messageCode: "assistant.conversation.hostScopeMismatch" });
|
|
2617
|
-
if (engine && engine !== entry.engine) throw failure("Use replace() to change this conversation's engine.", "conversation_engine_mismatch", 409, { messageCode: "assistant.conversation.useReplaceForEngine" });
|
|
2618
|
-
if (configuration) {
|
|
2619
|
-
const metadata = { runtime: await readState(entry) };
|
|
2620
|
-
const supplied = entry.conversation ? configuration
|
|
2621
|
-
: normalizeConversationConfiguration(configuration, { engine: entry.engine, defaultIntegrationId });
|
|
2622
|
-
if (!isDeepStrictEqual(supplied, metadata.runtime.configuration)) {
|
|
2623
|
-
throw failure("Use configure() to change a saved conversation's settings.", "conversation_configuration_mismatch");
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
return Object.freeze({
|
|
2627
|
-
id,
|
|
2628
|
-
get capabilities() { return capabilities(entry); },
|
|
2629
|
-
replace: input => serial(entry, () => replace(entry, context, input, false, representation)),
|
|
2630
|
-
select: input => serial(entry, () => replace(entry, context, input, true, representation)),
|
|
2631
|
-
async read(query) {
|
|
2632
|
-
await access(context, id, "read", entry);
|
|
2633
|
-
const result = await read(entry, representation, context, query);
|
|
2634
|
-
if (entry.conversation?.commands) await access(context, id, "read", entry);
|
|
2635
|
-
return result;
|
|
2636
|
-
},
|
|
2637
|
-
send: input => send(entry, context, input, "user", undefined, representation),
|
|
2638
|
-
wake: (input, options) => send(entry, context, input, "application", undefined, representation, options),
|
|
2639
|
-
...(completedEnvelope ? { prepareCompletedResponse: (input, options) =>
|
|
2640
|
-
serial(entry, () => prepareCompletedResponse(entry, context, input, options)) } : {}),
|
|
2641
|
-
readGoal: () => serial(entry, () => readGoal(entry, context, representation)),
|
|
2642
|
-
updateGoal: input => updateGoal(entry, context, input, representation),
|
|
2643
|
-
inspectDelivery({ messageId, threadId } = {}) {
|
|
2644
|
-
if (typeof messageId !== "string" || !messageId) throw new TypeError("A message id is required.");
|
|
2645
|
-
return serial(entry, async () => {
|
|
2646
|
-
if (representation !== "native") return inspectDelivery(entry, context, messageId);
|
|
2647
|
-
await access(context, id, "inspectDelivery", entry);
|
|
2648
|
-
return entry.provider.inspectAdmission({ messageId, threadId, context, representation });
|
|
2649
|
-
});
|
|
2650
|
-
},
|
|
2651
|
-
async wait(input) {
|
|
2652
|
-
await access(context, id, "read", entry);
|
|
2653
|
-
if (entry.conversation?.commands) throw failure("This conversation uses its original read and subscription operations.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.originalReadOnly" });
|
|
2654
|
-
if (entry.conversation?.native?.scoped) return entry.provider.waitNative({ input, context });
|
|
2655
|
-
await entry.pending;
|
|
2656
|
-
await entry.active?.done;
|
|
2657
|
-
await access(context, id, "read", entry);
|
|
2658
|
-
return read(entry);
|
|
2659
|
-
},
|
|
2660
|
-
// Cancel of one unsent or unconfirmed message always records it; an
|
|
2661
|
-
// application conversation supplies its own operation for that.
|
|
2662
|
-
async cancelDelivery({ messageId } = {}) {
|
|
2663
|
-
if (typeof messageId !== "string" || !messageId) throw new TypeError("A message id is required.");
|
|
2664
|
-
await access(context, id, "cancel", entry);
|
|
2665
|
-
if (entry.conversation?.commands) {
|
|
2666
|
-
if (typeof entry.conversation.commands.cancelDelivery !== "function") {
|
|
2667
|
-
throw failure("This conversation cannot cancel one message.", "conversation_unsupported", 400,
|
|
2668
|
-
{ messageCode: "assistant.conversation.messageCancelUnsupported" });
|
|
2669
|
-
}
|
|
2670
|
-
return entry.conversation.commands.cancelDelivery({ messageId }, context);
|
|
2671
|
-
}
|
|
2672
|
-
return cancelDelivery(entry, context, messageId, representation);
|
|
2673
|
-
},
|
|
2674
|
-
async cancel(input) {
|
|
2675
|
-
await access(context, id, "cancel", entry);
|
|
2676
|
-
if (entry.conversation?.commands) return entry.conversation.commands.cancel(input, context);
|
|
2677
|
-
await entry.pending;
|
|
2678
|
-
await access(context, id, "cancel", entry);
|
|
2679
|
-
return stop(entry, context, input, representation);
|
|
2680
|
-
},
|
|
2681
|
-
async retrySave() {
|
|
2682
|
-
await access(context, id, "retrySave", entry);
|
|
2683
|
-
return serial(entry, async () => {
|
|
2684
|
-
await access(context, id, "retrySave", entry);
|
|
2685
|
-
return retrySave(entry);
|
|
2686
|
-
});
|
|
2687
|
-
},
|
|
2688
|
-
async configure(patch) {
|
|
2689
|
-
await access(context, id, "configure", entry);
|
|
2690
|
-
if (entry.conversation?.commands) throw failure("Use the conversation's authorized selection operation.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.authorizedSelectionRequired" });
|
|
2691
|
-
return serial(entry, async () => {
|
|
2692
|
-
await access(context, id, "configure", entry);
|
|
2693
|
-
if (entry.active) throw failure("Stop the current turn before changing its settings.", "conversation_busy", 409, { messageCode: "assistant.conversation.stopBeforeSettings" });
|
|
2694
|
-
if (entry.storageFailure) throw entry.storageFailure;
|
|
2695
|
-
if (entry.executionFailure) throw entry.executionFailure;
|
|
2696
|
-
if (entry.conversation) {
|
|
2697
|
-
if (!entry.conversation.selection?.write) throw failure("This host requires its authorized assistant selection operation.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.hostSelectionRequired" });
|
|
2698
|
-
return entry.conversation.selection.write({ configuration: patch }, context);
|
|
2699
|
-
}
|
|
2700
|
-
return storage.write(id, async transaction => {
|
|
2701
|
-
const metadata = await transaction.readMetadata();
|
|
2702
|
-
if (metadata.runtime.replacement) throw failure("Finish the pending native replacement before changing settings.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementPendingBeforeSettings" });
|
|
2703
|
-
if (metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly)) {
|
|
2704
|
-
throw failure("Resolve uncertain delivery before changing settings.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.deliveryUncertainBeforeSettings" });
|
|
2705
|
-
}
|
|
2706
|
-
const next = normalizeConversationConfiguration({ ...metadata.runtime.configuration, ...patch }, {
|
|
2707
|
-
engine: metadata.runtime.engine, defaultIntegrationId
|
|
2708
|
-
});
|
|
2709
|
-
entry.driver.validateConfiguration(next);
|
|
2710
|
-
metadata.runtime.configuration = next;
|
|
2711
|
-
await transaction.writeMetadata(metadata);
|
|
2712
|
-
return structuredClone(next);
|
|
2713
|
-
});
|
|
2714
|
-
});
|
|
2715
|
-
},
|
|
2716
|
-
async subscribe(listener) {
|
|
2717
|
-
await access(context, id, "subscribe", entry);
|
|
2718
|
-
if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original event delivery.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.eventDeliveryRetained" });
|
|
2719
|
-
if (typeof listener !== "function") throw new TypeError("A conversation listener is required.");
|
|
2720
|
-
const subscription = { context, listener };
|
|
2721
|
-
entry.listeners.add(subscription);
|
|
2722
|
-
return () => entry.listeners.delete(subscription);
|
|
2723
|
-
},
|
|
2724
|
-
async dispose(options) {
|
|
2725
|
-
await access(context, id, "dispose", entry);
|
|
2726
|
-
return serial(entry, async () => {
|
|
2727
|
-
await access(context, id, "dispose", entry);
|
|
2728
|
-
const result = await disposeEntry(entry, context, options, representation);
|
|
2729
|
-
if (result?.ok === false || representation === "native") return result;
|
|
2730
|
-
});
|
|
2731
|
-
}
|
|
2732
|
-
});
|
|
2733
|
-
},
|
|
2734
|
-
// Trusted server lifecycle, like close(): the application has already
|
|
2735
|
-
// authorized its project operation. Never expose this through browser routes.
|
|
2736
|
-
// Acquisition is synchronous and creates no retained conversation or queue.
|
|
2737
|
-
async closeNativeProject({ context, input = {} } = {}) {
|
|
2738
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2739
|
-
throw new TypeError("Native project cleanup requires a configured conversation host.");
|
|
2740
|
-
}
|
|
2741
|
-
const prepared = defaultHost.conversation({ context, input, operation: "closeProject" });
|
|
2742
|
-
if (!prepared || typeof prepared.then === "function" ||
|
|
2743
|
-
!["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2744
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2745
|
-
throw new TypeError("Native project cleanup requires synchronous configured host preparation.");
|
|
2746
|
-
}
|
|
2747
|
-
const driver = createDriver(prepared.engine, defaultHost);
|
|
2748
|
-
if (typeof driver.closeProject !== "function") {
|
|
2749
|
-
throw new TypeError("The selected conversation driver does not implement native project cleanup.");
|
|
2750
|
-
}
|
|
2751
|
-
return driver.closeProject(prepared);
|
|
2752
|
-
},
|
|
2753
|
-
// Account revocation and shutdown use this same trusted lifecycle boundary.
|
|
2754
|
-
// The configured owner enters its native closing fence before its first await.
|
|
2755
|
-
invalidateNativeRuntimes({ context, input = {} } = {}) {
|
|
2756
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2757
|
-
throw new TypeError("Native runtime invalidation requires a configured conversation host.");
|
|
2758
|
-
}
|
|
2759
|
-
const prepared = defaultHost.conversation({ context, input, operation: "invalidateRuntimes" });
|
|
2760
|
-
if (!prepared || typeof prepared.then === "function" ||
|
|
2761
|
-
!["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2762
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2763
|
-
throw new TypeError("Native runtime invalidation requires synchronous configured host preparation.");
|
|
2764
|
-
}
|
|
2765
|
-
const driver = createDriver(prepared.engine, defaultHost);
|
|
2766
|
-
if (typeof driver.invalidateRuntimes !== "function") {
|
|
2767
|
-
throw new TypeError("The selected conversation driver does not implement native runtime invalidation.");
|
|
2768
|
-
}
|
|
2769
|
-
return driver.invalidateRuntimes(prepared);
|
|
2770
|
-
},
|
|
2771
|
-
// Startup reconciliation and subscription reset use the existing trusted
|
|
2772
|
-
// lifecycle caller. Native owners retain their own shutdown fences and tasks.
|
|
2773
|
-
reconcileNativeSessions({ context, sessions, options = {} } = {}) {
|
|
2774
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2775
|
-
throw new TypeError("Native reconciliation requires a configured conversation host.");
|
|
2776
|
-
}
|
|
2777
|
-
const prepared = defaultHost.conversation({ context, input: sessions, options, operation: "reconcileSessions" });
|
|
2778
|
-
if (!prepared || typeof prepared.then === "function" ||
|
|
2779
|
-
!["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2780
|
-
!Object.hasOwn(prepared, "sessions") || !Object.hasOwn(prepared, "options") ||
|
|
2781
|
-
!Object.hasOwn(prepared, "context")) {
|
|
2782
|
-
throw new TypeError("Native reconciliation requires synchronous configured host preparation.");
|
|
2783
|
-
}
|
|
2784
|
-
const driver = createDriver(prepared.engine, defaultHost);
|
|
2785
|
-
if (typeof driver.reconcileSessions !== "function") {
|
|
2786
|
-
throw new TypeError("The selected conversation driver does not implement native reconciliation.");
|
|
2787
|
-
}
|
|
2788
|
-
return driver.reconcileSessions(prepared);
|
|
2789
|
-
},
|
|
2790
|
-
unsubscribeNativeSessions({ context, sessions } = {}) {
|
|
2791
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2792
|
-
throw new TypeError("Native subscription reset requires a configured conversation host.");
|
|
2793
|
-
}
|
|
2794
|
-
const prepared = defaultHost.conversation({ context, input: sessions, operation: "unsubscribeSessions" });
|
|
2795
|
-
if (!prepared || typeof prepared.then === "function" ||
|
|
2796
|
-
!["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2797
|
-
!Object.hasOwn(prepared, "sessions") || !Object.hasOwn(prepared, "context")) {
|
|
2798
|
-
throw new TypeError("Native subscription reset requires synchronous configured host preparation.");
|
|
2799
|
-
}
|
|
2800
|
-
const driver = createDriver(prepared.engine, defaultHost);
|
|
2801
|
-
if (typeof driver.unsubscribeSessions !== "function") {
|
|
2802
|
-
throw new TypeError("The selected conversation driver does not implement native subscription reset.");
|
|
2803
|
-
}
|
|
2804
|
-
return driver.unsubscribeSessions(prepared);
|
|
2805
|
-
},
|
|
2806
|
-
// Historical cleanup has a saved native identity but no retained scope.
|
|
2807
|
-
// Recheck its original caller authority; native owners retain shutdown guards.
|
|
2808
|
-
async interruptNativeDetachedConversation({ id, context, input = {} } = {}) {
|
|
2809
|
-
if (!await authorize({ context, conversationId: id, operation: "interruptDetachedConversation" })) {
|
|
2810
|
-
throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
|
|
2811
|
-
}
|
|
2812
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2813
|
-
throw new TypeError("Historical native cleanup requires a configured conversation host.");
|
|
2814
|
-
}
|
|
2815
|
-
const prepared = await defaultHost.conversation({ id, context, input, operation: "interruptDetachedConversation" });
|
|
2816
|
-
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2817
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2818
|
-
throw new TypeError("Historical native cleanup requires its authorized host preparation.");
|
|
2819
|
-
}
|
|
2820
|
-
return createDriver(prepared.engine, defaultHost).interruptDetachedConversation(prepared);
|
|
2821
|
-
},
|
|
2822
|
-
async deleteNativeDetachedConversation({ id, context, input = {} } = {}) {
|
|
2823
|
-
if (!await authorize({ context, conversationId: id, operation: "deleteDetachedConversation" })) {
|
|
2824
|
-
throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
|
|
2825
|
-
}
|
|
2826
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2827
|
-
throw new TypeError("Historical native cleanup requires a configured conversation host.");
|
|
2828
|
-
}
|
|
2829
|
-
const prepared = await defaultHost.conversation({ id, context, input, operation: "deleteDetachedConversation" });
|
|
2830
|
-
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2831
|
-
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2832
|
-
throw new TypeError("Historical native cleanup requires its authorized host preparation.");
|
|
2833
|
-
}
|
|
2834
|
-
return createDriver(prepared.engine, defaultHost).deleteDetachedConversation(prepared);
|
|
2835
|
-
},
|
|
2836
|
-
// Trusted server storage operations retain the saved engine and preservation
|
|
2837
|
-
// policy. They neither authorize inference nor acquire a retained handle.
|
|
2838
|
-
async listNativeConversationStorage({ id, context, binding } = {}) {
|
|
2839
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2840
|
-
throw new TypeError("Native history storage requires a configured conversation host.");
|
|
2841
|
-
}
|
|
2842
|
-
const prepared = await defaultHost.conversation({ id, context, input: binding, operation: "listNativeConversationStorage" });
|
|
2843
|
-
if (prepared?.sessionId !== id || prepared.binding !== binding || prepared.engine !== binding?.engineId ||
|
|
2844
|
-
!["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2845
|
-
!Object.hasOwn(prepared, "context")) {
|
|
2846
|
-
throw new TypeError("Native history storage requires its saved binding and configured host preparation.");
|
|
2847
|
-
}
|
|
2848
|
-
return createDriver(prepared.engine, defaultHost).listConversationStorage(prepared);
|
|
2849
|
-
},
|
|
2850
|
-
async retireNativeConversationHistory({ id, context, binding } = {}) {
|
|
2851
|
-
if (typeof defaultHost?.conversation !== "function") {
|
|
2852
|
-
throw new TypeError("Native history storage requires a configured conversation host.");
|
|
2853
|
-
}
|
|
2854
|
-
const prepared = await defaultHost.conversation({ id, context, input: binding, operation: "retireConversationHistory" });
|
|
2855
|
-
if (prepared?.sessionId !== id || prepared.binding !== binding || prepared.engine !== binding?.engineId ||
|
|
2856
|
-
!["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2857
|
-
!Object.hasOwn(prepared, "context")) {
|
|
2858
|
-
throw new TypeError("Native history storage requires its saved binding and configured host preparation.");
|
|
2859
|
-
}
|
|
2860
|
-
return createDriver(prepared.engine, defaultHost).retireConversationHistory(prepared);
|
|
2861
|
-
},
|
|
2862
|
-
// Cleanup may retry after runtime closure. Missing or unready retained
|
|
2863
|
-
// handles use the configured native owner without opening a Main record.
|
|
2864
|
-
async disposeNative({ namespace, sessionId, context, options }) {
|
|
2865
|
-
const authorizeCleanup = async () => {
|
|
2866
|
-
if (!await authorize({ context, conversationId: sessionId, operation: "dispose" })) {
|
|
2867
|
-
throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
|
|
2868
|
-
}
|
|
2869
|
-
};
|
|
2870
|
-
const cleanupUnopened = async () => {
|
|
2871
|
-
if (typeof defaultHost?.conversation !== "function") return null;
|
|
2872
|
-
await authorizeCleanup();
|
|
2873
|
-
const prepared = await defaultHost.conversation({ id: sessionId, context, input: options, operation: "dispose" });
|
|
2874
|
-
if (prepared?.sessionId !== sessionId || prepared.namespace !== namespace ||
|
|
2875
|
-
!["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2876
|
-
!Object.hasOwn(prepared, "context") || !Object.hasOwn(prepared, "options")) {
|
|
2877
|
-
throw new TypeError("Native cleanup requires its authorized host preparation.");
|
|
2878
|
-
}
|
|
2879
|
-
return { result: await createDriver(prepared.engine, defaultHost).disposeNative(prepared) };
|
|
2880
|
-
};
|
|
2881
|
-
const entry = entries.get(namespace);
|
|
2882
|
-
if (!entry || entry.disposed) return cleanupUnopened();
|
|
2883
|
-
if (entry.id !== sessionId || !entry.conversation) {
|
|
2884
|
-
throw failure("Native cleanup requires its original bound conversation.", "conversation_host_mismatch", 409, { messageCode: "assistant.conversation.cleanupNeedsOriginalConversation" });
|
|
2885
|
-
}
|
|
2886
|
-
await authorizeCleanup();
|
|
2887
|
-
const retained = await serial(entry, async () => {
|
|
2888
|
-
if (entry.disposed) return null;
|
|
2889
|
-
await authorizeCleanup();
|
|
2890
|
-
if (!entry.provider) {
|
|
2891
|
-
retireEntry(entry);
|
|
2892
|
-
return null;
|
|
2893
|
-
}
|
|
2894
|
-
return { result: await disposeEntry(entry, context, options, "native") };
|
|
2895
|
-
});
|
|
2896
|
-
return retained || cleanupUnopened();
|
|
2897
|
-
},
|
|
2898
|
-
// Trusted server lifecycle: the application deleted or archived a logical
|
|
2899
|
-
// conversation that it binds through its own commands. Its open entry, if
|
|
2900
|
-
// any, leaves memory and its handles close. Nothing is opened; native and
|
|
2901
|
-
// storage-owned conversations keep their own disposal.
|
|
2902
|
-
async retireOpenConversation({ namespace, sessionId }) {
|
|
2903
|
-
const entry = entries.get(namespace);
|
|
2904
|
-
if (!entry || entry.id !== sessionId) return { retired: false };
|
|
2905
|
-
if (!entry.conversation?.commands) {
|
|
2906
|
-
throw new TypeError("Only a logical conversation bound through its commands retires without disposal.");
|
|
2907
|
-
}
|
|
2908
|
-
await entry.initializing.catch(() => {});
|
|
2909
|
-
return serial(entry, async () => {
|
|
2910
|
-
if (entry.disposed || entries.get(namespace) !== entry) return { retired: false };
|
|
2911
|
-
retireEntry(entry);
|
|
2912
|
-
return { retired: true };
|
|
2913
|
-
});
|
|
2914
|
-
},
|
|
2915
|
-
// Host publication from the same native owner, through the existing entry.
|
|
2916
|
-
// This does not subscribe to a provider or retain another observer registry.
|
|
2917
|
-
async publishNative({ namespace, sessionId, event }) {
|
|
2918
|
-
const entry = entries.get(namespace);
|
|
2919
|
-
if (!entry || entry.id !== sessionId || entry.disposed || !entry.conversation) return;
|
|
2920
|
-
await entry.initializing;
|
|
2921
|
-
return entry.conversation.commands ? publish(entry, event) : entry.provider.publishNative(event);
|
|
2922
|
-
},
|
|
2923
|
-
async close() {
|
|
2924
|
-
closed = true;
|
|
2925
|
-
const results = await Promise.allSettled([...entries.values()].map(entry => serial(entry, async () => {
|
|
2926
|
-
await entry.initializing;
|
|
2927
|
-
const result = await disposeEntry(entry);
|
|
2928
|
-
if (result?.ok === false) throw Object.assign(new Error(result.error || "Native cleanup could not be confirmed."), result);
|
|
2929
|
-
})));
|
|
2930
|
-
const errors = results.filter(result => result.status === "rejected").map(result => result.reason);
|
|
2931
|
-
if (errors.length) throw new AggregateError(errors, "Some conversations could not be safely closed. Restore their storage or execution host and retry close().");
|
|
2932
|
-
}
|
|
210
|
+
projectStoredPersistentConversation,
|
|
211
|
+
readPersistentConversation,
|
|
212
|
+
startPersistentConversationTurn,
|
|
213
|
+
inspectPersistentConversationAdmission,
|
|
214
|
+
preparePersistentConversationChangeover,
|
|
215
|
+
inspectPersistentConversationDelivery,
|
|
216
|
+
stopPersistentConversation,
|
|
217
|
+
deletePersistentConversation,
|
|
218
|
+
runScopedTurn,
|
|
219
|
+
inspectNativeTemporaryActivity: lifecycle.inspectNativeTemporaryActivity,
|
|
220
|
+
releaseNativeRenewalPredecessorProcessExitProof: lifecycle.releaseNativeRenewalPredecessorProcessExitProof,
|
|
221
|
+
releaseNativeRenewalSuccessorProcessExitProof: lifecycle.releaseNativeRenewalSuccessorProcessExitProof,
|
|
222
|
+
generateNativeRenewalHandover: lifecycle.generateNativeRenewalHandover,
|
|
223
|
+
seedNativeRenewalHandover: lifecycle.seedNativeRenewalHandover,
|
|
224
|
+
updateNativeConversationSettings: lifecycle.updateNativeConversationSettings,
|
|
225
|
+
ensureNativeConversation: lifecycle.ensureNativeConversation,
|
|
226
|
+
createNativeConversation: lifecycle.createNativeConversation,
|
|
227
|
+
runNativeDetachedConversation: lifecycle.runNativeDetachedConversation,
|
|
228
|
+
open: handles.open,
|
|
229
|
+
closeNativeProject: lifecycle.closeNativeProject,
|
|
230
|
+
invalidateNativeRuntimes: lifecycle.invalidateNativeRuntimes,
|
|
231
|
+
reconcileNativeSessions: lifecycle.reconcileNativeSessions,
|
|
232
|
+
unsubscribeNativeSessions: lifecycle.unsubscribeNativeSessions,
|
|
233
|
+
interruptNativeDetachedConversation: lifecycle.interruptNativeDetachedConversation,
|
|
234
|
+
deleteNativeDetachedConversation: lifecycle.deleteNativeDetachedConversation,
|
|
235
|
+
listNativeConversationStorage: lifecycle.listNativeConversationStorage,
|
|
236
|
+
retireNativeConversationHistory: lifecycle.retireNativeConversationHistory,
|
|
237
|
+
disposeNative: lifecycle.disposeNative,
|
|
238
|
+
retireOpenConversation,
|
|
239
|
+
publishNative,
|
|
240
|
+
close
|
|
2933
241
|
});
|
|
2934
242
|
}
|