@jskit-ai/assistant-core 0.1.243 → 0.2.0

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/examples/conversation/package.json +5 -5
  3. package/locales/en.json +383 -0
  4. package/package.json +7 -6
  5. package/src/client/components/AssistantSettingsFormCard.vue +4 -1
  6. package/src/client/conversation/AssistantAttachmentPreview.vue +6 -3
  7. package/src/client/conversation/AssistantAttachmentQueue.vue +36 -37
  8. package/src/client/conversation/AssistantComposerActions.vue +6 -3
  9. package/src/client/conversation/AssistantComposerSupport.vue +8 -5
  10. package/src/client/conversation/AssistantConversationElement.vue +16 -13
  11. package/src/client/conversation/AssistantConversationStatus.vue +5 -2
  12. package/src/client/conversation/AssistantGoalControl.vue +22 -16
  13. package/src/client/conversation/AssistantMessageAttachments.vue +7 -4
  14. package/src/client/conversation/AssistantModelControl.vue +23 -20
  15. package/src/client/conversation/AssistantProgress.vue +6 -3
  16. package/src/client/conversation/AssistantQuestionInputs.vue +9 -6
  17. package/src/client/conversation/AssistantTranscript.vue +39 -28
  18. package/src/client/conversation/LongTextPreviewBlocks.vue +10 -2
  19. package/src/client/conversation/attachmentSize.js +38 -0
  20. package/src/client/conversation/messageDelivery.js +14 -5
  21. package/src/client/conversation/submitText.js +11 -4
  22. package/src/client/conversation/useAssistantAttachments.js +40 -30
  23. package/src/client/conversation/useAssistantSuggestions.js +4 -1
  24. package/src/client/i18n.js +6 -0
  25. package/src/client/lib/assistantApi.js +14 -2
  26. package/src/server/conversation/attachments.js +24 -14
  27. package/src/server/conversation/claudeProcess.js +4 -2
  28. package/src/server/conversation/claudeTurn.js +14 -11
  29. package/src/server/conversation/codexConversations.js +4 -0
  30. package/src/server/conversation/codexGoalCommands.js +9 -9
  31. package/src/server/conversation/codexHelperExecution.js +4 -0
  32. package/src/server/conversation/codexHelperThreads.js +2 -0
  33. package/src/server/conversation/codexMessageCommands.js +7 -1
  34. package/src/server/conversation/codexTurnControl.js +2 -1
  35. package/src/server/conversation/codexTurnState.js +2 -0
  36. package/src/server/conversation/continuity.js +16 -6
  37. package/src/server/conversation/openCodeProcess.js +12 -3
  38. package/src/server/conversation/runtime.js +193 -141
  39. package/src/server/lib/ndjson.js +19 -1
  40. package/src/server/lib/providers/common.js +7 -2
  41. package/src/shared/conversation/numberedQuestionSugar.js +7 -1
  42. package/src/shared/conversation/turns.js +10 -2
  43. package/test/i18nCatalog.test.js +22 -0
  44. package/test/openCodeProcess.test.js +15 -1
@@ -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
- function failure(message, code, statusCode = 409) {
26
- return Object.assign(new Error(message), { code, statusCode });
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: logical ? logical.error || "" : entry.error || nativeTurn?.error || request?.error || last?.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 = failure(delivered?.error || "Native delivery could not be confirmed.", delivered?.code || "conversation_delivery_uncertain");
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, error: entry.error });
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 failure(delivered?.error || "Native delivery could not be confirmed.", delivered?.code || "conversation_delivery_uncertain");
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 failure(delivered?.error || "Native delivery could not be confirmed.", delivered?.code || "conversation_delivery_uncertain");
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 () => {