@jskit-ai/assistant-core 0.1.221 → 0.1.222

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.
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.1.221",
12
- "@jskit-ai/http-runtime": "0.1.241",
13
- "@jskit-ai/kernel": "0.1.243",
11
+ "@jskit-ai/assistant-core": "0.1.222",
12
+ "@jskit-ai/http-runtime": "0.1.242",
13
+ "@jskit-ai/kernel": "0.1.244",
14
14
  "@mdi/js": "^7.4.47",
15
15
  "vue": "^3.5.13",
16
16
  "vuetify": "^4.0.0",
17
- "@jskit-ai/connectors-core": "0.1.60",
18
- "@jskit-ai/connectors-catalog": "0.1.60"
17
+ "@jskit-ai/connectors-core": "0.1.61",
18
+ "@jskit-ai/connectors-catalog": "0.1.61"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@vitejs/plugin-vue": "^6.0.7",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-core",
3
- "version": "0.1.221",
3
+ "version": "0.1.222",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -47,8 +47,8 @@
47
47
  "@ai-sdk/perplexity": "^4.0.43",
48
48
  "@ai-sdk/togetherai": "^3.0.49",
49
49
  "@ai-sdk/xai": "^4.0.58",
50
- "@jskit-ai/resource-core": "0.1.185",
51
- "@jskit-ai/resource-crud-core": "0.1.185",
50
+ "@jskit-ai/resource-core": "0.1.186",
51
+ "@jskit-ai/resource-crud-core": "0.1.186",
52
52
  "@mdi/js": "^7.4.47",
53
53
  "@openrouter/ai-sdk-provider": "^3.0.0",
54
54
  "ai": "^7.0.101",
@@ -57,8 +57,8 @@
57
57
  "ws": "^8.20.1"
58
58
  },
59
59
  "peerDependencies": {
60
- "@jskit-ai/http-runtime": "0.1.241",
61
- "@jskit-ai/kernel": "0.1.243",
60
+ "@jskit-ai/http-runtime": "0.1.242",
61
+ "@jskit-ai/kernel": "0.1.244",
62
62
  "vue": "^3.5.13",
63
63
  "vuetify": "^4.0.0"
64
64
  },
@@ -449,6 +449,7 @@ export function createClaudeConversationOwner({
449
449
  thread: { id: entry.id }, turn: snapshot(entry), workdir: context.workdir, conversationTurn };
450
450
  }
451
451
  async function accept() {
452
+ (command || input).onNativeReceipt?.();
452
453
  // The new native ACK retires old effect custody while its canonical
453
454
  // admission/commit is awaited; it cannot retag a delayed old failure.
454
455
  if (applicationCommand) applicationCommand.active.nativeOwner = null;
@@ -465,6 +466,7 @@ export function createClaudeConversationOwner({
465
466
  if (admission) conversationTurn = await admission;
466
467
  }
467
468
  if (!command) await entry.nativeTurn.updateState("active");
469
+ (command || input).onReceiptPersisted?.();
468
470
  if (applicationCommand && conversationTurn) {
469
471
  const value = deliveryResult();
470
472
  const active = applicationCommand.active;
@@ -736,7 +738,8 @@ export function createClaudeConversationOwner({
736
738
  entry.command = active;
737
739
  entry.toolSchemas = active.tools?.schemas || [];
738
740
  }
739
- const value = await send(entry, { ...command.input.nativeMessage, onPromptSending: command.beforeDispatch }, { applicationCommand });
741
+ const value = await send(entry, { ...command.input.nativeMessage, onPromptSending: command.beforeDispatch,
742
+ onNativeReceipt: command.onNativeReceipt, onReceiptPersisted: command.onReceiptPersisted }, { applicationCommand });
740
743
  const completion = value.conversationTurn ? entry.completion?.promise : null;
741
744
  const delivered = { value, completion };
742
745
  if (!applicationCommand && value.delivered === true && value.conversationTurn) {
@@ -766,9 +769,10 @@ export function createClaudeConversationOwner({
766
769
  control.current = active;
767
770
  const ready = Promise.withResolvers();
768
771
  ready.promise.catch(() => {});
769
- active.steer = async ({ input, beforeDispatch, accept }) => {
772
+ active.steer = async ({ input, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted }) => {
770
773
  await ready.promise;
771
- const nativeDispatch = { active, signal, beforeDispatch, accept, timeoutMs, steering: true, interrupted: false, attempted: false, admitted: false };
774
+ const nativeDispatch = { active, signal, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted,
775
+ timeoutMs, steering: true, interrupted: false, attempted: false, admitted: false };
772
776
  try { await dispatch(control, { input, context, signal }, { acquire, entry, dispatch: nativeDispatch }); }
773
777
  catch (error) {
774
778
  if (nativeDispatch.attempted && !nativeDispatch.admitted) error.delivery = "uncertain";
@@ -776,7 +780,8 @@ export function createClaudeConversationOwner({
776
780
  throw error;
777
781
  }
778
782
  };
779
- const nativeDispatch = { active, signal, beforeDispatch, accept, timeoutMs, maximumOutput, attempted: false, admitted: false };
783
+ const nativeDispatch = { active, signal, beforeDispatch, accept, onNativeReceipt: command.onNativeReceipt,
784
+ onReceiptPersisted: command.onReceiptPersisted, timeoutMs, maximumOutput, attempted: false, admitted: false };
780
785
  let entry;
781
786
  let cancellation;
782
787
  let problem;
@@ -499,6 +499,7 @@ export function createCodexMessageCommands({
499
499
  if (!deliveredTurnId) {
500
500
  throw new Error("Codex app-server accepted the prompt without returning a turn id.");
501
501
  }
502
+ codexAppServerPendingUserMessages.get(`${promptDeliveryKey}\0${clientUserMessageId}`)?.onNativeReceipt?.();
502
503
  await markCodexAppServerTurnActive(sessionId, {
503
504
  requireTrackedTurn: true,
504
505
  status: "inProgress",
@@ -624,6 +625,7 @@ export function createCodexMessageCommands({
624
625
  turnId
625
626
  } };
626
627
  }
628
+ codexAppServerPendingUserMessages.get(`${namespace(sessionId)}\0${clientUserMessageId}`)?.onNativeReceipt?.();
627
629
  const conversationTurn = await writeCodexAppServerDeliveredUserMessage(
628
630
  runtime,
629
631
  sessionId,
@@ -95,6 +95,7 @@ export function createCodexAppServerMessageDelivery({
95
95
  if (!written) {
96
96
  return null;
97
97
  }
98
+ pendingMessage?.onReceiptPersisted?.();
98
99
  await publish(normalizedSessionId, {
99
100
  payload: {
100
101
  conversationLogPatch: {
@@ -144,6 +145,7 @@ export function createCodexAppServerMessageDelivery({
144
145
  Array.isArray(recoveredMessage?.displayAttachments) && recoveredMessage.displayAttachments.length
145
146
  ))) return;
146
147
  if (ownership) {
148
+ pendingMessage?.onNativeReceipt?.();
147
149
  // The provider's user-message receipt precedes its answer notifications.
148
150
  // Persist the authored message here so answers cannot overtake it while
149
151
  // the original HTTP request is still finishing startup bookkeeping.
@@ -0,0 +1,23 @@
1
+ const stages = new Set([
2
+ "received", "preparation-started", "provider-started", "runtime-ready", "dispatch-started",
3
+ "native-acknowledged", "receipt-persisted", "accepted", "first-response", "failed"
4
+ ]);
5
+
6
+ // Observer results are never awaited and failures cannot change delivery.
7
+ export function createConversationDeliveryTiming({ engine, conversationId, messageId, report, clock = () => performance.now() }) {
8
+ const startedAt = clock();
9
+ let previousAt = startedAt;
10
+ let previousStage = "";
11
+ const timings = {};
12
+ return stage => {
13
+ if (!report || !stages.has(stage) || Object.hasOwn(timings, stage)) return;
14
+ const at = clock();
15
+ timings[stage] = Math.max(0, at - startedAt);
16
+ const event = { engine, conversationId, messageId, stage, previousStage,
17
+ durationMs: Math.max(0, at - previousAt), elapsedMs: timings[stage], timings: { ...timings } };
18
+ previousAt = at;
19
+ previousStage = stage;
20
+ try { Promise.resolve(report(event)).catch(() => {}); }
21
+ catch { /* Instrumentation never changes admission or provider work. */ }
22
+ };
23
+ }
@@ -700,11 +700,14 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
700
700
  return saved;
701
701
  }
702
702
 
703
- async function withDelivery(input, operation) {
703
+ async function withDelivery(command, operation) {
704
+ const input = command.input;
704
705
  return owner.withMessageDelivery(sessionId, input.messageId, {
705
706
  attachments: input.attachments, text: input.authoredInput?.text ?? input.text,
706
707
  turnMetadata: input.nativeMessage?.turnMetadata || null, actorContext: input.nativeMessage?.actorContext || null,
707
- authoredInput: input.authoredInput
708
+ authoredInput: input.authoredInput,
709
+ onNativeReceipt: command.onNativeReceipt,
710
+ onReceiptPersisted: command.onReceiptPersisted
708
711
  }, operation);
709
712
  }
710
713
 
@@ -788,7 +791,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
788
791
  }
789
792
 
790
793
  async function sendMessage(command, configuration, context, signal, tools) {
791
- return withDelivery(command.input, async () => {
794
+ return withDelivery(command, async () => {
792
795
  let attempted = false;
793
796
  let rejected = false;
794
797
  let accepted = false;
@@ -1013,7 +1016,8 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
1013
1016
  if (!current || current.finished && !current.finishIfCurrent) throw new Error("Codex has no active turn to steer.");
1014
1017
  return current.steer(command);
1015
1018
  },
1016
- async run({ configuration, context, input, tools, signal, beforeDispatch, accept, onEvent, onNativeTurn, finishIfCurrent }) {
1019
+ async run({ configuration, context, input, tools, signal, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted,
1020
+ onEvent, onNativeTurn, finishIfCurrent }) {
1017
1021
  if (disposed || current) throw new Error("This Codex conversation is closed or already working.");
1018
1022
  const completion = Promise.withResolvers();
1019
1023
  const admission = Promise.withResolvers();
@@ -1130,7 +1134,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
1130
1134
  executingProvider = native;
1131
1135
  }
1132
1136
  if (!supplied) native.setServerRequestHandler(toolHandler(executingProvider));
1133
- const command = { input, beforeDispatch, accept };
1137
+ const command = { input, beforeDispatch, accept, onNativeReceipt, onReceiptPersisted };
1134
1138
  delivered = await sendMessage(command, configuration, context, signal, tools);
1135
1139
  waitForTurn = !supplied || delivered.value?.delivered === true;
1136
1140
  if (!finishIfCurrent && waitForTurn) {
@@ -257,6 +257,15 @@ export function createOpenCodeConversationDriver({ connections, host = {}, limit
257
257
  active.messageIds.set(prepared.input.id, { messageId: input.messageId, committed: command.deliveryCommitted });
258
258
  } : beforeDispatch;
259
259
  prepared = await supplied.preparation.message({ ...input.nativeMessage, onPromptSending: dispatch }, options);
260
+ if (prepared.application?.commit) {
261
+ const application = prepared.application;
262
+ prepared.application = { ...application, async commit(admitted) {
263
+ command.onNativeReceipt?.();
264
+ const result = await application.commit(admitted);
265
+ command.onReceiptPersisted?.();
266
+ return result;
267
+ } };
268
+ }
260
269
  if (active?.tools && prepared.application?.monitor) {
261
270
  const monitor = prepared.application.monitor;
262
271
  const create = monitor.create;
@@ -639,7 +648,9 @@ export function createOpenCodeConversationDriver({ connections, host = {}, limit
639
648
  };
640
649
  },
641
650
  async commit() {
651
+ command.onNativeReceipt?.();
642
652
  await command.accept();
653
+ command.onReceiptPersisted?.();
643
654
  active.deliveryCommitted = command.deliveryCommitted;
644
655
  accepted = true;
645
656
  },
@@ -1,4 +1,5 @@
1
1
  import { createConversationProviderFactory } from "./provider.js";
2
+ import { createConversationDeliveryTiming } from "./deliveryTiming.js";
2
3
  import { normalizeConversationConfiguration } from "./configuration.js";
3
4
  import { runScopedConversationTurn, readPersistentConversation, startPersistentConversationTurn,
4
5
  inspectPersistentConversationAdmission, preparePersistentConversationChangeover,
@@ -36,8 +37,9 @@ const sameRequest = (left, right) => left.text === right.text && (left.origin ||
36
37
 
37
38
  /** One application conversation API. Storage and authorization remain host facilities. */
38
39
  export function createConversationRuntime({ engine: defaultEngine = "api", defaultIntegrationId, storage, authorize, connections, apiClientFactory, apiHistory,
39
- actions, toolPolicy, toolCatalog, attachments, fetch, host: defaultHost, persistCommentary = true, completedEnvelope = false, limits = {} } = {}) {
40
+ actions, toolPolicy, toolCatalog, attachments, fetch, host: defaultHost, onDeliveryTiming, persistCommentary = true, completedEnvelope = false, limits = {} } = {}) {
40
41
  if (typeof authorize !== "function") throw new TypeError("Conversation access requires an authorize function.");
42
+ if (onDeliveryTiming !== undefined && typeof onDeliveryTiming !== "function") throw new TypeError("Delivery timing requires a server-owned observer.");
41
43
  if (typeof completedEnvelope !== "boolean") throw new TypeError("Completed envelope policy requires a server-side boolean.");
42
44
  if (typeof persistCommentary !== "boolean") throw new TypeError("Conversation commentary persistence requires an explicit server-side boolean.");
43
45
  if (apiHistory !== undefined && typeof apiHistory !== "function") throw new TypeError("API request history requires a server-owned selector.");
@@ -97,6 +99,19 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
97
99
 
98
100
  async function emit(entry, event, isCurrent) {
99
101
  const { completedEnvelope: _untrustedMarker, ...payload } = event;
102
+ if (payload.type === "message" && ["assistant", "commentary", "thinking"].includes(payload.role) && payload.text?.trim()) {
103
+ // Bound live streams deliberately omit private native turn identities.
104
+ // Their current admitted request owns them; historical rows retain IDs.
105
+ let request;
106
+ if (payload.turnId) {
107
+ request = [...(entry.active?.requests.values() || [])].find(request =>
108
+ !request.sealed && (request.turnId === payload.turnId || request.nativeTurnId === payload.turnId));
109
+ } else if (entry.conversation) {
110
+ const current = entry.active?.request;
111
+ if (current?.accepted && !current.sealed) request = current;
112
+ }
113
+ request?.deliveryTiming?.("first-response");
114
+ }
100
115
  if (completedEnvelope && !entry.conversation) {
101
116
  const request = payload.messageId && entry.active?.requests.get(payload.messageId) ||
102
117
  payload.turnId && [...(entry.active?.requests.values() || [])].find(request => request.turnId === payload.turnId);
@@ -777,6 +792,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
777
792
  }
778
793
 
779
794
  async function publishAdmission(request, nativeResult) {
795
+ request.deliveryTiming?.("accepted");
780
796
  const receipt = { status: "accepted", messageId: request.input.messageId, turnId: request.turnId, origin: request.input.origin };
781
797
  request.admission.resolve(entry.conversation ? { receipt, nativeResult } : receipt);
782
798
  request.admissionSettled = true;
@@ -856,6 +872,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
856
872
  if (!driver.canonicalTranscript) streams.clear(entry.id);
857
873
  }
858
874
  request.accepted = true;
875
+ if (!entry.conversation) request.deliveryTiming?.("receipt-persisted");
859
876
  request.nativeTurnId = nativeTurnId;
860
877
  if (request.input.goal) goalOwner = request;
861
878
  current = active.request = request;
@@ -1027,6 +1044,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1027
1044
  committed = Promise.withResolvers();
1028
1045
  committed.promise.catch(() => {});
1029
1046
  request.deliveryCommitted = committed.promise;
1047
+ request.deliveryTiming?.("provider-started");
1030
1048
  completion = Promise.resolve().then(() => dispatch({
1031
1049
  context: request.context,
1032
1050
  input: { ...request.input, authoredInput: { ...request.input, at: request.at },
@@ -1034,8 +1052,11 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1034
1052
  text: input.message, contextText: input.contextText || "",
1035
1053
  content: [...contextFiles.content, ...files.content], localFiles, attachmentManifest },
1036
1054
  signal: active.controller.signal,
1055
+ onNativeReceipt: () => request.deliveryTiming?.("native-acknowledged"),
1056
+ onReceiptPersisted: () => request.deliveryTiming?.("receipt-persisted"),
1037
1057
  ...(engine === "opencode" ? { deliveryCommitted: committed.promise } : {}),
1038
1058
  async beforeDispatch(native) {
1059
+ request.deliveryTiming?.("runtime-ready");
1039
1060
  await beforeDispatch(request);
1040
1061
  threadId = native?.threadId;
1041
1062
  if (!threadId) throw new Error("Native admission requires the saved conversation identity.");
@@ -1043,6 +1064,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1043
1064
  turnMetadata: { runtime: { engine, segmentId, origin: request.input.origin,
1044
1065
  ...(request.completedEnvelope ? { completedEnvelope: true } : {}),
1045
1066
  ...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}) } } });
1067
+ request.deliveryTiming?.("dispatch-started");
1046
1068
  },
1047
1069
  async accept(native) {
1048
1070
  if (entry.conversation) nativeResult = native.nativeResult;
@@ -1396,25 +1418,34 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1396
1418
  }
1397
1419
 
1398
1420
  async function send(entry, context, input, origin = "user", goal, representation, wakeOptions) {
1399
- await access(context, entry.id, goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send", entry);
1400
- if (entry.conversation?.commands) {
1401
- if (origin !== "user" || goal) throw failure("This conversation supports its original application Send operation.", "conversation_unsupported", 400);
1402
- return entry.conversation.commands.send(input, context);
1403
- }
1404
- if (entry.conversation?.native?.scoped) return entry.provider.sendNative({ input, context });
1405
- const originalInput = representation === "native" ? input : undefined;
1406
- const message = originalInput ? { ...input, text: input.displayMessage ?? input.message ?? input.text ?? "" } : input;
1407
- const admission = await serial(entry, () => admitSend(entry, context, message, origin, goal, originalInput, wakeOptions));
1408
- const result = await (admission.receipt || admission.promise);
1409
- if (!entry.conversation) return result;
1410
- if (representation === "native") return result.nativeResult;
1411
- if (result.error) throw result.error;
1412
- return result.receipt;
1421
+ const deliveryTiming = createConversationDeliveryTiming({ engine: entry.engine,
1422
+ conversationId: entry.id, messageId: input?.messageId, report: onDeliveryTiming });
1423
+ deliveryTiming("received");
1424
+ try {
1425
+ await access(context, entry.id, goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send", entry);
1426
+ if (entry.conversation?.commands) {
1427
+ if (origin !== "user" || goal) throw failure("This conversation supports its original application Send operation.", "conversation_unsupported", 400);
1428
+ return entry.conversation.commands.send(input, context);
1429
+ }
1430
+ if (entry.conversation?.native?.scoped) return entry.provider.sendNative({ input, context });
1431
+ const originalInput = representation === "native" ? input : undefined;
1432
+ const message = originalInput ? { ...input, text: input.displayMessage ?? input.message ?? input.text ?? "" } : input;
1433
+ const admission = await serial(entry, () => admitSend(entry, context, message, origin, goal, originalInput, wakeOptions, deliveryTiming));
1434
+ const result = await (admission.receipt || admission.promise);
1435
+ if (!entry.conversation) return result;
1436
+ if (representation === "native") return result.nativeResult;
1437
+ if (result.error) throw result.error;
1438
+ return result.receipt;
1439
+ } catch (error) {
1440
+ deliveryTiming("failed");
1441
+ throw error;
1442
+ }
1413
1443
  }
1414
1444
 
1415
1445
  // Called while holding the conversation queue, including Claude's existing
1416
1446
  // stop-then-Send goal commands. All commands retain the ordinary receipt path.
1417
- async function admitSend(entry, context, input, origin = "user", goal, originalInput, wakeOptions) {
1447
+ async function admitSend(entry, context, input, origin = "user", goal, originalInput, wakeOptions, deliveryTiming) {
1448
+ deliveryTiming?.("preparation-started");
1418
1449
  const operation = goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send";
1419
1450
  await access(context, entry.id, operation, entry);
1420
1451
  await entry.conversation?.admission(context, operation);
@@ -1484,7 +1515,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1484
1515
  // No native dispatch has occurred for this request. Finish the previous
1485
1516
  // waiter before reserving the same authored request on the ordinary path.
1486
1517
  await working.done;
1487
- return admitSend(entry, context, input, origin, goal, originalInput, wakeOptions);
1518
+ return admitSend(entry, context, input, origin, goal, originalInput, wakeOptions, deliveryTiming);
1488
1519
  }
1489
1520
  const nativeSteering = !working && (nativeState.nativeTurn?.active || entry.driver.canonicalTranscript && nativeState.binding?.codexAppServerRun &&
1490
1521
  codexAppServerTurnStateFromAgentRun(nativeState.binding.codexAppServerRun).active);
@@ -1537,7 +1568,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
1537
1568
  await transaction.writeMetadata(metadata);
1538
1569
  return metadata.runtime.configuration;
1539
1570
  }); } catch (error) { release(); throw error; }
1540
- const request = { input: message, originalInput, operation, context, at: requestAt,
1571
+ const request = { input: message, originalInput, operation, context, at: requestAt, deliveryTiming,
1541
1572
  ...(markedResponse ? { completedEnvelope: true, excludedMessageIds } : {}),
1542
1573
  ...(typeof nativeToolAttempt === "boolean" ? { nativeToolAttempt } : {}), steering: Boolean(working || input.steer && nativeSteering),
1543
1574
  admission: Promise.withResolvers(), progress: new Map() };
@@ -746,7 +746,7 @@ test("Claude verified native cleanup retains interrupted checkpoint refusal with
746
746
 
747
747
  // Retain the original supplied-store/native process fixture and drive it through
748
748
  // the actual common runtime, including its completion-only admitted receipt.
749
- async function suppliedCommonFixture(t, { tools = true } = {}) {
749
+ async function suppliedCommonFixture(t, { tools = true, onDeliveryTiming } = {}) {
750
750
  const f = await publishedClaudeFixture(t, { canonicalAdmission: true });
751
751
  const storage = createMemoryConversationStorage();
752
752
  const transcript = createConversationTranscript({ storage });
@@ -786,7 +786,7 @@ async function suppliedCommonFixture(t, { tools = true } = {}) {
786
786
  ...(tools ? { applicationTools: { storage, prepareContext(actual, admission) {
787
787
  admissions.push(admission); return actual;
788
788
  } } } : {}) };
789
- const runtime = createConversationRuntime({ ...(tools ? { actions } : {}), authorize: () => true,
789
+ const runtime = createConversationRuntime({ ...(tools ? { actions } : {}), authorize: () => true, onDeliveryTiming,
790
790
  host: { conversation: () => binding } });
791
791
  t.after(() => runtime.close());
792
792
  const conversation = await runtime.open({ id: "lesson", context });
@@ -822,6 +822,25 @@ async function sendCommon(f, messageId) {
822
822
  return receipt;
823
823
  }
824
824
 
825
+ test("Claude delivery timing separates native acknowledgement from the held canonical receipt", { timeout: 10_000 }, async t => {
826
+ const timings = [];
827
+ const f = await suppliedCommonFixture(t, { onDeliveryTiming: event => timings.push(event) });
828
+ const writing = Promise.withResolvers();
829
+ const release = Promise.withResolvers();
830
+ f.behavior.beforeAdmit = async () => { writing.resolve(); await release.promise; };
831
+ const sending = f.conversation.send({ messageId: "timed-request", text: "Private message" });
832
+ await writing.promise;
833
+ assert.deepEqual(timings.map(event => event.stage), [
834
+ "received", "preparation-started", "provider-started", "runtime-ready", "dispatch-started", "native-acknowledged"
835
+ ]);
836
+ release.resolve();
837
+ assert.equal((await sending).status, "accepted");
838
+ assert.deepEqual(timings.slice(-2).map(event => event.stage), ["receipt-persisted", "accepted"]);
839
+ await f.frame({ type: "result", subtype: "success", result: "Public answer" });
840
+ await f.conversation.wait();
841
+ assert.equal(JSON.stringify(timings).includes("Private message"), false);
842
+ });
843
+
825
844
  for (const tools of [true, false]) test(`Claude supplied common runtime checkpoints after its completion fence with tools=${tools}`, { timeout: 10_000 }, async t => {
826
845
  const f = await suppliedCommonFixture(t, { tools });
827
846
  const receipt = await sendCommon(f, "common-question");
@@ -1214,6 +1214,7 @@ async function fixture(t, options = {}) {
1214
1214
  TEST_HISTORY: path.join(directory, "history.json"), ...options.environment };
1215
1215
  function runtime({ nativeTools = options.nativeTools } = {}) {
1216
1216
  const value = createConversationRuntime({ engine: "codex", storage, authorize: options.authorize || (() => true),
1217
+ onDeliveryTiming: options.onDeliveryTiming,
1217
1218
  actions: options.actions, connections: options.connections, fetch: options.fetch, attachments: options.attachments,
1218
1219
  host: { workdir: directory, runtimeDirectory: directory, env, commands: { codex: command }, execution, nativeTools, commandWrapper: options.commandWrapper },
1219
1220
  limits: { admissionTimeoutMs: 2000, ...options.limits } });
@@ -1235,6 +1236,18 @@ async function fixture(t, options = {}) {
1235
1236
  async trace() { return (await readFile(trace, "utf8")).trim().split("\n").map(line => JSON.parse(line)); } };
1236
1237
  }
1237
1238
 
1239
+ test("Codex delivery timing uses the common native receipt and first-response stages", async t => {
1240
+ const timings = [];
1241
+ const f = await fixture(t, { onDeliveryTiming: event => timings.push(event) });
1242
+ assert.equal((await f.conversation.send({ messageId: "timed-request", text: "Private message" })).status, "accepted");
1243
+ await f.conversation.wait();
1244
+ const stages = timings.map(event => event.stage);
1245
+ assert.deepEqual(stages.slice(0, 8), ["received", "preparation-started", "provider-started", "runtime-ready",
1246
+ "dispatch-started", "native-acknowledged", "receipt-persisted", "accepted"]);
1247
+ assert.equal(stages.filter(stage => stage === "first-response").length, 1);
1248
+ assert.equal(JSON.stringify(timings).includes("Private message"), false);
1249
+ });
1250
+
1238
1251
  test("Codex admits, streams, persists reasoning and deduplicates using native receipts", async t => {
1239
1252
  const f = await fixture(t);
1240
1253
  const events = [];
@@ -1079,10 +1079,13 @@ test("original receipt owner persists authored metadata before releasing the rec
1079
1079
  f.run = { ...f.run, state: "starting", providerTurnId: "", pendingUserMessageClientIds: [messageId] };
1080
1080
  const receipt = Promise.withResolvers();
1081
1081
  let received = false;
1082
+ const deliveryTimings = [];
1082
1083
  receipt.promise.then(() => { received = true; f.operations.push("receipt"); });
1083
1084
  f.owner.pendingUserMessages.set(`project:${f.sessionId}\0${messageId}`, {
1084
1085
  attachments: [{ id: "attachment-1", name: "Diagram" }], actorContext,
1085
1086
  turnMetadata: { actorId: "saved-actor", label: "authored" }, receipt,
1087
+ onNativeReceipt: () => deliveryTimings.push("native-acknowledged"),
1088
+ onReceiptPersisted: () => deliveryTimings.push("receipt-persisted"),
1086
1089
  text: "Review the diagram."
1087
1090
  });
1088
1091
  f.observe();
@@ -1090,11 +1093,13 @@ test("original receipt owner persists authored metadata before releasing the rec
1090
1093
  f.emit({ method: "item/completed", params: { threadId: f.threadId, turnId: f.turnId,
1091
1094
  item: outputItem("early-commentary", "I am reviewing the diagram.", "commentary") } });
1092
1095
  await decorating.promise;
1096
+ assert.deepEqual(deliveryTimings, ["native-acknowledged"], "Native acknowledgement precedes the held canonical write");
1093
1097
  assert.equal(received, false);
1094
1098
  assert.equal(await f.store.conversationMessageIdExists(f.sessionId, messageId), false);
1095
1099
  assert.equal(f.publications.some(event => event.payload?.conversationLogPatch?.turn?.commentary), false);
1096
1100
  release.resolve();
1097
1101
  await f.drain();
1102
+ assert.deepEqual(deliveryTimings, ["native-acknowledged", "receipt-persisted"]);
1098
1103
  assert.deepEqual(await receipt.promise, { id: f.turnId });
1099
1104
  const turns = await f.store.readConversationLog(f.sessionId);
1100
1105
  const authored = turns.find(row => row.user?.messageId === messageId);
@@ -0,0 +1,50 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createConversationDeliveryTiming } from "../src/server/conversation/deliveryTiming.js";
4
+
5
+ test("native engines share bounded, content-free timing and independent message identities", () => {
6
+ for (const engine of ["codex", "claude", "opencode"]) {
7
+ let now = 0;
8
+ const events = [];
9
+ const mark = createConversationDeliveryTiming({ engine, conversationId: "chat", messageId: "one",
10
+ report: event => events.push(event), clock: () => now });
11
+ mark("received");
12
+ now = 3; mark("dispatch-started");
13
+ now = 1003; mark("native-acknowledged");
14
+ now = 1013; mark("receipt-persisted");
15
+ now = 1015; mark("accepted");
16
+ now = 1020; mark("first-response");
17
+ mark("first-response");
18
+ mark("arbitrary-provider-payload");
19
+ assert.equal(events.length, 6);
20
+ assert.equal(events[2].durationMs, 1000);
21
+ assert.equal(events[3].durationMs, 10);
22
+ assert.deepEqual(events[4].timings, {
23
+ received: 0, "dispatch-started": 3, "native-acknowledged": 1003, "receipt-persisted": 1013, accepted: 1015
24
+ });
25
+ assert.equal(events[0].timings.accepted, undefined, "Earlier reports are snapshots");
26
+ for (const event of events) assert.deepEqual(Object.keys(event).sort(),
27
+ ["engine", "conversationId", "messageId", "stage", "previousStage", "durationMs", "elapsedMs", "timings"].sort());
28
+ const next = createConversationDeliveryTiming({ engine, conversationId: "chat", messageId: "two",
29
+ report: event => events.push(event), clock: () => now });
30
+ next("received");
31
+ assert.equal(events.at(-1).messageId, "two");
32
+ assert.equal(events.at(-1).elapsedMs, 0);
33
+ }
34
+ });
35
+
36
+ test("diagnostic failures and unfinished asynchronous observers never block delivery", async () => {
37
+ const pending = Promise.withResolvers();
38
+ let calls = 0;
39
+ const mark = createConversationDeliveryTiming({ engine: "claude", conversationId: "chat", messageId: "one",
40
+ report() {
41
+ calls++;
42
+ if (calls === 1) throw new Error("Observer unavailable");
43
+ if (calls === 2) return Promise.reject(new Error("Observer rejected"));
44
+ return pending.promise;
45
+ } });
46
+ mark("received"); mark("dispatch-started"); mark("native-acknowledged"); mark("receipt-persisted");
47
+ assert.equal(calls, 4);
48
+ await Promise.resolve();
49
+ pending.resolve();
50
+ });
@@ -227,6 +227,7 @@ async function fixture(t, options = {}) {
227
227
  });
228
228
  const runtime = () => {
229
229
  const value = createConversationRuntime({ engine: "opencode", storage, authorize: options.authorize || (() => true), actions: options.actions, attachments: options.attachments,
230
+ onDeliveryTiming: options.onDeliveryTiming,
230
231
  connections: { resolve: options.resolve || (async () => ({ providerId: "test", model: "test-model", sdkPackage: "@ai-sdk/openai-compatible",
231
232
  modelLimits: { context: 32000, output: 4000 }, apiKey: key })) },
232
233
  host: { workdir: directory, env: { ...process.env, UNRELATED_SECRET: "must-not-inherit" },
@@ -249,6 +250,19 @@ async function fixture(t, options = {}) {
249
250
  async trace() { return (await readFile(trace, "utf8")).trim().split("\n").map(line => JSON.parse(line)); } };
250
251
  }
251
252
 
253
+ test("OpenCode delivery timing uses the common receipt stages without recording authored content", async t => {
254
+ const timings = [];
255
+ const f = await fixture(t, { onDeliveryTiming: event => timings.push(event) });
256
+ const receipt = await f.conversation.send({ messageId: "timed-request", text: "Private message" });
257
+ assert.equal(receipt.status, "accepted");
258
+ await f.conversation.wait();
259
+ const stages = timings.map(event => event.stage);
260
+ assert.deepEqual(stages.slice(0, 8), ["received", "preparation-started", "provider-started", "runtime-ready", "dispatch-started",
261
+ "native-acknowledged", "receipt-persisted", "accepted"]);
262
+ assert.equal(stages.filter(stage => stage === "first-response").length, 1);
263
+ assert.equal(JSON.stringify(timings).includes("Private message"), false);
264
+ });
265
+
252
266
  test("OpenCode structured output shares the original prompt and completed-result normalization across reopen", async t => {
253
267
  const outputSchema = { type: "object", additionalProperties: false,
254
268
  properties: { answer: { type: "string", maxLength: 32 } }, required: ["answer"] };