@jskit-ai/assistant-core 0.1.212 → 0.1.214
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/ConversationFixture.vue +7 -1
- package/package.json +5 -5
- package/src/client/conversation/AssistantConversationElement.vue +5 -1
- package/src/client/conversation/AssistantTranscript.vue +10 -1
- package/src/server/conversation/agentRun.js +38 -11
- package/src/server/conversation/claudeHistory.js +40 -5
- package/src/server/conversation/claudeProcess.js +28 -4
- package/src/server/conversation/claudeTools.js +11 -2
- package/src/server/conversation/claudeTurn.js +213 -23
- package/src/server/conversation/codexConfiguration.js +1 -1
- package/src/server/conversation/codexHistoryAdapter.js +11 -4
- package/src/server/conversation/codexNotificationQueue.js +11 -2
- package/src/server/conversation/codexObservationControl.js +8 -8
- package/src/server/conversation/codexOutput.js +20 -15
- package/src/server/conversation/codexProvider.js +40 -3
- package/src/server/conversation/codexProviderOwner.js +110 -1
- package/src/server/conversation/codexTurn.js +3 -0
- package/src/server/conversation/continuity.js +24 -6
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/nativeHistoryExport.js +97 -0
- package/src/server/conversation/openCodeClient.js +47 -1
- package/src/server/conversation/openCodeRuntime.js +183 -28
- package/src/server/conversation/openCodeTurn.js +65 -24
- package/src/server/conversation/providers/api.js +7 -0
- package/src/server/conversation/providers/claudeDriver.js +52 -7
- package/src/server/conversation/providers/codex.js +19 -2
- package/src/server/conversation/providers/codexDriver.js +92 -14
- package/src/server/conversation/providers/opencodeDriver.js +31 -9
- package/src/server/conversation/runtime.js +424 -65
- package/src/server/conversation/runtimeStateUpgrade.js +77 -2
- package/src/server/conversation/streams.js +25 -5
- package/src/server/conversation/tools.js +2 -2
- package/src/server/conversation/transcript.js +23 -8
- package/src/server/hosts/claudeHistory.js +1 -0
- package/src/server/hosts/claudeProcess.js +1 -0
- package/src/server/hosts/codexProvider.js +3 -0
- package/src/server/hosts/nativeHistory.js +1 -0
- package/src/server/hosts/openCodeClient.js +1 -0
- package/src/server/hosts/openCodeTurn.js +1 -0
- package/src/server/index.js +1 -1
- package/src/server/lib/assistantToolLoop.js +99 -2
- package/test/aiConnectionClient.test.js +57 -0
- package/test/assistantBoundedToolLoop.test.js +332 -0
- package/test/assistantScroll.browser.test.js +151 -0
- package/test/claudeConversationSession.test.js +1417 -3
- package/test/claudeProcess.test.js +175 -0
- package/test/claudeTurn.test.js +466 -0
- package/test/codexConversationSession.test.js +1257 -12
- package/test/codexHistoryAdapter.test.js +74 -1
- package/test/codexTurn.test.js +282 -1
- package/test/conversation.test.js +83 -0
- package/test/conversationAgentRun.test.js +181 -0
- package/test/conversationRuntime.test.js +95 -0
- package/test/conversationRuntimeStateUpgrade.test.js +89 -0
- package/test/conversationStreaming.test.js +85 -0
- package/test/conversationTools.test.js +165 -1
- package/test/openCodeClient.test.js +186 -0
- package/test/openCodeRuntime.test.js +372 -0
- package/test/opencodeConversationSession.test.js +637 -5
|
@@ -5,18 +5,20 @@ import { runScopedConversationTurn, readPersistentConversation, startPersistentC
|
|
|
5
5
|
inspectPersistentConversationDelivery, stopPersistentConversation, deletePersistentConversation } from "./providers/scoped.js";
|
|
6
6
|
import { isDeepStrictEqual } from "node:util";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { createConversationTranscript } from "./transcript.js";
|
|
8
|
+
import { createConversationTranscript, isCompletedEnvelopeTurn } from "./transcript.js";
|
|
9
9
|
import { createConversationStreams } from "./streams.js";
|
|
10
10
|
import { createReentrantConversationStorage } from "./storage.js";
|
|
11
11
|
import { createCodexConversationStore } from "./agentRun.js";
|
|
12
12
|
import { codexAppServerTurnStateFromAgentRun } from "./codexTurn.js";
|
|
13
|
-
import { hasUnfinishedConversationRewind
|
|
13
|
+
import { hasUnfinishedConversationRewind, createConversationRuntimeReplacement,
|
|
14
|
+
releaseConversationRuntimeBinding, finishConversationRuntimeReplacement } from "./runtimeStateUpgrade.js";
|
|
14
15
|
import { createServiceToolCatalog } from "../lib/serviceToolCatalog.js";
|
|
15
16
|
import { createConversationTools } from "./tools.js";
|
|
17
|
+
import { readAssistantResponseEnvelope } from "../lib/assistantToolLoop.js";
|
|
16
18
|
import { conversationAttachmentIds, createConversationAttachmentReader } from "./attachments.js";
|
|
17
19
|
import { conversationAttachmentManifest } from "../../shared/conversation/attachments.js";
|
|
18
20
|
import { createConversationChangeover, conversationContinuity, conversationHistoryVersions,
|
|
19
|
-
conversationNativeMessages, conversationRequestText } from "./continuity.js";
|
|
21
|
+
conversationMessageIdentity, conversationMessageVersion, conversationNativeMessages, conversationRequestText } from "./continuity.js";
|
|
20
22
|
|
|
21
23
|
function failure(message, code, statusCode = 409) {
|
|
22
24
|
return Object.assign(new Error(message), { code, statusCode });
|
|
@@ -34,8 +36,10 @@ const sameRequest = (left, right) => left.text === right.text && (left.origin ||
|
|
|
34
36
|
|
|
35
37
|
/** One application conversation API. Storage and authorization remain host facilities. */
|
|
36
38
|
export function createConversationRuntime({ engine: defaultEngine = "api", defaultIntegrationId, storage, authorize, connections, apiClientFactory, apiHistory,
|
|
37
|
-
actions, toolPolicy, toolCatalog, attachments, fetch, host: defaultHost, limits = {} } = {}) {
|
|
39
|
+
actions, toolPolicy, toolCatalog, attachments, fetch, host: defaultHost, persistCommentary = true, completedEnvelope = false, limits = {} } = {}) {
|
|
38
40
|
if (typeof authorize !== "function") throw new TypeError("Conversation access requires an authorize function.");
|
|
41
|
+
if (typeof completedEnvelope !== "boolean") throw new TypeError("Completed envelope policy requires a server-side boolean.");
|
|
42
|
+
if (typeof persistCommentary !== "boolean") throw new TypeError("Conversation commentary persistence requires an explicit server-side boolean.");
|
|
39
43
|
if (apiHistory !== undefined && typeof apiHistory !== "function") throw new TypeError("API request history requires a server-owned selector.");
|
|
40
44
|
if (toolCatalog && (actions || toolPolicy)) throw new TypeError("Supply the existing tool catalog or actions and toolPolicy, not both.");
|
|
41
45
|
storage = (storage || !defaultHost?.conversation) ? createReentrantConversationStorage(storage) : null;
|
|
@@ -46,6 +50,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
46
50
|
const providers = createConversationProviderFactory({ connections, apiClientFactory, fetch, limits, actions, toolCatalog });
|
|
47
51
|
const { createDriver } = providers;
|
|
48
52
|
const catalog = toolCatalog || (actions ? createServiceToolCatalog(actions, { ...limits, isActionAvailable: toolPolicy }) : null);
|
|
53
|
+
if (completedEnvelope && !catalog) throw new TypeError("Completed envelopes require an application tool catalogue.");
|
|
49
54
|
const maximumToolCalls = limits.maxToolCalls ?? 32;
|
|
50
55
|
if (!Number.isSafeInteger(maximumToolCalls) || maximumToolCalls < 1) throw new TypeError("Invalid application tool-call limit.");
|
|
51
56
|
const maximumInput = limits.maxInputCharacters ?? 32_000;
|
|
@@ -54,6 +59,10 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
54
59
|
if (limits.maxFinalReplyCharacters !== undefined && (!Number.isSafeInteger(maximumFinalReply) || maximumFinalReply < 1)) {
|
|
55
60
|
throw new TypeError("Invalid conversation final reply limit.");
|
|
56
61
|
}
|
|
62
|
+
const maximumCompletedOutput = limits.maxOutputCharacters ?? 64_000;
|
|
63
|
+
if (completedEnvelope && (!Number.isSafeInteger(maximumCompletedOutput) || maximumCompletedOutput < 1)) {
|
|
64
|
+
throw new TypeError("Invalid completed envelope output limit.");
|
|
65
|
+
}
|
|
57
66
|
const maximumContinuity = limits.maxContinuityCharacters ?? 128_000;
|
|
58
67
|
if (!Number.isSafeInteger(maximumContinuity) || maximumContinuity < 1) throw new TypeError("Invalid conversation continuity limit.");
|
|
59
68
|
const maximumAttachmentBytes = limits.maxAttachmentBytes ?? 8 * 1024 * 1024;
|
|
@@ -86,12 +95,28 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
86
95
|
return operation;
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
async function emit(entry, event) {
|
|
98
|
+
async function emit(entry, event, isCurrent) {
|
|
99
|
+
const { completedEnvelope: _untrustedMarker, ...payload } = event;
|
|
100
|
+
if (completedEnvelope && !entry.conversation) {
|
|
101
|
+
const request = payload.messageId && entry.active?.requests.get(payload.messageId) ||
|
|
102
|
+
payload.turnId && [...(entry.active?.requests.values() || [])].find(request => request.turnId === payload.turnId);
|
|
103
|
+
let marked = isCompletedEnvelopeTurn(payload.patch?.turn) ||
|
|
104
|
+
request?.completedEnvelope === true && request.input.origin === "application";
|
|
105
|
+
if (!marked && payload.turnId) {
|
|
106
|
+
try { marked = await storage.read(entry.id, async transaction => isCompletedEnvelopeTurn(await transaction.readTurn(payload.turnId))); }
|
|
107
|
+
catch { /* An unavailable receipt cannot establish presentation custody. */ }
|
|
108
|
+
}
|
|
109
|
+
if (!marked && payload.type === "message") marked =
|
|
110
|
+
[...(payload.streaming?.messages || []), ...(payload.streaming?.completedMessages || [])]
|
|
111
|
+
.some(message => message.messageId === payload.messageId && message.completedEnvelope === true && message.origin === "application");
|
|
112
|
+
if (marked) payload.completedEnvelope = true;
|
|
113
|
+
}
|
|
90
114
|
await Promise.all([...entry.listeners].map(async subscription => {
|
|
91
115
|
try { await access(subscription.context, entry.id, "subscribe", entry); }
|
|
92
116
|
catch { entry.listeners.delete(subscription); return; }
|
|
117
|
+
if (isCurrent && !isCurrent()) return;
|
|
93
118
|
// Presentation failures cannot interrupt persistence or provider work.
|
|
94
|
-
try { Promise.resolve(subscription.listener(structuredClone({ conversationId: entry.id, ...
|
|
119
|
+
try { Promise.resolve(subscription.listener(structuredClone({ conversationId: entry.id, ...payload }))).catch(() => {}); }
|
|
95
120
|
catch { /* A failed observer does not own the conversation. */ }
|
|
96
121
|
}));
|
|
97
122
|
}
|
|
@@ -107,7 +132,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
107
132
|
? entry.conversation.transcript.readConversationLog() : transcript.readConversationLog(entry.id);
|
|
108
133
|
|
|
109
134
|
function readTranscriptPage(entry, query) {
|
|
110
|
-
if (!entry.conversation) return transcript.readConversationLogPage(entry.id, query);
|
|
135
|
+
if (!entry.conversation) return transcript.readConversationLogPage(entry.id, { ...query, presentation: completedEnvelope });
|
|
111
136
|
if (typeof entry.conversation.transcript.readConversationLogPage !== "function") {
|
|
112
137
|
throw failure("This conversation does not supply paged history.", "conversation_unsupported", 400);
|
|
113
138
|
}
|
|
@@ -150,16 +175,20 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
150
175
|
if (representation === "native") return metadata.runtime.nativeResult;
|
|
151
176
|
const page = logical ? logical.pagination ? { conversationLog: logical.conversationLog, pagination: logical.pagination } : null
|
|
152
177
|
: query ? await readTranscriptPage(entry, query) : null;
|
|
153
|
-
const conversationLog = logical ? logical.conversationLog : page ? page.conversationLog
|
|
178
|
+
const conversationLog = logical ? logical.conversationLog : page ? page.conversationLog
|
|
179
|
+
: completedEnvelope ? await transcript.readConversationLog(entry.id, { presentation: true }) : await readTranscript(entry);
|
|
154
180
|
// Browsing an older page does not change the current turn's control/error state.
|
|
155
|
-
const latest = !logical && query?.beforeTurnId
|
|
181
|
+
const latest = !logical && (completedEnvelope || query?.beforeTurnId)
|
|
182
|
+
? entry.conversation ? (await readTranscriptPage(entry, { limit: 1 })).conversationLog
|
|
183
|
+
: (await transcript.readConversationLogPage(entry.id, { limit: 1 })).conversationLog : conversationLog;
|
|
156
184
|
const last = latest.at(-1)?.metadata?.runtime;
|
|
157
185
|
const savedRequest = metadata.runtime.request;
|
|
158
186
|
const nativeTurn = metadata.runtime.nativeTurn || (entry.driver.canonicalTranscript && metadata.runtime.binding?.codexAppServerRun
|
|
159
187
|
? codexAppServerTurnStateFromAgentRun(metadata.runtime.binding.codexAppServerRun) : null);
|
|
160
188
|
const request = savedRequest && (entry.engine === "api" || savedRequest.attempted || savedRequest.inspectionOnly ||
|
|
161
189
|
entry.active?.requests.has(savedRequest.messageId)) ? savedRequest : null;
|
|
162
|
-
const
|
|
190
|
+
const streaming = entry.conversation ? await entry.conversation.readStream() : streams.read(entry.id);
|
|
191
|
+
const pendingRequest = request && !(completedEnvelope && request.origin === "application" && request.completedEnvelope === true) ? Object.fromEntries([
|
|
163
192
|
"messageId", "text", "origin", "data", "attachments", "at", "goal", "steering", "error"
|
|
164
193
|
].filter(name => Object.hasOwn(request, name)).map(name => [name, request[name]])) : null;
|
|
165
194
|
return {
|
|
@@ -176,13 +205,19 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
176
205
|
...(logical?.presentation ? { presentation: logical.presentation } : {}),
|
|
177
206
|
conversationLog: conversationLog.filter(turn => !turn.metadata?.runtime?.supersededBy),
|
|
178
207
|
...(page ? { pagination: page.pagination } : {}),
|
|
179
|
-
streaming: entry.conversation ?
|
|
208
|
+
streaming: completedEnvelope && !entry.conversation ? { ...streaming,
|
|
209
|
+
messages: streaming.messages.filter(message => !(message.origin === "application" && message.completedEnvelope === true)) } : streaming
|
|
180
210
|
};
|
|
181
211
|
}
|
|
182
212
|
|
|
183
213
|
async function publish(entry, event) {
|
|
184
214
|
if (entry.disposed) return;
|
|
185
|
-
|
|
215
|
+
const publicationOwner = entry.bindingOwner;
|
|
216
|
+
const publicationSegment = entry.segmentId;
|
|
217
|
+
const publicationActive = entry.active;
|
|
218
|
+
// A native-backed bound conversation also uses configuration as a browser
|
|
219
|
+
// invalidation. It has no command facade, but owns the same subscriptions.
|
|
220
|
+
if (entry.conversation && (event.type === "configuration" || entry.conversation.commands && event.type === "phase")) {
|
|
186
221
|
return emit(entry, event);
|
|
187
222
|
}
|
|
188
223
|
const payload = event.payload || {};
|
|
@@ -211,6 +246,19 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
211
246
|
}
|
|
212
247
|
if (payload.conversationStream) {
|
|
213
248
|
const snapshot = payload.conversationStream;
|
|
249
|
+
for (const message of snapshot.completedMessages || []) {
|
|
250
|
+
if (turn?.messages?.some(saved => saved.messageId === message.messageId ||
|
|
251
|
+
(message.outputId && saved.outputId === message.outputId))) continue;
|
|
252
|
+
const request = publicationActive && [...publicationActive.requests.values()].find(request => request.turnId === message.turnId);
|
|
253
|
+
const isCurrent = () => !entry.disposed && !request?.sealed && entry.active === publicationActive &&
|
|
254
|
+
entry.bindingOwner === publicationOwner && entry.segmentId === publicationSegment;
|
|
255
|
+
if (!request || !isCurrent()) continue;
|
|
256
|
+
if (!request.admissionPublished) {
|
|
257
|
+
request.progress.set(message.messageId, message);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
await emit(entry, { type: "message", ...message, streaming: snapshot }, isCurrent);
|
|
261
|
+
}
|
|
214
262
|
for (const message of snapshot.messages) {
|
|
215
263
|
await emit(entry, { type: "message", ...message, streaming: snapshot });
|
|
216
264
|
}
|
|
@@ -240,6 +288,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
240
288
|
conversation = {
|
|
241
289
|
...entry.conversation,
|
|
242
290
|
sessionId: entry.id,
|
|
291
|
+
persistCommentary,
|
|
243
292
|
runtime: entry.conversation?.runtime || { store, getSession: store.getSession },
|
|
244
293
|
publish: event => isCurrent() ? publish(entry, event) : undefined,
|
|
245
294
|
checkpoint: entry.conversation?.checkpoint || (input => isCurrent() ? entry.active?.nativeCheckpoint?.(input) : null)
|
|
@@ -339,9 +388,16 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
339
388
|
|
|
340
389
|
function nativeChangeover(entry, context, request, sendMessage) {
|
|
341
390
|
let observation;
|
|
391
|
+
const initialWindow = limits.maxInitialNativeHistoryMessages;
|
|
392
|
+
if (initialWindow !== undefined && (!Number.isSafeInteger(initialWindow) || initialWindow < 0)) {
|
|
393
|
+
throw new TypeError("Initial native history requires a non-negative message limit.");
|
|
394
|
+
}
|
|
342
395
|
return createConversationChangeover({
|
|
343
396
|
state: entry.conversation?.state || changeoverState(entry, request),
|
|
344
397
|
captureContext: !entry.conversation, applicationMessages: !entry.conversation,
|
|
398
|
+
maximumInitialMessages: initialWindow === undefined ? undefined
|
|
399
|
+
: Math.max(0, initialWindow - (request?.input.origin === "user" ? 1 : 0)),
|
|
400
|
+
maximumInitialMessageCharacters: limits.maxInitialNativeHistoryMessageCharacters,
|
|
345
401
|
presentation: entry.conversation?.presentation || { unconfirmedCode: "conversation_delivery_uncertain",
|
|
346
402
|
unconfirmedMessage: "The previous submission has uncertain delivery. Retry this message to check its native receipt; it will not be resent automatically." },
|
|
347
403
|
transcript: entry.conversation?.transcript || {
|
|
@@ -397,6 +453,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
397
453
|
}
|
|
398
454
|
const metadata = await transaction.readMetadata();
|
|
399
455
|
entry.engine = metadata.runtime?.engine || requestedEngine || defaultEngine;
|
|
456
|
+
if (completedEnvelope && !["codex", "claude", "opencode"].includes(entry.engine)) {
|
|
457
|
+
throw failure("Completed envelopes require a runtime-owned native conversation.", "conversation_unsupported", 400);
|
|
458
|
+
}
|
|
400
459
|
if (configuration) configuration = normalizeConversationConfiguration(configuration, { engine: entry.engine, defaultIntegrationId });
|
|
401
460
|
const driver = entry.driver = createDriver(entry.engine, entry.host);
|
|
402
461
|
if (metadata.runtime) {
|
|
@@ -448,8 +507,207 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
448
507
|
return { storage: createReentrantConversationStorage(facilities.storage), prepareContext: facilities.prepareContext };
|
|
449
508
|
}
|
|
450
509
|
|
|
510
|
+
async function saveTools(entry, transaction, request, calls) {
|
|
511
|
+
if (entry.conversation) {
|
|
512
|
+
const saved = await transaction.readTurn(request.turnId);
|
|
513
|
+
if (submittedMessage(saved || {})?.messageId !== request.input.messageId) {
|
|
514
|
+
throw new Error("The application tool receipt does not belong to this authored request.");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
await transaction.updateTurnMetadata(request.turnId, { applicationTools: calls });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
async function prepareCompletedResponse(entry, context, { messageId, turnId } = {}, { signal, toolSet } = {}) {
|
|
522
|
+
if (!completedEnvelope || entry.conversation || !["codex", "claude", "opencode"].includes(entry.engine)) {
|
|
523
|
+
throw failure("This conversation does not own completed envelopes.", "conversation_unsupported", 400);
|
|
524
|
+
}
|
|
525
|
+
if (typeof messageId !== "string" || !messageId || typeof turnId !== "string" || !turnId || !(signal instanceof AbortSignal)) {
|
|
526
|
+
throw new TypeError("Completed responses require their exact receipt and the application worker signal.");
|
|
527
|
+
}
|
|
528
|
+
signal.throwIfAborted();
|
|
529
|
+
await access(context, entry.id, "tool", entry);
|
|
530
|
+
const owner = entry.bindingOwner;
|
|
531
|
+
const segmentId = entry.segmentId;
|
|
532
|
+
const engine = entry.engine;
|
|
533
|
+
const toolCallId = `${messageId}:operation`;
|
|
534
|
+
if (toolCallId.length > 256) throw new TypeError("The completed response call identity exceeds its limit.");
|
|
535
|
+
let captured;
|
|
536
|
+
|
|
537
|
+
async function exact(transaction, { retry = false } = {}) {
|
|
538
|
+
if (closed || entry.disposed || entry.bindingOwner !== owner || entry.segmentId !== segmentId || entry.engine !== engine) {
|
|
539
|
+
throw failure("The completed response owner changed.", "conversation_tool_request_retired");
|
|
540
|
+
}
|
|
541
|
+
if (entry.active) throw failure("Wait for the native response to finish.", "conversation_busy");
|
|
542
|
+
if (entry.executionFailure) throw entry.executionFailure;
|
|
543
|
+
if (entry.storageFailure && !retry) throw entry.storageFailure;
|
|
544
|
+
const { runtime } = await transaction.readMetadata();
|
|
545
|
+
const turn = await transaction.readTurn(turnId);
|
|
546
|
+
const authored = submittedMessage(turn || {});
|
|
547
|
+
const saved = turn?.metadata?.runtime;
|
|
548
|
+
if (runtime?.version !== 3 || runtime.engine !== engine || runtime.segmentId !== segmentId ||
|
|
549
|
+
runtime.replacement || runtime.request || !nativeIdentity(runtime) || authored?.messageId !== messageId ||
|
|
550
|
+
authored.origin !== "application" || saved?.completedEnvelope !== true || saved.engine !== engine ||
|
|
551
|
+
saved.segmentId !== segmentId || saved.status !== "complete" || saved.supersededBy ||
|
|
552
|
+
typeof turn.assistant?.text !== "string" || !turn.assistant.text || !turn.assistant.messageId) {
|
|
553
|
+
throw failure("The completed response has no current verified receipt.", "conversation_completed_response_unavailable");
|
|
554
|
+
}
|
|
555
|
+
const turnIds = await transaction.listTurnIds();
|
|
556
|
+
for (const id of turnIds.slice(turnIds.indexOf(turnId) + 1)) {
|
|
557
|
+
const later = await transaction.readTurn(id);
|
|
558
|
+
if (later.metadata?.runtime?.segmentId === segmentId && later.metadata.runtime.completedEnvelope === true) {
|
|
559
|
+
throw failure("A newer completed response owns this worker.", "conversation_tool_request_retired");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// These verified process references may refresh on cold inspection;
|
|
563
|
+
// all native conversation, path and account custody remains exact.
|
|
564
|
+
const { codexAppServerRun: run, executionId, processDirectory, ...binding } = runtime.binding;
|
|
565
|
+
if (engine === "codex" && (!saved.nativeTurnId || run?.outerTurnId !== messageId ||
|
|
566
|
+
run.providerThreadId !== binding.threadId || run.providerTurnId !== saved.nativeTurnId || run.active || run.state !== "completed")) {
|
|
567
|
+
throw failure("The completed response has no exact native turn receipt.", "conversation_completed_response_unavailable");
|
|
568
|
+
}
|
|
569
|
+
if (engine === "claude" && typeof saved.nativeToolAttempt !== "boolean") {
|
|
570
|
+
throw failure("The completed Claude response has no native tool-attempt evidence.", "conversation_completed_response_unavailable");
|
|
571
|
+
}
|
|
572
|
+
const calls = turn.metadata.applicationTools || [];
|
|
573
|
+
if (!Array.isArray(calls) || calls.length > 1 || calls.some(call => call.id !== toolCallId)) {
|
|
574
|
+
throw failure("This response already belongs to a different application operation.", "conversation_tool_receipt_conflict");
|
|
575
|
+
}
|
|
576
|
+
const identity = { configuration: runtime.configuration, binding, assistant: turn.assistant,
|
|
577
|
+
nativeTurnId: saved.nativeTurnId, ...(engine === "claude" ? { nativeToolAttempt: saved.nativeToolAttempt } : {}) };
|
|
578
|
+
if (captured && !isDeepStrictEqual(captured, identity)) {
|
|
579
|
+
throw failure("The completed response identity changed.", "conversation_tool_request_retired");
|
|
580
|
+
}
|
|
581
|
+
return { runtime, turn, calls, identity };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function inspect() {
|
|
585
|
+
signal.throwIfAborted();
|
|
586
|
+
await access(context, entry.id, "tool", entry);
|
|
587
|
+
const saved = await storage.read(entry.id, transaction => exact(transaction));
|
|
588
|
+
const observation = await entry.provider.inspectAdmission({ messageId,
|
|
589
|
+
threadId: nativeIdentity(saved.runtime), nativeTurnId: saved.turn.metadata.runtime.nativeTurnId,
|
|
590
|
+
configuration: saved.runtime.configuration, receiptBinding: saved.runtime.binding, context, verifyAccount: true });
|
|
591
|
+
signal.throwIfAborted();
|
|
592
|
+
await access(context, entry.id, "tool", entry);
|
|
593
|
+
if (!observation?.accepted || observation.recoveryLimitation || observation.conversationTurn &&
|
|
594
|
+
observation.conversationTurn.turnId !== turnId || engine === "codex" && observation.nativeTurnId !== saved.turn.metadata.runtime.nativeTurnId) {
|
|
595
|
+
throw failure("The native owner could not verify this response association.", "conversation_completed_response_unavailable");
|
|
596
|
+
}
|
|
597
|
+
// Canonical successful completion owns the exact final, including a
|
|
598
|
+
// StructuredOutput carrier whose native history may contain only commentary.
|
|
599
|
+
captured ||= structuredClone(saved.identity);
|
|
600
|
+
return storage.read(entry.id, transaction => exact(transaction));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const response = await inspect();
|
|
604
|
+
return { text: response.turn.assistant.text, toolCallId,
|
|
605
|
+
...(engine === "claude" ? { nativeToolAttempt: response.turn.metadata.runtime.nativeToolAttempt } : {}),
|
|
606
|
+
publishReply(input) {
|
|
607
|
+
return serial(entry, async () => {
|
|
608
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some(key => key !== "turnId") ||
|
|
609
|
+
typeof input.turnId !== "string" || !input.turnId || input.turnId !== input.turnId.trim() || input.turnId.length > 128) {
|
|
610
|
+
throw new TypeError("Reply publication requires only its exact existing plain turn id.");
|
|
611
|
+
}
|
|
612
|
+
await inspect();
|
|
613
|
+
return storage.write(entry.id, async transaction => {
|
|
614
|
+
const saved = await exact(transaction);
|
|
615
|
+
signal.throwIfAborted();
|
|
616
|
+
const decoded = readAssistantResponseEnvelope(saved.turn.assistant.text);
|
|
617
|
+
if (decoded.kind !== "reply" || saved.turn.metadata.runtime.nativeToolAttempt === true || saved.calls.length) {
|
|
618
|
+
throw failure("This completed response does not own a plain reply.", "conversation_tool_receipt_conflict");
|
|
619
|
+
}
|
|
620
|
+
const text = decoded.text.trim();
|
|
621
|
+
const target = await transaction.readTurn(input.turnId);
|
|
622
|
+
if (!text || input.turnId === turnId || !target || isCompletedEnvelopeTurn(target) ||
|
|
623
|
+
target.metadata?.runtime?.engine || !(target.user || target.system) || saved.runtime.lastEngine !== engine ||
|
|
624
|
+
!saved.runtime.seen || typeof saved.runtime.seen !== "object" || Array.isArray(saved.runtime.seen)) {
|
|
625
|
+
throw failure("The completed reply has no current plain authored target.", "conversation_tool_receipt_conflict");
|
|
626
|
+
}
|
|
627
|
+
const assistantId = conversationMessageIdentity(input.turnId, { role: "assistant" });
|
|
628
|
+
const published = saved.turn.metadata.runtime.publishedReplyTurnId;
|
|
629
|
+
if (published !== undefined) {
|
|
630
|
+
if (published !== input.turnId || !target.assistant || target.assistant.text !== text ||
|
|
631
|
+
saved.runtime.seen[assistantId] !== conversationMessageVersion(target.assistant, { includeData: true })) {
|
|
632
|
+
throw failure("The published reply target or text changed.", "conversation_tool_receipt_conflict");
|
|
633
|
+
}
|
|
634
|
+
signal.throwIfAborted();
|
|
635
|
+
return target;
|
|
636
|
+
}
|
|
637
|
+
if (target.assistant || Object.hasOwn(saved.runtime.seen, assistantId)) {
|
|
638
|
+
throw failure("This plain reply target already has unassociated content or consumption.", "conversation_tool_receipt_conflict");
|
|
639
|
+
}
|
|
640
|
+
// The runtime transcript uses this same reentrant wrapper: its
|
|
641
|
+
// original upsert joins this draft rather than starting a second write.
|
|
642
|
+
const plain = await transcript.upsertConversationAssistantMessage(entry.id, { turnId: input.turnId, text });
|
|
643
|
+
saved.runtime.seen[assistantId] = conversationMessageVersion(plain.assistant, { includeData: true });
|
|
644
|
+
await transaction.updateTurnMetadata(turnId, { runtime: { ...saved.turn.metadata.runtime, publishedReplyTurnId: input.turnId } });
|
|
645
|
+
const metadata = await transaction.readMetadata();
|
|
646
|
+
await transaction.writeMetadata({ ...metadata, runtime: saved.runtime });
|
|
647
|
+
await exact(transaction);
|
|
648
|
+
signal.throwIfAborted();
|
|
649
|
+
return plain;
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
},
|
|
653
|
+
tools: { execute(input, options = {}) {
|
|
654
|
+
return serial(entry, async () => {
|
|
655
|
+
if (input?.id !== toolCallId) throw failure("The operation has a different completed response identity.", "conversation_tool_receipt_conflict");
|
|
656
|
+
const saved = await inspect();
|
|
657
|
+
if (saved.turn.metadata.runtime.publishedReplyTurnId !== undefined) {
|
|
658
|
+
throw failure("This completed response already published its plain reply.", "conversation_tool_receipt_conflict");
|
|
659
|
+
}
|
|
660
|
+
const workerSignal = options.signal && options.signal !== signal ? AbortSignal.any([signal, options.signal]) : signal;
|
|
661
|
+
workerSignal.throwIfAborted();
|
|
662
|
+
const request = { turnId, input: { messageId } };
|
|
663
|
+
async function persist(calls, retry = false) {
|
|
664
|
+
await storage.write(entry.id, async transaction => {
|
|
665
|
+
const current = await exact(transaction, { retry });
|
|
666
|
+
const previous = current.calls[0];
|
|
667
|
+
const next = calls[0];
|
|
668
|
+
if (calls.length !== 1 || next?.id !== toolCallId || previous &&
|
|
669
|
+
(previous.name !== next.name || previous.arguments !== next.arguments ||
|
|
670
|
+
previous.status !== "running" && next.status === "running")) {
|
|
671
|
+
throw failure("The saved response operation changed.", "conversation_tool_receipt_conflict");
|
|
672
|
+
}
|
|
673
|
+
await saveTools(entry, transaction, request, calls);
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
const tools = createConversationTools({ catalog, toolSet, context, signal: workerSignal, previousCalls: saved.calls,
|
|
677
|
+
maximumCalls: maximumToolCalls, discoveryOnly: false,
|
|
678
|
+
authorize: () => access(context, entry.id, "tool", entry),
|
|
679
|
+
prepareContext: async () => {
|
|
680
|
+
await storage.read(entry.id, transaction => exact(transaction));
|
|
681
|
+
return context;
|
|
682
|
+
},
|
|
683
|
+
async save(calls) {
|
|
684
|
+
try { await persist(calls); }
|
|
685
|
+
catch (error) {
|
|
686
|
+
if (calls[0]?.status !== "running") {
|
|
687
|
+
const snapshot = structuredClone(calls);
|
|
688
|
+
entry.error = "The application operation result could not be saved. Restore storage access and retry saving.";
|
|
689
|
+
entry.storageFailure = failure(entry.error, "conversation_storage_unavailable", 503);
|
|
690
|
+
entry.storageFailure.cause = error;
|
|
691
|
+
entry.saveProgress = async () => {
|
|
692
|
+
await persist(snapshot, true);
|
|
693
|
+
entry.storageFailure = null;
|
|
694
|
+
entry.saveProgress = null;
|
|
695
|
+
entry.error = "";
|
|
696
|
+
return { type: "tool", turnId, call: snapshot[0] };
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
emit: event => emit(entry, { ...event, turnId }) });
|
|
703
|
+
return tools.execute(input, { signal: workerSignal });
|
|
704
|
+
});
|
|
705
|
+
} } };
|
|
706
|
+
}
|
|
707
|
+
|
|
451
708
|
async function perform(entry, active, configuration) {
|
|
452
709
|
const { driver, engine, segmentId } = entry;
|
|
710
|
+
const bindingOwner = entry.bindingOwner;
|
|
453
711
|
const initial = active.request;
|
|
454
712
|
let current = initial;
|
|
455
713
|
let goalOwner = initial.input.goal ? initial : null;
|
|
@@ -462,18 +720,8 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
462
720
|
const applicationTools = entry.applicationTools;
|
|
463
721
|
const toolStorage = applicationTools?.storage || storage;
|
|
464
722
|
|
|
465
|
-
async function saveTools(transaction, request, calls) {
|
|
466
|
-
if (entry.conversation) {
|
|
467
|
-
const saved = await transaction.readTurn(request.turnId);
|
|
468
|
-
if (submittedMessage(saved || {})?.messageId !== request.input.messageId) {
|
|
469
|
-
throw new Error("The application tool receipt does not belong to this authored request.");
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
await transaction.updateTurnMetadata(request.turnId, { applicationTools: calls });
|
|
473
|
-
}
|
|
474
|
-
|
|
475
723
|
function prepareTools(request) {
|
|
476
|
-
if (!catalog || entry.conversation && !applicationTools) return;
|
|
724
|
+
if (request.completedEnvelope || !catalog || entry.conversation && !applicationTools) return;
|
|
477
725
|
request.tools = createConversationTools({ catalog, context: request.context, signal: active.controller.signal,
|
|
478
726
|
...(applicationTools ? { prepareContext() {
|
|
479
727
|
if (!request.accepted) throw new Error("The application tool does not belong to an admitted message.");
|
|
@@ -485,13 +733,22 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
485
733
|
if (entry.active !== active || current !== request || !request.accepted || request.finished) {
|
|
486
734
|
throw failure("This application tool's admitted message is no longer current.", "conversation_tool_request_retired");
|
|
487
735
|
}
|
|
488
|
-
}
|
|
736
|
+
},
|
|
737
|
+
...(entry.conversation ? { assertCompletionCurrent() {
|
|
738
|
+
active.controller.signal.throwIfAborted();
|
|
739
|
+
if (closed || entry.disposed || entry.bindingOwner !== bindingOwner || entry.segmentId !== segmentId ||
|
|
740
|
+
!active.finishing || active.timedOut || problem || toolFailure || entry.executionFailure || entry.storageFailure ||
|
|
741
|
+
current !== request || !request.accepted || entry.lastAcceptedRequest !== request ||
|
|
742
|
+
entry.active && entry.active !== active) {
|
|
743
|
+
throw failure("This native completion's admitted message is no longer current.", "conversation_tool_request_retired");
|
|
744
|
+
}
|
|
745
|
+
} } : {}) }));
|
|
489
746
|
} } : {}),
|
|
490
747
|
maximumCalls: maximumToolCalls, discoveryOnly: driver.toolDiscovery === true,
|
|
491
748
|
authorize: () => access(request.context, entry.id, "tool", entry),
|
|
492
749
|
save: calls => {
|
|
493
750
|
if (!request.accepted) throw new Error("The engine requested an application tool before admitting the message.");
|
|
494
|
-
return toolStorage.write(entry.id, transaction => saveTools(transaction, request, calls));
|
|
751
|
+
return toolStorage.write(entry.id, transaction => saveTools(entry, transaction, request, calls));
|
|
495
752
|
},
|
|
496
753
|
emit: event => emit(entry, { ...event, turnId: request.turnId })
|
|
497
754
|
});
|
|
@@ -501,17 +758,21 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
501
758
|
if (request.tools) await transaction.updateTurnMetadata(request.turnId, { applicationTools: request.tools.records() });
|
|
502
759
|
if (!driver.canonicalTranscript) {
|
|
503
760
|
for (const message of request.progress.values()) {
|
|
761
|
+
if (!persistCommentary && message.role === "commentary") continue;
|
|
504
762
|
if (!await transaction.hasMessage(message.messageId)) await transaction.appendMessage(request.turnId, message);
|
|
505
763
|
}
|
|
506
764
|
if (request.answer?.text.trim()) await transaction.replaceAssistant(request.turnId, request.answer);
|
|
507
765
|
}
|
|
508
766
|
const saved = driver.canonicalTranscript ? await transaction.readTurn(request.turnId) : null;
|
|
509
767
|
await transaction.updateTurnMetadata(request.turnId, { runtime: { ...saved?.metadata?.runtime, status, engine, segmentId, origin: request.input.origin,
|
|
768
|
+
...(request.completedEnvelope ? { completedEnvelope: true } : {}),
|
|
769
|
+
...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}),
|
|
510
770
|
...(request.nativeTurnId ? { nativeTurnId: request.nativeTurnId } : {}),
|
|
511
771
|
...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
|
|
512
772
|
...(request.continuedBy ? { continuedBy: request.continuedBy } : {}), ...extra, ...(error ? { error } : {}) } });
|
|
513
773
|
return conversationHistoryVersions([{ turnId: request.turnId,
|
|
514
|
-
messages: [...request.progress.values()
|
|
774
|
+
messages: [...request.progress.values()].filter(message => !(message.turnId === request.turnId && message.status === "complete"))
|
|
775
|
+
.concat(request.answer?.text.trim() ? [request.answer] : []) }]);
|
|
515
776
|
}
|
|
516
777
|
|
|
517
778
|
async function publishAdmission(request, nativeResult) {
|
|
@@ -519,6 +780,20 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
519
780
|
request.admission.resolve(entry.conversation ? { receipt, nativeResult } : receipt);
|
|
520
781
|
request.admissionSettled = true;
|
|
521
782
|
await emit(entry, { type: "accepted", ...receipt });
|
|
783
|
+
if (driver.canonicalTranscript && !entry.conversation) {
|
|
784
|
+
// Native items can complete before common admission finishes. Their
|
|
785
|
+
// exact request already owns them; publish before tools join the
|
|
786
|
+
// existing delivery commit, without blocking the native event queue.
|
|
787
|
+
for (const [id, message] of request.progress) {
|
|
788
|
+
if (message.turnId !== request.turnId || message.status !== "complete") continue;
|
|
789
|
+
request.progress.delete(id);
|
|
790
|
+
if (request.sealed || entry.disposed || entry.active !== active ||
|
|
791
|
+
entry.bindingOwner !== bindingOwner || entry.segmentId !== segmentId) continue;
|
|
792
|
+
await emit(entry, { type: "message", ...message, streaming: streams.read(entry.id) },
|
|
793
|
+
() => !entry.disposed && !request.sealed && entry.active === active && entry.bindingOwner === bindingOwner && entry.segmentId === segmentId);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
request.admissionPublished = true;
|
|
522
797
|
await setPhase(entry, "working", request.turnId);
|
|
523
798
|
if ((entry.conversation || driver.canonicalTranscript) && entry.nativePhase && entry.nativePhase !== "working") await setPhase(entry, entry.nativePhase, request.turnId);
|
|
524
799
|
}
|
|
@@ -549,6 +824,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
549
824
|
if (submittedMessage(saved || {})?.messageId !== request.input.messageId || saved.metadata?.runtime?.segmentId !== segmentId) {
|
|
550
825
|
throw new Error("The native receipt does not belong to this authored request.");
|
|
551
826
|
}
|
|
827
|
+
if (request.completedEnvelope && saved.metadata.runtime.completedEnvelope !== true) {
|
|
828
|
+
throw new Error("The native receipt lost its completed-response marker.");
|
|
829
|
+
}
|
|
552
830
|
id = saved.turnId;
|
|
553
831
|
await transaction.updateTurnMetadata(id, { runtime: { ...saved.metadata.runtime,
|
|
554
832
|
...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
|
|
@@ -557,6 +835,8 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
557
835
|
id = await transaction.nextTurnId();
|
|
558
836
|
await transaction.appendMessage(id, { role: request.input.origin === "application" ? "system" : "user", ...request.input, at: new Date().toISOString(),
|
|
559
837
|
turnMetadata: { runtime: { status: "running", engine, segmentId, origin: request.input.origin,
|
|
838
|
+
...(request.completedEnvelope ? { completedEnvelope: true } : {}),
|
|
839
|
+
...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}),
|
|
560
840
|
...(request.goalMessageId ? { goalMessageId: request.goalMessageId } : {}),
|
|
561
841
|
...(nativeTurnId ? { nativeTurnId } : {}) } } });
|
|
562
842
|
}
|
|
@@ -578,6 +858,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
578
858
|
request.nativeTurnId = nativeTurnId;
|
|
579
859
|
if (request.input.goal) goalOwner = request;
|
|
580
860
|
current = active.request = request;
|
|
861
|
+
if (entry.conversation) entry.lastAcceptedRequest = request;
|
|
581
862
|
if (!nativeDelivery) await publishAdmission(request);
|
|
582
863
|
}
|
|
583
864
|
|
|
@@ -650,7 +931,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
650
931
|
await setPhase(entry, "working", request.turnId);
|
|
651
932
|
if (request.sealed) return;
|
|
652
933
|
const messageId = `${request.turnId}:${message.id}`;
|
|
653
|
-
|
|
934
|
+
const finalReplyLimit = completedEnvelope && request.completedEnvelope && request.input.origin === "application" && !request.input.goal
|
|
935
|
+
? maximumCompletedOutput : maximumFinalReply;
|
|
936
|
+
if (message.role === "assistant" && message.complete && message.text.length > finalReplyLimit) {
|
|
654
937
|
if (request.answer?.messageId === messageId) request.answer = null;
|
|
655
938
|
throw new Error("The assistant exceeded the configured final reply limit.");
|
|
656
939
|
}
|
|
@@ -659,10 +942,12 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
659
942
|
const outputId = previous?.outputId || (message.outputId ? `${outputOwner.turnId}:${message.outputId}` : "");
|
|
660
943
|
const value = { messageId, ...(outputId ? { outputId } : {}),
|
|
661
944
|
role: message.role, text: message.text, at: previous?.at || new Date().toISOString() };
|
|
945
|
+
const authorship = request.completedEnvelope ? { authorship: { messageId: request.input.messageId,
|
|
946
|
+
turnId: request.turnId, origin: request.input.origin, completedEnvelope: true } } : {};
|
|
662
947
|
// Some native streams have temporary block identities. Only their final
|
|
663
948
|
// history snapshots belong in the transcript, as in the original owner.
|
|
664
949
|
if (message.transient) {
|
|
665
|
-
streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value });
|
|
950
|
+
streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value, ...authorship });
|
|
666
951
|
} else if (message.role === "assistant") {
|
|
667
952
|
if (request.answer && request.answer.messageId !== messageId) {
|
|
668
953
|
request.progress.set(request.answer.messageId, { ...request.answer, role: "commentary" });
|
|
@@ -670,12 +955,12 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
670
955
|
}
|
|
671
956
|
request.progress.delete(messageId);
|
|
672
957
|
request.answer = value;
|
|
673
|
-
streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value });
|
|
958
|
+
streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value, ...authorship });
|
|
674
959
|
} else {
|
|
675
960
|
if (request.answer?.messageId === messageId) request.answer = null;
|
|
676
961
|
if (message.complete) request.progress.set(messageId, value);
|
|
677
962
|
if (message.complete) streams.complete(entry.id, messageId);
|
|
678
|
-
else streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value });
|
|
963
|
+
else streams.update(entry.id, { turnId: request.turnId, origin: request.input.origin, ...value, ...authorship });
|
|
679
964
|
}
|
|
680
965
|
await emit(entry, { type: "message", turnId: request.turnId, messageId, origin: request.input.origin, role: message.role,
|
|
681
966
|
...(outputId ? { outputId } : {}),
|
|
@@ -732,7 +1017,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
732
1017
|
throw failure("Authorized attachment paths changed after this instruction was prepared. Restore the original files or send a new message.", "conversation_attachment_manifest_changed");
|
|
733
1018
|
}
|
|
734
1019
|
metadata.runtime.request = { ...(pending?.messageId === request.input.messageId ? pending
|
|
735
|
-
: { ...request.input, at: request.at, attempted: false
|
|
1020
|
+
: { ...request.input, at: request.at, attempted: false,
|
|
1021
|
+
...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}),
|
|
1022
|
+
...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}) }), attachmentManifest };
|
|
736
1023
|
await transaction.writeMetadata(metadata);
|
|
737
1024
|
});
|
|
738
1025
|
const admitted = Promise.withResolvers();
|
|
@@ -752,7 +1039,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
752
1039
|
threadId = native?.threadId;
|
|
753
1040
|
if (!threadId) throw new Error("Native admission requires the saved conversation identity.");
|
|
754
1041
|
await input.onPromptSending(entry.conversation ? native : { threadId, displayAttachments: files.attachments,
|
|
755
|
-
turnMetadata: { runtime: { engine, segmentId, origin: request.input.origin
|
|
1042
|
+
turnMetadata: { runtime: { engine, segmentId, origin: request.input.origin,
|
|
1043
|
+
...(request.completedEnvelope ? { completedEnvelope: true } : {}),
|
|
1044
|
+
...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}) } } });
|
|
756
1045
|
},
|
|
757
1046
|
async accept(native) {
|
|
758
1047
|
if (entry.conversation) nativeResult = native.nativeResult;
|
|
@@ -801,6 +1090,8 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
801
1090
|
const pending = metadata.runtime.request;
|
|
802
1091
|
if (pending && pending.messageId !== request.input.messageId && (pending.attempted || pending.inspectionOnly)) throw new Error("The pending conversation instruction changed.");
|
|
803
1092
|
metadata.runtime.request = { ...request.input, at: request.at,
|
|
1093
|
+
...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}),
|
|
1094
|
+
...(typeof request.nativeToolAttempt === "boolean" ? { nativeToolAttempt: request.nativeToolAttempt } : {}),
|
|
804
1095
|
message: rendered.message, displayMessage: rendered.displayMessage,
|
|
805
1096
|
displayAttachments, attachmentIds: rendered.attachmentIds,
|
|
806
1097
|
attachmentManifest,
|
|
@@ -827,7 +1118,8 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
827
1118
|
? await dispatchPrepared(input)
|
|
828
1119
|
: await nativeChangeover(entry, request.context, request, dispatchPrepared).send({ engineId: engine,
|
|
829
1120
|
messages: entry.conversation ? await entry.conversation.transcript.history() : conversationNativeMessages(history),
|
|
830
|
-
...(entry.conversation ? { turnMetadata: { engineId: engine } } : {}),
|
|
1121
|
+
...(entry.conversation ? { turnMetadata: { engineId: engine } } : {}),
|
|
1122
|
+
...(request.completedEnvelope ? { completedEnvelope: true, excludedMessageIds: request.excludedMessageIds } : {}), input });
|
|
831
1123
|
if (!delivered?.delivered) {
|
|
832
1124
|
const error = failure(delivered?.error || "Native delivery could not be confirmed.", delivered?.code || "conversation_delivery_uncertain");
|
|
833
1125
|
if (entry.conversation) {
|
|
@@ -852,6 +1144,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
852
1144
|
}
|
|
853
1145
|
if (!saved) throw new Error("The native receipt has no stored authored message.");
|
|
854
1146
|
request.accepted = request.finished = true;
|
|
1147
|
+
if (entry.conversation) entry.lastAcceptedRequest = request;
|
|
855
1148
|
request.turnId = saved.turnId;
|
|
856
1149
|
request.recovered = saved.metadata?.runtime;
|
|
857
1150
|
const receipt = { status: "accepted", messageId: request.input.messageId, turnId: saved.turnId,
|
|
@@ -910,11 +1203,44 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
910
1203
|
// Calls already in flight retain their actor and turn. Keep application
|
|
911
1204
|
// mutations ordered across steering as well as within each request.
|
|
912
1205
|
const work = toolWork.then(() => request.tools.execute(input, options));
|
|
913
|
-
toolWork = work.catch(error => {
|
|
1206
|
+
toolWork = work.catch(error => {
|
|
1207
|
+
// Claude accepts a separate successor query; its predecessor's
|
|
1208
|
+
// effect cannot fail that query. Other engines retain the original
|
|
1209
|
+
// active-turn failure guard across steering.
|
|
1210
|
+
if (engine !== "claude" || current === request) toolFailure ||= error;
|
|
1211
|
+
});
|
|
914
1212
|
return work;
|
|
915
1213
|
}
|
|
916
1214
|
};
|
|
1215
|
+
const completedToolSet = engine === "claude" && initial.completedEnvelope && typeof initial.nativeToolAttempt === "boolean"
|
|
1216
|
+
? catalog.resolveToolSet(initial.context) : null;
|
|
917
1217
|
const providerInput = { configuration, context: initial.context, tools,
|
|
1218
|
+
...(completedToolSet ? {
|
|
1219
|
+
async onNativeToolUse({ messageId, threadId, name }) {
|
|
1220
|
+
await initial.deliveryCommitted;
|
|
1221
|
+
active.controller.signal.throwIfAborted();
|
|
1222
|
+
if (entry.disposed || entry.active !== active || current !== initial || !initial.accepted || initial.finished || initial.sealed ||
|
|
1223
|
+
entry.bindingOwner !== bindingOwner || entry.segmentId !== segmentId ||
|
|
1224
|
+
messageId !== initial.input.messageId || threadId !== initial.nativeThreadId) {
|
|
1225
|
+
throw failure("The native tool observation belongs to a retired response.", "conversation_tool_request_retired");
|
|
1226
|
+
}
|
|
1227
|
+
if (!completedToolSet.byName.has(name)) return;
|
|
1228
|
+
await storage.write(entry.id, async transaction => {
|
|
1229
|
+
const metadata = await transaction.readMetadata();
|
|
1230
|
+
const turn = await transaction.readTurn(initial.turnId);
|
|
1231
|
+
const saved = turn?.metadata?.runtime;
|
|
1232
|
+
if (metadata.runtime.engine !== engine || metadata.runtime.segmentId !== segmentId ||
|
|
1233
|
+
nativeIdentity(metadata.runtime) !== threadId || submittedMessage(turn || {})?.messageId !== messageId ||
|
|
1234
|
+
saved?.engine !== engine || saved.segmentId !== segmentId || saved.completedEnvelope !== true ||
|
|
1235
|
+
typeof saved.nativeToolAttempt !== "boolean" || saved.status !== "running" || saved.supersededBy ||
|
|
1236
|
+
entry.active !== active || current !== initial || initial.finished || initial.sealed || active.controller.signal.aborted) {
|
|
1237
|
+
throw failure("The native tool observation lost its exact admitted receipt.", "conversation_tool_request_retired");
|
|
1238
|
+
}
|
|
1239
|
+
await transaction.updateTurnMetadata(initial.turnId, { runtime: { ...saved, nativeToolAttempt: true } });
|
|
1240
|
+
});
|
|
1241
|
+
initial.nativeToolAttempt = true;
|
|
1242
|
+
}
|
|
1243
|
+
} : {}),
|
|
918
1244
|
...(engine === "opencode" || entry.conversation && ["codex", "claude"].includes(engine) ? {
|
|
919
1245
|
async finishIfCurrent(isCurrent) {
|
|
920
1246
|
// A reserved Send may still be preparing when the native turn ends.
|
|
@@ -1015,7 +1341,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1015
1341
|
if (entry.conversation) {
|
|
1016
1342
|
if (applicationTools) await toolStorage.write(entry.id, async transaction => {
|
|
1017
1343
|
for (const request of active.requests.values()) {
|
|
1018
|
-
if (request.accepted && request.tools?.records().length) await saveTools(transaction, request, request.tools.records());
|
|
1344
|
+
if (request.accepted && request.tools?.records().length) await saveTools(entry, transaction, request, request.tools.records());
|
|
1019
1345
|
}
|
|
1020
1346
|
});
|
|
1021
1347
|
// Only opted-in application receipts are saved above. Native output,
|
|
@@ -1068,7 +1394,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1068
1394
|
await emit(entry, { type: "settled", turnId: current.turnId, status: entry.storageFailure || entry.executionFailure ? "failed" : status, error: entry.error });
|
|
1069
1395
|
}
|
|
1070
1396
|
|
|
1071
|
-
async function send(entry, context, input, origin = "user", goal, representation) {
|
|
1397
|
+
async function send(entry, context, input, origin = "user", goal, representation, wakeOptions) {
|
|
1072
1398
|
await access(context, entry.id, goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send", entry);
|
|
1073
1399
|
if (entry.conversation?.commands) {
|
|
1074
1400
|
if (origin !== "user" || goal) throw failure("This conversation supports its original application Send operation.", "conversation_unsupported", 400);
|
|
@@ -1077,7 +1403,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1077
1403
|
if (entry.conversation?.native?.scoped) return entry.provider.sendNative({ input, context });
|
|
1078
1404
|
const originalInput = representation === "native" ? input : undefined;
|
|
1079
1405
|
const message = originalInput ? { ...input, text: input.displayMessage ?? input.message ?? input.text ?? "" } : input;
|
|
1080
|
-
const admission = await serial(entry, () => admitSend(entry, context, message, origin, goal, originalInput));
|
|
1406
|
+
const admission = await serial(entry, () => admitSend(entry, context, message, origin, goal, originalInput, wakeOptions));
|
|
1081
1407
|
const result = await (admission.receipt || admission.promise);
|
|
1082
1408
|
if (!entry.conversation) return result;
|
|
1083
1409
|
if (representation === "native") return result.nativeResult;
|
|
@@ -1087,10 +1413,22 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1087
1413
|
|
|
1088
1414
|
// Called while holding the conversation queue, including Claude's existing
|
|
1089
1415
|
// stop-then-Send goal commands. All commands retain the ordinary receipt path.
|
|
1090
|
-
async function admitSend(entry, context, input, origin = "user", goal, originalInput) {
|
|
1416
|
+
async function admitSend(entry, context, input, origin = "user", goal, originalInput, wakeOptions) {
|
|
1091
1417
|
const operation = goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send";
|
|
1092
1418
|
await access(context, entry.id, operation, entry);
|
|
1093
1419
|
await entry.conversation?.admission(context, operation);
|
|
1420
|
+
let suppliedExclusions;
|
|
1421
|
+
if (wakeOptions !== undefined) {
|
|
1422
|
+
if (!completedEnvelope || entry.conversation || origin !== "application" || goal ||
|
|
1423
|
+
!wakeOptions || typeof wakeOptions !== "object" || Array.isArray(wakeOptions) ||
|
|
1424
|
+
Object.keys(wakeOptions).some(name => name !== "excludedMessageIds") ||
|
|
1425
|
+
!Array.isArray(wakeOptions.excludedMessageIds) ||
|
|
1426
|
+
wakeOptions.excludedMessageIds.some(id => typeof id !== "string" || !id.trim() || id.length > 128) ||
|
|
1427
|
+
new Set(wakeOptions.excludedMessageIds).size !== wakeOptions.excludedMessageIds.length) {
|
|
1428
|
+
throw failure("Native history exclusions require a server-owned completed application wake and exact message IDs.", "conversation_invalid_message", 400);
|
|
1429
|
+
}
|
|
1430
|
+
suppliedExclusions = [...wakeOptions.excludedMessageIds];
|
|
1431
|
+
}
|
|
1094
1432
|
const attachmentIds = conversationAttachmentIds(input?.attachmentIds);
|
|
1095
1433
|
if (!input || typeof input.messageId !== "string" || !input.messageId.trim() || input.messageId.length > 128 ||
|
|
1096
1434
|
entry.engine !== "api" && !/^[\w-]{1,128}$/u.test(input.messageId) ||
|
|
@@ -1120,6 +1458,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1120
1458
|
if (goal && goal.expectedSegmentId !== entry.segmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed");
|
|
1121
1459
|
const pending = entry.active?.requests.get(message.messageId);
|
|
1122
1460
|
if (pending && entry.engine !== "api" && !pending.admissionSettled) {
|
|
1461
|
+
if (suppliedExclusions && !isDeepStrictEqual(pending.excludedMessageIds || [], suppliedExclusions)) throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict");
|
|
1123
1462
|
if (!entry.conversation && !sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
|
|
1124
1463
|
return { promise: pending.admission.promise };
|
|
1125
1464
|
}
|
|
@@ -1136,6 +1475,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1136
1475
|
}
|
|
1137
1476
|
if (pending && !entry.conversation) {
|
|
1138
1477
|
if (!sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
|
|
1478
|
+
if (suppliedExclusions && !isDeepStrictEqual(pending.excludedMessageIds || [], suppliedExclusions)) throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict");
|
|
1139
1479
|
return { promise: pending.admission.promise };
|
|
1140
1480
|
}
|
|
1141
1481
|
const working = entry.active;
|
|
@@ -1143,10 +1483,15 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1143
1483
|
// No native dispatch has occurred for this request. Finish the previous
|
|
1144
1484
|
// waiter before reserving the same authored request on the ordinary path.
|
|
1145
1485
|
await working.done;
|
|
1146
|
-
return admitSend(entry, context, input, origin, goal, originalInput);
|
|
1486
|
+
return admitSend(entry, context, input, origin, goal, originalInput, wakeOptions);
|
|
1147
1487
|
}
|
|
1148
1488
|
const nativeSteering = !working && (nativeState.nativeTurn?.active || entry.driver.canonicalTranscript && nativeState.binding?.codexAppServerRun &&
|
|
1149
1489
|
codexAppServerTurnStateFromAgentRun(nativeState.binding.codexAppServerRun).active);
|
|
1490
|
+
if (completedEnvelope && entry.engine === "claude" &&
|
|
1491
|
+
(working?.request.completedEnvelope || nativePending?.completedEnvelope === true || origin === "application" && !goal) &&
|
|
1492
|
+
(working || input.steer || nativeSteering)) {
|
|
1493
|
+
throw failure("Completed Claude responses cannot share a native steering turn. Wait for the response or stop it.", "conversation_not_steerable");
|
|
1494
|
+
}
|
|
1150
1495
|
if (working && !input.steer && !goal && !entry.conversation) throw failure("Wait for this answer or use steer to send another instruction.", "conversation_busy");
|
|
1151
1496
|
if (input.steer && (!working && !nativeSteering || working?.finishing) && !(nativePending?.attempted || nativePending?.inspectionOnly)) throw failure("There is no active native turn to steer. Send a new message.", "conversation_not_steerable");
|
|
1152
1497
|
if (goal && working?.finishing) throw failure("Wait for the current turn to finish saving before starting the goal.", "conversation_busy");
|
|
@@ -1158,6 +1503,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1158
1503
|
if (working && working.steering === barrier?.promise) working.steering = null;
|
|
1159
1504
|
};
|
|
1160
1505
|
let configuration;
|
|
1506
|
+
let markedResponse = false;
|
|
1507
|
+
let nativeToolAttempt;
|
|
1508
|
+
let excludedMessageIds;
|
|
1161
1509
|
let requestAt = new Date().toISOString();
|
|
1162
1510
|
try { configuration = entry.conversation ? (await readState(entry)).configuration : await storage.write(entry.id, async transaction => {
|
|
1163
1511
|
const metadata = await transaction.readMetadata();
|
|
@@ -1166,17 +1514,31 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1166
1514
|
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");
|
|
1167
1515
|
}
|
|
1168
1516
|
if (metadata.runtime.request) {
|
|
1517
|
+
if (suppliedExclusions && metadata.runtime.request.messageId !== message.messageId) {
|
|
1518
|
+
throw failure("Check delivery of the previous instruction before preparing another completed response.", "conversation_delivery_uncertain");
|
|
1519
|
+
}
|
|
1169
1520
|
if (entry.engine === "api") throw failure("The previous submission has uncertain delivery. It will not be resent automatically.", "conversation_delivery_uncertain");
|
|
1170
1521
|
if (metadata.runtime.request.messageId === message.messageId) {
|
|
1171
1522
|
if (!sameRequest(metadata.runtime.request, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
|
|
1523
|
+
if (suppliedExclusions && !isDeepStrictEqual(metadata.runtime.request.excludedMessageIds || [], suppliedExclusions)) {
|
|
1524
|
+
throw failure("This messageId already belongs to different native history exclusions.", "conversation_message_conflict");
|
|
1525
|
+
}
|
|
1172
1526
|
requestAt = metadata.runtime.request.at;
|
|
1173
1527
|
}
|
|
1174
1528
|
} else metadata.runtime.request = { ...message, at: requestAt,
|
|
1529
|
+
...(completedEnvelope && origin === "application" && !goal ? { completedEnvelope: true, excludedMessageIds: suppliedExclusions || [],
|
|
1530
|
+
...(entry.engine === "claude" ? { nativeToolAttempt: false } : {}) } : {}),
|
|
1175
1531
|
...(entry.engine !== "api" ? { attempted: false } : {}), ...(input.steer ? { steering: true } : {}) };
|
|
1532
|
+
markedResponse = origin === "application" && !goal && metadata.runtime.request?.messageId === message.messageId &&
|
|
1533
|
+
metadata.runtime.request.completedEnvelope === true;
|
|
1534
|
+
if (markedResponse) excludedMessageIds = structuredClone(metadata.runtime.request.excludedMessageIds || []);
|
|
1535
|
+
if (entry.engine === "claude" && markedResponse && typeof metadata.runtime.request.nativeToolAttempt === "boolean") nativeToolAttempt = metadata.runtime.request.nativeToolAttempt;
|
|
1176
1536
|
await transaction.writeMetadata(metadata);
|
|
1177
1537
|
return metadata.runtime.configuration;
|
|
1178
1538
|
}); } catch (error) { release(); throw error; }
|
|
1179
|
-
const request = { input: message, originalInput, operation, context, at: requestAt,
|
|
1539
|
+
const request = { input: message, originalInput, operation, context, at: requestAt,
|
|
1540
|
+
...(markedResponse ? { completedEnvelope: true, excludedMessageIds } : {}),
|
|
1541
|
+
...(typeof nativeToolAttempt === "boolean" ? { nativeToolAttempt } : {}), steering: Boolean(working || input.steer && nativeSteering),
|
|
1180
1542
|
admission: Promise.withResolvers(), progress: new Map() };
|
|
1181
1543
|
request.admission.promise.catch(() => {});
|
|
1182
1544
|
if (working) {
|
|
@@ -1452,7 +1814,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1452
1814
|
await transaction.appendMessage(turnId, { role: input.origin === "application" ? "system" : "user", origin: input.origin || "user",
|
|
1453
1815
|
messageId: id, text: input.text, attachments: input.attachments, at: input.at,
|
|
1454
1816
|
...(input.data !== undefined ? { data: input.data } : {}), ...(input.goal ? { goal: input.goal } : {}),
|
|
1455
|
-
turnMetadata: { runtime: { engine: entry.engine, segmentId: entry.segmentId, origin: input.origin || "user"
|
|
1817
|
+
turnMetadata: { runtime: { engine: entry.engine, segmentId: entry.segmentId, origin: input.origin || "user",
|
|
1818
|
+
...(input.completedEnvelope === true ? { completedEnvelope: true } : {}),
|
|
1819
|
+
...(typeof input.nativeToolAttempt === "boolean" ? { nativeToolAttempt: input.nativeToolAttempt } : {}) } } });
|
|
1456
1820
|
turn = await transaction.readTurn(turnId);
|
|
1457
1821
|
history.push(turn);
|
|
1458
1822
|
}
|
|
@@ -1460,6 +1824,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1460
1824
|
if (runtime.nativeTurnId && native.nativeTurnId && runtime.nativeTurnId !== native.nativeTurnId) throw new Error("Native history changed the accepted turn's identity.");
|
|
1461
1825
|
const replies = [];
|
|
1462
1826
|
for (const message of native.messages || []) {
|
|
1827
|
+
if (!persistCommentary && message.role === "commentary") continue;
|
|
1463
1828
|
if (message.role === "assistant") replies.push(message.text);
|
|
1464
1829
|
else {
|
|
1465
1830
|
const messageId = `${turn.turnId}:${message.id}`;
|
|
@@ -1471,7 +1836,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1471
1836
|
const nativeReplies = (native.messages || []).filter(message => message.role === "assistant");
|
|
1472
1837
|
const outputId = nativeReplies.length === 1 && nativeReplies[0].outputId;
|
|
1473
1838
|
const replyText = replies.join("\n\n");
|
|
1474
|
-
const
|
|
1839
|
+
const finalReplyLimit = completedEnvelope && isCompletedEnvelopeTurn(turn) && !submittedMessage(turn)?.goal
|
|
1840
|
+
? maximumCompletedOutput : maximumFinalReply;
|
|
1841
|
+
const oversizedReply = replyText.length > finalReplyLimit;
|
|
1475
1842
|
if (replies.length && !oversizedReply) await transaction.replaceAssistant(turn.turnId, { messageId: turn.assistant?.messageId || `${turn.turnId}:assistant`,
|
|
1476
1843
|
...(!turn.assistant?.outputId && outputId ? { outputId: `${turn.turnId}:${outputId}` } : {}),
|
|
1477
1844
|
role: "assistant", text: replyText, at: submittedMessage(turn).at });
|
|
@@ -1550,6 +1917,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1550
1917
|
|
|
1551
1918
|
async function replace(entry, context, input, selection = false, representation) {
|
|
1552
1919
|
const operation = selection ? "select" : "replace";
|
|
1920
|
+
if (completedEnvelope && input?.engine === "api") throw failure("Completed envelopes require a native engine.", "conversation_unsupported", 400);
|
|
1553
1921
|
await access(context, entry.id, operation, entry);
|
|
1554
1922
|
if (entry.conversation?.commands) {
|
|
1555
1923
|
if (!selection) throw failure("This conversation retains its original replacement policy.", "conversation_unsupported", 400);
|
|
@@ -1662,12 +2030,10 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1662
2030
|
const continuity = destination ? { text: destination.continuity, attachments: destination.continuityAttachments || [] }
|
|
1663
2031
|
: engine === "api" ? conversationContinuity({ history, briefing: input.briefing, maximumCharacters: maximumContinuity })
|
|
1664
2032
|
: { text: undefined, attachments: [] };
|
|
1665
|
-
pending = { request, engine, configuration: structuredClone(configuration),
|
|
2033
|
+
pending = createConversationRuntimeReplacement({ request, engine, configuration: structuredClone(configuration),
|
|
1666
2034
|
reason: selection ? state.engine === engine ? "model-change" : "engine-change" : input.reason,
|
|
1667
|
-
segmentId: destination?.segmentId || randomUUID(),
|
|
1668
|
-
|
|
1669
|
-
seen: destination ? destination.seen : engine === "api" ? conversationHistoryVersions(history) : {},
|
|
1670
|
-
...(destination ? { binding: destination.binding || null, submission: destination.request } : {}) };
|
|
2035
|
+
segmentId: destination?.segmentId || randomUUID(), continuity,
|
|
2036
|
+
seen: destination ? destination.seen : engine === "api" ? conversationHistoryVersions(history) : {}, destination });
|
|
1671
2037
|
}
|
|
1672
2038
|
|
|
1673
2039
|
function saveReplacement() {
|
|
@@ -1715,25 +2081,10 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1715
2081
|
if (replacement && nativeIdentity(state) !== replacement.previous.conversationId) {
|
|
1716
2082
|
throw failure("The native predecessor identity changed before its binding was released.", "conversation_replacement_conflict");
|
|
1717
2083
|
}
|
|
1718
|
-
state
|
|
1719
|
-
configuration: state.configuration, binding: state.binding, seen: state.seen,
|
|
1720
|
-
continuity: state.continuity, continuityAttachments: state.continuityAttachments, request: state.request,
|
|
1721
|
-
successorId: pending.segmentId, replacement: request,
|
|
1722
|
-
...(pending.preparedAt ? { preparedAt: pending.preparedAt } : {}) });
|
|
1723
|
-
Object.assign(state, { engine, configuration: pending.configuration, binding: pending.binding,
|
|
1724
|
-
segmentId: pending.segmentId, continuity: pending.continuity, continuityAttachments: pending.continuityAttachments, seen: pending.seen });
|
|
1725
|
-
if (pending.submission) state.request = pending.submission;
|
|
1726
|
-
else delete state.request;
|
|
2084
|
+
releaseConversationRuntimeBinding(state, pending, request, engine);
|
|
1727
2085
|
},
|
|
1728
2086
|
async write(value) {
|
|
1729
|
-
|
|
1730
|
-
if (engine !== "api") state.seen = value.engines[engine].seen;
|
|
1731
|
-
state.lastEngine = value.lastEngine;
|
|
1732
|
-
} else if (engine !== "api" &&
|
|
1733
|
-
(!selection || input.retireNative === true || engine === "opencode" && !pending.binding.sessionId)) {
|
|
1734
|
-
state.lastEngine = "";
|
|
1735
|
-
}
|
|
1736
|
-
delete state.replacement;
|
|
2087
|
+
finishConversationRuntimeReplacement(state, { engine, selection, retireNative: input.retireNative, pending, value });
|
|
1737
2088
|
await transaction.writeMetadata(metadata);
|
|
1738
2089
|
}
|
|
1739
2090
|
});
|
|
@@ -1973,6 +2324,12 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1973
2324
|
await access(context, id, "open");
|
|
1974
2325
|
const selectedHost = host || defaultHost;
|
|
1975
2326
|
const conversation = selectedHost?.conversation ? await selectedHost.conversation({ id, context }) : null;
|
|
2327
|
+
if (completedEnvelope && (conversation || representation !== "canonical")) {
|
|
2328
|
+
throw failure("Completed envelopes require runtime-owned canonical storage.", "conversation_unsupported", 400);
|
|
2329
|
+
}
|
|
2330
|
+
if (conversation && !persistCommentary) {
|
|
2331
|
+
throw new TypeError("Transient commentary requires a runtime-owned transcript; supplied conversations retain their host's policy.");
|
|
2332
|
+
}
|
|
1976
2333
|
const scoped = conversation?.native?.scoped;
|
|
1977
2334
|
const commands = conversation?.commands;
|
|
1978
2335
|
const invalidClaudeOwner = conversation?.engine === "claude" && (!conversation.native?.owner ||
|
|
@@ -2041,7 +2398,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2041
2398
|
return result;
|
|
2042
2399
|
},
|
|
2043
2400
|
send: input => send(entry, context, input, "user", undefined, representation),
|
|
2044
|
-
wake: input => send(entry, context, input, "application", undefined, representation),
|
|
2401
|
+
wake: (input, options) => send(entry, context, input, "application", undefined, representation, options),
|
|
2402
|
+
...(completedEnvelope ? { prepareCompletedResponse: (input, options) =>
|
|
2403
|
+
serial(entry, () => prepareCompletedResponse(entry, context, input, options)) } : {}),
|
|
2045
2404
|
readGoal: () => serial(entry, () => readGoal(entry, context, representation)),
|
|
2046
2405
|
updateGoal: input => updateGoal(entry, context, input, representation),
|
|
2047
2406
|
inspectDelivery({ messageId, threadId } = {}) {
|