@jskit-ai/assistant-core 0.1.213 → 0.1.215
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 +16 -2
- package/src/server/conversation/claudeTurn.js +75 -16
- 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 +132 -24
- 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 +418 -64
- 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 +1329 -3
- package/test/claudeProcess.test.js +101 -0
- package/test/claudeTurn.test.js +236 -1
- 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 +112 -1
- package/test/openCodeClient.test.js +186 -0
- package/test/openCodeRuntime.test.js +78 -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,
|
|
@@ -919,7 +1212,35 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
919
1212
|
return work;
|
|
920
1213
|
}
|
|
921
1214
|
};
|
|
1215
|
+
const completedToolSet = engine === "claude" && initial.completedEnvelope && typeof initial.nativeToolAttempt === "boolean"
|
|
1216
|
+
? catalog.resolveToolSet(initial.context) : null;
|
|
922
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
|
+
} : {}),
|
|
923
1244
|
...(engine === "opencode" || entry.conversation && ["codex", "claude"].includes(engine) ? {
|
|
924
1245
|
async finishIfCurrent(isCurrent) {
|
|
925
1246
|
// A reserved Send may still be preparing when the native turn ends.
|
|
@@ -1020,7 +1341,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1020
1341
|
if (entry.conversation) {
|
|
1021
1342
|
if (applicationTools) await toolStorage.write(entry.id, async transaction => {
|
|
1022
1343
|
for (const request of active.requests.values()) {
|
|
1023
|
-
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());
|
|
1024
1345
|
}
|
|
1025
1346
|
});
|
|
1026
1347
|
// Only opted-in application receipts are saved above. Native output,
|
|
@@ -1073,7 +1394,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1073
1394
|
await emit(entry, { type: "settled", turnId: current.turnId, status: entry.storageFailure || entry.executionFailure ? "failed" : status, error: entry.error });
|
|
1074
1395
|
}
|
|
1075
1396
|
|
|
1076
|
-
async function send(entry, context, input, origin = "user", goal, representation) {
|
|
1397
|
+
async function send(entry, context, input, origin = "user", goal, representation, wakeOptions) {
|
|
1077
1398
|
await access(context, entry.id, goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send", entry);
|
|
1078
1399
|
if (entry.conversation?.commands) {
|
|
1079
1400
|
if (origin !== "user" || goal) throw failure("This conversation supports its original application Send operation.", "conversation_unsupported", 400);
|
|
@@ -1082,7 +1403,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1082
1403
|
if (entry.conversation?.native?.scoped) return entry.provider.sendNative({ input, context });
|
|
1083
1404
|
const originalInput = representation === "native" ? input : undefined;
|
|
1084
1405
|
const message = originalInput ? { ...input, text: input.displayMessage ?? input.message ?? input.text ?? "" } : input;
|
|
1085
|
-
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));
|
|
1086
1407
|
const result = await (admission.receipt || admission.promise);
|
|
1087
1408
|
if (!entry.conversation) return result;
|
|
1088
1409
|
if (representation === "native") return result.nativeResult;
|
|
@@ -1092,10 +1413,22 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1092
1413
|
|
|
1093
1414
|
// Called while holding the conversation queue, including Claude's existing
|
|
1094
1415
|
// stop-then-Send goal commands. All commands retain the ordinary receipt path.
|
|
1095
|
-
async function admitSend(entry, context, input, origin = "user", goal, originalInput) {
|
|
1416
|
+
async function admitSend(entry, context, input, origin = "user", goal, originalInput, wakeOptions) {
|
|
1096
1417
|
const operation = goal ? "goal" : input?.steer === true ? "steer" : origin === "application" ? "wake" : "send";
|
|
1097
1418
|
await access(context, entry.id, operation, entry);
|
|
1098
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
|
+
}
|
|
1099
1432
|
const attachmentIds = conversationAttachmentIds(input?.attachmentIds);
|
|
1100
1433
|
if (!input || typeof input.messageId !== "string" || !input.messageId.trim() || input.messageId.length > 128 ||
|
|
1101
1434
|
entry.engine !== "api" && !/^[\w-]{1,128}$/u.test(input.messageId) ||
|
|
@@ -1125,6 +1458,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1125
1458
|
if (goal && goal.expectedSegmentId !== entry.segmentId) throw failure("The conversation changed before this goal command was sent.", "conversation_goal_changed");
|
|
1126
1459
|
const pending = entry.active?.requests.get(message.messageId);
|
|
1127
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");
|
|
1128
1462
|
if (!entry.conversation && !sameRequest(pending.input, message)) throw failure("This messageId already belongs to different content.", "conversation_message_conflict");
|
|
1129
1463
|
return { promise: pending.admission.promise };
|
|
1130
1464
|
}
|
|
@@ -1141,6 +1475,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1141
1475
|
}
|
|
1142
1476
|
if (pending && !entry.conversation) {
|
|
1143
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");
|
|
1144
1479
|
return { promise: pending.admission.promise };
|
|
1145
1480
|
}
|
|
1146
1481
|
const working = entry.active;
|
|
@@ -1148,10 +1483,15 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1148
1483
|
// No native dispatch has occurred for this request. Finish the previous
|
|
1149
1484
|
// waiter before reserving the same authored request on the ordinary path.
|
|
1150
1485
|
await working.done;
|
|
1151
|
-
return admitSend(entry, context, input, origin, goal, originalInput);
|
|
1486
|
+
return admitSend(entry, context, input, origin, goal, originalInput, wakeOptions);
|
|
1152
1487
|
}
|
|
1153
1488
|
const nativeSteering = !working && (nativeState.nativeTurn?.active || entry.driver.canonicalTranscript && nativeState.binding?.codexAppServerRun &&
|
|
1154
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
|
+
}
|
|
1155
1495
|
if (working && !input.steer && !goal && !entry.conversation) throw failure("Wait for this answer or use steer to send another instruction.", "conversation_busy");
|
|
1156
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");
|
|
1157
1497
|
if (goal && working?.finishing) throw failure("Wait for the current turn to finish saving before starting the goal.", "conversation_busy");
|
|
@@ -1163,6 +1503,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1163
1503
|
if (working && working.steering === barrier?.promise) working.steering = null;
|
|
1164
1504
|
};
|
|
1165
1505
|
let configuration;
|
|
1506
|
+
let markedResponse = false;
|
|
1507
|
+
let nativeToolAttempt;
|
|
1508
|
+
let excludedMessageIds;
|
|
1166
1509
|
let requestAt = new Date().toISOString();
|
|
1167
1510
|
try { configuration = entry.conversation ? (await readState(entry)).configuration : await storage.write(entry.id, async transaction => {
|
|
1168
1511
|
const metadata = await transaction.readMetadata();
|
|
@@ -1171,17 +1514,31 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1171
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");
|
|
1172
1515
|
}
|
|
1173
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
|
+
}
|
|
1174
1520
|
if (entry.engine === "api") throw failure("The previous submission has uncertain delivery. It will not be resent automatically.", "conversation_delivery_uncertain");
|
|
1175
1521
|
if (metadata.runtime.request.messageId === message.messageId) {
|
|
1176
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
|
+
}
|
|
1177
1526
|
requestAt = metadata.runtime.request.at;
|
|
1178
1527
|
}
|
|
1179
1528
|
} else metadata.runtime.request = { ...message, at: requestAt,
|
|
1529
|
+
...(completedEnvelope && origin === "application" && !goal ? { completedEnvelope: true, excludedMessageIds: suppliedExclusions || [],
|
|
1530
|
+
...(entry.engine === "claude" ? { nativeToolAttempt: false } : {}) } : {}),
|
|
1180
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;
|
|
1181
1536
|
await transaction.writeMetadata(metadata);
|
|
1182
1537
|
return metadata.runtime.configuration;
|
|
1183
1538
|
}); } catch (error) { release(); throw error; }
|
|
1184
|
-
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),
|
|
1185
1542
|
admission: Promise.withResolvers(), progress: new Map() };
|
|
1186
1543
|
request.admission.promise.catch(() => {});
|
|
1187
1544
|
if (working) {
|
|
@@ -1457,7 +1814,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1457
1814
|
await transaction.appendMessage(turnId, { role: input.origin === "application" ? "system" : "user", origin: input.origin || "user",
|
|
1458
1815
|
messageId: id, text: input.text, attachments: input.attachments, at: input.at,
|
|
1459
1816
|
...(input.data !== undefined ? { data: input.data } : {}), ...(input.goal ? { goal: input.goal } : {}),
|
|
1460
|
-
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 } : {}) } } });
|
|
1461
1820
|
turn = await transaction.readTurn(turnId);
|
|
1462
1821
|
history.push(turn);
|
|
1463
1822
|
}
|
|
@@ -1465,6 +1824,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1465
1824
|
if (runtime.nativeTurnId && native.nativeTurnId && runtime.nativeTurnId !== native.nativeTurnId) throw new Error("Native history changed the accepted turn's identity.");
|
|
1466
1825
|
const replies = [];
|
|
1467
1826
|
for (const message of native.messages || []) {
|
|
1827
|
+
if (!persistCommentary && message.role === "commentary") continue;
|
|
1468
1828
|
if (message.role === "assistant") replies.push(message.text);
|
|
1469
1829
|
else {
|
|
1470
1830
|
const messageId = `${turn.turnId}:${message.id}`;
|
|
@@ -1476,7 +1836,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1476
1836
|
const nativeReplies = (native.messages || []).filter(message => message.role === "assistant");
|
|
1477
1837
|
const outputId = nativeReplies.length === 1 && nativeReplies[0].outputId;
|
|
1478
1838
|
const replyText = replies.join("\n\n");
|
|
1479
|
-
const
|
|
1839
|
+
const finalReplyLimit = completedEnvelope && isCompletedEnvelopeTurn(turn) && !submittedMessage(turn)?.goal
|
|
1840
|
+
? maximumCompletedOutput : maximumFinalReply;
|
|
1841
|
+
const oversizedReply = replyText.length > finalReplyLimit;
|
|
1480
1842
|
if (replies.length && !oversizedReply) await transaction.replaceAssistant(turn.turnId, { messageId: turn.assistant?.messageId || `${turn.turnId}:assistant`,
|
|
1481
1843
|
...(!turn.assistant?.outputId && outputId ? { outputId: `${turn.turnId}:${outputId}` } : {}),
|
|
1482
1844
|
role: "assistant", text: replyText, at: submittedMessage(turn).at });
|
|
@@ -1555,6 +1917,7 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1555
1917
|
|
|
1556
1918
|
async function replace(entry, context, input, selection = false, representation) {
|
|
1557
1919
|
const operation = selection ? "select" : "replace";
|
|
1920
|
+
if (completedEnvelope && input?.engine === "api") throw failure("Completed envelopes require a native engine.", "conversation_unsupported", 400);
|
|
1558
1921
|
await access(context, entry.id, operation, entry);
|
|
1559
1922
|
if (entry.conversation?.commands) {
|
|
1560
1923
|
if (!selection) throw failure("This conversation retains its original replacement policy.", "conversation_unsupported", 400);
|
|
@@ -1667,12 +2030,10 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1667
2030
|
const continuity = destination ? { text: destination.continuity, attachments: destination.continuityAttachments || [] }
|
|
1668
2031
|
: engine === "api" ? conversationContinuity({ history, briefing: input.briefing, maximumCharacters: maximumContinuity })
|
|
1669
2032
|
: { text: undefined, attachments: [] };
|
|
1670
|
-
pending = { request, engine, configuration: structuredClone(configuration),
|
|
2033
|
+
pending = createConversationRuntimeReplacement({ request, engine, configuration: structuredClone(configuration),
|
|
1671
2034
|
reason: selection ? state.engine === engine ? "model-change" : "engine-change" : input.reason,
|
|
1672
|
-
segmentId: destination?.segmentId || randomUUID(),
|
|
1673
|
-
|
|
1674
|
-
seen: destination ? destination.seen : engine === "api" ? conversationHistoryVersions(history) : {},
|
|
1675
|
-
...(destination ? { binding: destination.binding || null, submission: destination.request } : {}) };
|
|
2035
|
+
segmentId: destination?.segmentId || randomUUID(), continuity,
|
|
2036
|
+
seen: destination ? destination.seen : engine === "api" ? conversationHistoryVersions(history) : {}, destination });
|
|
1676
2037
|
}
|
|
1677
2038
|
|
|
1678
2039
|
function saveReplacement() {
|
|
@@ -1720,25 +2081,10 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1720
2081
|
if (replacement && nativeIdentity(state) !== replacement.previous.conversationId) {
|
|
1721
2082
|
throw failure("The native predecessor identity changed before its binding was released.", "conversation_replacement_conflict");
|
|
1722
2083
|
}
|
|
1723
|
-
state
|
|
1724
|
-
configuration: state.configuration, binding: state.binding, seen: state.seen,
|
|
1725
|
-
continuity: state.continuity, continuityAttachments: state.continuityAttachments, request: state.request,
|
|
1726
|
-
successorId: pending.segmentId, replacement: request,
|
|
1727
|
-
...(pending.preparedAt ? { preparedAt: pending.preparedAt } : {}) });
|
|
1728
|
-
Object.assign(state, { engine, configuration: pending.configuration, binding: pending.binding,
|
|
1729
|
-
segmentId: pending.segmentId, continuity: pending.continuity, continuityAttachments: pending.continuityAttachments, seen: pending.seen });
|
|
1730
|
-
if (pending.submission) state.request = pending.submission;
|
|
1731
|
-
else delete state.request;
|
|
2084
|
+
releaseConversationRuntimeBinding(state, pending, request, engine);
|
|
1732
2085
|
},
|
|
1733
2086
|
async write(value) {
|
|
1734
|
-
|
|
1735
|
-
if (engine !== "api") state.seen = value.engines[engine].seen;
|
|
1736
|
-
state.lastEngine = value.lastEngine;
|
|
1737
|
-
} else if (engine !== "api" &&
|
|
1738
|
-
(!selection || input.retireNative === true || engine === "opencode" && !pending.binding.sessionId)) {
|
|
1739
|
-
state.lastEngine = "";
|
|
1740
|
-
}
|
|
1741
|
-
delete state.replacement;
|
|
2087
|
+
finishConversationRuntimeReplacement(state, { engine, selection, retireNative: input.retireNative, pending, value });
|
|
1742
2088
|
await transaction.writeMetadata(metadata);
|
|
1743
2089
|
}
|
|
1744
2090
|
});
|
|
@@ -1978,6 +2324,12 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
1978
2324
|
await access(context, id, "open");
|
|
1979
2325
|
const selectedHost = host || defaultHost;
|
|
1980
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
|
+
}
|
|
1981
2333
|
const scoped = conversation?.native?.scoped;
|
|
1982
2334
|
const commands = conversation?.commands;
|
|
1983
2335
|
const invalidClaudeOwner = conversation?.engine === "claude" && (!conversation.native?.owner ||
|
|
@@ -2046,7 +2398,9 @@ export function createConversationRuntime({ engine: defaultEngine = "api", defau
|
|
|
2046
2398
|
return result;
|
|
2047
2399
|
},
|
|
2048
2400
|
send: input => send(entry, context, input, "user", undefined, representation),
|
|
2049
|
-
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)) } : {}),
|
|
2050
2404
|
readGoal: () => serial(entry, () => readGoal(entry, context, representation)),
|
|
2051
2405
|
updateGoal: input => updateGoal(entry, context, input, representation),
|
|
2052
2406
|
inspectDelivery({ messageId, threadId } = {}) {
|