@jskit-ai/assistant-core 0.1.244 → 0.2.1
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/CHANGELOG.md +539 -0
- package/examples/conversation/package.json +5 -5
- package/locales/en.json +383 -0
- package/package.json +7 -6
- package/src/client/components/AssistantSettingsFormCard.vue +4 -1
- package/src/client/conversation/AssistantAttachmentPreview.vue +6 -3
- package/src/client/conversation/AssistantAttachmentQueue.vue +36 -37
- package/src/client/conversation/AssistantComposerActions.vue +6 -3
- package/src/client/conversation/AssistantComposerSupport.vue +8 -5
- package/src/client/conversation/AssistantConversationElement.vue +16 -13
- package/src/client/conversation/AssistantConversationStatus.vue +5 -2
- package/src/client/conversation/AssistantGoalControl.vue +22 -16
- package/src/client/conversation/AssistantMessageAttachments.vue +7 -4
- package/src/client/conversation/AssistantModelControl.vue +23 -20
- package/src/client/conversation/AssistantProgress.vue +6 -3
- package/src/client/conversation/AssistantQuestionInputs.vue +9 -6
- package/src/client/conversation/AssistantTranscript.vue +39 -28
- package/src/client/conversation/LongTextPreviewBlocks.vue +10 -2
- package/src/client/conversation/attachmentSize.js +38 -0
- package/src/client/conversation/messageDelivery.js +14 -5
- package/src/client/conversation/submitText.js +11 -4
- package/src/client/conversation/useAssistantAttachments.js +40 -30
- package/src/client/conversation/useAssistantSuggestions.js +4 -1
- package/src/client/i18n.js +6 -0
- package/src/client/lib/assistantApi.js +14 -2
- package/src/server/conversation/attachments.js +24 -14
- package/src/server/conversation/claudeProcess.js +4 -2
- package/src/server/conversation/claudeTurn.js +31 -12
- package/src/server/conversation/codexConversations.js +4 -0
- package/src/server/conversation/codexGoalCommands.js +9 -9
- package/src/server/conversation/codexHelperExecution.js +4 -0
- package/src/server/conversation/codexHelperThreads.js +2 -0
- package/src/server/conversation/codexMessageCommands.js +7 -1
- package/src/server/conversation/codexTurnControl.js +2 -1
- package/src/server/conversation/codexTurnState.js +2 -0
- package/src/server/conversation/continuity.js +16 -6
- package/src/server/conversation/runtime.js +193 -141
- package/src/server/lib/ndjson.js +19 -1
- package/src/server/lib/providers/common.js +7 -2
- package/src/shared/conversation/numberedQuestionSugar.js +7 -1
- package/src/shared/conversation/turns.js +10 -2
- package/test/claudeTurn.test.js +17 -0
- package/test/i18nCatalog.test.js +22 -0
|
@@ -22,8 +22,55 @@ import { conversationAttachmentManifest } from "../../shared/conversation/attach
|
|
|
22
22
|
import { createConversationChangeover, conversationContinuity, conversationHistoryVersions,
|
|
23
23
|
conversationMessageIdentity, conversationMessageVersion, conversationNativeMessages, conversationRequestText } from "./continuity.js";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// code stays the value callers branch on; messageCode names this sentence so a
|
|
26
|
+
// client can show it in the user's language.
|
|
27
|
+
function failure(message, code, statusCode = 409, { messageCode, messageParams } = {}) {
|
|
28
|
+
return Object.assign(new Error(message), { code, statusCode }, messageCode ? { messageCode, messageParams: messageParams || {} } : {});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A native delivery result keeps its own sentence and messageCode when it has one.
|
|
32
|
+
function deliveryFailure(delivered) {
|
|
33
|
+
const code = delivered?.code || "conversation_delivery_uncertain";
|
|
34
|
+
return delivered?.error
|
|
35
|
+
? failure(delivered.error, code, 409, delivered)
|
|
36
|
+
: failure("Native delivery could not be confirmed.", code, 409, { messageCode: "assistant.conversation.deliveryUnconfirmed" });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Error texts that conversation state keeps, including saved records written by
|
|
40
|
+
// earlier releases and the API engine's turn failures. A snapshot or event names
|
|
41
|
+
// them so a client can show them in the user's language; any other error text is
|
|
42
|
+
// shown as sent.
|
|
43
|
+
const STATE_ERROR_MESSAGES = Object.freeze([
|
|
44
|
+
{ message: "This conversation exceeds the configured API history limit. Start another conversation or increase the limit.",
|
|
45
|
+
messageCode: "assistant.conversation.apiHistoryLimit" },
|
|
46
|
+
{ message: "The model reached its output token limit before completing the answer. Increase maxOutputTokens in the conversation limits.",
|
|
47
|
+
messageCode: "assistant.conversation.outputTokenLimit" },
|
|
48
|
+
{ message: "The model stream ended without confirming its application tool calls.", messageCode: "assistant.conversation.toolCallsUnconfirmed" },
|
|
49
|
+
{ message: "The model stream ended without confirming a completed answer.", messageCode: "assistant.conversation.answerUnconfirmed" },
|
|
50
|
+
{ message: "Application tool progress text exceeds the configured API limit.", messageCode: "assistant.conversation.toolProgressLimit" },
|
|
51
|
+
{ message: "The assistant exceeded the configured final reply limit.", messageCode: "assistant.conversation.finalReplyLimit" },
|
|
52
|
+
{ message: "Conversation updates exceed the configured history budget. Renew the context with a continuity briefing before continuing.",
|
|
53
|
+
messageCode: "assistant.conversation.historyBudgetExceeded" },
|
|
54
|
+
{ message: "The continuity briefing exceeds the configured history budget.", messageCode: "assistant.conversation.briefingBudgetExceeded" },
|
|
55
|
+
{ message: "Assistant provider is not configured.", messageCode: "assistant.provider.notConfigured" },
|
|
56
|
+
{ message: "The previous submission was interrupted. Check delivery before submitting it again.",
|
|
57
|
+
messageCode: "assistant.conversation.submissionInterrupted" },
|
|
58
|
+
{ message: "The application restarted before this answer completed. The request was not replayed.",
|
|
59
|
+
messageCode: "assistant.conversation.restartInterrupted" },
|
|
60
|
+
{ message: "Copied into a stopped development environment. This request was not replayed.",
|
|
61
|
+
messageCode: "assistant.conversation.copiedInterrupted" },
|
|
62
|
+
{ message: "The application operation result could not be saved. Restore storage access and retry saving.",
|
|
63
|
+
messageCode: "assistant.conversation.operationResultUnsaved" },
|
|
64
|
+
{ message: "Conversation progress could not be saved. Restore access to its storage before continuing.",
|
|
65
|
+
messageCode: "assistant.conversation.progressUnsaved" },
|
|
66
|
+
{ message: "The conversation request exceeded its time limit.", messageCode: "assistant.conversation.requestTimedOut" },
|
|
67
|
+
{ message: "Work stopped.", messageCode: "assistant.conversation.workStopped" },
|
|
68
|
+
{ message: "Stopped by user.", messageCode: "assistant.conversation.stoppedByUser" }
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
function stateErrorFields(error) {
|
|
72
|
+
const known = STATE_ERROR_MESSAGES.find(entry => entry.message === error);
|
|
73
|
+
return known ? { errorMessageCode: known.messageCode, errorMessageParams: {} } : {};
|
|
27
74
|
}
|
|
28
75
|
|
|
29
76
|
const submittedMessage = turn => turn.user || turn.system;
|
|
@@ -86,14 +133,14 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
86
133
|
// Send and its immediate admission share one authorization. Dispatch,
|
|
87
134
|
// receipt admission and every other operation authorize again.
|
|
88
135
|
async function access(context, id, operation, entry, grant) {
|
|
89
|
-
if (closed || entry?.disposed) throw failure("This conversation handle is closed.", "conversation_closed");
|
|
136
|
+
if (closed || entry?.disposed) throw failure("This conversation handle is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleClosed" });
|
|
90
137
|
if (!grant?.has(operation)) {
|
|
91
138
|
if (!await authorize({ context, conversationId: id, operation })) {
|
|
92
139
|
throw failure("This conversation is not available to this identity.", "conversation_forbidden", 403);
|
|
93
140
|
}
|
|
94
141
|
grant?.add(operation);
|
|
95
142
|
}
|
|
96
|
-
if (closed || entry?.disposed) throw failure("This conversation handle is closed.", "conversation_closed");
|
|
143
|
+
if (closed || entry?.disposed) throw failure("This conversation handle is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleClosed" });
|
|
97
144
|
}
|
|
98
145
|
|
|
99
146
|
function serial(entry, callback) {
|
|
@@ -175,7 +222,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
175
222
|
function readTranscriptPage(entry, query) {
|
|
176
223
|
if (!entry.conversation) return transcript.readConversationLogPage(entry.id, { ...query, presentation: completedEnvelope });
|
|
177
224
|
if (typeof entry.conversation.transcript.readConversationLogPage !== "function") {
|
|
178
|
-
throw failure("This conversation does not supply paged history.", "conversation_unsupported", 400);
|
|
225
|
+
throw failure("This conversation does not supply paged history.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.pagedHistoryUnsupported" });
|
|
179
226
|
}
|
|
180
227
|
return entry.conversation.transcript.readConversationLogPage(query);
|
|
181
228
|
}
|
|
@@ -234,9 +281,11 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
234
281
|
const request = savedRequest && (entry.engine === "api" || savedRequest.attempted || savedRequest.inspectionOnly ||
|
|
235
282
|
entry.active?.requests.has(savedRequest.messageId)) ? savedRequest : null;
|
|
236
283
|
const streaming = entry.conversation ? await entry.conversation.readStream() : streams.read(entry.id);
|
|
284
|
+
const error = logical ? logical.error || "" : entry.error || nativeTurn?.error || request?.error || last?.error || "";
|
|
237
285
|
const pendingRequest = request && !(completedEnvelope && request.origin === "application" && request.completedEnvelope === true) ? Object.fromEntries([
|
|
238
286
|
"messageId", "text", "origin", "data", "attachments", "at", "goal", "steering", "error"
|
|
239
287
|
].filter(name => Object.hasOwn(request, name)).map(name => [name, request[name]])) : null;
|
|
288
|
+
if (pendingRequest?.error) Object.assign(pendingRequest, stateErrorFields(pendingRequest.error));
|
|
240
289
|
return {
|
|
241
290
|
id: entry.id, engine: entry.engine, capabilities: capabilities(entry), configuration: metadata.runtime.configuration,
|
|
242
291
|
segmentId: metadata.runtime.segmentId,
|
|
@@ -245,7 +294,8 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
245
294
|
status: logical ? logical.status : entry.storageFailure || entry.executionFailure ? "unavailable" : entry.active || nativeTurn?.active ? "working" : metadata.runtime.replacement ? "replacement-pending"
|
|
246
295
|
: metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly) ? "unconfirmed" : "ready",
|
|
247
296
|
phase: logical ? logical.phase || "" : entry.phase || (nativeTurn?.active && nativeTurn.status !== "observation_lost" ? ["compacting", "background"].includes(nativeTurn.phase) ? nativeTurn.phase : "working" : ""),
|
|
248
|
-
error
|
|
297
|
+
error,
|
|
298
|
+
...stateErrorFields(error),
|
|
249
299
|
goal: entry.goal || null,
|
|
250
300
|
pendingRequest: logical ? logical.pendingRequest || null : pendingRequest,
|
|
251
301
|
...(logical?.presentation ? { presentation: logical.presentation } : {}),
|
|
@@ -349,7 +399,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
349
399
|
if (recovered && entry.executionFailure !== error) return;
|
|
350
400
|
entry.executionFailure = recovered ? null : error;
|
|
351
401
|
entry.error = error.message;
|
|
352
|
-
await emit(entry, { type: "error", error: error.message });
|
|
402
|
+
await emit(entry, { type: "error", error: error.message, ...stateErrorFields(error.message) });
|
|
353
403
|
},
|
|
354
404
|
async writeBinding(value) {
|
|
355
405
|
if (entry.conversation) throw new Error("A bound native conversation writes identity through its original preparation owner.");
|
|
@@ -496,18 +546,18 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
496
546
|
if (entry.conversation) {
|
|
497
547
|
entry.applicationTools = boundApplicationTools(entry.conversation);
|
|
498
548
|
entry.engine = entry.conversation.engine;
|
|
499
|
-
if (requestedEngine && requestedEngine !== entry.engine) throw failure("The selected native engine changed.", "conversation_engine_mismatch");
|
|
549
|
+
if (requestedEngine && requestedEngine !== entry.engine) throw failure("The selected native engine changed.", "conversation_engine_mismatch", 409, { messageCode: "assistant.conversation.engineChanged" });
|
|
500
550
|
entry.driver = createDriver(entry.engine, entry.host);
|
|
501
551
|
if (entry.conversation.commands) return;
|
|
502
552
|
if (entry.conversation.native?.scoped) {
|
|
503
553
|
entry.provider = await providers.open(entry.driver, { conversation: entry.conversation });
|
|
504
|
-
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed");
|
|
554
|
+
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleDisposed" });
|
|
505
555
|
return;
|
|
506
556
|
}
|
|
507
557
|
const state = await readState(entry);
|
|
508
|
-
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed");
|
|
558
|
+
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleDisposed" });
|
|
509
559
|
entry.provider = await openProvider(entry, entry.driver, state.binding, state.segmentId);
|
|
510
|
-
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed");
|
|
560
|
+
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleDisposed" });
|
|
511
561
|
return;
|
|
512
562
|
}
|
|
513
563
|
const binding = await storage.write(entry.id, async transaction => {
|
|
@@ -517,7 +567,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
517
567
|
const metadata = await transaction.readMetadata();
|
|
518
568
|
entry.engine = metadata.runtime?.engine || requestedEngine || defaultEngine;
|
|
519
569
|
if (completedEnvelope && !["codex", "claude", "opencode"].includes(entry.engine)) {
|
|
520
|
-
throw failure("Completed envelopes require a runtime-owned native conversation.", "conversation_unsupported", 400);
|
|
570
|
+
throw failure("Completed envelopes require a runtime-owned native conversation.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.completedEnvelopesNeedNativeOwner" });
|
|
521
571
|
}
|
|
522
572
|
if (configuration) configuration = normalizeConversationConfiguration(configuration, { engine: entry.engine, defaultIntegrationId });
|
|
523
573
|
const driver = entry.driver = createDriver(entry.engine, entry.host);
|
|
@@ -526,9 +576,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
526
576
|
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");
|
|
527
577
|
}
|
|
528
578
|
if (hasUnfinishedConversationRewind(metadata.runtime)) {
|
|
529
|
-
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");
|
|
579
|
+
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" });
|
|
530
580
|
}
|
|
531
|
-
if (requestedEngine && requestedEngine !== entry.engine) throw failure("Use replace() to change this conversation's engine.", "conversation_engine_mismatch");
|
|
581
|
+
if (requestedEngine && requestedEngine !== entry.engine) throw failure("Use replace() to change this conversation's engine.", "conversation_engine_mismatch", 409, { messageCode: "assistant.conversation.useReplaceForEngine" });
|
|
532
582
|
driver.validateConfiguration(metadata.runtime.configuration);
|
|
533
583
|
if (configuration && !isDeepStrictEqual(configuration, metadata.runtime.configuration)) {
|
|
534
584
|
throw failure("Use configure() to change a saved conversation's settings.", "conversation_configuration_mismatch");
|
|
@@ -583,7 +633,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
583
633
|
|
|
584
634
|
async function prepareCompletedResponse(entry, context, { messageId, turnId } = {}, { signal, toolSet } = {}) {
|
|
585
635
|
if (!completedEnvelope || entry.conversation || !["codex", "claude", "opencode"].includes(entry.engine)) {
|
|
586
|
-
throw failure("This conversation does not own completed envelopes.", "conversation_unsupported", 400);
|
|
636
|
+
throw failure("This conversation does not own completed envelopes.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.completedEnvelopesNotOwned" });
|
|
587
637
|
}
|
|
588
638
|
if (typeof messageId !== "string" || !messageId || typeof turnId !== "string" || !turnId || !(signal instanceof AbortSignal)) {
|
|
589
639
|
throw new TypeError("Completed responses require their exact receipt and the application worker signal.");
|
|
@@ -599,9 +649,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
599
649
|
|
|
600
650
|
async function exact(transaction, { retry = false } = {}) {
|
|
601
651
|
if (closed || entry.disposed || entry.bindingOwner !== owner || entry.segmentId !== segmentId || entry.engine !== engine) {
|
|
602
|
-
throw failure("The completed response owner changed.", "conversation_tool_request_retired");
|
|
652
|
+
throw failure("The completed response owner changed.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.responseOwnerChanged" });
|
|
603
653
|
}
|
|
604
|
-
if (entry.active) throw failure("Wait for the native response to finish.", "conversation_busy");
|
|
654
|
+
if (entry.active) throw failure("Wait for the native response to finish.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForNativeResponse" });
|
|
605
655
|
if (entry.executionFailure) throw entry.executionFailure;
|
|
606
656
|
if (entry.storageFailure && !retry) throw entry.storageFailure;
|
|
607
657
|
const { runtime } = await transaction.readMetadata();
|
|
@@ -613,13 +663,13 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
613
663
|
authored.origin !== "application" || saved?.completedEnvelope !== true || saved.engine !== engine ||
|
|
614
664
|
saved.segmentId !== segmentId || saved.status !== "complete" || saved.supersededBy ||
|
|
615
665
|
typeof turn.assistant?.text !== "string" || !turn.assistant.text || !turn.assistant.messageId) {
|
|
616
|
-
throw failure("The completed response has no current verified receipt.", "conversation_completed_response_unavailable");
|
|
666
|
+
throw failure("The completed response has no current verified receipt.", "conversation_completed_response_unavailable", 409, { messageCode: "assistant.conversation.completedResponseNoReceipt" });
|
|
617
667
|
}
|
|
618
668
|
const turnIds = await transaction.listTurnIds();
|
|
619
669
|
for (const id of turnIds.slice(turnIds.indexOf(turnId) + 1)) {
|
|
620
670
|
const later = await transaction.readTurn(id);
|
|
621
671
|
if (later.metadata?.runtime?.segmentId === segmentId && later.metadata.runtime.completedEnvelope === true) {
|
|
622
|
-
throw failure("A newer completed response owns this worker.", "conversation_tool_request_retired");
|
|
672
|
+
throw failure("A newer completed response owns this worker.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.newerResponseOwnsWorker" });
|
|
623
673
|
}
|
|
624
674
|
}
|
|
625
675
|
// These verified process references may refresh on cold inspection;
|
|
@@ -627,19 +677,19 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
627
677
|
const { codexAppServerRun: run, executionId, processDirectory, ...binding } = runtime.binding;
|
|
628
678
|
if (engine === "codex" && (!saved.nativeTurnId || run?.outerTurnId !== messageId ||
|
|
629
679
|
run.providerThreadId !== binding.threadId || run.providerTurnId !== saved.nativeTurnId || run.active || run.state !== "completed")) {
|
|
630
|
-
throw failure("The completed response has no exact native turn receipt.", "conversation_completed_response_unavailable");
|
|
680
|
+
throw failure("The completed response has no exact native turn receipt.", "conversation_completed_response_unavailable", 409, { messageCode: "assistant.conversation.completedResponseNoTurnReceipt" });
|
|
631
681
|
}
|
|
632
682
|
if (engine === "claude" && typeof saved.nativeToolAttempt !== "boolean") {
|
|
633
|
-
throw failure("The completed Claude response has no native tool-attempt evidence.", "conversation_completed_response_unavailable");
|
|
683
|
+
throw failure("The completed Claude response has no native tool-attempt evidence.", "conversation_completed_response_unavailable", 409, { messageCode: "assistant.conversation.completedClaudeNoToolEvidence" });
|
|
634
684
|
}
|
|
635
685
|
const calls = turn.metadata.applicationTools || [];
|
|
636
686
|
if (!Array.isArray(calls) || calls.length > 1 || calls.some(call => call.id !== toolCallId)) {
|
|
637
|
-
throw failure("This response already belongs to a different application operation.", "conversation_tool_receipt_conflict");
|
|
687
|
+
throw failure("This response already belongs to a different application operation.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.responseOtherOperation" });
|
|
638
688
|
}
|
|
639
689
|
const identity = { configuration: runtime.configuration, binding, assistant: turn.assistant,
|
|
640
690
|
nativeTurnId: saved.nativeTurnId, ...(engine === "claude" ? { nativeToolAttempt: saved.nativeToolAttempt } : {}) };
|
|
641
691
|
if (captured && !isDeepStrictEqual(captured, identity)) {
|
|
642
|
-
throw failure("The completed response identity changed.", "conversation_tool_request_retired");
|
|
692
|
+
throw failure("The completed response identity changed.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.responseIdentityChanged" });
|
|
643
693
|
}
|
|
644
694
|
return { runtime, turn, calls, identity };
|
|
645
695
|
}
|
|
@@ -655,7 +705,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
655
705
|
await access(context, entry.id, "tool", entry);
|
|
656
706
|
if (!observation?.accepted || observation.recoveryLimitation || observation.conversationTurn &&
|
|
657
707
|
observation.conversationTurn.turnId !== turnId || engine === "codex" && observation.nativeTurnId !== saved.turn.metadata.runtime.nativeTurnId) {
|
|
658
|
-
throw failure("The native owner could not verify this response association.", "conversation_completed_response_unavailable");
|
|
708
|
+
throw failure("The native owner could not verify this response association.", "conversation_completed_response_unavailable", 409, { messageCode: "assistant.conversation.responseAssociationUnverified" });
|
|
659
709
|
}
|
|
660
710
|
// Canonical successful completion owns the exact final, including a
|
|
661
711
|
// StructuredOutput carrier whose native history may contain only commentary.
|
|
@@ -678,27 +728,27 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
678
728
|
signal.throwIfAborted();
|
|
679
729
|
const decoded = readAssistantResponseEnvelope(saved.turn.assistant.text);
|
|
680
730
|
if (decoded.kind !== "reply" || saved.turn.metadata.runtime.nativeToolAttempt === true || saved.calls.length) {
|
|
681
|
-
throw failure("This completed response does not own a plain reply.", "conversation_tool_receipt_conflict");
|
|
731
|
+
throw failure("This completed response does not own a plain reply.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.completedResponseNoPlainReply" });
|
|
682
732
|
}
|
|
683
733
|
const text = decoded.text.trim();
|
|
684
734
|
const target = await transaction.readTurn(input.turnId);
|
|
685
735
|
if (!text || input.turnId === turnId || !target || isCompletedEnvelopeTurn(target) ||
|
|
686
736
|
target.metadata?.runtime?.engine || !(target.user || target.system) || saved.runtime.lastEngine !== engine ||
|
|
687
737
|
!saved.runtime.seen || typeof saved.runtime.seen !== "object" || Array.isArray(saved.runtime.seen)) {
|
|
688
|
-
throw failure("The completed reply has no current plain authored target.", "conversation_tool_receipt_conflict");
|
|
738
|
+
throw failure("The completed reply has no current plain authored target.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.completedReplyNoTarget" });
|
|
689
739
|
}
|
|
690
740
|
const assistantId = conversationMessageIdentity(input.turnId, { role: "assistant" });
|
|
691
741
|
const published = saved.turn.metadata.runtime.publishedReplyTurnId;
|
|
692
742
|
if (published !== undefined) {
|
|
693
743
|
if (published !== input.turnId || !target.assistant || target.assistant.text !== text ||
|
|
694
744
|
saved.runtime.seen[assistantId] !== conversationMessageVersion(target.assistant, { includeData: true })) {
|
|
695
|
-
throw failure("The published reply target or text changed.", "conversation_tool_receipt_conflict");
|
|
745
|
+
throw failure("The published reply target or text changed.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.publishedReplyChanged" });
|
|
696
746
|
}
|
|
697
747
|
signal.throwIfAborted();
|
|
698
748
|
return target;
|
|
699
749
|
}
|
|
700
750
|
if (target.assistant || Object.hasOwn(saved.runtime.seen, assistantId)) {
|
|
701
|
-
throw failure("This plain reply target already has unassociated content or consumption.", "conversation_tool_receipt_conflict");
|
|
751
|
+
throw failure("This plain reply target already has unassociated content or consumption.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.replyTargetUsed" });
|
|
702
752
|
}
|
|
703
753
|
// The runtime transcript uses this same reentrant wrapper: its
|
|
704
754
|
// original upsert joins this draft rather than starting a second write.
|
|
@@ -715,10 +765,10 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
715
765
|
},
|
|
716
766
|
tools: { execute(input, options = {}) {
|
|
717
767
|
return serial(entry, async () => {
|
|
718
|
-
if (input?.id !== toolCallId) throw failure("The operation has a different completed response identity.", "conversation_tool_receipt_conflict");
|
|
768
|
+
if (input?.id !== toolCallId) throw failure("The operation has a different completed response identity.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.operationResponseIdentity" });
|
|
719
769
|
const saved = await inspect();
|
|
720
770
|
if (saved.turn.metadata.runtime.publishedReplyTurnId !== undefined) {
|
|
721
|
-
throw failure("This completed response already published its plain reply.", "conversation_tool_receipt_conflict");
|
|
771
|
+
throw failure("This completed response already published its plain reply.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.plainReplyPublished" });
|
|
722
772
|
}
|
|
723
773
|
const workerSignal = options.signal && options.signal !== signal ? AbortSignal.any([signal, options.signal]) : signal;
|
|
724
774
|
workerSignal.throwIfAborted();
|
|
@@ -731,7 +781,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
731
781
|
if (calls.length !== 1 || next?.id !== toolCallId || previous &&
|
|
732
782
|
(previous.name !== next.name || previous.arguments !== next.arguments ||
|
|
733
783
|
previous.status !== "running" && next.status === "running")) {
|
|
734
|
-
throw failure("The saved response operation changed.", "conversation_tool_receipt_conflict");
|
|
784
|
+
throw failure("The saved response operation changed.", "conversation_tool_receipt_conflict", 409, { messageCode: "assistant.conversation.savedOperationChanged" });
|
|
735
785
|
}
|
|
736
786
|
await saveTools(entry, transaction, request, calls);
|
|
737
787
|
});
|
|
@@ -801,7 +851,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
801
851
|
assertCurrent() {
|
|
802
852
|
active.controller.signal.throwIfAborted();
|
|
803
853
|
if (entry.active !== active || current !== request || !request.accepted || request.finished) {
|
|
804
|
-
throw failure("This application tool's admitted message is no longer current.", "conversation_tool_request_retired");
|
|
854
|
+
throw failure("This application tool's admitted message is no longer current.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.applicationToolMessageStale" });
|
|
805
855
|
}
|
|
806
856
|
},
|
|
807
857
|
...(entry.conversation ? { assertCompletionCurrent() {
|
|
@@ -810,7 +860,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
810
860
|
!active.finishing || active.timedOut || problem || toolFailure || entry.executionFailure || entry.storageFailure ||
|
|
811
861
|
current !== request || !request.accepted || entry.lastAcceptedRequest !== request ||
|
|
812
862
|
entry.active && entry.active !== active) {
|
|
813
|
-
throw failure("This native completion's admitted message is no longer current.", "conversation_tool_request_retired");
|
|
863
|
+
throw failure("This native completion's admitted message is no longer current.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.nativeCompletionMessageStale" });
|
|
814
864
|
}
|
|
815
865
|
} } : {}) }));
|
|
816
866
|
} } : {}),
|
|
@@ -1049,7 +1099,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1049
1099
|
if (request.input.goal && request.context?.signal?.aborted) {
|
|
1050
1100
|
throw Object.assign(failure("The goal command was stopped before delivery.", "conversation_goal_stopped"), { delivery: "not-sent" });
|
|
1051
1101
|
}
|
|
1052
|
-
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"), { delivery: "not-sent" });
|
|
1102
|
+
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" });
|
|
1053
1103
|
}
|
|
1054
1104
|
|
|
1055
1105
|
async function deliverNative(request, dispatch) {
|
|
@@ -1064,10 +1114,10 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1064
1114
|
const dispatchPrepared = async input => {
|
|
1065
1115
|
rendered = input;
|
|
1066
1116
|
rejectionSaves.delete(request.input.messageId);
|
|
1067
|
-
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"), { delivery: "not-sent" });
|
|
1117
|
+
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" });
|
|
1068
1118
|
if ((input.contextText || "").length > maximumContinuity) throw new Error("Conversation updates exceed the configured history budget. Renew the context with a continuity briefing before continuing.");
|
|
1069
1119
|
if (request.input.goal && input.contextText) {
|
|
1070
|
-
throw failure("Send a chat message to deliver the saved continuity briefing before starting a native terminal or goal.", "conversation_replacement_briefing_pending");
|
|
1120
|
+
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" });
|
|
1071
1121
|
}
|
|
1072
1122
|
const readAttachments = attachmentReader(request);
|
|
1073
1123
|
// The bound host has already resolved its original attachment records;
|
|
@@ -1080,7 +1130,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1080
1130
|
request.deliveryTiming?.("input-prepared");
|
|
1081
1131
|
const localFiles = [...contextFiles.localFiles, ...files.localFiles];
|
|
1082
1132
|
if (request.input.goal && localFiles.length) {
|
|
1083
|
-
throw failure("Native goals require extracted text or supported image bytes for attachments.", "conversation_unsupported", 400);
|
|
1133
|
+
throw failure("Native goals require extracted text or supported image bytes for attachments.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.goalAttachmentContentUnsupported" });
|
|
1084
1134
|
}
|
|
1085
1135
|
attachmentManifest = conversationAttachmentManifest(localFiles);
|
|
1086
1136
|
request.input.attachments = files.attachments;
|
|
@@ -1201,7 +1251,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1201
1251
|
...(entry.conversation ? { turnMetadata: { engineId: engine } } : {}),
|
|
1202
1252
|
...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}), input });
|
|
1203
1253
|
if (!delivered?.delivered) {
|
|
1204
|
-
const error =
|
|
1254
|
+
const error = deliveryFailure(delivered);
|
|
1205
1255
|
if (entry.conversation) {
|
|
1206
1256
|
request.admission.resolve({ nativeResult: nativeResult || { value: delivered }, error });
|
|
1207
1257
|
request.admissionSettled = true;
|
|
@@ -1303,7 +1353,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1303
1353
|
if (entry.disposed || entry.active !== active || current !== initial || !initial.accepted || initial.finished || initial.sealed ||
|
|
1304
1354
|
entry.bindingOwner !== bindingOwner || entry.segmentId !== segmentId ||
|
|
1305
1355
|
messageId !== initial.input.messageId || threadId !== initial.nativeThreadId) {
|
|
1306
|
-
throw failure("The native tool observation belongs to a retired response.", "conversation_tool_request_retired");
|
|
1356
|
+
throw failure("The native tool observation belongs to a retired response.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.toolObservationRetired" });
|
|
1307
1357
|
}
|
|
1308
1358
|
if (!completedToolSet.byName.has(name)) return;
|
|
1309
1359
|
await storage.write(entry.id, async transaction => {
|
|
@@ -1315,7 +1365,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1315
1365
|
saved?.engine !== engine || saved.segmentId !== segmentId || saved.completedEnvelope !== true ||
|
|
1316
1366
|
typeof saved.nativeToolAttempt !== "boolean" || saved.status !== "running" || saved.supersededBy ||
|
|
1317
1367
|
entry.active !== active || current !== initial || initial.finished || initial.sealed || active.controller.signal.aborted) {
|
|
1318
|
-
throw failure("The native tool observation lost its exact admitted receipt.", "conversation_tool_request_retired");
|
|
1368
|
+
throw failure("The native tool observation lost its exact admitted receipt.", "conversation_tool_request_retired", 409, { messageCode: "assistant.conversation.toolObservationReceiptLost" });
|
|
1319
1369
|
}
|
|
1320
1370
|
await transaction.updateTurnMetadata(initial.turnId, { runtime: { ...saved, nativeToolAttempt: true } });
|
|
1321
1371
|
});
|
|
@@ -1430,7 +1480,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1430
1480
|
entry.error = error;
|
|
1431
1481
|
entry.storageFailure = null;
|
|
1432
1482
|
entry.saveProgress = null;
|
|
1433
|
-
return { type: "settled", turnId: current.turnId, status, error };
|
|
1483
|
+
return { type: "settled", turnId: current.turnId, status, error, ...stateErrorFields(error) };
|
|
1434
1484
|
}
|
|
1435
1485
|
// Preserve the common storage-retry facility by retrying the original
|
|
1436
1486
|
// failed rejection save, never by deleting or rebuilding its frozen draft.
|
|
@@ -1460,7 +1510,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1460
1510
|
entry.error = error;
|
|
1461
1511
|
entry.storageFailure = null;
|
|
1462
1512
|
entry.saveProgress = null;
|
|
1463
|
-
return { type: "settled", turnId: current.turnId, status, error: entry.error };
|
|
1513
|
+
return { type: "settled", turnId: current.turnId, status, error: entry.error, ...stateErrorFields(entry.error) };
|
|
1464
1514
|
};
|
|
1465
1515
|
try { await entry.saveProgress(); }
|
|
1466
1516
|
catch (saveError) {
|
|
@@ -1472,7 +1522,8 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1472
1522
|
if (!initial.admissionSettled) initial.admission.reject(problem || new Error("The engine did not acknowledge the message."));
|
|
1473
1523
|
entry.active = null;
|
|
1474
1524
|
await setPhase(entry, "", current.turnId);
|
|
1475
|
-
await emit(entry, { type: "settled", turnId: current.turnId, status: entry.storageFailure || entry.executionFailure ? "failed" : status,
|
|
1525
|
+
await emit(entry, { type: "settled", turnId: current.turnId, status: entry.storageFailure || entry.executionFailure ? "failed" : status,
|
|
1526
|
+
error: entry.error, ...stateErrorFields(entry.error) });
|
|
1476
1527
|
}
|
|
1477
1528
|
|
|
1478
1529
|
async function send(entry, context, input, origin = "user", goal, representation, wakeOptions) {
|
|
@@ -1483,7 +1534,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1483
1534
|
try {
|
|
1484
1535
|
await access(context, entry.id, goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send", entry, grant);
|
|
1485
1536
|
if (entry.conversation?.commands) {
|
|
1486
|
-
if (origin !== "user" || goal) throw failure("This conversation supports its original application Send operation.", "conversation_unsupported", 400);
|
|
1537
|
+
if (origin !== "user" || goal) throw failure("This conversation supports its original application Send operation.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.originalSendOnly" });
|
|
1487
1538
|
return entry.conversation.commands.send(input, context);
|
|
1488
1539
|
}
|
|
1489
1540
|
if (entry.conversation?.native?.scoped) return entry.provider.sendNative({ input, context });
|
|
@@ -1518,7 +1569,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1518
1569
|
!Array.isArray(wakeOptions.excludedMessageIds) ||
|
|
1519
1570
|
wakeOptions.excludedMessageIds.some(id => typeof id !== "string" || !id.trim() || id.length > 128) ||
|
|
1520
1571
|
new Set(wakeOptions.excludedMessageIds).size !== wakeOptions.excludedMessageIds.length) {
|
|
1521
|
-
throw failure("Native history exclusions require a server-owned completed application wake and exact message IDs.", "conversation_invalid_message", 400);
|
|
1572
|
+
throw failure("Native history exclusions require a server-owned completed application wake and exact message IDs.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.historyExclusionsInvalid" });
|
|
1522
1573
|
}
|
|
1523
1574
|
suppliedExclusions = [...wakeOptions.excludedMessageIds];
|
|
1524
1575
|
}
|
|
@@ -1526,12 +1577,12 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1526
1577
|
if (!input || typeof input.messageId !== "string" || !input.messageId.trim() || input.messageId.length > 128 ||
|
|
1527
1578
|
entry.engine !== "api" && !/^[\w-]{1,128}$/u.test(input.messageId) ||
|
|
1528
1579
|
typeof input.text !== "string" || !input.text.trim() && !attachmentIds.length || input.text.length > maximumInput) {
|
|
1529
|
-
throw failure("Bounded text or attachment IDs and a stable messageId are required.", "conversation_invalid_message", 400);
|
|
1580
|
+
throw failure("Bounded text or attachment IDs and a stable messageId are required.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.messageContentRequired" });
|
|
1530
1581
|
}
|
|
1531
|
-
if (!originalInput && input.attachments?.length) throw failure("Supply attachmentIds; only the application's resolver can supply file content.", "conversation_unsupported", 400);
|
|
1532
|
-
if (attachmentIds.length && !capabilities(entry).attachments) throw failure("File attachments are not configured for this conversation.", "conversation_unsupported", 400);
|
|
1533
|
-
if (input.steer !== undefined && typeof input.steer !== "boolean") throw failure("steer must be a boolean.", "conversation_invalid_message", 400);
|
|
1534
|
-
if (input.steer && !entry.driver.capabilities.steering) throw failure("This connection does not support steering.", "conversation_unsupported", 400);
|
|
1582
|
+
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" });
|
|
1583
|
+
if (attachmentIds.length && !capabilities(entry).attachments) throw failure("File attachments are not configured for this conversation.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.attachmentsNotConfigured" });
|
|
1584
|
+
if (input.steer !== undefined && typeof input.steer !== "boolean") throw failure("steer must be a boolean.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.steerNotBoolean" });
|
|
1585
|
+
if (input.steer && !entry.driver.capabilities.steering) throw failure("This connection does not support steering.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.steeringUnsupported" });
|
|
1535
1586
|
let data;
|
|
1536
1587
|
if (input.data !== undefined) {
|
|
1537
1588
|
try {
|
|
@@ -1541,18 +1592,18 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1541
1592
|
data = JSON.parse(encoded);
|
|
1542
1593
|
if (!data || typeof data !== "object" || Array.isArray(data)) throw new TypeError();
|
|
1543
1594
|
} catch {
|
|
1544
|
-
throw failure("Application data must be a JSON object within the conversation input limit.", "conversation_invalid_message", 400);
|
|
1595
|
+
throw failure("Application data must be a JSON object within the conversation input limit.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.applicationDataInvalid" });
|
|
1545
1596
|
}
|
|
1546
1597
|
}
|
|
1547
1598
|
const message = { messageId: input.messageId, text: input.text.trim(), origin,
|
|
1548
1599
|
attachments: attachmentIds.map(attachmentId => ({ attachmentId })), ...(data !== undefined ? { data } : {}), ...(goal ? { goal } : {}) };
|
|
1549
1600
|
if (entry.storageFailure) throw entry.storageFailure;
|
|
1550
1601
|
if (entry.executionFailure) throw entry.executionFailure;
|
|
1551
|
-
if (goal && goal.expectedSegmentId !== entry.segmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed");
|
|
1602
|
+
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" });
|
|
1552
1603
|
const pending = entry.active?.requests.get(message.messageId);
|
|
1553
1604
|
if (pending && entry.engine !== "api" && !pending.admissionSettled) {
|
|
1554
|
-
if (suppliedExclusions && !isDeepStrictEqual(pending.excludedMessageIds || [], suppliedExclusions)) throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict");
|
|
1555
|
-
if (!entry.conversation && !sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
|
|
1605
|
+
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" });
|
|
1606
|
+
if (!entry.conversation && !sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
1556
1607
|
return { promise: pending.admission.promise };
|
|
1557
1608
|
}
|
|
1558
1609
|
const existing = await findSubmittedTurn(entry, message.messageId);
|
|
@@ -1563,13 +1614,13 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1563
1614
|
if (existing) {
|
|
1564
1615
|
const previous = submittedMessage(existing);
|
|
1565
1616
|
if (!entry.conversation && !sameRequest(previous, message)) {
|
|
1566
|
-
throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
|
|
1617
|
+
throw failure("This messageId already belongs to different content.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
1567
1618
|
}
|
|
1568
1619
|
if (!entry.conversation && !nativePending) return { receipt: { status: "accepted", messageId: message.messageId, turnId: existing.turnId, origin, duplicate: true } };
|
|
1569
1620
|
}
|
|
1570
1621
|
if (pending && !entry.conversation) {
|
|
1571
|
-
if (!sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
|
|
1572
|
-
if (suppliedExclusions && !isDeepStrictEqual(pending.excludedMessageIds || [], suppliedExclusions)) throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict");
|
|
1622
|
+
if (!sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
1623
|
+
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" });
|
|
1573
1624
|
return { promise: pending.admission.promise };
|
|
1574
1625
|
}
|
|
1575
1626
|
const working = entry.active;
|
|
@@ -1584,18 +1635,18 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1584
1635
|
if (completedEnvelope && entry.engine === "claude" &&
|
|
1585
1636
|
(working?.request.completedEnvelope || nativePending?.completedEnvelope === true || origin === "application" && !goal) &&
|
|
1586
1637
|
(working || input.steer || nativeSteering)) {
|
|
1587
|
-
throw failure("Completed Claude responses cannot share a native steering turn. Wait for the response or stop it.", "conversation_not_steerable");
|
|
1638
|
+
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" });
|
|
1588
1639
|
}
|
|
1589
|
-
if (working && !input.steer && !goal && !entry.conversation) throw failure("Wait for this answer or use steer to send another instruction.", "conversation_busy");
|
|
1640
|
+
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" });
|
|
1590
1641
|
if (input.steer && (!working && !nativeSteering || working?.finishing) && !(nativePending?.attempted || nativePending?.inspectionOnly)) {
|
|
1591
1642
|
const previousState = nativeState?.nativeTurn?.runState || nativeState?.nativeTurn?.state ||
|
|
1592
1643
|
(await lastTranscriptTurn(entry))?.metadata?.runtime?.status;
|
|
1593
1644
|
const completed = working ? !working.controller.signal.aborted && !working.timedOut : ["complete", "completed"].includes(previousState);
|
|
1594
1645
|
throw Object.assign(failure("There is no active native turn to steer. Send a new message.",
|
|
1595
|
-
completed ? "conversation_steer_turn_finished" : "conversation_not_steerable"), { delivery: "not-sent" });
|
|
1646
|
+
completed ? "conversation_steer_turn_finished" : "conversation_not_steerable", 409, { messageCode: "assistant.conversation.noActiveTurnToSteer" }), { delivery: "not-sent" });
|
|
1596
1647
|
}
|
|
1597
|
-
if (goal && working?.finishing) throw failure("Wait for the current turn to finish saving before starting the goal.", "conversation_busy");
|
|
1598
|
-
if (working && (!working.request.admissionSettled || working.steering)) throw failure("Wait for the pending instruction to be admitted before steering again.", "conversation_busy");
|
|
1648
|
+
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" });
|
|
1649
|
+
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" });
|
|
1599
1650
|
const barrier = working ? Promise.withResolvers() : null;
|
|
1600
1651
|
if (barrier) working.steering = barrier.promise;
|
|
1601
1652
|
const release = () => {
|
|
@@ -1610,19 +1661,19 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1610
1661
|
// The bound state was read above unless this is an API conversation.
|
|
1611
1662
|
try { configuration = entry.conversation ? (nativeState || await readState(entry)).configuration : await storage.write(entry.id, async transaction => {
|
|
1612
1663
|
const metadata = await transaction.readMetadata();
|
|
1613
|
-
if (metadata.runtime.replacement) throw failure("Finish the pending native replacement before sending another message.", "conversation_replacement_pending");
|
|
1664
|
+
if (metadata.runtime.replacement) throw failure("Finish the pending native replacement before sending another message.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementPendingBeforeSend" });
|
|
1614
1665
|
if (retainedSegments(metadata.runtime).some(segment => segment.segmentId !== entry.segmentId && segment.request?.messageId === message.messageId)) {
|
|
1615
|
-
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");
|
|
1666
|
+
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" });
|
|
1616
1667
|
}
|
|
1617
1668
|
if (metadata.runtime.request) {
|
|
1618
1669
|
if (suppliedExclusions && metadata.runtime.request.messageId !== message.messageId) {
|
|
1619
|
-
throw failure("Check delivery of the previous instruction before preparing another completed response.", "conversation_delivery_uncertain");
|
|
1670
|
+
throw failure("Check delivery of the previous instruction before preparing another completed response.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.previousInstructionUnchecked" });
|
|
1620
1671
|
}
|
|
1621
|
-
if (entry.engine === "api") throw failure("The previous submission has uncertain delivery. It will not be resent automatically.", "conversation_delivery_uncertain");
|
|
1672
|
+
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" });
|
|
1622
1673
|
if (metadata.runtime.request.messageId === message.messageId) {
|
|
1623
|
-
if (!sameRequest(metadata.runtime.request, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
|
|
1674
|
+
if (!sameRequest(metadata.runtime.request, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdContentConflict" });
|
|
1624
1675
|
if (suppliedExclusions && !isDeepStrictEqual(metadata.runtime.request.excludedMessageIds || [], suppliedExclusions)) {
|
|
1625
|
-
throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict");
|
|
1676
|
+
throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict", 409, { messageCode: "assistant.conversation.messageIdExclusionsConflict" });
|
|
1626
1677
|
}
|
|
1627
1678
|
requestAt = metadata.runtime.request.at;
|
|
1628
1679
|
}
|
|
@@ -1702,7 +1753,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1702
1753
|
async function updateGoal(entry, context, input, representation, retryFinishedTurn = true) {
|
|
1703
1754
|
await access(context, entry.id, "goal", entry);
|
|
1704
1755
|
context?.signal?.throwIfAborted();
|
|
1705
|
-
if (!capabilities(entry).goals) throw failure("This connection does not support native goals.", "conversation_unsupported", 400);
|
|
1756
|
+
if (!capabilities(entry).goals) throw failure("This connection does not support native goals.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.goalsUnsupported" });
|
|
1706
1757
|
if (representation === "native") {
|
|
1707
1758
|
const { operation } = await serial(entry, async () => {
|
|
1708
1759
|
await access(context, entry.id, "goal", entry);
|
|
@@ -1720,37 +1771,37 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1720
1771
|
if (!input || !["set", "resume", "pause", "cancel"].includes(input.action) ||
|
|
1721
1772
|
(typeof input.expectedSegmentId !== "string" && !(entry.conversation && input.expectedSegmentId === null)) ||
|
|
1722
1773
|
input.expectedGoalId != null && (typeof input.expectedGoalId !== "string" || !input.expectedGoalId)) {
|
|
1723
|
-
throw failure("A goal action and current conversation segment are required.", "conversation_invalid_goal", 400);
|
|
1774
|
+
throw failure("A goal action and current conversation segment are required.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalActionRequired" });
|
|
1724
1775
|
}
|
|
1725
1776
|
const starting = input.action === "set" || input.action === "resume";
|
|
1726
1777
|
const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId, expectedGoalId: input.expectedGoalId || null };
|
|
1727
1778
|
if (starting) {
|
|
1728
1779
|
if (input.action === "set") {
|
|
1729
|
-
if (input.tokenBudget != null && !entry.driver.capabilities.goalBudgets) throw failure("This connection does not support goal token budgets.", "conversation_unsupported", 400);
|
|
1780
|
+
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" });
|
|
1730
1781
|
if (typeof input.objective !== "string" || !input.objective.trim() || input.objective.length > 4000 ||
|
|
1731
1782
|
input.tokenBudget != null && (!Number.isSafeInteger(input.tokenBudget) || input.tokenBudget < 1)) {
|
|
1732
|
-
throw failure("A bounded objective and optional positive token budget are required.", "conversation_invalid_goal", 400);
|
|
1783
|
+
throw failure("A bounded objective and optional positive token budget are required.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalObjectiveInvalid" });
|
|
1733
1784
|
}
|
|
1734
1785
|
goal.objective = input.objective.trim();
|
|
1735
1786
|
if (input.tokenBudget != null) goal.tokenBudget = input.tokenBudget;
|
|
1736
|
-
} else if (!goal.expectedGoalId) throw failure("Select the goal to resume.", "conversation_invalid_goal", 400);
|
|
1787
|
+
} else if (!goal.expectedGoalId) throw failure("Select the goal to resume.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalToResumeRequired" });
|
|
1737
1788
|
}
|
|
1738
|
-
if (!starting && !input.expectedGoalId) throw failure("Select the goal to stop.", "conversation_invalid_goal", 400);
|
|
1789
|
+
if (!starting && !input.expectedGoalId) throw failure("Select the goal to stop.", "conversation_invalid_goal", 400, { messageCode: "assistant.conversation.goalToStopRequired" });
|
|
1739
1790
|
const delivery = entry.driver.capabilities.goalCommands?.[input.action]?.delivery;
|
|
1740
|
-
if (!["message", "control"].includes(delivery)) throw failure("This connection does not support that goal command.", "conversation_unsupported", 400);
|
|
1791
|
+
if (!["message", "control"].includes(delivery)) throw failure("This connection does not support that goal command.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.goalCommandUnsupported" });
|
|
1741
1792
|
const messageCommand = delivery === "message";
|
|
1742
1793
|
if (messageCommand && (typeof input.messageId !== "string" || !/^[\w-]{1,128}$/u.test(input.messageId))) {
|
|
1743
|
-
throw failure("A stable messageId is required for this goal command.", "conversation_invalid_message", 400);
|
|
1794
|
+
throw failure("A stable messageId is required for this goal command.", "conversation_invalid_message", 400, { messageCode: "assistant.conversation.goalMessageIdRequired" });
|
|
1744
1795
|
}
|
|
1745
1796
|
if (conversationAttachmentIds(input.attachmentIds).length || input.attachments?.length) {
|
|
1746
|
-
throw failure("This engine's native goal commands do not accept attachments.", "conversation_unsupported", 400);
|
|
1797
|
+
throw failure("This engine's native goal commands do not accept attachments.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.goalAttachmentsUnsupported" });
|
|
1747
1798
|
}
|
|
1748
1799
|
const result = await serial(entry, async () => {
|
|
1749
1800
|
await access(context, entry.id, "goal", entry);
|
|
1750
1801
|
if (entry.conversation) await readState(entry);
|
|
1751
|
-
if (entry.segmentId !== input.expectedSegmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed");
|
|
1802
|
+
if (entry.segmentId !== input.expectedSegmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalConversationChanged" });
|
|
1752
1803
|
if (entry.conversation && input.expectedSegmentId === null && input.action !== "set") {
|
|
1753
|
-
throw failure("This command requires an existing native conversation.", "conversation_goal_changed");
|
|
1804
|
+
throw failure("This command requires an existing native conversation.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalNeedsNativeConversation" });
|
|
1754
1805
|
}
|
|
1755
1806
|
if (entry.storageFailure) throw entry.storageFailure;
|
|
1756
1807
|
if (entry.executionFailure) throw entry.executionFailure;
|
|
@@ -1758,20 +1809,20 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1758
1809
|
const history = await readTranscript(entry);
|
|
1759
1810
|
const existing = history.find(turn => submittedMessage(turn)?.messageId === input.messageId);
|
|
1760
1811
|
if (existing) {
|
|
1761
|
-
if (!entry.conversation && !isDeepStrictEqual(submittedMessage(existing).goal, goal)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
|
|
1812
|
+
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" });
|
|
1762
1813
|
return { receipt: { status: "accepted", messageId: input.messageId, turnId: existing.turnId, origin: "user", duplicate: true } };
|
|
1763
1814
|
}
|
|
1764
1815
|
}
|
|
1765
|
-
if (entry.active && !entry.active.request.accepted) throw failure("Wait for the goal command to be admitted.", "conversation_busy");
|
|
1816
|
+
if (entry.active && !entry.active.request.accepted) throw failure("Wait for the goal command to be admitted.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForGoalCommand" });
|
|
1766
1817
|
const metadata = { runtime: await readState(entry) };
|
|
1767
1818
|
if (metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly)) {
|
|
1768
|
-
throw failure("Check the uncertain command's delivery before changing the goal.", "conversation_delivery_uncertain");
|
|
1819
|
+
throw failure("Check the uncertain command's delivery before changing the goal.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.goalCommandDeliveryUncertain" });
|
|
1769
1820
|
}
|
|
1770
1821
|
if (entry.driver.canonicalTranscript && metadata.runtime.binding?.goalRequest) {
|
|
1771
|
-
throw failure("A goal command from an earlier runtime has unresolved delivery. Inspect its saved state before changing the goal.", "conversation_delivery_uncertain");
|
|
1822
|
+
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" });
|
|
1772
1823
|
}
|
|
1773
1824
|
if (starting && (metadata.runtime.replacement || !entry.conversation && pendingSuccessor(metadata.runtime) || metadata.runtime.lastEngine !== entry.engine)) {
|
|
1774
|
-
throw failure("Send a message to catch this AI up before starting or resuming its goal.", "conversation_replacement_briefing_pending");
|
|
1825
|
+
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" });
|
|
1775
1826
|
}
|
|
1776
1827
|
// Definitions keep native thread discovery stable. A control has no
|
|
1777
1828
|
// admitted request, application-tool actor or executor of its own.
|
|
@@ -1789,7 +1840,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1789
1840
|
const selectedSegment = `${entry.engine}:${threadId}`;
|
|
1790
1841
|
if (!threadId || state.segmentId !== selectedSegment ||
|
|
1791
1842
|
input.expectedSegmentId !== null && input.expectedSegmentId !== selectedSegment) {
|
|
1792
|
-
throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed");
|
|
1843
|
+
throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed", 409, { messageCode: "assistant.conversation.goalConversationChanged" });
|
|
1793
1844
|
}
|
|
1794
1845
|
return admitSend(entry, context, { messageId: input.messageId, text: message }, "user",
|
|
1795
1846
|
{ ...goal, expectedSegmentId: selectedSegment });
|
|
@@ -1843,7 +1894,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1843
1894
|
|
|
1844
1895
|
async function recoverDelivery(entry, context, messageId, { observation: suppliedObservation, allowActive = false } = {}) {
|
|
1845
1896
|
await access(context, entry.id, "inspectDelivery", entry);
|
|
1846
|
-
if (entry.active && !allowActive) throw failure("Wait for the current submission before checking delivery.", "conversation_busy");
|
|
1897
|
+
if (entry.active && !allowActive) throw failure("Wait for the current submission before checking delivery.", "conversation_busy", 409, { messageCode: "assistant.conversation.waitForSubmission" });
|
|
1847
1898
|
if (entry.storageFailure) throw entry.storageFailure;
|
|
1848
1899
|
if (entry.executionFailure) throw entry.executionFailure;
|
|
1849
1900
|
const history = await readTranscript(entry);
|
|
@@ -1863,13 +1914,14 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1863
1914
|
return { ...receipt, recovered: result.recovered === true };
|
|
1864
1915
|
}
|
|
1865
1916
|
const metadata = { runtime: await readState(entry) };
|
|
1866
|
-
if (metadata.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending");
|
|
1917
|
+
if (metadata.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
|
|
1867
1918
|
const savedRequests = [...metadata.runtime.predecessors, metadata.runtime].filter(segment => segment.request?.messageId === messageId);
|
|
1868
1919
|
if (!existing && savedRequests.length && savedRequests.every(segment =>
|
|
1869
1920
|
["codex", "claude", "opencode"].includes(segment.engine) && segment.request.attempted === false && !segment.request.inspectionOnly &&
|
|
1870
1921
|
isDeepStrictEqual(segment.request, savedRequests[0].request))) {
|
|
1871
1922
|
await access(context, entry.id, "inspectDelivery", entry);
|
|
1872
|
-
return { status: "not-sent", messageId, error: "This message was not sent. Review it before sending a new message."
|
|
1923
|
+
return { status: "not-sent", messageId, error: "This message was not sent. Review it before sending a new message.",
|
|
1924
|
+
messageCode: "assistant.conversation.messageNotSent", messageParams: {} };
|
|
1873
1925
|
}
|
|
1874
1926
|
const request = existing ? submittedMessage(existing) : metadata.runtime.request;
|
|
1875
1927
|
if (!request || request.messageId !== messageId || !entry.provider.inspectAdmission) return receipt || { status: "unknown", messageId };
|
|
@@ -1895,7 +1947,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1895
1947
|
await storage.write(entry.id, async transaction => {
|
|
1896
1948
|
const current = await transaction.readMetadata();
|
|
1897
1949
|
if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) {
|
|
1898
|
-
throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending");
|
|
1950
|
+
throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementPendingBeforeRecovery" });
|
|
1899
1951
|
}
|
|
1900
1952
|
const exact = await transaction.readTurn(saved.turnId);
|
|
1901
1953
|
const { error: _previousError, ...runtime } = exact.metadata.runtime;
|
|
@@ -1906,12 +1958,12 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1906
1958
|
const result = { ...(receipt || { status: "accepted", messageId, turnId: saved.turnId }),
|
|
1907
1959
|
recovered: observation.recovered === true, ...(observation.recoveryLimitation ? { recoveryLimitation: observation.recoveryLimitation } : {}) };
|
|
1908
1960
|
if (!existing) await emit(entry, { type: "accepted", ...result });
|
|
1909
|
-
if (result.recovered) await emit(entry, { type: "settled", turnId: saved.turnId, messageId, status, error });
|
|
1961
|
+
if (result.recovered) await emit(entry, { type: "settled", turnId: saved.turnId, messageId, status, error, ...stateErrorFields(error) });
|
|
1910
1962
|
return result;
|
|
1911
1963
|
}
|
|
1912
1964
|
const recovered = await storage.write(entry.id, async transaction => {
|
|
1913
1965
|
const current = await transaction.readMetadata();
|
|
1914
|
-
if (current.runtime.segmentId !== entry.segmentId || current.runtime.replacement) throw failure("Finish the native replacement before recovering output.", "conversation_replacement_pending");
|
|
1966
|
+
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" });
|
|
1915
1967
|
const native = { ...observation, messageId };
|
|
1916
1968
|
const results = [];
|
|
1917
1969
|
const id = messageId;
|
|
@@ -1973,7 +2025,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1973
2025
|
entry.error = recovered.find(turn => turn.error)?.error || "";
|
|
1974
2026
|
const result = { ...(receipt || { status: "accepted", messageId, turnId: recovered[0].turnId }), recovered: recovered.length > 0 };
|
|
1975
2027
|
if (!existing) await emit(entry, { type: "accepted", ...result });
|
|
1976
|
-
for (const turn of recovered) await emit(entry, { type: "settled", ...turn });
|
|
2028
|
+
for (const turn of recovered) await emit(entry, { type: "settled", ...turn, ...stateErrorFields(turn.error) });
|
|
1977
2029
|
return result;
|
|
1978
2030
|
}
|
|
1979
2031
|
|
|
@@ -1995,7 +2047,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1995
2047
|
});
|
|
1996
2048
|
const delivered = await sender.send({ engineId: entry.engine,
|
|
1997
2049
|
messages: await entry.conversation.transcript.history(), input: { messageId } });
|
|
1998
|
-
if (!delivered?.delivered) throw
|
|
2050
|
+
if (!delivered?.delivered) throw deliveryFailure(delivered);
|
|
1999
2051
|
}
|
|
2000
2052
|
const row = (await readTranscript(entry)).find(turn => submittedMessage(turn)?.messageId === messageId);
|
|
2001
2053
|
if (row) return { status: "accepted", messageId, turnId: row.turnId, duplicate: true };
|
|
@@ -2021,7 +2073,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2021
2073
|
const pending = (await readState(entry)).request;
|
|
2022
2074
|
if (pending?.messageId === messageId) {
|
|
2023
2075
|
const delivered = await sender.send({ engineId: entry.engine, messages, input: { messageId } });
|
|
2024
|
-
if (!delivered?.delivered) throw
|
|
2076
|
+
if (!delivered?.delivered) throw deliveryFailure(delivered);
|
|
2025
2077
|
}
|
|
2026
2078
|
await sender.remember({ engineId: entry.engine, messages });
|
|
2027
2079
|
return result;
|
|
@@ -2029,13 +2081,13 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2029
2081
|
|
|
2030
2082
|
async function replace(entry, context, input, selection = false, representation) {
|
|
2031
2083
|
const operation = selection ? "select" : "replace";
|
|
2032
|
-
if (completedEnvelope && input?.engine === "api") throw failure("Completed envelopes require a native engine.", "conversation_unsupported", 400);
|
|
2084
|
+
if (completedEnvelope && input?.engine === "api") throw failure("Completed envelopes require a native engine.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.completedEnvelopesNeedNativeEngine" });
|
|
2033
2085
|
await access(context, entry.id, operation, entry);
|
|
2034
2086
|
if (entry.conversation?.commands) {
|
|
2035
|
-
if (!selection) throw failure("This conversation retains its original replacement policy.", "conversation_unsupported", 400);
|
|
2087
|
+
if (!selection) throw failure("This conversation retains its original replacement policy.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.replacementPolicyRetained" });
|
|
2036
2088
|
return entry.conversation.commands.select(input, context);
|
|
2037
2089
|
}
|
|
2038
|
-
if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original selected profile.", "conversation_unsupported");
|
|
2090
|
+
if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original selected profile.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.selectedProfileRetained" });
|
|
2039
2091
|
const keys = selection ? ["operationId", "expectedSegmentId", "engine", "configuration", "retireNative"]
|
|
2040
2092
|
: ["operationId", "expectedSegmentId", "reason", "engine", "configuration", "briefing"];
|
|
2041
2093
|
if (!input || !/^[\w-]{1,128}$/u.test(input.operationId || "") || typeof input.expectedSegmentId !== "string" ||
|
|
@@ -2043,21 +2095,21 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2043
2095
|
input.briefing !== undefined && typeof input.briefing !== "string" ||
|
|
2044
2096
|
input.retireNative !== undefined && typeof input.retireNative !== "boolean" ||
|
|
2045
2097
|
Object.keys(input).some(key => !keys.includes(key))) {
|
|
2046
|
-
throw failure("Replacement needs an operationId, exact predecessor segment, and an engine-change, model-change or renewal reason.", "conversation_invalid_replacement", 400);
|
|
2098
|
+
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" });
|
|
2047
2099
|
}
|
|
2048
|
-
if (entry.active) throw failure("Stop the current turn before replacing its native conversation.", "conversation_busy");
|
|
2100
|
+
if (entry.active) throw failure("Stop the current turn before replacing its native conversation.", "conversation_busy", 409, { messageCode: "assistant.conversation.stopBeforeReplace" });
|
|
2049
2101
|
if (entry.storageFailure) throw entry.storageFailure;
|
|
2050
2102
|
if (entry.conversation) {
|
|
2051
2103
|
const state = await readState(entry);
|
|
2052
2104
|
const saved = await entry.conversation.state.read();
|
|
2053
2105
|
const retry = saved?.replacement?.operationId === input.operationId;
|
|
2054
2106
|
let nativeResult;
|
|
2055
|
-
if (state.segmentId !== input.expectedSegmentId && !retry) throw failure("The native predecessor has changed. Read the conversation before replacing it.", "conversation_replacement_conflict");
|
|
2107
|
+
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" });
|
|
2056
2108
|
if (selection) {
|
|
2057
|
-
if (!entry.conversation.selection?.write) throw failure("This host requires its authorized assistant selection operation.", "conversation_unsupported");
|
|
2109
|
+
if (!entry.conversation.selection?.write) throw failure("This host requires its authorized assistant selection operation.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.hostSelectionRequired" });
|
|
2058
2110
|
await entry.conversation.selection.write(input, context);
|
|
2059
2111
|
} else {
|
|
2060
|
-
if (!input.briefing?.trim()) throw failure("Native replacement requires a continuity briefing.", "conversation_invalid_replacement", 400);
|
|
2112
|
+
if (!input.briefing?.trim()) throw failure("Native replacement requires a continuity briefing.", "conversation_invalid_replacement", 400, { messageCode: "assistant.conversation.replacementBriefingRequired" });
|
|
2061
2113
|
const replacement = createConversationChangeover({ state: entry.conversation.state,
|
|
2062
2114
|
identity: entry.conversation.identity, presentation: entry.conversation.presentation,
|
|
2063
2115
|
agent: { async closeSession() { await entry.provider.dispose({ changeover: true, forgetConversationBinding: true }); return { ok: true }; } } });
|
|
@@ -2089,18 +2141,18 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2089
2141
|
else if (request.configuration) request.configuration = normalizeConversationConfiguration(request.configuration);
|
|
2090
2142
|
let state = (await readState(entry));
|
|
2091
2143
|
if (hasUnfinishedConversationRewind(state)) {
|
|
2092
|
-
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");
|
|
2144
|
+
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" });
|
|
2093
2145
|
}
|
|
2094
2146
|
const completed = state.predecessors.find(item => item.replacement?.operationId === input.operationId);
|
|
2095
2147
|
if (completed) {
|
|
2096
|
-
if (!isDeepStrictEqual(completed.replacement, request)) throw failure("This replacement operationId already belongs to another request.", "conversation_replacement_conflict");
|
|
2148
|
+
if (!isDeepStrictEqual(completed.replacement, request)) throw failure("This replacement operationId already belongs to another request.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.replacementOperationConflict" });
|
|
2097
2149
|
return { operationId: input.operationId, segmentId: completed.successorId, duplicate: true };
|
|
2098
2150
|
}
|
|
2099
2151
|
let pending = state.replacement;
|
|
2100
|
-
if (pending && !isDeepStrictEqual(pending.request, request)) throw failure("Retry the unfinished native replacement before starting another one.", "conversation_replacement_pending");
|
|
2101
|
-
if (state.segmentId !== input.expectedSegmentId) throw failure("The native predecessor has changed. Read the conversation before replacing it.", "conversation_replacement_conflict");
|
|
2102
|
-
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");
|
|
2103
|
-
if (!selection && !pending && pendingSuccessor(state)) throw failure("Deliver the previous replacement's briefing before replacing native history again.", "conversation_replacement_briefing_pending");
|
|
2152
|
+
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" });
|
|
2153
|
+
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" });
|
|
2154
|
+
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" });
|
|
2155
|
+
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" });
|
|
2104
2156
|
if (entry.executionFailure && !pending) throw entry.executionFailure;
|
|
2105
2157
|
const engine = pending?.engine || input.engine || state.engine;
|
|
2106
2158
|
const configuration = pending?.configuration || (input.configuration
|
|
@@ -2153,7 +2205,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2153
2205
|
const metadata = await transaction.readMetadata();
|
|
2154
2206
|
if (metadata.runtime.segmentId !== input.expectedSegmentId || metadata.runtime.replacement &&
|
|
2155
2207
|
!isDeepStrictEqual(metadata.runtime.replacement.request, request)) {
|
|
2156
|
-
throw failure("The saved replacement changed before it could be written.", "conversation_replacement_conflict");
|
|
2208
|
+
throw failure("The saved replacement changed before it could be written.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.replacementChangedBeforeWrite" });
|
|
2157
2209
|
}
|
|
2158
2210
|
metadata.runtime.replacement = pending;
|
|
2159
2211
|
await transaction.writeMetadata(metadata);
|
|
@@ -2186,12 +2238,12 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2186
2238
|
const metadata = await transaction.readMetadata();
|
|
2187
2239
|
state = metadata.runtime;
|
|
2188
2240
|
if (state.segmentId !== input.expectedSegmentId || !isDeepStrictEqual(state.replacement?.request, request)) {
|
|
2189
|
-
throw failure("The saved replacement changed before its binding was released.", "conversation_replacement_conflict");
|
|
2241
|
+
throw failure("The saved replacement changed before its binding was released.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.replacementChangedBeforeRelease" });
|
|
2190
2242
|
}
|
|
2191
2243
|
await callback({
|
|
2192
2244
|
async releaseBinding(replacement) {
|
|
2193
2245
|
if (replacement && conversationNativeIdentity(state) !== replacement.previous.conversationId) {
|
|
2194
|
-
throw failure("The native predecessor identity changed before its binding was released.", "conversation_replacement_conflict");
|
|
2246
|
+
throw failure("The native predecessor identity changed before its binding was released.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.predecessorChangedBeforeRelease" });
|
|
2195
2247
|
}
|
|
2196
2248
|
releaseConversationRuntimeBinding(state, pending, request, engine);
|
|
2197
2249
|
},
|
|
@@ -2218,7 +2270,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2218
2270
|
async inspect(expectedId) {
|
|
2219
2271
|
const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
|
|
2220
2272
|
if (runtime.segmentId !== input.expectedSegmentId || conversationNativeIdentity(runtime) !== expectedId) {
|
|
2221
|
-
throw failure("The native predecessor identity changed or is incomplete.", "conversation_replacement_conflict");
|
|
2273
|
+
throw failure("The native predecessor identity changed or is incomplete.", "conversation_replacement_conflict", 409, { messageCode: "assistant.conversation.predecessorIncomplete" });
|
|
2222
2274
|
}
|
|
2223
2275
|
return { bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: expectedId } };
|
|
2224
2276
|
}
|
|
@@ -2267,39 +2319,39 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2267
2319
|
// Internal host composition uses its existing admission/write lease. These
|
|
2268
2320
|
// original coordinators share the scoped owner and introduce no entry or queue.
|
|
2269
2321
|
async projectStoredPersistentConversation(options) {
|
|
2270
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2322
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2271
2323
|
return projectStoredPersistentConversation(options);
|
|
2272
2324
|
},
|
|
2273
2325
|
async readPersistentConversation(options) {
|
|
2274
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2326
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2275
2327
|
return readPersistentConversation(options);
|
|
2276
2328
|
},
|
|
2277
2329
|
async startPersistentConversationTurn(options) {
|
|
2278
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2330
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2279
2331
|
return startPersistentConversationTurn(options);
|
|
2280
2332
|
},
|
|
2281
2333
|
async inspectPersistentConversationAdmission(options) {
|
|
2282
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2334
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2283
2335
|
return inspectPersistentConversationAdmission(options);
|
|
2284
2336
|
},
|
|
2285
2337
|
async preparePersistentConversationChangeover(options) {
|
|
2286
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2338
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2287
2339
|
return preparePersistentConversationChangeover(options);
|
|
2288
2340
|
},
|
|
2289
2341
|
async inspectPersistentConversationDelivery(options) {
|
|
2290
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2342
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2291
2343
|
return inspectPersistentConversationDelivery(options);
|
|
2292
2344
|
},
|
|
2293
2345
|
async stopPersistentConversation(options) {
|
|
2294
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2346
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2295
2347
|
return stopPersistentConversation(options);
|
|
2296
2348
|
},
|
|
2297
2349
|
async deletePersistentConversation(options) {
|
|
2298
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2350
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2299
2351
|
return deletePersistentConversation(options);
|
|
2300
2352
|
},
|
|
2301
2353
|
async runScopedTurn(options = {}) {
|
|
2302
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2354
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2303
2355
|
if (["createEphemeralConversation", "startEphemeralConversationTurn", "waitForEphemeralConversationTurn", "stopEphemeralConversation"]
|
|
2304
2356
|
.some(name => typeof options.operations?.[name] !== "function")) {
|
|
2305
2357
|
throw new TypeError("A scoped turn requires its existing admitted conversation operations.");
|
|
@@ -2362,7 +2414,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2362
2414
|
throw new TypeError("Native renewal requires a configured conversation host.");
|
|
2363
2415
|
}
|
|
2364
2416
|
const prepared = await defaultHost.conversation({ id, context, input, operation: "generateRenewalHandover" });
|
|
2365
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2417
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2366
2418
|
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2367
2419
|
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2368
2420
|
throw new TypeError("Native renewal requires its authorized host preparation.");
|
|
@@ -2377,7 +2429,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2377
2429
|
throw new TypeError("Native renewal requires a configured conversation host.");
|
|
2378
2430
|
}
|
|
2379
2431
|
const prepared = await defaultHost.conversation({ id, context, input, operation: "seedRenewalHandover" });
|
|
2380
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2432
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2381
2433
|
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2382
2434
|
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2383
2435
|
throw new TypeError("Native renewal requires its authorized host preparation.");
|
|
@@ -2387,16 +2439,16 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2387
2439
|
async updateNativeConversationSettings({ id, context, input = {} } = {}) {
|
|
2388
2440
|
if (typeof id !== "string" || !id.trim()) throw new TypeError("A conversation id is required.");
|
|
2389
2441
|
await access(context, id, "configure");
|
|
2390
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2442
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2391
2443
|
const prepared = await defaultHost?.conversation?.({ id, context, input, operation: "updateSettings" });
|
|
2392
2444
|
if (prepared?.sessionId !== id || !prepared.native || !Object.hasOwn(prepared, "context")) {
|
|
2393
2445
|
throw new TypeError("Native settings require their authorized host preparation.");
|
|
2394
2446
|
}
|
|
2395
2447
|
const driver = createDriver(prepared.engine, defaultHost);
|
|
2396
2448
|
if (typeof driver.updateConversationSettings !== "function") {
|
|
2397
|
-
throw failure("This provider does not support live native settings.", "conversation_unsupported");
|
|
2449
|
+
throw failure("This provider does not support live native settings.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.liveSettingsUnsupported" });
|
|
2398
2450
|
}
|
|
2399
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2451
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2400
2452
|
return driver.updateConversationSettings({ ...prepared, input });
|
|
2401
2453
|
},
|
|
2402
2454
|
// Readiness uses the same authorized host and original native owner without
|
|
@@ -2408,7 +2460,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2408
2460
|
throw new TypeError("Native conversation readiness requires a configured conversation host.");
|
|
2409
2461
|
}
|
|
2410
2462
|
const prepared = await defaultHost.conversation({ id, context, operation: "ensure" });
|
|
2411
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2463
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2412
2464
|
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) ||
|
|
2413
2465
|
!prepared.native || !Object.hasOwn(prepared, "context")) {
|
|
2414
2466
|
throw new TypeError("Native conversation readiness requires its authorized host preparation.");
|
|
@@ -2428,7 +2480,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2428
2480
|
throw new TypeError("Native conversation creation requires a configured conversation host.");
|
|
2429
2481
|
}
|
|
2430
2482
|
const prepared = await defaultHost.conversation({ id, context, input, operation: "create" });
|
|
2431
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2483
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2432
2484
|
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2433
2485
|
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context")) {
|
|
2434
2486
|
throw new TypeError("Native conversation creation requires its authorized host preparation.");
|
|
@@ -2443,7 +2495,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2443
2495
|
}
|
|
2444
2496
|
const prepared = await defaultHost.conversation({ id, context, input,
|
|
2445
2497
|
...(options === undefined ? {} : { options }), operation: "runDetachedConversation" });
|
|
2446
|
-
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed");
|
|
2498
|
+
if (closed) throw failure("This conversation runtime is closed.", "conversation_closed", 409, { messageCode: "assistant.conversation.runtimeClosed" });
|
|
2447
2499
|
if (prepared?.sessionId !== id || !["codex", "claude", "opencode"].includes(prepared.engine) || !prepared.native ||
|
|
2448
2500
|
!Object.hasOwn(prepared, "input") || !Object.hasOwn(prepared, "context") || !Object.hasOwn(prepared, "options")) {
|
|
2449
2501
|
throw new TypeError("Native detached execution requires its authorized host preparation.");
|
|
@@ -2456,7 +2508,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2456
2508
|
const selectedHost = host || defaultHost;
|
|
2457
2509
|
const conversation = selectedHost?.conversation ? await selectedHost.conversation({ id, context }) : null;
|
|
2458
2510
|
if (completedEnvelope && (conversation || representation !== "canonical")) {
|
|
2459
|
-
throw failure("Completed envelopes require runtime-owned canonical storage.", "conversation_unsupported", 400);
|
|
2511
|
+
throw failure("Completed envelopes require runtime-owned canonical storage.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.completedEnvelopesNeedStorage" });
|
|
2460
2512
|
}
|
|
2461
2513
|
if (conversation && !persistCommentary) {
|
|
2462
2514
|
throw new TypeError("Transient commentary requires a runtime-owned transcript; supplied conversations retain their host's policy.");
|
|
@@ -2509,9 +2561,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2509
2561
|
});
|
|
2510
2562
|
}
|
|
2511
2563
|
await entry.initializing;
|
|
2512
|
-
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed");
|
|
2513
|
-
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");
|
|
2514
|
-
if (engine && engine !== entry.engine) throw failure("Use replace() to change this conversation's engine.", "conversation_engine_mismatch");
|
|
2564
|
+
if (entry.disposed) throw failure("This conversation handle is disposed.", "conversation_closed", 409, { messageCode: "assistant.conversation.handleDisposed" });
|
|
2565
|
+
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" });
|
|
2566
|
+
if (engine && engine !== entry.engine) throw failure("Use replace() to change this conversation's engine.", "conversation_engine_mismatch", 409, { messageCode: "assistant.conversation.useReplaceForEngine" });
|
|
2515
2567
|
if (configuration) {
|
|
2516
2568
|
const metadata = { runtime: await readState(entry) };
|
|
2517
2569
|
const supplied = entry.conversation ? configuration
|
|
@@ -2547,7 +2599,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2547
2599
|
},
|
|
2548
2600
|
async wait(input) {
|
|
2549
2601
|
await access(context, id, "read", entry);
|
|
2550
|
-
if (entry.conversation?.commands) throw failure("This conversation uses its original read and subscription operations.", "conversation_unsupported", 400);
|
|
2602
|
+
if (entry.conversation?.commands) throw failure("This conversation uses its original read and subscription operations.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.originalReadOnly" });
|
|
2551
2603
|
if (entry.conversation?.native?.scoped) return entry.provider.waitNative({ input, context });
|
|
2552
2604
|
await entry.pending;
|
|
2553
2605
|
await entry.active?.done;
|
|
@@ -2570,21 +2622,21 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2570
2622
|
},
|
|
2571
2623
|
async configure(patch) {
|
|
2572
2624
|
await access(context, id, "configure", entry);
|
|
2573
|
-
if (entry.conversation?.commands) throw failure("Use the conversation's authorized selection operation.", "conversation_unsupported", 400);
|
|
2625
|
+
if (entry.conversation?.commands) throw failure("Use the conversation's authorized selection operation.", "conversation_unsupported", 400, { messageCode: "assistant.conversation.authorizedSelectionRequired" });
|
|
2574
2626
|
return serial(entry, async () => {
|
|
2575
2627
|
await access(context, id, "configure", entry);
|
|
2576
|
-
if (entry.active) throw failure("Stop the current turn before changing its settings.", "conversation_busy");
|
|
2628
|
+
if (entry.active) throw failure("Stop the current turn before changing its settings.", "conversation_busy", 409, { messageCode: "assistant.conversation.stopBeforeSettings" });
|
|
2577
2629
|
if (entry.storageFailure) throw entry.storageFailure;
|
|
2578
2630
|
if (entry.executionFailure) throw entry.executionFailure;
|
|
2579
2631
|
if (entry.conversation) {
|
|
2580
|
-
if (!entry.conversation.selection?.write) throw failure("This host requires its authorized assistant selection operation.", "conversation_unsupported");
|
|
2632
|
+
if (!entry.conversation.selection?.write) throw failure("This host requires its authorized assistant selection operation.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.hostSelectionRequired" });
|
|
2581
2633
|
return entry.conversation.selection.write({ configuration: patch }, context);
|
|
2582
2634
|
}
|
|
2583
2635
|
return storage.write(id, async transaction => {
|
|
2584
2636
|
const metadata = await transaction.readMetadata();
|
|
2585
|
-
if (metadata.runtime.replacement) throw failure("Finish the pending native replacement before changing settings.", "conversation_replacement_pending");
|
|
2637
|
+
if (metadata.runtime.replacement) throw failure("Finish the pending native replacement before changing settings.", "conversation_replacement_pending", 409, { messageCode: "assistant.conversation.replacementPendingBeforeSettings" });
|
|
2586
2638
|
if (metadata.runtime.request && (entry.engine === "api" || metadata.runtime.request.attempted || metadata.runtime.request.inspectionOnly)) {
|
|
2587
|
-
throw failure("Resolve uncertain delivery before changing settings.", "conversation_delivery_uncertain");
|
|
2639
|
+
throw failure("Resolve uncertain delivery before changing settings.", "conversation_delivery_uncertain", 409, { messageCode: "assistant.conversation.deliveryUncertainBeforeSettings" });
|
|
2588
2640
|
}
|
|
2589
2641
|
const next = normalizeConversationConfiguration({ ...metadata.runtime.configuration, ...patch }, {
|
|
2590
2642
|
engine: metadata.runtime.engine, defaultIntegrationId
|
|
@@ -2598,7 +2650,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2598
2650
|
},
|
|
2599
2651
|
async subscribe(listener) {
|
|
2600
2652
|
await access(context, id, "subscribe", entry);
|
|
2601
|
-
if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original event delivery.", "conversation_unsupported");
|
|
2653
|
+
if (entry.conversation?.native?.scoped) throw failure("This scoped conversation retains its original event delivery.", "conversation_unsupported", 409, { messageCode: "assistant.conversation.eventDeliveryRetained" });
|
|
2602
2654
|
if (typeof listener !== "function") throw new TypeError("A conversation listener is required.");
|
|
2603
2655
|
const subscription = { context, listener };
|
|
2604
2656
|
entry.listeners.add(subscription);
|
|
@@ -2764,7 +2816,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2764
2816
|
const entry = entries.get(namespace);
|
|
2765
2817
|
if (!entry || entry.disposed) return cleanupUnopened();
|
|
2766
2818
|
if (entry.id !== sessionId || !entry.conversation) {
|
|
2767
|
-
throw failure("Native cleanup requires its original bound conversation.", "conversation_host_mismatch");
|
|
2819
|
+
throw failure("Native cleanup requires its original bound conversation.", "conversation_host_mismatch", 409, { messageCode: "assistant.conversation.cleanupNeedsOriginalConversation" });
|
|
2768
2820
|
}
|
|
2769
2821
|
await authorizeCleanup();
|
|
2770
2822
|
const retained = await serial(entry, async () => {
|