@jskit-ai/assistant-core 0.1.240 → 0.1.242

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 (101) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +5 -5
  3. package/src/client/conversation/AssistantTranscript.vue +26 -29
  4. package/src/client/conversation/LongTextPreviewBlocks.vue +11 -0
  5. package/src/client/conversation/messageDelivery.js +8 -2
  6. package/src/client/conversation/useAssistantAttachments.js +1 -1
  7. package/src/server/conversation/agentRun.js +12 -198
  8. package/src/server/conversation/attachments.js +6 -5
  9. package/src/server/conversation/boundedToolLoop.js +160 -0
  10. package/src/server/conversation/claudeHistory.js +37 -23
  11. package/src/server/conversation/claudeProcess.js +2 -2
  12. package/src/server/conversation/claudeTurn.js +41 -38
  13. package/src/server/conversation/codexAuthPreflight.js +104 -0
  14. package/src/server/conversation/codexClient.js +11 -16
  15. package/src/server/conversation/codexConfiguration.js +112 -98
  16. package/src/server/conversation/codexConversationStore.js +189 -0
  17. package/src/server/conversation/codexConversations.js +47 -44
  18. package/src/server/conversation/codexDetachedTurn.js +12 -11
  19. package/src/server/conversation/codexEvents.js +95 -99
  20. package/src/server/conversation/codexHelperExecution.js +3 -13
  21. package/src/server/conversation/codexHelperThreadLedger.js +7 -13
  22. package/src/server/conversation/codexHelperThreadRecovery.js +2 -16
  23. package/src/server/conversation/codexHelperThreads.js +5 -6
  24. package/src/server/conversation/codexHistoryAdapter.js +32 -26
  25. package/src/server/conversation/codexMessageCommands.js +22 -22
  26. package/src/server/conversation/codexMessageDelivery.js +12 -7
  27. package/src/server/conversation/codexModelCatalog.js +97 -0
  28. package/src/server/conversation/codexNotificationObserver.js +24 -40
  29. package/src/server/conversation/codexNotificationQueue.js +2 -3
  30. package/src/server/conversation/codexOutput.js +20 -27
  31. package/src/server/conversation/codexProcess.js +123 -127
  32. package/src/server/conversation/codexProcessLeader.js +3 -3
  33. package/src/server/conversation/codexProvider.js +31 -614
  34. package/src/server/conversation/codexProviderOwner.js +2 -2
  35. package/src/server/conversation/codexProviderSelection.js +38 -40
  36. package/src/server/conversation/codexRenewalCommands.js +13 -10
  37. package/src/server/conversation/codexRenewalThreads.js +412 -0
  38. package/src/server/conversation/codexRunJournal.js +24 -42
  39. package/src/server/conversation/codexRuntimeLifecycle.js +19 -15
  40. package/src/server/conversation/codexSessionCleanup.js +11 -10
  41. package/src/server/conversation/codexSessionReconciliation.js +6 -6
  42. package/src/server/conversation/codexThreadReadiness.js +19 -33
  43. package/src/server/conversation/codexThreadStatus.js +3 -3
  44. package/src/server/conversation/codexTurn.js +124 -297
  45. package/src/server/conversation/codexTurnRecovery.js +49 -28
  46. package/src/server/conversation/codexTurnState.js +34 -13
  47. package/src/server/conversation/commandWrapper.js +3 -0
  48. package/src/server/conversation/deliveryTiming.js +14 -5
  49. package/src/server/conversation/index.js +1 -0
  50. package/src/server/conversation/localExecution.js +13 -17
  51. package/src/server/conversation/normalize.js +8 -3
  52. package/src/server/conversation/openCodeClient.js +9 -10
  53. package/src/server/conversation/openCodeProcess.js +9 -5
  54. package/src/server/conversation/openCodeRuntime.js +10 -11
  55. package/src/server/conversation/openCodeTurn.js +43 -29
  56. package/src/server/conversation/providers/claudeDriver.js +8 -16
  57. package/src/server/conversation/providers/codexDriver.js +25 -41
  58. package/src/server/conversation/providers/opencodeDriver.js +17 -42
  59. package/src/server/conversation/providers/scoped.js +48 -4
  60. package/src/server/conversation/runtime.js +50 -49
  61. package/src/server/conversation/runtimeStateUpgrade.js +34 -22
  62. package/src/server/conversation/transcript.js +10 -10
  63. package/src/server/hosts/codexConfiguration.js +1 -0
  64. package/src/server/hosts/codexProvider.js +8 -6
  65. package/src/server/index.js +2 -1
  66. package/src/server/lib/assistantToolLoop.js +1 -153
  67. package/src/server/lib/serviceToolCatalog.js +17 -42
  68. package/src/shared/assistantResource.js +4 -2
  69. package/src/shared/conversation/attachments.js +4 -0
  70. package/src/shared/conversation/longTextBlocks.js +54 -19
  71. package/src/shared/conversation/numberedQuestionSugar.js +17 -10
  72. package/src/shared/conversation/pagination.js +4 -6
  73. package/src/shared/conversation/patches.js +1 -4
  74. package/src/shared/conversation/streaming.js +3 -11
  75. package/src/shared/conversation/turns.js +11 -0
  76. package/src/shared/index.js +1 -0
  77. package/test/assistantBoundedToolLoop.test.js +2 -2
  78. package/test/assistantResource.test.js +4 -4
  79. package/test/assistantSettingsResource.test.js +2 -2
  80. package/test/claudeConversationSession.test.js +11 -1
  81. package/test/claudeTurn.test.js +35 -7
  82. package/test/codexClient.test.js +20 -0
  83. package/test/codexConversationSession.test.js +2 -2
  84. package/test/codexConversationsCheckpoint.test.js +51 -0
  85. package/test/codexModelCatalog.test.js +1 -1
  86. package/test/codexProviderHome.test.js +51 -0
  87. package/test/codexProviderSelection.test.js +71 -0
  88. package/test/codexTurn.test.js +222 -6
  89. package/test/conversationAgentRun.test.js +2 -1
  90. package/test/conversationAttachments.test.js +27 -2
  91. package/test/conversationDeliveryTiming.test.js +21 -0
  92. package/test/conversationNormalize.test.js +11 -0
  93. package/test/fixtures/nestedListQuestionMessage.md +37 -0
  94. package/test/frameworkActions.test.js +2 -2
  95. package/test/longTextBlocks.test.js +102 -0
  96. package/test/messageDelivery.test.js +7 -1
  97. package/test/openCodeClient.test.js +20 -0
  98. package/test/openCodeRuntime.test.js +1 -1
  99. package/test/openCodeTurn.test.js +90 -0
  100. package/test/opencodeConversationSession.test.js +1 -1
  101. package/test/serviceToolCatalog.test.js +27 -0
@@ -1,5 +1,5 @@
1
1
  import { createConversationProviderFactory } from "./provider.js";
2
- import { createConversationDeliveryTiming } from "./deliveryTiming.js";
2
+ import { createConversationDeliveryTiming, normalizeConversationDeliveryHostStages } from "./deliveryTiming.js";
3
3
  import { normalizeConversationConfiguration } from "./configuration.js";
4
4
  import { runScopedConversationTurn, readPersistentConversation, projectStoredPersistentConversation, startPersistentConversationTurn,
5
5
  inspectPersistentConversationAdmission, preparePersistentConversationChangeover,
@@ -9,13 +9,14 @@ import { randomUUID } from "node:crypto";
9
9
  import { createConversationTranscript, isCompletedEnvelopeTurn } from "./transcript.js";
10
10
  import { createConversationStreams } from "./streams.js";
11
11
  import { createReentrantConversationStorage } from "./storage.js";
12
- import { createCodexConversationStore } from "./agentRun.js";
12
+ import { createCodexConversationStore } from "./codexConversationStore.js";
13
13
  import { codexAppServerTurnStateFromAgentRun } from "./codexTurn.js";
14
14
  import { hasUnfinishedConversationRewind, createConversationRuntimeReplacement,
15
- releaseConversationRuntimeBinding, finishConversationRuntimeReplacement } from "./runtimeStateUpgrade.js";
15
+ releaseConversationRuntimeBinding, finishConversationRuntimeReplacement,
16
+ conversationNativeIdentity, conversationNativeBinding } from "./runtimeStateUpgrade.js";
16
17
  import { createServiceToolCatalog } from "../lib/serviceToolCatalog.js";
17
18
  import { createConversationTools } from "./tools.js";
18
- import { readAssistantResponseEnvelope } from "../lib/assistantToolLoop.js";
19
+ import { readAssistantResponseEnvelope } from "./boundedToolLoop.js";
19
20
  import { conversationAttachmentIds, createConversationAttachmentReader } from "./attachments.js";
20
21
  import { conversationAttachmentManifest } from "../../shared/conversation/attachments.js";
21
22
  import { createConversationChangeover, conversationContinuity, conversationHistoryVersions,
@@ -27,8 +28,6 @@ function failure(message, code, statusCode = 409) {
27
28
 
28
29
  const submittedMessage = turn => turn.user || turn.system;
29
30
  const retainedSegments = state => [...new Map([...state.predecessors, state].map(segment => [segment.segmentId, segment])).values()];
30
- const nativeIdentity = segment => segment.engine === "codex" ? segment.binding?.threadId
31
- : segment.engine === "claude" ? segment.binding?.conversationId : segment.binding?.sessionId;
32
31
  const pendingSuccessor = state => state.engine !== "api" && state.predecessors.findLast(segment => segment.successorId === state.segmentId &&
33
32
  segment.segmentId !== state.segmentId && segment.replacement?.operation === "replace" && !segment.acceptedAt);
34
33
  const sameRequest = (left, right) => left.text === right.text && (left.origin || "user") === right.origin &&
@@ -37,9 +36,10 @@ const sameRequest = (left, right) => left.text === right.text && (left.origin ||
37
36
 
38
37
  /** One application conversation API. Storage and authorization remain host facilities. */
39
38
  export function createConversationRuntime({ engine: defaultEngine = "api", defaultIntegrationId, storage, authorize, connections, apiClientFactory, apiHistory,
40
- actions, toolPolicy, toolCatalog, attachments, fetch, host: defaultHost, onDeliveryTiming, persistCommentary = true, completedEnvelope = false, limits = {} } = {}) {
39
+ actions, toolPolicy, toolCatalog, attachments, fetch, host: defaultHost, onDeliveryTiming, hostDeliveryStages, persistCommentary = true, completedEnvelope = false, limits = {} } = {}) {
41
40
  if (typeof authorize !== "function") throw new TypeError("Conversation access requires an authorize function.");
42
41
  if (onDeliveryTiming !== undefined && typeof onDeliveryTiming !== "function") throw new TypeError("Delivery timing requires a server-owned observer.");
42
+ const deliveryHostStages = normalizeConversationDeliveryHostStages(hostDeliveryStages);
43
43
  if (typeof completedEnvelope !== "boolean") throw new TypeError("Completed envelope policy requires a server-side boolean.");
44
44
  if (typeof persistCommentary !== "boolean") throw new TypeError("Conversation commentary persistence requires an explicit server-side boolean.");
45
45
  if (apiHistory !== undefined && typeof apiHistory !== "function") throw new TypeError("API request history requires a server-owned selector.");
@@ -188,10 +188,15 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
188
188
  ? await entry.driver.readState({ conversation: entry.conversation, context })
189
189
  : await entry.conversation.read(context);
190
190
  entry.segmentId = value.threadId ? `${entry.engine}:${value.threadId}` : null;
191
- const pending = value.delivery?.engines[entry.engine]?.pending;
191
+ const saved = value.delivery?.engines[entry.engine]?.pending;
192
+ // The saved receipt is the delivery proof, as in changeover recovery. A
193
+ // restart after native admission leaves only a stale attempt marker, which
194
+ // must not leave the conversation unconfirmed until another message is sent.
195
+ const pending = (saved?.attempted && !saved.inspectionOnly &&
196
+ await entry.conversation.transcript.hasMessage(saved.messageId)) ? null : saved;
192
197
  return { engine: entry.engine, configuration: value.configuration, segmentId: entry.segmentId,
193
- binding: entry.engine === "codex" ? { threadId: value.threadId, codexAppServerRun: value.run }
194
- : entry.engine === "claude" ? { conversationId: value.threadId } : { sessionId: value.threadId },
198
+ binding: { ...conversationNativeBinding(entry.engine, value.threadId),
199
+ ...(entry.engine === "codex" ? { codexAppServerRun: value.run } : {}) },
195
200
  nativeTurn: value.turn, nativeResult: value.nativeResult,
196
201
  lastEngine: value.delivery?.lastEngine ?? entry.engine,
197
202
  replacement: ["preparing", "ready"].includes(value.delivery?.replacement?.status) ? value.delivery.replacement : null,
@@ -386,11 +391,11 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
386
391
  return { lastEngine: runtime.lastEngine, engines,
387
392
  ...(preparing ? { replacement: { engineId: preparing.engine, status: "preparing",
388
393
  operationId: preparing.request.operationId, handover: preparing.request.briefing || "",
389
- bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: nativeIdentity(runtime) },
394
+ bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: conversationNativeIdentity(runtime) },
390
395
  ...(preparing.preparedAt ? { preparedAt: preparing.preparedAt } : {}) } }
391
396
  : predecessor ? { replacement: { engineId: runtime.engine, status: "ready",
392
397
  operationId: predecessor.replacement.operationId, handover: predecessor.replacement.briefing || "",
393
- previous: { engineId: predecessor.engine, conversationId: nativeIdentity(predecessor) },
398
+ previous: { engineId: predecessor.engine, conversationId: conversationNativeIdentity(predecessor) },
394
399
  ...(predecessor.preparedAt ? { preparedAt: predecessor.preparedAt } : {}) } } : {}) };
395
400
  },
396
401
  write(value) {
@@ -473,7 +478,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
473
478
  return { admission: observation.accepted ? "accepted" : "unknown" };
474
479
  }
475
480
  const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
476
- if (!threadId || threadId !== nativeIdentity(runtime) || runtime.request?.messageId !== messageId) return null;
481
+ if (!threadId || threadId !== conversationNativeIdentity(runtime) || runtime.request?.messageId !== messageId) return null;
477
482
  observation = await entry.provider.inspectAdmission({ ...runtime.request, context, configuration: runtime.configuration });
478
483
  await access(context, entry.id, request?.operation || "inspectDelivery", entry);
479
484
  return { admission: observation.accepted ? "accepted" : "unknown" };
@@ -604,7 +609,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
604
609
  const authored = submittedMessage(turn || {});
605
610
  const saved = turn?.metadata?.runtime;
606
611
  if (runtime?.version !== 3 || runtime.engine !== engine || runtime.segmentId !== segmentId ||
607
- runtime.replacement || runtime.request || !nativeIdentity(runtime) || authored?.messageId !== messageId ||
612
+ runtime.replacement || runtime.request || !conversationNativeIdentity(runtime) || authored?.messageId !== messageId ||
608
613
  authored.origin !== "application" || saved?.completedEnvelope !== true || saved.engine !== engine ||
609
614
  saved.segmentId !== segmentId || saved.status !== "complete" || saved.supersededBy ||
610
615
  typeof turn.assistant?.text !== "string" || !turn.assistant.text || !turn.assistant.messageId) {
@@ -644,7 +649,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
644
649
  await access(context, entry.id, "tool", entry);
645
650
  const saved = await storage.read(entry.id, transaction => exact(transaction));
646
651
  const observation = await entry.provider.inspectAdmission({ messageId,
647
- threadId: nativeIdentity(saved.runtime), nativeTurnId: saved.turn.metadata.runtime.nativeTurnId,
652
+ threadId: conversationNativeIdentity(saved.runtime), nativeTurnId: saved.turn.metadata.runtime.nativeTurnId,
648
653
  configuration: saved.runtime.configuration, receiptBinding: saved.runtime.binding, context, verifyAccount: true });
649
654
  signal.throwIfAborted();
650
655
  await access(context, entry.id, "tool", entry);
@@ -777,6 +782,13 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
777
782
  let toolFailure;
778
783
  const applicationTools = entry.applicationTools;
779
784
  const toolStorage = applicationTools?.storage || storage;
785
+ // Every saved turn and pending request records its attribution identically.
786
+ const turnAttribution = (request, extra = {}) => ({ engine, segmentId, origin: request.input.origin,
787
+ ...(request.completedEnvelope ? { completedEnvelope: true } : {}),
788
+ ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}), ...extra });
789
+ const authoredRequestRecord = request => ({ ...request.input, at: request.at,
790
+ ...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}),
791
+ ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}) });
780
792
 
781
793
  function prepareTools(request) {
782
794
  if (request.completedEnvelope || !catalog || entry.conversation && !applicationTools) return;
@@ -822,12 +834,10 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
822
834
  if (request.answer?.text.trim()) await transaction.replaceAssistant(request.turnId, request.answer);
823
835
  }
824
836
  const saved = driver.canonicalTranscript ? await transaction.readTurn(request.turnId) : null;
825
- await transaction.updateTurnMetadata(request.turnId, { runtime: { ...saved?.metadata?.runtime, status, engine, segmentId, origin: request.input.origin,
826
- ...(request.completedEnvelope ? { completedEnvelope: true } : {}),
827
- ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}),
837
+ await transaction.updateTurnMetadata(request.turnId, { runtime: { ...saved?.metadata?.runtime, status, ...turnAttribution(request, {
828
838
  ...(request.nativeTurnId ? { nativeTurnId: request.nativeTurnId } : {}),
829
839
  ...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
830
- ...(request.continuedBy ? { continuedBy: request.continuedBy } : {}), ...extra, ...(error ? { error } : {}) } });
840
+ ...(request.continuedBy ? { continuedBy: request.continuedBy } : {}) }), ...extra, ...(error ? { error } : {}) } });
831
841
  return conversationHistoryVersions([{ turnId: request.turnId,
832
842
  messages: [...request.progress.values()].filter(message => !(message.turnId === request.turnId && message.status === "complete"))
833
843
  .concat(request.answer?.text.trim() ? [request.answer] : []) }]);
@@ -895,11 +905,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
895
905
  } else {
896
906
  id = await transaction.nextTurnId();
897
907
  await transaction.appendMessage(id, { role: request.input.origin === "application" ? "system" : "user", ...request.input, at: new Date().toISOString(),
898
- turnMetadata: { runtime: { status: "running", engine, segmentId, origin: request.input.origin,
899
- ...(request.completedEnvelope ? { completedEnvelope: true } : {}),
900
- ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}),
908
+ turnMetadata: { runtime: { status: "running", ...turnAttribution(request, {
901
909
  ...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
902
- ...(nativeTurnId ? { nativeTurnId } : {}) } } });
910
+ ...(nativeTurnId ? { nativeTurnId } : {}) }) } } });
903
911
  }
904
912
  if (engine === "api") {
905
913
  metadata.runtime.seen = { ...(seen || metadata.runtime.seen), ...conversationHistoryVersions([await transaction.readTurn(id)]) };
@@ -1085,9 +1093,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1085
1093
  throw failure("Authorized attachment paths changed after this instruction was prepared. Restore the original files or send a new message.", "conversation_attachment_manifest_changed");
1086
1094
  }
1087
1095
  metadata.runtime.request = { ...(pending?.messageId === request.input.messageId ? pending
1088
- : { ...request.input, at: request.at, attempted: false,
1089
- ...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}),
1090
- ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}) }), attachmentManifest };
1096
+ : { ...authoredRequestRecord(request), attempted: false }), attachmentManifest };
1091
1097
  await transaction.writeMetadata(metadata);
1092
1098
  });
1093
1099
  const admitted = Promise.withResolvers();
@@ -1113,9 +1119,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1113
1119
  threadId = native?.threadId;
1114
1120
  if (!threadId) throw new Error("Native admission requires the saved conversation identity.");
1115
1121
  await input.onPromptSending(entry.conversation ? native : { threadId, displayAttachments: files.attachments,
1116
- turnMetadata: { runtime: { engine, segmentId, origin: request.input.origin,
1117
- ...(request.completedEnvelope ? { completedEnvelope: true } : {}),
1118
- ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}) } } });
1122
+ turnMetadata: { runtime: turnAttribution(request) } });
1119
1123
  if (request.input.goal && request.context?.signal?.aborted) {
1120
1124
  throw Object.assign(failure("The goal command was stopped before delivery.", "conversation_goal_stopped"), { delivery: "not-sent" });
1121
1125
  }
@@ -1167,9 +1171,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1167
1171
  const metadata = await transaction.readMetadata();
1168
1172
  const pending = metadata.runtime.request;
1169
1173
  if (pending && pending.messageId !== request.input.messageId && (pending.attempted || pending.inspectionOnly)) throw new Error("The pending conversation instruction changed.");
1170
- metadata.runtime.request = { ...request.input, at: request.at,
1171
- ...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}),
1172
- ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}),
1174
+ metadata.runtime.request = { ...authoredRequestRecord(request),
1173
1175
  message: rendered.message, displayMessage: rendered.displayMessage,
1174
1176
  displayAttachments, attachmentIds: rendered.attachmentIds,
1175
1177
  attachmentManifest,
@@ -1181,7 +1183,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1181
1183
  },
1182
1184
  async onPromptRejected() {
1183
1185
  if (entry.conversation) return request.originalInput?.onPromptRejected?.();
1184
- if (!entry.conversation) await storage.write(entry.id, async transaction => {
1186
+ await storage.write(entry.id, async transaction => {
1185
1187
  const metadata = await transaction.readMetadata();
1186
1188
  if (metadata.runtime.request?.messageId === request.input.messageId) {
1187
1189
  metadata.runtime.request.attempted = false;
@@ -1309,7 +1311,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1309
1311
  const turn = await transaction.readTurn(initial.turnId);
1310
1312
  const saved = turn?.metadata?.runtime;
1311
1313
  if (metadata.runtime.engine !== engine || metadata.runtime.segmentId !== segmentId ||
1312
- nativeIdentity(metadata.runtime) !== threadId || submittedMessage(turn || {})?.messageId !== messageId ||
1314
+ conversationNativeIdentity(metadata.runtime) !== threadId || submittedMessage(turn || {})?.messageId !== messageId ||
1313
1315
  saved?.engine !== engine || saved.segmentId !== segmentId || saved.completedEnvelope !== true ||
1314
1316
  typeof saved.nativeToolAttempt !== "boolean" || saved.status !== "running" || saved.supersededBy ||
1315
1317
  entry.active !== active || current !== initial || initial.finished || initial.sealed || active.controller.signal.aborted) {
@@ -1475,7 +1477,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1475
1477
 
1476
1478
  async function send(entry, context, input, origin = "user", goal, representation, wakeOptions) {
1477
1479
  const deliveryTiming = createConversationDeliveryTiming({ engine: entry.engine,
1478
- conversationId: entry.id, messageId: input?.messageId, report: onDeliveryTiming });
1480
+ conversationId: entry.id, messageId: input?.messageId, report: onDeliveryTiming, hostStages: deliveryHostStages });
1479
1481
  deliveryTiming("received");
1480
1482
  const grant = new Set();
1481
1483
  try {
@@ -1701,9 +1703,6 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1701
1703
  await access(context, entry.id, "goal", entry);
1702
1704
  context?.signal?.throwIfAborted();
1703
1705
  if (!capabilities(entry).goals) throw failure("This connection does not support native goals.", "conversation_unsupported", 400);
1704
- if (representation !== "native" && !capabilities(entry).goals) {
1705
- throw failure("This bound conversation does not yet map native goal messages to canonical receipts.", "conversation_unsupported", 400);
1706
- }
1707
1706
  if (representation === "native") {
1708
1707
  const { operation } = await serial(entry, async () => {
1709
1708
  await access(context, entry.id, "goal", entry);
@@ -1817,8 +1816,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1817
1816
  // Release the queue once Send is reserved, just as an ordinary message
1818
1817
  // does. Stop remains available while native acknowledgement is pending.
1819
1818
  if (delivery) return delivery;
1820
- if (entry.conversation && updated.nativeResult.ok === false) throw Object.assign(new Error(updated.nativeResult.error), updated.nativeResult);
1821
- entry.goal = entry.conversation ? updated.goal : updated;
1819
+ entry.goal = updated;
1822
1820
  await emit(entry, { type: "goal", goal: entry.goal });
1823
1821
  return { goal: structuredClone(entry.goal) };
1824
1822
  });
@@ -1851,13 +1849,16 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1851
1849
  const history = await readTranscript(entry);
1852
1850
  const existing = history.find(turn => submittedMessage(turn)?.messageId === messageId);
1853
1851
  const receipt = existing && { status: "accepted", messageId, turnId: existing.turnId, duplicate: true };
1854
- const goalMessageId = existing?.metadata.runtime.goalMessageId;
1852
+ // Turns written before the first runtime segment carry no runtime metadata.
1853
+ const existingRuntime = existing?.metadata?.runtime;
1854
+ const goalMessageId = existingRuntime?.goalMessageId;
1855
1855
  const goalRequest = Boolean(submittedMessage(existing || {})?.goal || goalMessageId === messageId);
1856
- if (existing && (existing.metadata?.runtime?.segmentId !== entry.segmentId || existing.metadata.runtime.supersededBy ||
1857
- existing.metadata.runtime.status === "complete" && !goalRequest)) return receipt;
1856
+ if (existing && (existingRuntime?.segmentId !== entry.segmentId || existingRuntime.supersededBy ||
1857
+ existingRuntime.status === "complete" && !goalRequest)) return receipt;
1858
1858
  if (goalMessageId && goalMessageId !== messageId) {
1859
1859
  const owner = history.find(turn => submittedMessage(turn)?.messageId === goalMessageId);
1860
- if (owner?.metadata.runtime.goalMessageId !== goalMessageId || owner.metadata.runtime.segmentId !== entry.segmentId) throw new Error("The goal command for this native continuation is missing.");
1860
+ const ownerRuntime = owner?.metadata?.runtime;
1861
+ if (ownerRuntime?.goalMessageId !== goalMessageId || ownerRuntime.segmentId !== entry.segmentId) throw new Error("The goal command for this native continuation is missing.");
1861
1862
  const result = await inspectDelivery(entry, context, goalMessageId);
1862
1863
  return { ...receipt, recovered: result.recovered === true };
1863
1864
  }
@@ -1873,7 +1874,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1873
1874
  const request = existing ? submittedMessage(existing) : metadata.runtime.request;
1874
1875
  if (!request || request.messageId !== messageId || !entry.provider.inspectAdmission) return receipt || { status: "unknown", messageId };
1875
1876
  const pending = metadata.runtime.request?.messageId === messageId ? metadata.runtime.request : null;
1876
- if (pending?.threadId && pending.threadId !== nativeIdentity(metadata.runtime)) return receipt || { status: "unknown", messageId };
1877
+ if (pending?.threadId && pending.threadId !== conversationNativeIdentity(metadata.runtime)) return receipt || { status: "unknown", messageId };
1877
1878
  const observation = suppliedObservation || await entry.provider.inspectAdmission({ ...request, nativeTurnId: existing?.metadata?.runtime?.nativeTurnId,
1878
1879
  context, configuration: metadata.runtime.configuration });
1879
1880
  await access(context, entry.id, "inspectDelivery", entry);
@@ -1998,7 +1999,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1998
1999
  }
1999
2000
  const row = (await readTranscript(entry)).find(turn => submittedMessage(turn)?.messageId === messageId);
2000
2001
  if (row) return { status: "accepted", messageId, turnId: row.turnId, duplicate: true };
2001
- const threadId = state.request?.messageId === messageId ? state.request.threadId : nativeIdentity(state);
2002
+ const threadId = state.request?.messageId === messageId ? state.request.threadId : conversationNativeIdentity(state);
2002
2003
  if (!threadId) return { status: "unknown", messageId };
2003
2004
  const observation = await entry.provider.inspectAdmission({ messageId, threadId, context });
2004
2005
  await access(context, entry.id, "inspectDelivery", entry);
@@ -2106,7 +2107,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
2106
2107
  ? normalizeConversationConfiguration(input.configuration, { engine, defaultIntegrationId }) : state.configuration);
2107
2108
  const driver = createDriver(engine, entry.host);
2108
2109
  driver.validateConfiguration(configuration);
2109
- const predecessorId = nativeIdentity(state);
2110
+ const predecessorId = conversationNativeIdentity(state);
2110
2111
  const nativeReplacement = !selection && state.engine !== "api" && predecessorId &&
2111
2112
  (state.engine !== "claude" || state.binding.sent);
2112
2113
  const retainNativeBinding = input.retireNative !== true && engine === state.engine &&
@@ -2189,7 +2190,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
2189
2190
  }
2190
2191
  await callback({
2191
2192
  async releaseBinding(replacement) {
2192
- if (replacement && nativeIdentity(state) !== replacement.previous.conversationId) {
2193
+ if (replacement && conversationNativeIdentity(state) !== replacement.previous.conversationId) {
2193
2194
  throw failure("The native predecessor identity changed before its binding was released.", "conversation_replacement_conflict");
2194
2195
  }
2195
2196
  releaseConversationRuntimeBinding(state, pending, request, engine);
@@ -2216,7 +2217,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
2216
2217
  identity: {
2217
2218
  async inspect(expectedId) {
2218
2219
  const { runtime } = await storage.read(entry.id, transaction => transaction.readMetadata());
2219
- if (runtime.segmentId !== input.expectedSegmentId || nativeIdentity(runtime) !== expectedId) {
2220
+ if (runtime.segmentId !== input.expectedSegmentId || conversationNativeIdentity(runtime) !== expectedId) {
2220
2221
  throw failure("The native predecessor identity changed or is incomplete.", "conversation_replacement_conflict");
2221
2222
  }
2222
2223
  return { bindingNames: ["binding"], previous: { engineId: runtime.engine, conversationId: expectedId } };
@@ -31,9 +31,15 @@ function validateSegment(segment) {
31
31
  }
32
32
  }
33
33
 
34
- function nativeIdentity(segment) {
35
- return segment.engine === "codex" ? segment.binding.threadId
36
- : segment.engine === "claude" ? segment.binding.conversationId : segment.binding.sessionId;
34
+ const nativeIdentityField = engine => engine === "codex" ? "threadId" : engine === "claude" ? "conversationId" : "sessionId";
35
+
36
+ /** The saved binding field that holds a native segment's conversation identity. */
37
+ export function conversationNativeIdentity(segment) {
38
+ return segment.binding?.[nativeIdentityField(segment.engine)];
39
+ }
40
+
41
+ export function conversationNativeBinding(engine, id) {
42
+ return { [nativeIdentityField(engine)]: id };
37
43
  }
38
44
 
39
45
  function inspectRequest(request, segment, version, warnings) {
@@ -44,7 +50,7 @@ function inspectRequest(request, segment, version, warnings) {
44
50
  invalid("Conversation runtime has an unsupported pending request. Inspect it before upgrading.");
45
51
  }
46
52
  if (segment.engine === "api") return;
47
- const threadId = nativeIdentity(segment);
53
+ const threadId = conversationNativeIdentity(segment);
48
54
  if (version === 3 && !Object.hasOwn(request, "inspectionOnly")) {
49
55
  if (typeof request.attempted !== "boolean" || request.attempted &&
50
56
  (typeof threadId !== "string" || !threadId || request.threadId !== threadId || typeof request.message !== "string") ||
@@ -143,30 +149,21 @@ function retireCodexBinding(runtime, retirement) {
143
149
  finishConversationRuntimeReplacement(runtime, { engine: "codex", selection: true, retireNative: true, pending });
144
150
  }
145
151
 
146
- /** Pure offline transform. The application owns enumeration, backups and publication. */
147
- export function upgradeConversationRuntimeState({ metadata, conversationLog, retirement }) {
148
- if (!object(metadata) || !Array.isArray(conversationLog)) {
149
- invalid("Conversation runtime upgrade requires metadata and the complete stored transcript.");
150
- }
151
- if (metadata.runtime === undefined) {
152
- if (retirement !== undefined) invalid("Offline native retirement requires the exact saved runtime; it cannot create a historical binding.");
153
- return { metadata, changed: false, warnings: [] };
154
- }
155
- const source = metadata.runtime;
156
- if (!object(source) || ![2, 3].includes(source.version) || !Array.isArray(source.predecessors)) {
152
+ /** Validates a saved runtime and its retained requests. A version 2 request is
153
+ * marked inspection-only in place, so the upgrade passes its own copy. */
154
+ function validateConversationRuntimeState(runtime, warnings) {
155
+ if (!object(runtime) || ![2, 3].includes(runtime.version) || !Array.isArray(runtime.predecessors)) {
157
156
  invalid("This conversation has an unsupported runtime version. Inspect its original format and provide an offline conversion before opening it.");
158
157
  }
159
- if (hasUnfinishedConversationRewind(source)) {
158
+ if (hasUnfinishedConversationRewind(runtime)) {
160
159
  invalid("This conversation has an unfinished Undo operation from an earlier runtime. Inspect its saved native and local history offline before upgrading; no history was changed.");
161
160
  }
162
- if (source.version === 3 && (typeof source.lastEngine !== "string" || source.lastEngine && !engines.has(source.lastEngine))) {
161
+ if (runtime.version === 3 && (typeof runtime.lastEngine !== "string" || runtime.lastEngine && !engines.has(runtime.lastEngine))) {
163
162
  invalid("Conversation runtime has an invalid last-delivered engine. Inspect it before upgrading.");
164
163
  }
165
- const runtime = structuredClone(source);
166
- const warnings = [];
167
164
  for (const segment of [...runtime.predecessors, runtime]) {
168
165
  validateSegment(segment);
169
- inspectRequest(segment.request, segment, source.version, warnings);
166
+ inspectRequest(segment.request, segment, runtime.version, warnings);
170
167
  }
171
168
  if (runtime.replacement !== undefined) {
172
169
  const pending = runtime.replacement;
@@ -177,11 +174,26 @@ export function upgradeConversationRuntimeState({ metadata, conversationLog, ret
177
174
  }
178
175
  if (pending.binding) {
179
176
  validateSegment(pending);
180
- inspectRequest(pending.submission, pending, source.version, warnings);
177
+ inspectRequest(pending.submission, pending, runtime.version, warnings);
181
178
  } else if (pending.submission !== undefined) {
182
179
  invalid("An unfinished replacement has a request without its exact saved binding. Inspect it before upgrading.");
183
180
  }
184
181
  }
182
+ }
183
+
184
+ /** Pure offline transform. The application owns enumeration, backups and publication. */
185
+ export function upgradeConversationRuntimeState({ metadata, conversationLog, retirement }) {
186
+ if (!object(metadata) || !Array.isArray(conversationLog)) {
187
+ invalid("Conversation runtime upgrade requires metadata and the complete stored transcript.");
188
+ }
189
+ if (metadata.runtime === undefined) {
190
+ if (retirement !== undefined) invalid("Offline native retirement requires the exact saved runtime; it cannot create a historical binding.");
191
+ return { metadata, changed: false, warnings: [] };
192
+ }
193
+ const source = metadata.runtime;
194
+ const runtime = structuredClone(source);
195
+ const warnings = [];
196
+ validateConversationRuntimeState(runtime, warnings);
185
197
  if (retirement !== undefined) {
186
198
  retireCodexBinding(runtime, retirement);
187
199
  return { metadata: { ...metadata, runtime }, changed: true, warnings };
@@ -211,7 +223,7 @@ export async function prepareConversationRuntimeCopy(transaction, { at }) {
211
223
  const runtime = metadata.runtime;
212
224
  if (runtime) {
213
225
  if (runtime.version !== 3) invalid("Upgrade the parent conversation before copying its runtime state.");
214
- upgradeConversationRuntimeState({ metadata, conversationLog: turns });
226
+ validateConversationRuntimeState(runtime, []);
215
227
  if (runtime.replacement) invalid("Finish the parent conversation changeover before copying it.");
216
228
  for (const segment of [...runtime.predecessors, runtime]) {
217
229
  if (segment.binding) {
@@ -1,4 +1,4 @@
1
- import { normalizeText } from "./normalize.js";
1
+ import { normalizeNullableText } from "./normalize.js";
2
2
  import { conversationLogPage } from "../../shared/conversation/pagination.js";
3
3
 
4
4
  /** Conversation policy, independent of database, filesystem, identity and HTTP. */
@@ -25,7 +25,7 @@ export function createConversationTranscript({ storage, clock = () => new Date()
25
25
  ? (await Promise.all(allIds.map(id => transaction.readTurn(id))))
26
26
  .filter(turn => hasMessages(turn) && (!presentation || !isCompletedEnvelopeTurn(turn))) : null;
27
27
  const ids = visible ? visible.map(turn => turn.turnId) : allIds;
28
- const before = normalizeText(beforeTurnId);
28
+ const before = normalizeNullableText(beforeTurnId);
29
29
  const requestedLimit = Number.parseInt(String(limit || ""), 10);
30
30
  const pageLimit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? Math.min(requestedLimit, 100) : 0;
31
31
  let page;
@@ -66,13 +66,13 @@ export function createConversationTranscript({ storage, clock = () => new Date()
66
66
  }
67
67
 
68
68
  async function append(scope, role, { text = "", messageId = "", at = "", attachments = [], data, turnMetadata = null, requireOpenTurn = false, outputId = "", turnId: requestedTurnId = "" } = {}) {
69
- const messageText = normalizeText(text);
70
- const id = normalizeText(messageId);
69
+ const messageText = normalizeNullableText(text);
70
+ const id = normalizeNullableText(messageId);
71
71
  if (!messageText && !(role === "user" && Array.isArray(attachments) && attachments.length)) return null;
72
72
  return storage.write(scope, async (transaction) => {
73
73
  if (id && await transaction.hasMessage(id)) return null;
74
74
  const createdAt = new Date(at || clock());
75
- const targetId = ["assistant", "commentary", "thinking"].includes(role) ? normalizeText(requestedTurnId) : "";
75
+ const targetId = ["assistant", "commentary", "thinking"].includes(role) ? normalizeNullableText(requestedTurnId) : "";
76
76
  const target = targetId ? await transaction.readTurn(targetId) : null;
77
77
  if (targetId && !target) throw new Error("The native output's original turn no longer exists.");
78
78
  const tail = target || (["assistant", "commentary", "thinking"].includes(role) ? await tailTurn(transaction) : null);
@@ -84,7 +84,7 @@ export function createConversationTranscript({ storage, clock = () => new Date()
84
84
  const turnId = target?.turnId || open?.turnId || (thinkingOnly ? tail.turnId : await transaction.nextTurnId());
85
85
  await transaction.appendMessage(turnId, {
86
86
  role, text: messageText, messageId: id, at: createdAt.toISOString(),
87
- ...(["assistant", "commentary", "thinking"].includes(role) && normalizeText(outputId) ? { outputId: normalizeText(outputId) } : {}),
87
+ ...(["assistant", "commentary", "thinking"].includes(role) && normalizeNullableText(outputId) ? { outputId: normalizeNullableText(outputId) } : {}),
88
88
  ...(role === "user" ? { attachments, turnMetadata } : {}),
89
89
  ...(data !== undefined ? { data: structuredClone(data) } : {})
90
90
  });
@@ -94,7 +94,7 @@ export function createConversationTranscript({ storage, clock = () => new Date()
94
94
 
95
95
  // The first turn whose submitted (user or application) message has this ID.
96
96
  async function findSubmittedTurn(scope, messageId) {
97
- const id = normalizeText(messageId);
97
+ const id = normalizeNullableText(messageId);
98
98
  if (!id) return null;
99
99
  return storage.read(scope, async (transaction) => {
100
100
  const submitted = turn => hasMessages(turn) && (turn.user || turn.system)?.messageId === id ? turn : null;
@@ -116,17 +116,17 @@ export function createConversationTranscript({ storage, clock = () => new Date()
116
116
  readConversationLog,
117
117
  findSubmittedTurn,
118
118
  readConversationLogPage,
119
- conversationMessageIdExists: (scope, messageId) => storage.read(scope, (transaction) => transaction.hasMessage(normalizeText(messageId))),
119
+ conversationMessageIdExists: (scope, messageId) => storage.read(scope, (transaction) => transaction.hasMessage(normalizeNullableText(messageId))),
120
120
  writeConversationUserMessage: (scope, input) => append(scope, "user", input),
121
121
  writeConversationAssistantMessage: (scope, input) => append(scope, "assistant", input),
122
122
  writeConversationCommentaryMessage: (scope, input) => append(scope, "commentary", input),
123
123
  writeConversationThinkingMessage: (scope, input) => append(scope, "thinking", input),
124
124
  writeConversationSystemMessage: (scope, input) => append(scope, "system", input),
125
125
  async upsertConversationAssistantMessage(scope, { turnId = "", text = "" } = {}) {
126
- const messageText = normalizeText(text);
126
+ const messageText = normalizeNullableText(text);
127
127
  if (!messageText) return null;
128
128
  return storage.write(scope, async (transaction) => {
129
- const id = normalizeText(turnId);
129
+ const id = normalizeNullableText(turnId);
130
130
  await transaction.replaceAssistant(id, { role: "assistant", text: messageText, at: new Date(clock()).toISOString() });
131
131
  return transaction.readTurn(id);
132
132
  });
@@ -8,6 +8,7 @@ export {
8
8
  codexAuthOutputRequiresReconnect,
9
9
  codexProviderConfiguration,
10
10
  codexProviderFileConfiguration,
11
+ codexProviderCatalogPath,
11
12
  verifyCodexProviderKey,
12
13
  codexProviderModelCatalog,
13
14
  codexCatalogRows,
@@ -1,26 +1,28 @@
1
1
  export {
2
2
  CODEX_APP_SERVER_INVALID_REQUEST_CODE,
3
3
  CodexAppServerAgentProvider,
4
- assertCodexAuthPreflightReady,
5
4
  codexAppServerEndpointForTarget,
6
5
  codexAppServerRequestIsInvalid,
7
6
  codexAppServerRuntimeStopWasVerified,
8
7
  codexCliResumeCommand,
9
- codexRenewalThreadError,
10
8
  codexTextInput,
11
9
  codexTurnInput,
12
10
  createCodexAppServerProviderOwner,
13
- defineCodexRenewalThreadIds,
14
11
  deleteCodexAppServerHelperThread,
15
12
  deleteCodexAppServerThread,
16
13
  ensureCodexAppServerThread,
17
14
  inspectCodexAppServerMessageAdmission,
18
15
  retireCodexConversationHistory,
19
16
  readCodexNativeGoal,
20
- resumeExactCodexAppServerThread,
21
17
  sendCodexAppServerPrompt,
22
- shellQuote,
23
- startFreshCodexAppServerThread
18
+ shellQuote
24
19
  } from "../conversation/codexProvider.js";
20
+ export { assertCodexAuthPreflightReady } from "../conversation/codexAuthPreflight.js";
21
+ export {
22
+ codexRenewalThreadError,
23
+ defineCodexRenewalThreadIds,
24
+ resumeExactCodexAppServerThread,
25
+ startFreshCodexAppServerThread
26
+ } from "../conversation/codexRenewalThreads.js";
25
27
 
26
28
  export { readCodexHistoryRows } from "../conversation/codexHistoryAdapter.js";
@@ -15,4 +15,5 @@ export {
15
15
  export { resolveWorkspaceSlug } from "./lib/resolveWorkspaceSlug.js";
16
16
  export { createServiceToolCatalog } from "./lib/serviceToolCatalog.js";
17
17
  export { consumeCompletionStream, sanitizeAssistantMessageText } from "./lib/assistantCompletion.js";
18
- export { runAssistantToolLoop, runBoundedAssistantToolLoop, readAssistantResponseEnvelope, readPartialAssistantReply } from "./lib/assistantToolLoop.js";
18
+ export { runAssistantToolLoop } from "./lib/assistantToolLoop.js";
19
+ export { runBoundedAssistantToolLoop, readAssistantResponseEnvelope, readPartialAssistantReply } from "./conversation/boundedToolLoop.js";