@jskit-ai/assistant-core 0.1.213 → 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 +16 -2
- package/src/server/conversation/claudeTurn.js +28 -8
- 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 +31 -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 +112 -1
- package/test/openCodeClient.test.js +186 -0
- package/test/openCodeRuntime.test.js +78 -0
- package/test/opencodeConversationSession.test.js +637 -5
|
@@ -20,6 +20,75 @@ import { createAiConnectionResolver } from "../../connectors-catalog/src/server/
|
|
|
20
20
|
const configuration = { systemPrompt: "Keep these instructions fresh.", model: "test-model", effort: "high" };
|
|
21
21
|
const input = { messageId: "first", text: "Hello" };
|
|
22
22
|
|
|
23
|
+
test("marked native output keeps its admitted private turn while human messages are queued", async t => {
|
|
24
|
+
const directory = await mkdtemp(path.join(tmpdir(), "codex-private-output-"));
|
|
25
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
26
|
+
const storage = createReentrantConversationStorage(createFileConversationStorage({ directory }));
|
|
27
|
+
const transcript = createConversationTranscript({ storage, applicationTurns: true });
|
|
28
|
+
for (const [role, method] of [["assistant", "writeConversationAssistantMessage"],
|
|
29
|
+
["commentary", "writeConversationCommentaryMessage"], ["thinking", "writeConversationThinkingMessage"]]) {
|
|
30
|
+
const scope = `private-${role}`, segmentId = `segment-${role}`;
|
|
31
|
+
const nativeIdentity = { threadId: `thread-${role}`, turnId: `native-${role}` };
|
|
32
|
+
const privateTurnId = await storage.write(scope, async transaction => {
|
|
33
|
+
await transaction.writeMetadata({ runtime: { engine: "codex", segmentId, binding: {
|
|
34
|
+
threadId: nativeIdentity.threadId, codexAppServerRun: {
|
|
35
|
+
providerThreadId: nativeIdentity.threadId, providerTurnId: nativeIdentity.turnId, outerTurnId: "private-input"
|
|
36
|
+
}
|
|
37
|
+
} } });
|
|
38
|
+
const turnId = await transaction.nextTurnId();
|
|
39
|
+
await transaction.appendMessage(turnId, { role: "system", origin: "application", messageId: "private-input",
|
|
40
|
+
text: "The admitted application envelope", at: "2026-10-10T00:00:00.000Z",
|
|
41
|
+
turnMetadata: { runtime: { status: "running", engine: "codex", segmentId, origin: "application",
|
|
42
|
+
completedEnvelope: true, nativeTurnId: nativeIdentity.turnId } } });
|
|
43
|
+
return turnId;
|
|
44
|
+
});
|
|
45
|
+
await transcript.writeConversationUserMessage(scope, { messageId: "queued-B", text: "Wait, discuss it first" });
|
|
46
|
+
await transcript.writeConversationUserMessage(scope, { messageId: "queued-C", text: "Only read the guide" });
|
|
47
|
+
const before = await transcript.readConversationLog(scope);
|
|
48
|
+
const store = createCodexConversationStore({ storage, scope, segmentId, isCurrent: () => true,
|
|
49
|
+
transcript, streams: createConversationStreams() });
|
|
50
|
+
assert.equal(await store[method](scope), null, "Omitted native output remains an empty no-op");
|
|
51
|
+
await assert.rejects(transcript[method](scope, { turnId: "missing-private-turn", text: "Do not retarget" }),
|
|
52
|
+
/original turn no longer exists/);
|
|
53
|
+
assert.deepEqual(await transcript.readConversationLog(scope), before, "Missing exact targets leave every row untouched");
|
|
54
|
+
const input = { messageId: `native-message-${role}`, outputId: `native-output-${role}`,
|
|
55
|
+
text: "Private native output", nativeIdentity };
|
|
56
|
+
const written = await store[method](scope, input);
|
|
57
|
+
assert.equal(written.turnId, privateTurnId);
|
|
58
|
+
const message = written.messages.find(message => message.role === role);
|
|
59
|
+
assert.equal(message.messageId, input.messageId);
|
|
60
|
+
assert.equal(message.outputId, input.outputId);
|
|
61
|
+
assert.equal(message.text, input.text);
|
|
62
|
+
assert.deepEqual((await transcript.readConversationLog(scope)).slice(1), before.slice(1),
|
|
63
|
+
"Later human questions retain their exact words and no native reply/progress");
|
|
64
|
+
assert.equal(await store[method](scope, input), null, "The same native message is saved only once");
|
|
65
|
+
assert.equal((await transcript.readConversationLog(scope, { presentation: true })).some(turn =>
|
|
66
|
+
turn.messages.some(message => message.text === input.text)), false, "Private carrier output stays out of human history");
|
|
67
|
+
if (role === "assistant") {
|
|
68
|
+
const corrected = await store.upsertConversationAssistantMessage(scope, {
|
|
69
|
+
turnId: privateTurnId, text: "Corrected private native output", nativeIdentity
|
|
70
|
+
});
|
|
71
|
+
assert.equal(corrected.assistant.messageId, input.messageId);
|
|
72
|
+
assert.equal(corrected.assistant.outputId, input.outputId);
|
|
73
|
+
assert.equal(corrected.assistant.text, "Corrected private native output");
|
|
74
|
+
}
|
|
75
|
+
await transcript.writeConversationAssistantMessage(scope, { messageId: "ordinary-final", text: "Ordinary unpinned final" });
|
|
76
|
+
assert.equal((await transcript.readConversationLog(scope)).at(-1).assistant.text, "Ordinary unpinned final",
|
|
77
|
+
"The generic transcript still pairs unpinned output with the latest open human turn");
|
|
78
|
+
const retained = await transcript.readConversationLog(scope);
|
|
79
|
+
await storage.write(scope, async transaction => {
|
|
80
|
+
const metadata = await transaction.readMetadata();
|
|
81
|
+
metadata.runtime.binding.codexAppServerRun.outerTurnId = "missing-private-input";
|
|
82
|
+
metadata.runtime.request = { messageId: "missing-private-input", origin: "application", completedEnvelope: true };
|
|
83
|
+
await transaction.writeMetadata(metadata);
|
|
84
|
+
});
|
|
85
|
+
await assert.rejects(store[method](scope, { ...input, messageId: "lost-private-output" }), /original turn no longer exists/,
|
|
86
|
+
"A pending marked receipt cannot fall back to a later human row when its private turn is absent");
|
|
87
|
+
assert.throws(() => store[method]("foreign-scope", input), /another conversation/);
|
|
88
|
+
assert.deepEqual(await transcript.readConversationLog(scope), retained);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
23
92
|
test("Codex structured output uses the existing native turn setting without changing conversation identity", async t => {
|
|
24
93
|
const outputSchema = { type: "object", additionalProperties: false,
|
|
25
94
|
properties: { answer: { type: "string", maxLength: 32 } }, required: ["answer"] };
|
|
@@ -487,11 +556,13 @@ test("automatic goal turns retain all nine application tool receipts on their au
|
|
|
487
556
|
assert.equal(trace.some(row => row.method === "thread/inject_items" || row.method === "thread/goal/set"), false);
|
|
488
557
|
});
|
|
489
558
|
|
|
490
|
-
|
|
559
|
+
for (const deferBudgetCompletion of [false, true]) test(deferBudgetCompletion
|
|
560
|
+
? "automatic goal tool failure verifies recovery when native completion follows the saved terminal"
|
|
561
|
+
: "automatic goal turns cannot reset their authored request's application tool budget", async t => {
|
|
491
562
|
const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
|
|
492
563
|
let calls = 0;
|
|
493
564
|
const f = await fixture(t, { actions: applicationActions(async () => ({ value: ++calls })), context,
|
|
494
|
-
nativeGoalObjective: "tools goal", limits: { maxToolCalls: 3 } });
|
|
565
|
+
nativeGoalObjective: "tools goal", limits: { admissionTimeoutMs: 30_000, maxToolCalls: 3 }, deferBudgetCompletion });
|
|
495
566
|
await f.conversation.send({ messageId: "bounded-goal", text: "native-goal" });
|
|
496
567
|
const state = await f.conversation.wait();
|
|
497
568
|
const trace = await f.trace();
|
|
@@ -517,6 +588,22 @@ test("automatic goal turns cannot reset their authored request's application too
|
|
|
517
588
|
assert.equal(stopped.codexAppServerRun.providerStatus, "observation_lost");
|
|
518
589
|
assert.equal(stopped.codexAppServerRun.outerTurnId, "bounded-goal");
|
|
519
590
|
assert.equal(trace.filter(row => row.method === "turn/start").length, 1);
|
|
591
|
+
if (deferBudgetCompletion) {
|
|
592
|
+
const fourth = trace.find(row => row.budgetCompletion?.kind === "fourth-request").budgetCompletion;
|
|
593
|
+
assert.equal(fourth.threadId, stopped.threadId);
|
|
594
|
+
assert.equal(fourth.turnId, stopped.codexAppServerRun.providerTurnId);
|
|
595
|
+
assert.equal(fourth.callId, fourth.turnId + "-4");
|
|
596
|
+
assert.equal(trace.find(row => row.budgetCompletion?.kind === "fourth-response").budgetCompletion.budgetFailure, true);
|
|
597
|
+
for (const kind of ["failed-held", "interrupted-held"]) {
|
|
598
|
+
assert.ok(trace.some(row => row.budgetCompletion?.kind === kind &&
|
|
599
|
+
row.budgetCompletion.threadId === fourth.threadId && row.budgetCompletion.turnId === fourth.turnId));
|
|
600
|
+
}
|
|
601
|
+
const release = JSON.parse(await readFile(path.join(f.directory, "history.json") + ".budget-terminal-" + fourth.turnId, "utf8"));
|
|
602
|
+
assert.deepEqual(release, { threadId: fourth.threadId, turnId: fourth.turnId, outerTurnId: "bounded-goal" });
|
|
603
|
+
const native = JSON.parse(await readFile(path.join(f.directory, "history.json"), "utf8"));
|
|
604
|
+
assert.equal(native.id, fourth.threadId);
|
|
605
|
+
assert.equal(native.goal.status, "paused");
|
|
606
|
+
}
|
|
520
607
|
});
|
|
521
608
|
|
|
522
609
|
test("failed tracked Codex history storage stops owned work and recovers the exact result without replay", async t => {
|
|
@@ -772,6 +859,8 @@ async function fixture(t, options = {}) {
|
|
|
772
859
|
import { execFileSync } from "node:child_process";
|
|
773
860
|
import { WebSocketServer } from ${JSON.stringify(import.meta.resolve("ws"))};
|
|
774
861
|
const args = process.argv.slice(2);
|
|
862
|
+
const structuredResponse = ${JSON.stringify(options.structuredResponse || null)};
|
|
863
|
+
const delayedFinal = ${JSON.stringify(options.completionBeforeDelayedFinal || null)};
|
|
775
864
|
if (args.includes("debug") && args.includes("models")) {
|
|
776
865
|
process.stdout.write(JSON.stringify({ models: [{ slug: "test-model", priority: 0 }] }));
|
|
777
866
|
process.exit(0);
|
|
@@ -796,6 +885,10 @@ async function fixture(t, options = {}) {
|
|
|
796
885
|
if (!method) {
|
|
797
886
|
log({ toolResponse: { id, result, error } });
|
|
798
887
|
const pending = toolRequests.get(id); toolRequests.delete(id);
|
|
888
|
+
if (${Boolean(options.deferBudgetCompletion)} && id === "tool-request-4") {
|
|
889
|
+
log({ budgetCompletion: { kind: "fourth-response", requestId: id,
|
|
890
|
+
budgetFailure: !!error && /application tool-call limit/.test(error.message) } });
|
|
891
|
+
}
|
|
799
892
|
if (error) pending.reject(new Error(error.message)); else pending.resolve(result);
|
|
800
893
|
return;
|
|
801
894
|
}
|
|
@@ -807,10 +900,22 @@ async function fixture(t, options = {}) {
|
|
|
807
900
|
ws.send(JSON.stringify({ id, error: { code: -32000, message: "Native history is temporarily unavailable" } }));
|
|
808
901
|
return;
|
|
809
902
|
}
|
|
810
|
-
const emit = (method, params) =>
|
|
903
|
+
const emit = (method, params) => {
|
|
904
|
+
const notification = { method, params: { threadId: thread.id, ...params } };
|
|
905
|
+
if (${Boolean(options.holdAfterToolRefusal)} || delayedFinal) log({ notification });
|
|
906
|
+
ws.send(JSON.stringify(notification));
|
|
907
|
+
};
|
|
811
908
|
const callTool = (turn, tool, arguments_, foreign = false) => new Promise((resolve, reject) => {
|
|
812
909
|
const id = "tool-request-" + (++requestId);
|
|
813
910
|
toolRequests.set(id, { resolve, reject });
|
|
911
|
+
if (${Boolean(options.holdAfterToolRefusal)} && tool === "assistant_action_execute") {
|
|
912
|
+
log({ oversizedNativeToolRequest: { id, threadId: thread.id, turnId: turn.id,
|
|
913
|
+
argumentsByteLength: Buffer.byteLength(JSON.stringify(arguments_)), inputValueByteLength: Buffer.byteLength(arguments_.input.value) } });
|
|
914
|
+
}
|
|
915
|
+
if (${Boolean(options.deferBudgetCompletion)} && requestId === 4) {
|
|
916
|
+
state.budgetFailureTurnId = turn.id;
|
|
917
|
+
log({ budgetCompletion: { kind: "fourth-request", threadId: thread.id, turnId: turn.id, callId: turn.id + "-" + requestId } });
|
|
918
|
+
}
|
|
814
919
|
ws.send(JSON.stringify({ id, method: "item/tool/call", params: {
|
|
815
920
|
threadId: foreign ? "another-thread" : thread.id, turnId: turn.id,
|
|
816
921
|
callId: turn.id + "-" + requestId, tool, arguments: arguments_
|
|
@@ -832,7 +937,13 @@ async function fixture(t, options = {}) {
|
|
|
832
937
|
await callTool(turn, "assistant_action_execute", { actionId: "numbers.read", version: 1, input: {} });
|
|
833
938
|
} catch (error) {
|
|
834
939
|
turn.status = "failed"; save();
|
|
835
|
-
|
|
940
|
+
const failedTurn = { ...turn, error: { message: error.message } };
|
|
941
|
+
if (${Boolean(options.deferBudgetCompletion)} && state.budgetFailureTurnId === turn.id && /application tool-call limit/.test(error.message)) {
|
|
942
|
+
log({ budgetCompletion: { kind: "failed-held", threadId: thread.id, turnId: turn.id } });
|
|
943
|
+
while (!existsSync(file + ".budget-terminal-" + turn.id) && ws.readyState === 1) await new Promise(resolve => setTimeout(resolve, 5));
|
|
944
|
+
log({ budgetCompletion: { kind: "failed-released", threadId: thread.id, turnId: turn.id } });
|
|
945
|
+
}
|
|
946
|
+
if (ws.readyState === 1) emit("turn/completed", { turnId: turn.id, turn: failedTurn });
|
|
836
947
|
return;
|
|
837
948
|
}
|
|
838
949
|
}
|
|
@@ -866,6 +977,21 @@ async function fixture(t, options = {}) {
|
|
|
866
977
|
trustStatus: trusted ? "trusted" : "untrusted", command: JSON.parse(flag.match(/command=(.*),timeout=30/u)[1]) });
|
|
867
978
|
return reply({ data: [{ cwd: params.cwds[0], hooks, errors: [] }] });
|
|
868
979
|
}
|
|
980
|
+
if (delayedFinal && ["thread/read", "thread/turns/list"].includes(method)) {
|
|
981
|
+
const full = method === "thread/read" ? params.includeTurns !== false : params.itemsView === "full";
|
|
982
|
+
const delayMs = full ? state.completionReadDelayMs || 0 : 0;
|
|
983
|
+
if (delayMs) {
|
|
984
|
+
// Freeze the actual stale full-history result BEFORE its slow read.
|
|
985
|
+
const snapshot = structuredClone(method === "thread/read" ? { thread: { ...thread,
|
|
986
|
+
status: { type: !state.loaded ? "notLoaded" : state.runningTurn?.status === "inProgress" ? "active" : "idle" } } }
|
|
987
|
+
: { data: [...thread.turns].reverse(), nextCursor: null });
|
|
988
|
+
state.completionReadDelayMs = 0;
|
|
989
|
+
log({ historyReadHeld: { threadId: thread.id, turnId: state.runningTurn.id, delayMs } });
|
|
990
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
991
|
+
log({ historyReadReturned: { threadId: thread.id, turnId: state.runningTurn.id } });
|
|
992
|
+
return reply(snapshot);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
869
995
|
if (method === "thread/start") {
|
|
870
996
|
thread = { id: randomUUID(), historyMode: "paginated", modelProvider: params.modelProvider,
|
|
871
997
|
environment: params.config.shell_environment_policy.set,
|
|
@@ -873,6 +999,8 @@ async function fixture(t, options = {}) {
|
|
|
873
999
|
state.thread = thread; state.loaded = true; threads.set(thread.id, state); save();
|
|
874
1000
|
return reply({ thread, modelProvider: thread.modelProvider });
|
|
875
1001
|
}
|
|
1002
|
+
if (method === "thread/loaded/list") return reply({ data: [...threads.values()].filter(state => state.loaded).map(state => state.thread.id), nextCursor: null });
|
|
1003
|
+
if (method === "thread/read" && existsSync(file + ".unknown-provider")) return reply({ thread: { id: thread.id, status: { type: "idle" } } });
|
|
876
1004
|
if (method === "thread/read") return reply({ thread: { ...thread,
|
|
877
1005
|
status: { type: !state.loaded ? "notLoaded" : state.runningTurn?.status === "inProgress" ? "active" : "idle" } } });
|
|
878
1006
|
if (method === "thread/turns/list") return reply({ data: [...thread.turns].reverse(), nextCursor: null });
|
|
@@ -894,12 +1022,24 @@ async function fixture(t, options = {}) {
|
|
|
894
1022
|
return reply({});
|
|
895
1023
|
}
|
|
896
1024
|
if (method === "thread/goal/get") return reply({ goal: thread?.goal || null });
|
|
897
|
-
if (method === "thread/unsubscribe") { state.loaded = false; return reply({ status: "unsubscribed" }); }
|
|
1025
|
+
if (method === "thread/unsubscribe") { if (!existsSync(file + ".retained-subscriber")) state.loaded = false; return reply({ status: "unsubscribed" }); }
|
|
898
1026
|
if (method === "turn/interrupt") {
|
|
1027
|
+
if (existsSync(file + ".refuse-interrupt")) {
|
|
1028
|
+
ws.send(JSON.stringify({ id, error: { code: -32602, message: "Controlled native interrupt refusal" } }));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
899
1031
|
if (state.runningTurn?.id === params.turnId) state.runningTurn.status = "interrupted";
|
|
900
1032
|
save();
|
|
901
1033
|
reply({});
|
|
902
|
-
if (state.runningTurn?.id === params.turnId)
|
|
1034
|
+
if (state.runningTurn?.id === params.turnId) {
|
|
1035
|
+
const interruptedTurn = { ...state.runningTurn };
|
|
1036
|
+
if (${Boolean(options.deferBudgetCompletion)} && state.budgetFailureTurnId === params.turnId) {
|
|
1037
|
+
log({ budgetCompletion: { kind: "interrupted-held", threadId: thread.id, turnId: params.turnId } });
|
|
1038
|
+
while (!existsSync(file + ".budget-terminal-" + params.turnId) && ws.readyState === 1) await new Promise(resolve => setTimeout(resolve, 5));
|
|
1039
|
+
log({ budgetCompletion: { kind: "interrupted-released", threadId: thread.id, turnId: params.turnId } });
|
|
1040
|
+
}
|
|
1041
|
+
if (ws.readyState === 1) emit("turn/completed", { turn: interruptedTurn });
|
|
1042
|
+
}
|
|
903
1043
|
return;
|
|
904
1044
|
}
|
|
905
1045
|
if (method === "thread/goal/clear") {
|
|
@@ -970,15 +1110,35 @@ async function fixture(t, options = {}) {
|
|
|
970
1110
|
}
|
|
971
1111
|
if (text === "failed") { turn.status = "failed"; save(); emitTurn("turn/completed", { turn: { ...turn, error: { message: "Model unavailable" } } }); return; }
|
|
972
1112
|
if (text === "disconnect") { setTimeout(() => ws.close(), 50); return; }
|
|
1113
|
+
if (delayedFinal) {
|
|
1114
|
+
// The REAL query-contract request joins active.inputReady; it never
|
|
1115
|
+
// invokes numbers.read or simulates native/application admission.
|
|
1116
|
+
await callTool(turn, "assistant_action_contract", { actionId: "numbers.read", version: 1 });
|
|
1117
|
+
state.completionReadDelayMs = delayedFinal.readDelayMs;
|
|
1118
|
+
turn.status = "completed"; save();
|
|
1119
|
+
emitTurn("turn/completed", { turn: { id: turn.id, status: turn.status } });
|
|
1120
|
+
setTimeout(() => {
|
|
1121
|
+
if (ws.readyState !== 1) return;
|
|
1122
|
+
const answer = { id: "answer", type: "agentMessage", phase: "final_answer", text: delayedFinal.text };
|
|
1123
|
+
turn.items.push(answer); save();
|
|
1124
|
+
emitTurn("item/completed", { item: answer });
|
|
1125
|
+
}, delayedFinal.delayMs);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
973
1128
|
let toolAnswer;
|
|
974
1129
|
if (text === "tools" || text === "foreign-tool") {
|
|
975
1130
|
const invoke = (tool, arguments_) => callTool(turn, tool, arguments_, text === "foreign-tool");
|
|
976
1131
|
try {
|
|
977
1132
|
await invoke("assistant_action_search", { query: "numbers" });
|
|
978
1133
|
await invoke("assistant_action_contract", { actionId: "numbers.read", version: 1 });
|
|
979
|
-
const result = await invoke("assistant_action_execute", { actionId: "numbers.read", version: 1,
|
|
1134
|
+
const result = await invoke("assistant_action_execute", { actionId: "numbers.read", version: 1,
|
|
1135
|
+
input: ${JSON.stringify(options.oversizedNativeToolInput || {})} });
|
|
980
1136
|
toolAnswer = result.contentItems[0].text;
|
|
981
1137
|
} catch (error) {
|
|
1138
|
+
if (${Boolean(options.holdAfterToolRefusal)}) {
|
|
1139
|
+
log({ heldToolRefusal: { threadId: thread.id, turnId: turn.id, message: error.message } });
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
982
1142
|
turn.status = "failed"; save();
|
|
983
1143
|
if (ws.readyState === 1) emitTurn("turn/completed", { turn: { ...turn, error: { message: error.message } } });
|
|
984
1144
|
return;
|
|
@@ -998,8 +1158,9 @@ async function fixture(t, options = {}) {
|
|
|
998
1158
|
emitTurn("item/started", { item: { id: "answer", type: "agentMessage", phase } });
|
|
999
1159
|
emitTurn("item/reasoning/summaryTextDelta", { itemId: "reasoning", delta: "Reasoning" });
|
|
1000
1160
|
emitTurn("item/reasoning/summaryTextDelta", { itemId: "reasoning", delta: " summary" });
|
|
1001
|
-
|
|
1002
|
-
|
|
1161
|
+
const answerText = toolAnswer || (structuredResponse ? JSON.stringify(structuredResponse) : "Answer: " + text);
|
|
1162
|
+
emitTurn("item/agentMessage/delta", { itemId: "answer", delta: structuredResponse ? answerText.slice(0, 8) : "Answer: " });
|
|
1163
|
+
const answer = { id: "answer", type: "agentMessage", phase, text: answerText };
|
|
1003
1164
|
turn.items.push(answer);
|
|
1004
1165
|
emitTurn("item/completed", { item: answer });
|
|
1005
1166
|
if (split) {
|
|
@@ -1022,7 +1183,17 @@ async function fixture(t, options = {}) {
|
|
|
1022
1183
|
const disk = createFileConversationStorage({ directory: path.join(directory, "storage") });
|
|
1023
1184
|
const storageFile = path.join(directory, "storage", `${createHash("sha256").update("conversation").digest("hex")}.json`);
|
|
1024
1185
|
const checkpoints = [];
|
|
1025
|
-
const
|
|
1186
|
+
const originalBacking = options.storage ? options.storage(disk) : disk;
|
|
1187
|
+
const backing = options.deferBudgetCompletion ? { read: originalBacking.read, async write(scope, callback) {
|
|
1188
|
+
const result = await originalBacking.write(scope, callback);
|
|
1189
|
+
const run = (await disk.read(scope, transaction => transaction.readMetadata())).runtime?.binding?.codexAppServerRun;
|
|
1190
|
+
if (scope === "conversation" && run?.outerTurnId === "bounded-goal" && run.providerThreadId && run.providerTurnId &&
|
|
1191
|
+
run.active === false && ["interrupted", "failed"].includes(run.state)) {
|
|
1192
|
+
await writeFile(path.join(directory, "history.json") + ".budget-terminal-" + run.providerTurnId,
|
|
1193
|
+
JSON.stringify({ threadId: run.providerThreadId, turnId: run.providerTurnId, outerTurnId: run.outerTurnId }));
|
|
1194
|
+
}
|
|
1195
|
+
return result;
|
|
1196
|
+
} } : originalBacking;
|
|
1026
1197
|
const storage = options.checkpoints ? { read: backing.read, async write(scope, callback) {
|
|
1027
1198
|
const result = await backing.write(scope, callback);
|
|
1028
1199
|
checkpoints.push(JSON.parse(await readFile(storageFile, "utf8")));
|
|
@@ -1742,7 +1913,8 @@ test("steering resolves current attachment access and saves only its authorized
|
|
|
1742
1913
|
test("an uncertain application effect cannot become successful when steering has already produced an answer", async t => {
|
|
1743
1914
|
const entered = Promise.withResolvers(), complete = Promise.withResolvers();
|
|
1744
1915
|
const actions = applicationActions(async () => { entered.resolve(); return complete.promise; });
|
|
1745
|
-
const f = await fixture(t, { actions,
|
|
1916
|
+
const f = await fixture(t, { actions, limits: { admissionTimeoutMs: 30_000 },
|
|
1917
|
+
context: { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] } });
|
|
1746
1918
|
await f.conversation.send({ messageId: "original-tools", text: "tools" });
|
|
1747
1919
|
await entered.promise;
|
|
1748
1920
|
await f.conversation.send({ messageId: "new-instruction", text: "Continue differently", steer: true });
|
|
@@ -2471,7 +2643,7 @@ async function boundCodexToolsFixture(t, options = {}) {
|
|
|
2471
2643
|
for (const cleanup of cleanups) await cleanup();
|
|
2472
2644
|
}
|
|
2473
2645
|
});
|
|
2474
|
-
const f = await fixture({ after: callback => cleanups.push(callback) });
|
|
2646
|
+
const f = await fixture({ after: callback => cleanups.push(callback) }, { limits: { admissionTimeoutMs: 30_000, ...options.limits } });
|
|
2475
2647
|
await f.conversation.send({ messageId: "original-process-seed", text: "Hello" });
|
|
2476
2648
|
await f.conversation.wait();
|
|
2477
2649
|
assert.ok(sharedProvider);
|
|
@@ -2575,6 +2747,7 @@ async function boundCodexToolsFixture(t, options = {}) {
|
|
|
2575
2747
|
preparation: { cleanup: () => ({}) } }
|
|
2576
2748
|
});
|
|
2577
2749
|
}
|
|
2750
|
+
f.driverOptions.limits = { admissionTimeoutMs: 2000, ...options.limits };
|
|
2578
2751
|
const runtime = createConversationRuntime({ engine: "codex", toolCatalog: catalog, authorize: () => true,
|
|
2579
2752
|
host: { ...f.driverOptions.host, conversation: ({ id }) => bindings.get(id) } });
|
|
2580
2753
|
boundRuntime = runtime;
|
|
@@ -2790,3 +2963,1075 @@ test("closing a bound native owner refuses tool callbacks before and after input
|
|
|
2790
2963
|
await f.owner.notificationQueue.drain("bound-two");
|
|
2791
2964
|
assert.equal(f.sharedProvider.threadRequestHandlers.size, 0);
|
|
2792
2965
|
});
|
|
2966
|
+
|
|
2967
|
+
|
|
2968
|
+
test("provider retirement unloads only matching native threads and retains the shared server, peer goal and account binding", async t => {
|
|
2969
|
+
const f = await fixture(t);
|
|
2970
|
+
const goal = await f.conversation.updateGoal({ action: "set", objective: "wait goal", expectedSegmentId: (await f.conversation.read()).segmentId });
|
|
2971
|
+
const owner = createCodexAppServerProviderOwner({ runtimeRoot: f.directory });
|
|
2972
|
+
const provider = [...owner.ownedRuntimes.values()][0].provider;
|
|
2973
|
+
const peerBinding = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding);
|
|
2974
|
+
const originalParams = (await f.trace()).find(row => row.method === "thread/start").params;
|
|
2975
|
+
const selectedParams = { ...originalParams, modelProvider: "deepseek", config: { ...originalParams.config,
|
|
2976
|
+
"model_providers.deepseek": { experimental_bearer_token: "selected-old-fixture", requires_openai_auth: false } } };
|
|
2977
|
+
const { thread } = await provider.client.request("thread/start", selectedParams);
|
|
2978
|
+
const result = await owner.invalidateRuntimes({ includeOwned: true, modelProviderId: "deepseek" });
|
|
2979
|
+
assert.equal(result.ok, true, JSON.stringify(result));
|
|
2980
|
+
assert.equal(result.stopped, 1);
|
|
2981
|
+
assert.deepEqual(result.results[0].threadIds, [thread.id]);
|
|
2982
|
+
assert.equal(result.results[0].sharedProcessRetained, true);
|
|
2983
|
+
assert.equal((await provider.client.request("thread/read", { threadId: thread.id, includeTurns: false })).thread.status.type, "notLoaded");
|
|
2984
|
+
assert.equal((await f.conversation.read()).status, "working");
|
|
2985
|
+
assert.equal((await f.conversation.readGoal()).id, goal.id);
|
|
2986
|
+
assert.equal((await f.conversation.readGoal()).status, "active");
|
|
2987
|
+
assert.deepEqual(await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding), peerBinding);
|
|
2988
|
+
const nextParams = { ...selectedParams, threadId: thread.id, config: { ...selectedParams.config,
|
|
2989
|
+
"model_providers.deepseek": { experimental_bearer_token: "selected-new-fixture", requires_openai_auth: false } } };
|
|
2990
|
+
await provider.client.request("thread/resume", nextParams);
|
|
2991
|
+
const trace = await f.trace();
|
|
2992
|
+
assert.equal(trace.filter(row => row.args).length, 1);
|
|
2993
|
+
assert.equal(trace.filter(row => row.method === "thread/resume").at(-1).params.config["model_providers.deepseek"].experimental_bearer_token, "selected-new-fixture");
|
|
2994
|
+
assert.equal(trace.some(row => row.method === "turn/interrupt" && row.params.threadId === peerBinding.threadId), false);
|
|
2995
|
+
assert.equal(trace.filter(row => row.method === "thread/goal/set" && row.params.threadId === peerBinding.threadId).length, 1);
|
|
2996
|
+
await f.conversation.cancel();
|
|
2997
|
+
});
|
|
2998
|
+
|
|
2999
|
+
test("provider retirement fails closed on unknown loaded providers and retained native subscribers", async t => {
|
|
3000
|
+
const f = await fixture(t);
|
|
3001
|
+
await f.conversation.send(input); await f.conversation.wait();
|
|
3002
|
+
const owner = createCodexAppServerProviderOwner({ runtimeRoot: f.directory });
|
|
3003
|
+
const provider = [...owner.ownedRuntimes.values()][0].provider;
|
|
3004
|
+
const originalParams = (await f.trace()).find(row => row.method === "thread/start").params;
|
|
3005
|
+
const { thread } = await provider.client.request("thread/start", { ...originalParams, modelProvider: "deepseek" });
|
|
3006
|
+
const unknown = path.join(f.directory, "history.json.unknown-provider");
|
|
3007
|
+
await writeFile(unknown, "fixture");
|
|
3008
|
+
const rejected = await owner.invalidateRuntimes({ includeOwned: true, modelProviderId: "deepseek" });
|
|
3009
|
+
assert.equal(rejected.ok, false);
|
|
3010
|
+
assert.match(rejected.failed[0].error, /untrustworthy thread\/provider inventory/);
|
|
3011
|
+
assert.equal((await f.trace()).some(row => row.method === "thread/unsubscribe"), false);
|
|
3012
|
+
await rm(unknown);
|
|
3013
|
+
const retained = path.join(f.directory, "history.json.retained-subscriber");
|
|
3014
|
+
await writeFile(retained, "fixture");
|
|
3015
|
+
const blocked = await owner.invalidateRuntimes({ includeOwned: true, modelProviderId: "deepseek" });
|
|
3016
|
+
assert.equal(blocked.ok, false);
|
|
3017
|
+
assert.match(blocked.failed[0].error, /Close the native Codex terminal/);
|
|
3018
|
+
assert.equal(provider.client.isOpen(), true);
|
|
3019
|
+
assert.equal((await provider.client.request("thread/read", { threadId: thread.id, includeTurns: false })).thread.status.type, "idle");
|
|
3020
|
+
await rm(retained);
|
|
3021
|
+
assert.equal((await owner.invalidateRuntimes({ includeOwned: true, modelProviderId: "deepseek" })).ok, true);
|
|
3022
|
+
assert.equal((await f.trace()).filter(row => row.args).length, 1);
|
|
3023
|
+
});
|
|
3024
|
+
|
|
3025
|
+
test("provider retirement cannot skip an owned native runtime absent from cached consumers", async t => {
|
|
3026
|
+
const root = await mkdtemp(path.join(tmpdir(), "jskit-provider-retirement-"));
|
|
3027
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
3028
|
+
const owner = createCodexAppServerProviderOwner({ runtimeRoot: root });
|
|
3029
|
+
const provider = { runtime: { pid: 123 }, client: null };
|
|
3030
|
+
owner.rememberOwnedRuntime({ provider, providerKey: "retained-owned", providerOptions: { runtimeDir: path.join(root, "native") } });
|
|
3031
|
+
t.after(() => owner.forgetOwnedRuntime(provider));
|
|
3032
|
+
const result = await owner.invalidateRuntimes({ includeOwned: true, modelProviderId: "deepseek" });
|
|
3033
|
+
assert.equal(result.ok, false);
|
|
3034
|
+
assert.match(result.failed[0].error, /existing control connection is unavailable/);
|
|
3035
|
+
assert.equal([...owner.ownedRuntimes.values()].length, 1);
|
|
3036
|
+
});
|
|
3037
|
+
|
|
3038
|
+
|
|
3039
|
+
for (const mixed of [false, true]) test(`dedicated provider home ${mixed ? "refuses mixed-provider threads without stopping" : "uses its existing verified process retirement"}`, async t => {
|
|
3040
|
+
const root = await mkdtemp(path.join(tmpdir(), "jskit-dedicated-provider-"));
|
|
3041
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
3042
|
+
const owner = createCodexAppServerProviderOwner({ runtimeRoot: root });
|
|
3043
|
+
const unrelated = createCodexAppServerProviderOwner({ runtimeRoot: `${root}-other` });
|
|
3044
|
+
let stops = 0, unrelatedStops = 0, loaded = true;
|
|
3045
|
+
const home = path.join(root, "provider-home");
|
|
3046
|
+
const providerOptions = { runtimeDir: path.join(root, "native"), toolHomeSource: home };
|
|
3047
|
+
const provider = owner.createProvider({ providerKey: "dedicated", providerOptions, create: () => ({
|
|
3048
|
+
client: { isOpen: () => true, async request(method, params) {
|
|
3049
|
+
if (method === "thread/loaded/list") return { data: mixed ? ["selected", "unrelated"] : ["selected"], nextCursor: null };
|
|
3050
|
+
if (method === "thread/read") return { thread: { id: params.threadId,
|
|
3051
|
+
modelProvider: params.threadId === "selected" ? "deepseek" : "openai", status: { type: loaded ? "idle" : "notLoaded" } } };
|
|
3052
|
+
if (method === "thread/unsubscribe") { loaded = false; return { status: "unsubscribed" }; }
|
|
3053
|
+
assert.fail(method);
|
|
3054
|
+
} },
|
|
3055
|
+
async stopThreadForObservationLoss() {},
|
|
3056
|
+
async stopRuntime() { stops++; return { stopped: true }; }, close() {}
|
|
3057
|
+
}) });
|
|
3058
|
+
await owner.acquireRuntime({ providerKey: "dedicated", providerOptions, provider, operation: async () => {} });
|
|
3059
|
+
const peer = { async stopRuntime() { unrelatedStops++; return { stopped: true }; }, close() {} };
|
|
3060
|
+
unrelated.rememberOwnedRuntime({ provider: peer, providerKey: "unrelated", providerOptions: { runtimeDir: `${root}-other/native` } });
|
|
3061
|
+
t.after(() => { owner.forgetOwnedRuntime(provider); owner.closeProvider("dedicated"); unrelated.forgetOwnedRuntime(peer); });
|
|
3062
|
+
const result = await owner.invalidateRuntimes({ includeOwned: true, modelProviderId: "deepseek", toolHomeSource: home });
|
|
3063
|
+
assert.equal(result.ok, !mixed, JSON.stringify(result));
|
|
3064
|
+
assert.equal(stops, mixed ? 0 : 1);
|
|
3065
|
+
assert.equal(unrelatedStops, 0);
|
|
3066
|
+
if (mixed) { assert.match(result.failed[0].error, /another provider thread/); assert.equal(loaded, true); }
|
|
3067
|
+
else { assert.equal(result.results[0].dedicatedRuntimeRetired, true); assert.equal(result.results[0].sharedProcessRetained, false); }
|
|
3068
|
+
});
|
|
3069
|
+
|
|
3070
|
+
// The original supplied Main owner keeps its own canonical policy. A runtime
|
|
3071
|
+
// cannot opt that shared owner out of persistence or replace its process.
|
|
3072
|
+
test("supplied Codex commentary remains host-owned and refuses a runtime opt-out before opening", async t => {
|
|
3073
|
+
const f = await boundCodexToolsFixture(t);
|
|
3074
|
+
await f.one.send({ messageId: "host-policy-default", text: "blocks" });
|
|
3075
|
+
const first = await f.one.wait();
|
|
3076
|
+
assert.deepEqual(first.conversationLog.flatMap(turn => turn.commentary || []).map(message => message.text), ["Checking the numbers."]);
|
|
3077
|
+
const nativeThreadId = await f.bindings.get("bound-one").identity.read();
|
|
3078
|
+
const before = await f.trace();
|
|
3079
|
+
const originalOwner = f.bindings.get("bound-one").native.runOwner;
|
|
3080
|
+
const refused = createConversationRuntime({ engine: "codex", authorize: () => true, persistCommentary: false,
|
|
3081
|
+
host: { ...f.driverOptions.host, conversation: ({ id }) => f.bindings.get(id) } });
|
|
3082
|
+
t.after(() => refused.close());
|
|
3083
|
+
await assert.rejects(refused.open({ id: "bound-one" }), /runtime-owned transcript/);
|
|
3084
|
+
assert.deepEqual(await f.trace(), before, "refusal opens no native thread and issues no request or interrupt");
|
|
3085
|
+
assert.equal(f.bindings.get("bound-one").native.runOwner, originalOwner);
|
|
3086
|
+
await f.one.send({ messageId: "host-policy-continue", text: "Continue" });
|
|
3087
|
+
const continued = await f.one.wait();
|
|
3088
|
+
assert.equal(continued.conversationLog.at(-1).assistant.text, "Answer: Continue");
|
|
3089
|
+
assert.equal(await f.bindings.get("bound-one").identity.read(), nativeThreadId);
|
|
3090
|
+
assert.deepEqual(f.effects, []);
|
|
3091
|
+
});
|
|
3092
|
+
|
|
3093
|
+
|
|
3094
|
+
test("default Codex commentary publishes its saved output identity rather than a second transient completion", async t => {
|
|
3095
|
+
const f = await fixture(t);
|
|
3096
|
+
const events = [];
|
|
3097
|
+
await f.conversation.subscribe(event => events.push(event));
|
|
3098
|
+
await f.conversation.send({ messageId: "default-carrier-dedup", text: "blocks" });
|
|
3099
|
+
const state = await f.conversation.wait();
|
|
3100
|
+
const progress = state.conversationLog.flatMap(turn => turn.commentary || []);
|
|
3101
|
+
assert.equal(progress.length, 1);
|
|
3102
|
+
assert.equal(progress[0].text, "Checking the numbers.");
|
|
3103
|
+
assert.notEqual(progress[0].messageId, progress[0].outputId);
|
|
3104
|
+
const completed = events.filter(event => event.type === "message" && event.role === "commentary" && event.status === "complete");
|
|
3105
|
+
assert.ok(completed.length);
|
|
3106
|
+
assert.equal(completed.every(event => event.messageId === progress[0].messageId), true,
|
|
3107
|
+
"The existing canonical publication owns this exact output; its native stream must not complete a second identity");
|
|
3108
|
+
assert.equal(completed.every(event => event.outputId === progress[0].outputId && event.text === progress[0].text), true);
|
|
3109
|
+
assert.equal((await f.conversation.read()).streaming.completedMessages, undefined);
|
|
3110
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3111
|
+
});
|
|
3112
|
+
|
|
3113
|
+
|
|
3114
|
+
for (const text of ["blocks", "blocks-without-phase"]) test(`transient Codex completed ${text} waits for its exact accepted publication without replay`, async t => {
|
|
3115
|
+
let runtime;
|
|
3116
|
+
t.after(() => runtime?.close());
|
|
3117
|
+
const f = await fixture(t);
|
|
3118
|
+
await f.first.close();
|
|
3119
|
+
runtime = createConversationRuntime({ engine: "codex", storage: f.storage, authorize: () => true,
|
|
3120
|
+
persistCommentary: false, host: f.driverOptions.host, limits: f.driverOptions.limits });
|
|
3121
|
+
const conversation = await runtime.open({ id: "conversation" });
|
|
3122
|
+
const events = [];
|
|
3123
|
+
await conversation.subscribe(event => events.push(event));
|
|
3124
|
+
const receipt = await conversation.send({ messageId: "transient-admission-order", text });
|
|
3125
|
+
const state = await conversation.wait();
|
|
3126
|
+
assert.equal(state.status, "ready", state.error);
|
|
3127
|
+
const accepted = events.findIndex(event => event.type === "accepted" && event.messageId === receipt.messageId);
|
|
3128
|
+
assert.ok(accepted >= 0);
|
|
3129
|
+
const progress = events.filter(event => event.type === "message" && event.status === "complete" &&
|
|
3130
|
+
event.role === "commentary" && event.text === "Checking the numbers.");
|
|
3131
|
+
assert.equal(progress.length, 1);
|
|
3132
|
+
assert.ok(events.indexOf(progress[0]) > accepted, "The exact accepted event must precede this completed native carrier");
|
|
3133
|
+
assert.equal(progress[0].turnId, receipt.turnId);
|
|
3134
|
+
assert.equal(progress[0].origin, "user");
|
|
3135
|
+
assert.deepEqual(state.conversationLog.flatMap(turn => turn.commentary || []), []);
|
|
3136
|
+
assert.deepEqual(state.conversationLog.filter(turn => turn.assistant).map(turn => turn.assistant.text),
|
|
3137
|
+
[`Answer: ${text}`, "The second paragraph."]);
|
|
3138
|
+
const count = events.length;
|
|
3139
|
+
assert.equal((await conversation.read()).streaming.completedMessages, undefined);
|
|
3140
|
+
assert.equal((await conversation.read()).streaming.completedMessages, undefined);
|
|
3141
|
+
assert.equal(events.length, count, "Ordinary reads cannot replay completed progress");
|
|
3142
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3143
|
+
});
|
|
3144
|
+
|
|
3145
|
+
|
|
3146
|
+
for (const ending of ["steer", "close"]) test(`transient native completion declines ${ending} after subscriber authorization starts`, { timeout: 10_000 }, async t => {
|
|
3147
|
+
const authorization = Promise.withResolvers();
|
|
3148
|
+
const releaseAuthorization = Promise.withResolvers();
|
|
3149
|
+
t.after(() => releaseAuthorization.resolve());
|
|
3150
|
+
let runtime;
|
|
3151
|
+
t.after(() => runtime?.close());
|
|
3152
|
+
const f = await fixture(t);
|
|
3153
|
+
await f.first.close();
|
|
3154
|
+
const command = path.join(f.directory, "codex.mjs");
|
|
3155
|
+
const original = await readFile(command, "utf8");
|
|
3156
|
+
const marker = 'emitTurn("item/completed", { item: progress });';
|
|
3157
|
+
assert.equal(original.split(marker).length, 2, "The original generated progress completion is transformed exactly once");
|
|
3158
|
+
const gated = original.replace(marker, `
|
|
3159
|
+
while (!existsSync(file + ".r07-release-completion") && ws.readyState === 1) await new Promise(resolve => setTimeout(resolve, 5));
|
|
3160
|
+
${marker}
|
|
3161
|
+
while (!existsSync(file + ".r07-release-final") && ws.readyState === 1) await new Promise(resolve => setTimeout(resolve, 5));`);
|
|
3162
|
+
const [prefix, suffix] = original.split(marker);
|
|
3163
|
+
assert.equal(gated.startsWith(prefix), true);
|
|
3164
|
+
assert.equal(gated.endsWith(suffix), true, "All other original native command behavior remains unchanged");
|
|
3165
|
+
await writeFile(command, gated);
|
|
3166
|
+
let armed = false, claimed = false;
|
|
3167
|
+
runtime = createConversationRuntime({ engine: "codex", storage: f.storage, persistCommentary: false,
|
|
3168
|
+
host: f.driverOptions.host, limits: f.driverOptions.limits, async authorize({ operation }) {
|
|
3169
|
+
if (operation === "subscribe" && armed && !claimed) {
|
|
3170
|
+
claimed = true;
|
|
3171
|
+
authorization.resolve();
|
|
3172
|
+
await releaseAuthorization.promise;
|
|
3173
|
+
}
|
|
3174
|
+
return true;
|
|
3175
|
+
} });
|
|
3176
|
+
const conversation = await runtime.open({ id: "conversation" });
|
|
3177
|
+
const events = [];
|
|
3178
|
+
const firstDelta = Promise.withResolvers();
|
|
3179
|
+
const initialWorking = Promise.withResolvers();
|
|
3180
|
+
await conversation.subscribe(event => {
|
|
3181
|
+
events.push(event);
|
|
3182
|
+
if (event.type === "phase" && event.phase === "working") initialWorking.resolve();
|
|
3183
|
+
if (event.type === "message" && event.status === "inProgress" && event.text === "Checking the numbers.") firstDelta.resolve();
|
|
3184
|
+
});
|
|
3185
|
+
const first = await conversation.send({ messageId: "predecessor-race", text: "blocks" });
|
|
3186
|
+
await firstDelta.promise;
|
|
3187
|
+
await initialWorking.promise;
|
|
3188
|
+
assert.equal(events.some(event => event.type === "accepted" && event.messageId === first.messageId), true);
|
|
3189
|
+
// The original accepted and working events have been published. Allow their
|
|
3190
|
+
// synchronous authorization continuation to finish the delivery commit.
|
|
3191
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
3192
|
+
armed = true;
|
|
3193
|
+
await writeFile(path.join(f.directory, "history.json.r07-release-completion"), "release");
|
|
3194
|
+
await authorization.promise;
|
|
3195
|
+
if (ending === "steer") {
|
|
3196
|
+
const successor = await conversation.send({ messageId: "successor-race", text: "Keep the successor", steer: true });
|
|
3197
|
+
assert.notEqual(successor.turnId, first.turnId);
|
|
3198
|
+
releaseAuthorization.resolve();
|
|
3199
|
+
const state = await conversation.wait();
|
|
3200
|
+
assert.equal(state.status, "ready", state.error);
|
|
3201
|
+
assert.equal(events.some(event => event.type === "accepted" && event.messageId === "successor-race"), true);
|
|
3202
|
+
assert.equal(events.some(event => event.type === "message" && event.status === "complete" &&
|
|
3203
|
+
event.turnId === successor.turnId && event.text === "Steered: Keep the successor"), true,
|
|
3204
|
+
"Declining the sealed carrier retains the legitimate successor subscription");
|
|
3205
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/steer").length, 1);
|
|
3206
|
+
} else {
|
|
3207
|
+
const closing = runtime.close();
|
|
3208
|
+
releaseAuthorization.resolve();
|
|
3209
|
+
await closing;
|
|
3210
|
+
await assert.rejects(conversation.read(), { code: "conversation_closed" });
|
|
3211
|
+
}
|
|
3212
|
+
assert.equal(events.some(event => event.type === "message" && event.status === "complete" &&
|
|
3213
|
+
event.role === "commentary" && event.text === "Checking the numbers."), false,
|
|
3214
|
+
"Subscriber authorization must not deliver the stale completed carrier after its owner changes");
|
|
3215
|
+
});
|
|
3216
|
+
|
|
3217
|
+
// The original controlled app-server owns every generated thread and turn below.
|
|
3218
|
+
const completedCodexValue = { kind: "tool", text: "Checking.", toolName: "numbers_read", arguments: "{}" };
|
|
3219
|
+
const completedCodexSchema = { type: "object", additionalProperties: false,
|
|
3220
|
+
required: ["kind", "text", "toolName", "arguments"],
|
|
3221
|
+
properties: { kind: { type: "string", enum: ["reply", "tool"] }, text: { type: "string", maxLength: 64 },
|
|
3222
|
+
toolName: { type: "string", maxLength: 64 }, arguments: { type: "string", maxLength: 64 } } };
|
|
3223
|
+
async function completedCodexFixture(t, options = {}) {
|
|
3224
|
+
const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
|
|
3225
|
+
let effects = 0;
|
|
3226
|
+
const appendedRows = [];
|
|
3227
|
+
const actions = applicationActions(async () => { effects++; return { value: 42 }; });
|
|
3228
|
+
const f = await fixture(t, { ...options, context, actions, structuredResponse: completedCodexValue,
|
|
3229
|
+
configuration: { ...configuration, outputSchema: completedCodexSchema, ...options.configuration },
|
|
3230
|
+
storage: disk => ({ ...disk, write: (id, callback) => disk.write(id, transaction => callback({ ...transaction,
|
|
3231
|
+
async appendMessage(turnId, message) {
|
|
3232
|
+
await transaction.appendMessage(turnId, message);
|
|
3233
|
+
if (message.role === "system" && message.origin === "application") {
|
|
3234
|
+
appendedRows.push(structuredClone(await transaction.readTurn(turnId)));
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
})) }) });
|
|
3238
|
+
await f.first.close();
|
|
3239
|
+
let runtime;
|
|
3240
|
+
let conversation;
|
|
3241
|
+
async function reopen() {
|
|
3242
|
+
if (runtime) await runtime.close();
|
|
3243
|
+
runtime = createConversationRuntime({ engine: "codex", storage: f.storage, actions,
|
|
3244
|
+
authorize: () => true, completedEnvelope: true, ...f.driverOptions });
|
|
3245
|
+
try { conversation = await runtime.open({ id: "conversation", context }); }
|
|
3246
|
+
catch (error) { await runtime.close(); throw error; }
|
|
3247
|
+
return conversation;
|
|
3248
|
+
}
|
|
3249
|
+
await reopen();
|
|
3250
|
+
const controller = new AbortController();
|
|
3251
|
+
return { ...f, controller, appendedRows, context, actions, effects: () => effects, reopen,
|
|
3252
|
+
get conversation() { return conversation; }, close: () => runtime.close(),
|
|
3253
|
+
async complete(messageId = "completed-codex-response") {
|
|
3254
|
+
const receipt = await conversation.wake({ messageId, text: "Read a number" });
|
|
3255
|
+
const state = await conversation.wait();
|
|
3256
|
+
const turn = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3257
|
+
assert.equal(turn.metadata.runtime.status, "complete", state.error);
|
|
3258
|
+
assert.equal(turn.assistant.text, JSON.stringify(completedCodexValue));
|
|
3259
|
+
return { messageId, turnId: receipt.turnId };
|
|
3260
|
+
},
|
|
3261
|
+
prepare: receipt => conversation.prepareCompletedResponse(receipt, { signal: controller.signal }) };
|
|
3262
|
+
}
|
|
3263
|
+
const executeCompletedCodex = (prepared, argumentsText = "{}") => prepared.tools.execute({
|
|
3264
|
+
id: prepared.toolCallId, name: "numbers_read", arguments: argumentsText
|
|
3265
|
+
});
|
|
3266
|
+
|
|
3267
|
+
test("completed Codex canonical admission preserves its trusted marker and actual native completed tuple", async t => {
|
|
3268
|
+
const f = await completedCodexFixture(t);
|
|
3269
|
+
try {
|
|
3270
|
+
const receipt = await f.complete();
|
|
3271
|
+
const firstAppend = f.appendedRows.find(row => row.system.messageId === receipt.messageId);
|
|
3272
|
+
assert.ok(firstAppend, "The actual canonical writer must append the admitted application row");
|
|
3273
|
+
assert.equal(firstAppend.user, null);
|
|
3274
|
+
assert.equal(firstAppend.system.origin, "application");
|
|
3275
|
+
assert.equal(firstAppend.metadata.runtime.completedEnvelope, true);
|
|
3276
|
+
assert.equal(firstAppend.metadata.runtime.status, "running");
|
|
3277
|
+
const metadata = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3278
|
+
const turn = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3279
|
+
const run = metadata.runtime.binding.codexAppServerRun;
|
|
3280
|
+
const savedHistory = JSON.parse(await readFile(path.join(f.directory, "history.json"), "utf8"));
|
|
3281
|
+
const nativeThread = (savedHistory.threads || [savedHistory]).find(row => row.id === metadata.runtime.binding.threadId);
|
|
3282
|
+
const nativeTurn = nativeThread.turns.find(row => row.id === turn.metadata.runtime.nativeTurnId);
|
|
3283
|
+
assert.ok(nativeTurn, "The original app-server must persist this generated native turn");
|
|
3284
|
+
assert.equal(nativeTurn.status, "completed");
|
|
3285
|
+
assert.equal(nativeTurn.items.find(row => row.type === "userMessage").clientId, receipt.messageId);
|
|
3286
|
+
assert.equal(nativeTurn.items.find(row => row.type === "agentMessage").text, turn.assistant.text);
|
|
3287
|
+
assert.equal(turn.metadata.runtime.completedEnvelope, true);
|
|
3288
|
+
assert.equal(turn.metadata.runtime.origin, "application");
|
|
3289
|
+
assert.equal(turn.metadata.runtime.segmentId, metadata.runtime.segmentId);
|
|
3290
|
+
assert.equal(turn.metadata.runtime.nativeTurnId, firstAppend.metadata.runtime.nativeTurnId);
|
|
3291
|
+
assert.equal(run.outerTurnId, receipt.messageId);
|
|
3292
|
+
assert.equal(run.providerThreadId, nativeThread.id);
|
|
3293
|
+
assert.equal(run.providerTurnId, nativeTurn.id);
|
|
3294
|
+
assert.equal(run.state, "completed");
|
|
3295
|
+
assert.equal(run.active, false);
|
|
3296
|
+
assert.equal(f.effects(), 0);
|
|
3297
|
+
const starts = (await f.trace()).filter(row => row.method === "turn/start");
|
|
3298
|
+
assert.equal(starts.length, 1);
|
|
3299
|
+
assert.equal(starts[0].params.clientUserMessageId, receipt.messageId);
|
|
3300
|
+
assert.equal((await f.trace()).find(row => row.method === "thread/start").params.dynamicTools.length, 0);
|
|
3301
|
+
} finally { await f.close(); }
|
|
3302
|
+
});
|
|
3303
|
+
|
|
3304
|
+
test("completed Codex prepared handles and reopen retain one durable effect without native replay", async t => {
|
|
3305
|
+
const f = await completedCodexFixture(t);
|
|
3306
|
+
try {
|
|
3307
|
+
const receipt = await f.complete();
|
|
3308
|
+
const one = await f.prepare(receipt), two = await f.prepare(receipt);
|
|
3309
|
+
assert.equal(one.text, JSON.stringify(completedCodexValue));
|
|
3310
|
+
assert.equal(one.toolCallId, `${receipt.messageId}:operation`);
|
|
3311
|
+
assert.equal(two.toolCallId, one.toolCallId);
|
|
3312
|
+
const [first, second] = await Promise.all([executeCompletedCodex(one), executeCompletedCodex(two)]);
|
|
3313
|
+
assert.equal(first.ok, true);
|
|
3314
|
+
assert.deepEqual(second, first);
|
|
3315
|
+
assert.equal(f.effects(), 1);
|
|
3316
|
+
await assert.rejects(executeCompletedCodex(two, '{"changed":true}'), /different arguments/);
|
|
3317
|
+
const before = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3318
|
+
assert.equal(before.metadata.applicationTools.length, 1);
|
|
3319
|
+
assert.equal(before.metadata.applicationTools[0].id, one.toolCallId);
|
|
3320
|
+
assert.equal(before.metadata.applicationTools[0].status, "complete");
|
|
3321
|
+
await f.reopen();
|
|
3322
|
+
assert.deepEqual(await executeCompletedCodex(await f.prepare(receipt)), first);
|
|
3323
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), before);
|
|
3324
|
+
assert.equal(f.effects(), 1);
|
|
3325
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3326
|
+
assert.equal((await f.trace()).filter(row => row.method === "thread/start").length, 1);
|
|
3327
|
+
} finally { await f.close(); }
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
test("completed Codex cached bindings cannot mask missing or changed persisted native fingerprints", async t => {
|
|
3331
|
+
const f = await completedCodexFixture(t);
|
|
3332
|
+
try {
|
|
3333
|
+
const receipt = await f.complete();
|
|
3334
|
+
await f.prepare(receipt);
|
|
3335
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3336
|
+
for (const missing of [true, false]) {
|
|
3337
|
+
const changed = structuredClone(before);
|
|
3338
|
+
if (missing) delete changed.runtime.binding.accountIdentity;
|
|
3339
|
+
else changed.runtime.binding.accountIdentity = "different-persisted-fixture-identity";
|
|
3340
|
+
await f.storage.write("conversation", tx => tx.writeMetadata(changed));
|
|
3341
|
+
try {
|
|
3342
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3343
|
+
await assert.rejects(f.prepare(receipt), missing ? /no saved native account fingerprint/ : /different native account binding/);
|
|
3344
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), changed);
|
|
3345
|
+
}
|
|
3346
|
+
} finally { await f.storage.write("conversation", tx => tx.writeMetadata(before)); }
|
|
3347
|
+
}
|
|
3348
|
+
assert.equal(f.effects(), 0);
|
|
3349
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3350
|
+
} finally { await f.close(); }
|
|
3351
|
+
});
|
|
3352
|
+
|
|
3353
|
+
test("completed Codex cached processes still read the actual native account before an effect", async t => {
|
|
3354
|
+
const f = await completedCodexFixture(t);
|
|
3355
|
+
try {
|
|
3356
|
+
const receipt = await f.complete();
|
|
3357
|
+
await f.prepare(receipt);
|
|
3358
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3359
|
+
const reads = (await f.trace()).filter(row => row.method === "account/read").length;
|
|
3360
|
+
await writeFile(f.account, "another@example.test");
|
|
3361
|
+
try {
|
|
3362
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3363
|
+
await assert.rejects(f.prepare(receipt), /another Codex account/);
|
|
3364
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), before);
|
|
3365
|
+
}
|
|
3366
|
+
assert.ok((await f.trace()).filter(row => row.method === "account/read").length >= reads + 2);
|
|
3367
|
+
assert.equal(f.effects(), 0);
|
|
3368
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3369
|
+
} finally { await writeFile(f.account, "owner@example.test"); }
|
|
3370
|
+
} finally { await f.close(); }
|
|
3371
|
+
});
|
|
3372
|
+
|
|
3373
|
+
test("completed Codex foreign accounts compare persisted cached and freshly resolved identity without promotion", async t => {
|
|
3374
|
+
let apiKey = "completed-codex-original-fixture-key";
|
|
3375
|
+
const f = await completedCodexFixture(t, { configuration: { model: undefined, integrationId: "foreign", effort: "low" },
|
|
3376
|
+
connections: { resolve: async () => ({ providerId: "deepseek", model: "deepseek-flash", apiKey }) } });
|
|
3377
|
+
try {
|
|
3378
|
+
const receipt = await f.complete();
|
|
3379
|
+
await f.prepare(receipt);
|
|
3380
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3381
|
+
for (const missing of [true, false]) {
|
|
3382
|
+
const changed = structuredClone(before);
|
|
3383
|
+
if (missing) delete changed.runtime.binding.connectionIdentities.deepseek;
|
|
3384
|
+
else changed.runtime.binding.connectionIdentities.deepseek = "different-persisted-fixture-identity";
|
|
3385
|
+
await f.storage.write("conversation", tx => tx.writeMetadata(changed));
|
|
3386
|
+
try {
|
|
3387
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3388
|
+
await assert.rejects(f.prepare(receipt), missing ? /no saved provider account fingerprint/ : /different provider account binding/);
|
|
3389
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), changed);
|
|
3390
|
+
}
|
|
3391
|
+
} finally { await f.storage.write("conversation", tx => tx.writeMetadata(before)); }
|
|
3392
|
+
}
|
|
3393
|
+
apiKey = "completed-codex-changed-fixture-key";
|
|
3394
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3395
|
+
await assert.rejects(f.prepare(receipt), /different provider account binding/);
|
|
3396
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), before);
|
|
3397
|
+
}
|
|
3398
|
+
assert.equal(f.effects(), 0);
|
|
3399
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3400
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/interrupt").length, 0);
|
|
3401
|
+
} finally { apiKey = "completed-codex-original-fixture-key"; await f.close(); }
|
|
3402
|
+
});
|
|
3403
|
+
|
|
3404
|
+
test("completed Codex response custody refuses foreign native tuples and host scope before effects", async t => {
|
|
3405
|
+
const f = await completedCodexFixture(t);
|
|
3406
|
+
try {
|
|
3407
|
+
const receipt = await f.complete();
|
|
3408
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3409
|
+
const row = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3410
|
+
for (const alter of [
|
|
3411
|
+
binding => { binding.codexAppServerRun.outerTurnId = "different-authored-request"; },
|
|
3412
|
+
binding => { binding.codexAppServerRun.providerThreadId = "foreign-native-thread"; },
|
|
3413
|
+
binding => { binding.codexAppServerRun.providerTurnId = "foreign-native-turn"; },
|
|
3414
|
+
binding => { binding.codexAppServerRun.active = true; },
|
|
3415
|
+
binding => { binding.codexAppServerRun.state = "failed"; },
|
|
3416
|
+
binding => { binding.workdir = "/different-fixture-workdir"; },
|
|
3417
|
+
binding => { binding.configRoot = "/different-fixture-config-root"; }
|
|
3418
|
+
]) {
|
|
3419
|
+
const changed = structuredClone(before); alter(changed.runtime.binding);
|
|
3420
|
+
await f.storage.write("conversation", tx => tx.writeMetadata(changed));
|
|
3421
|
+
try {
|
|
3422
|
+
await assert.rejects(f.prepare(receipt), /no exact native turn receipt|different native conversation or host scope/);
|
|
3423
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), changed);
|
|
3424
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), row);
|
|
3425
|
+
} finally { await f.storage.write("conversation", tx => tx.writeMetadata(before)); }
|
|
3426
|
+
}
|
|
3427
|
+
await f.storage.write("conversation", tx => tx.updateTurnMetadata(receipt.turnId, {
|
|
3428
|
+
runtime: { ...row.metadata.runtime, nativeTurnId: "foreign-canonical-turn" }
|
|
3429
|
+
}));
|
|
3430
|
+
try { await assert.rejects(f.prepare(receipt), /no exact native turn receipt/); }
|
|
3431
|
+
finally { await f.storage.write("conversation", tx => tx.updateTurnMetadata(receipt.turnId, { runtime: row.metadata.runtime })); }
|
|
3432
|
+
assert.equal(f.effects(), 0);
|
|
3433
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3434
|
+
} finally { await f.close(); }
|
|
3435
|
+
});
|
|
3436
|
+
|
|
3437
|
+
test("completed Codex caller markers do not replace ordinary native discovery or create completion authority", async t => {
|
|
3438
|
+
const f = await completedCodexFixture(t);
|
|
3439
|
+
try {
|
|
3440
|
+
const receipt = await f.conversation.send({ messageId: "ordinary-codex", text: "tools", data: { completedEnvelope: true } });
|
|
3441
|
+
const state = await f.conversation.wait();
|
|
3442
|
+
const turn = state.conversationLog.find(row => row.turnId === receipt.turnId);
|
|
3443
|
+
assert.equal(turn.metadata.runtime.status, "complete", state.error);
|
|
3444
|
+
assert.equal(turn.metadata.runtime.completedEnvelope, undefined);
|
|
3445
|
+
assert.equal(f.effects(), 0, "Caller data does not itself request or authorize an effect");
|
|
3446
|
+
const initialTrace = await f.trace();
|
|
3447
|
+
assert.deepEqual(initialTrace.find(row => row.method === "thread/start").params.dynamicTools.map(tool => tool.name),
|
|
3448
|
+
["assistant_action_search", "assistant_action_contract", "assistant_action_execute"]);
|
|
3449
|
+
assert.match(initialTrace.find(row => row.method === "turn/start").params.input[0].text, /\[Application data\]/);
|
|
3450
|
+
await assert.rejects(f.prepare({ messageId: "ordinary-codex", turnId: receipt.turnId }), /no current verified receipt/);
|
|
3451
|
+
// The original controlled CLI invokes discovery only for exact plain "tools".
|
|
3452
|
+
const toolReceipt = await f.conversation.send({ messageId: "ordinary-tools", text: "tools" });
|
|
3453
|
+
const toolsState = await f.conversation.wait();
|
|
3454
|
+
const toolsTurn = toolsState.conversationLog.find(row => row.turnId === toolReceipt.turnId);
|
|
3455
|
+
assert.equal(toolsTurn.metadata.runtime.status, "complete", toolsState.error);
|
|
3456
|
+
assert.equal(toolsTurn.metadata.runtime.completedEnvelope, undefined);
|
|
3457
|
+
assert.equal(toolsTurn.metadata.applicationTools.length, 3);
|
|
3458
|
+
assert.equal(f.effects(), 1);
|
|
3459
|
+
await assert.rejects(f.prepare({ messageId: "ordinary-tools", turnId: toolReceipt.turnId }), /no current verified receipt/);
|
|
3460
|
+
const trace = await f.trace();
|
|
3461
|
+
assert.equal(trace.filter(row => row.method === "thread/start").length, 1);
|
|
3462
|
+
const starts = trace.filter(row => row.method === "turn/start");
|
|
3463
|
+
assert.equal(starts.length, 2);
|
|
3464
|
+
assert.equal(starts[1].params.input[0].text, "tools");
|
|
3465
|
+
} finally { await f.close(); }
|
|
3466
|
+
});
|
|
3467
|
+
|
|
3468
|
+
for (const markedFirst of [true, false]) test(`completed Codex ${markedFirst ? "marked to ordinary" : "ordinary to marked"} transitions retain the original incompatible-schema refusal`, async t => {
|
|
3469
|
+
const f = await completedCodexFixture(t);
|
|
3470
|
+
try {
|
|
3471
|
+
if (markedFirst) await f.complete();
|
|
3472
|
+
else {
|
|
3473
|
+
await f.conversation.send({ messageId: "ordinary-first", text: "tools" });
|
|
3474
|
+
const state = await f.conversation.wait();
|
|
3475
|
+
assert.equal(state.conversationLog[0].metadata.runtime.status, "complete", state.error);
|
|
3476
|
+
assert.equal(state.conversationLog[0].metadata.applicationTools.length, 3);
|
|
3477
|
+
}
|
|
3478
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3479
|
+
const starts = (await f.trace()).filter(row => row.method === "turn/start").length;
|
|
3480
|
+
const effects = f.effects();
|
|
3481
|
+
const submit = markedFirst
|
|
3482
|
+
? f.conversation.send({ messageId: "ordinary-next", text: "tools" })
|
|
3483
|
+
: f.conversation.wake({ messageId: "marked-next", text: "Read a number" });
|
|
3484
|
+
await assert.rejects(submit, /different application tool entry points/);
|
|
3485
|
+
assert.equal(f.effects(), effects);
|
|
3486
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, starts);
|
|
3487
|
+
const after = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3488
|
+
assert.equal(after.runtime.binding.threadId, before.runtime.binding.threadId);
|
|
3489
|
+
assert.equal(after.runtime.binding.toolSchemaIdentity, before.runtime.binding.toolSchemaIdentity);
|
|
3490
|
+
assert.equal(after.runtime.binding.accountIdentity, before.runtime.binding.accountIdentity);
|
|
3491
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/interrupt").length, 0);
|
|
3492
|
+
} finally { await f.close(); }
|
|
3493
|
+
});
|
|
3494
|
+
|
|
3495
|
+
|
|
3496
|
+
test("completed Codex human presentation retains product history and private actual native receipts", async t => {
|
|
3497
|
+
const f = await completedCodexFixture(t);
|
|
3498
|
+
const events = [];
|
|
3499
|
+
const observations = [];
|
|
3500
|
+
await f.conversation.subscribe(event => {
|
|
3501
|
+
events.push(event);
|
|
3502
|
+
if (event.type === "message" && event.completedEnvelope === true) observations.push(f.conversation.read());
|
|
3503
|
+
});
|
|
3504
|
+
try {
|
|
3505
|
+
await f.storage.write("conversation", async tx => {
|
|
3506
|
+
await tx.appendMessage("000001", { role: "user", messageId: "product-question", text: "Visible product question",
|
|
3507
|
+
at: "2026-10-10T00:00:00.000Z" });
|
|
3508
|
+
await tx.replaceAssistant("000001", { role: "assistant", messageId: "product-reply", text: "Visible prior reply",
|
|
3509
|
+
at: "2026-10-10T00:00:01.000Z" });
|
|
3510
|
+
});
|
|
3511
|
+
const receipt = await f.complete("internal-presentation-response");
|
|
3512
|
+
const state = await f.conversation.read();
|
|
3513
|
+
assert.equal(state.status, "ready", state.error);
|
|
3514
|
+
assert.equal(state.error, "");
|
|
3515
|
+
assert.equal(state.pendingRequest, null);
|
|
3516
|
+
assert.deepEqual(state.streaming.messages, []);
|
|
3517
|
+
assert.deepEqual(state.conversationLog.map(turn => turn.turnId), ["000001"]);
|
|
3518
|
+
assert.equal(state.conversationLog[0].user.messageId, "product-question");
|
|
3519
|
+
assert.equal(state.conversationLog[0].assistant.messageId, "product-reply");
|
|
3520
|
+
assert.equal(state.conversationLog[0].assistant.text, "Visible prior reply");
|
|
3521
|
+
const page = await f.conversation.read({ limit: 1, presentation: false });
|
|
3522
|
+
assert.deepEqual(page.conversationLog, state.conversationLog);
|
|
3523
|
+
assert.equal(page.pagination.totalTurnCount, 1);
|
|
3524
|
+
assert.equal(page.pagination.count, 1);
|
|
3525
|
+
assert.equal(page.pagination.oldestTurnId, "000001");
|
|
3526
|
+
assert.equal(page.pagination.newestTurnId, "000001");
|
|
3527
|
+
assert.equal(page.pagination.hasMoreBefore, false);
|
|
3528
|
+
const raw = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3529
|
+
const metadata = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3530
|
+
assert.equal(raw.system.messageId, receipt.messageId);
|
|
3531
|
+
assert.equal(raw.metadata.runtime.completedEnvelope, true);
|
|
3532
|
+
assert.equal(raw.metadata.runtime.status, "complete");
|
|
3533
|
+
assert.equal(raw.metadata.runtime.segmentId, state.segmentId);
|
|
3534
|
+
assert.equal(raw.assistant.text, JSON.stringify(completedCodexValue));
|
|
3535
|
+
assert.ok(raw.metadata.runtime.nativeTurnId);
|
|
3536
|
+
assert.equal(metadata.runtime.binding.codexAppServerRun.providerTurnId, raw.metadata.runtime.nativeTurnId);
|
|
3537
|
+
assert.equal(metadata.runtime.binding.codexAppServerRun.outerTurnId, receipt.messageId);
|
|
3538
|
+
const savedHistory = JSON.parse(await readFile(path.join(f.directory, "history.json"), "utf8"));
|
|
3539
|
+
const nativeThread = (savedHistory.threads || [savedHistory]).find(thread => thread.id === metadata.runtime.binding.threadId);
|
|
3540
|
+
const nativeTurn = nativeThread.turns.find(turn => turn.id === raw.metadata.runtime.nativeTurnId);
|
|
3541
|
+
assert.equal(nativeTurn.status, "completed");
|
|
3542
|
+
assert.equal(nativeTurn.items.find(item => item.type === "userMessage").clientId, receipt.messageId);
|
|
3543
|
+
assert.equal(nativeTurn.items.find(item => item.type === "agentMessage").text, raw.assistant.text);
|
|
3544
|
+
const rawFinal = events.filter(event => event.type === "message" && event.status === "complete")
|
|
3545
|
+
.find(event => event.turnId === receipt.turnId && event.text === raw.assistant.text);
|
|
3546
|
+
assert.ok(rawFinal, "The raw worker subscriber still receives this actual native completed response");
|
|
3547
|
+
assert.equal(rawFinal.completedEnvelope, true);
|
|
3548
|
+
assert.ok(events.some(event => event.type === "message" && event.status === "inProgress" &&
|
|
3549
|
+
event.completedEnvelope === true && event.streaming.messages.some(message => message.completedEnvelope === true)),
|
|
3550
|
+
"The original app-server emits a trusted marked native stream, not a manufactured canonical completion");
|
|
3551
|
+
assert.ok(observations.length);
|
|
3552
|
+
for (const observed of await Promise.all(observations)) {
|
|
3553
|
+
assert.ok(["working", "ready"].includes(observed.status), observed.error);
|
|
3554
|
+
assert.deepEqual(observed.conversationLog.map(turn => turn.turnId), ["000001"]);
|
|
3555
|
+
assert.equal(observed.pendingRequest, null);
|
|
3556
|
+
assert.deepEqual(observed.streaming.messages, []);
|
|
3557
|
+
}
|
|
3558
|
+
const trace = await f.trace();
|
|
3559
|
+
const starts = trace.filter(row => row.method === "turn/start");
|
|
3560
|
+
assert.equal(starts.length, 1);
|
|
3561
|
+
assert.equal(starts[0].params.clientUserMessageId, receipt.messageId);
|
|
3562
|
+
assert.equal(trace.find(row => row.method === "thread/start").params.dynamicTools.length, 0);
|
|
3563
|
+
assert.equal(f.effects(), 0);
|
|
3564
|
+
} finally { await f.close(); }
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
|
|
3568
|
+
// The original controlled app-server owns large wire output and its native receipt.
|
|
3569
|
+
const completedCodexWireSchema = { type: "object", additionalProperties: false,
|
|
3570
|
+
required: ["kind", "text", "toolName", "arguments"], properties: {
|
|
3571
|
+
kind: { type: "string", enum: ["reply", "tool"] }, text: { type: "string", maxLength: 16000 },
|
|
3572
|
+
toolName: { type: "string", maxLength: 256 }, arguments: { type: "string", maxLength: 262144 }
|
|
3573
|
+
} };
|
|
3574
|
+
async function completedCodexWireFixture(t, value, options = {}) {
|
|
3575
|
+
let effects = 0;
|
|
3576
|
+
const actions = applicationActions(async () => { effects++; return { value: 42 }; });
|
|
3577
|
+
const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
|
|
3578
|
+
const f = await fixture(t, { ...options, actions, context, structuredResponse: value,
|
|
3579
|
+
configuration: { ...configuration, outputSchema: options.outputSchema || completedCodexWireSchema },
|
|
3580
|
+
limits: { admissionTimeoutMs: 30_000, maxFinalReplyCharacters: 16000, maxOutputCharacters: 1670455, ...options.limits } });
|
|
3581
|
+
await f.first.close();
|
|
3582
|
+
let runtime, conversation;
|
|
3583
|
+
async function reopen() {
|
|
3584
|
+
if (runtime) await runtime.close();
|
|
3585
|
+
runtime = createConversationRuntime({ engine: "codex", storage: f.storage, actions,
|
|
3586
|
+
authorize: () => true, completedEnvelope: true, ...f.driverOptions });
|
|
3587
|
+
try { conversation = await runtime.open({ id: "conversation", context }); }
|
|
3588
|
+
catch (error) { await runtime.close(); throw error; }
|
|
3589
|
+
}
|
|
3590
|
+
await reopen();
|
|
3591
|
+
return { ...f, reopen, effects: () => effects, get conversation() { return conversation; }, close: () => runtime.close() };
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
for (const kind of ["tool", "reply"]) test(`completed Codex ${kind} wire preserves large decoded fields beyond the human cap`, async t => {
|
|
3595
|
+
const handover = "😀".repeat(20000);
|
|
3596
|
+
const argumentsText = JSON.stringify({ value: handover }).replace(/[\u0080-\uffff]/g,
|
|
3597
|
+
character => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
3598
|
+
const value = kind === "tool" ? { kind, text: "", toolName: "numbers_read", arguments: argumentsText }
|
|
3599
|
+
: { kind, text: "\u0000".repeat(16000), toolName: "", arguments: "" };
|
|
3600
|
+
const raw = JSON.stringify(value);
|
|
3601
|
+
assert.ok(raw.length > (kind === "tool" ? 280000 : 16000));
|
|
3602
|
+
if (kind === "tool") assert.equal(value.arguments.length, 240012);
|
|
3603
|
+
const f = await completedCodexWireFixture(t, value);
|
|
3604
|
+
try {
|
|
3605
|
+
const receipt = await f.conversation.wake({ messageId: `large-codex-${kind}`, text: "Read the completed envelope" });
|
|
3606
|
+
const state = await f.conversation.wait();
|
|
3607
|
+
assert.equal(state.status, "ready", state.error);
|
|
3608
|
+
const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3609
|
+
const metadata = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3610
|
+
const run = metadata.runtime.binding.codexAppServerRun;
|
|
3611
|
+
assert.equal(saved.metadata.runtime.status, "complete");
|
|
3612
|
+
assert.equal(saved.metadata.runtime.completedEnvelope, true);
|
|
3613
|
+
assert.equal(saved.system.origin, "application");
|
|
3614
|
+
assert.equal(saved.assistant.text, raw);
|
|
3615
|
+
assert.equal(saved.metadata.runtime.nativeTurnId, run.providerTurnId);
|
|
3616
|
+
assert.equal(run.outerTurnId, receipt.messageId);
|
|
3617
|
+
const prepared = await f.conversation.prepareCompletedResponse(receipt, { signal: new AbortController().signal });
|
|
3618
|
+
assert.equal(prepared.text, raw);
|
|
3619
|
+
const { readAssistantResponseEnvelope } = await import("../src/server/lib/assistantToolLoop.js");
|
|
3620
|
+
const decoded = readAssistantResponseEnvelope(prepared.text);
|
|
3621
|
+
assert.deepEqual(decoded, value);
|
|
3622
|
+
if (kind === "tool") assert.equal(JSON.parse(decoded.arguments).value, handover);
|
|
3623
|
+
assert.equal(f.effects(), 0);
|
|
3624
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3625
|
+
assert.deepEqual((await f.conversation.read()).conversationLog, []);
|
|
3626
|
+
} finally { await f.close(); }
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
test("completed Codex wire overflow remains bounded before any decoded effect", async t => {
|
|
3630
|
+
const value = { kind: "tool", text: "", toolName: "numbers_read", arguments: JSON.stringify({ content: "x".repeat(2000) }) };
|
|
3631
|
+
assert.ok(JSON.stringify(value).length > 2000);
|
|
3632
|
+
const f = await completedCodexWireFixture(t, value, { outputSchema: completedCodexSchema,
|
|
3633
|
+
limits: { maxOutputCharacters: 2000, maxFinalReplyCharacters: 16 } });
|
|
3634
|
+
try {
|
|
3635
|
+
const receipt = await f.conversation.wake({ messageId: "codex-wire-overflow", text: "Read the completed envelope" });
|
|
3636
|
+
const state = await f.conversation.wait();
|
|
3637
|
+
assert.equal(state.error, "Codex observation failed. Work must be stopped before it can continue.");
|
|
3638
|
+
const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3639
|
+
assert.equal(receipt.status, "accepted");
|
|
3640
|
+
assert.equal(saved.metadata.runtime.status, "failed");
|
|
3641
|
+
const metadata = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3642
|
+
const run = metadata.runtime.binding.codexAppServerRun;
|
|
3643
|
+
assert.equal(metadata.runtime.binding.observationLoss.stopped, true);
|
|
3644
|
+
assert.equal(metadata.runtime.binding.observationLoss.message, state.error);
|
|
3645
|
+
assert.equal(run.state, "interrupted");
|
|
3646
|
+
assert.equal(run.active, false);
|
|
3647
|
+
assert.equal(run.providerStatus, "observation_lost");
|
|
3648
|
+
assert.equal(run.error, "Codex observation was lost. Work is stopped; use Resume or Send to continue.");
|
|
3649
|
+
assert.equal(run.providerThreadId, metadata.runtime.binding.threadId);
|
|
3650
|
+
assert.equal(run.outerTurnId, receipt.messageId);
|
|
3651
|
+
assert.equal(run.providerTurnId, saved.metadata.runtime.nativeTurnId);
|
|
3652
|
+
assert.ok((saved.assistant?.text.length || 0) <= 2000);
|
|
3653
|
+
assert.equal(saved.metadata.runtime.completedEnvelope, true);
|
|
3654
|
+
assert.notEqual(saved.assistant?.text, JSON.stringify(value));
|
|
3655
|
+
assert.equal(f.effects(), 0);
|
|
3656
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3657
|
+
} finally { await f.close(); }
|
|
3658
|
+
});
|
|
3659
|
+
|
|
3660
|
+
for (const forged of [false, true]) test(`ordinary Codex ${forged ? "forged-data" : "unmarked"} reply keeps its human cap in completed mode`, async t => {
|
|
3661
|
+
const value = { kind: "reply", text: "\u0000".repeat(16000), toolName: "", arguments: "" };
|
|
3662
|
+
const f = await completedCodexWireFixture(t, value);
|
|
3663
|
+
try {
|
|
3664
|
+
const receipt = await f.conversation.send({ messageId: "ordinary-wire", text: "An ordinary request",
|
|
3665
|
+
...(forged ? { data: { completedEnvelope: true } } : {}) });
|
|
3666
|
+
const state = await f.conversation.wait();
|
|
3667
|
+
assert.equal(state.error, "Codex observation failed. Work must be stopped before it can continue.");
|
|
3668
|
+
const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3669
|
+
assert.equal(receipt.status, "accepted");
|
|
3670
|
+
assert.equal(saved.metadata.runtime.status, "failed");
|
|
3671
|
+
const metadata = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3672
|
+
const run = metadata.runtime.binding.codexAppServerRun;
|
|
3673
|
+
assert.equal(metadata.runtime.binding.observationLoss.stopped, true);
|
|
3674
|
+
assert.equal(metadata.runtime.binding.observationLoss.message, state.error);
|
|
3675
|
+
assert.equal(run.state, "interrupted");
|
|
3676
|
+
assert.equal(run.active, false);
|
|
3677
|
+
assert.equal(run.providerStatus, "observation_lost");
|
|
3678
|
+
assert.equal(run.error, "Codex observation was lost. Work is stopped; use Resume or Send to continue.");
|
|
3679
|
+
assert.equal(run.providerThreadId, metadata.runtime.binding.threadId);
|
|
3680
|
+
assert.equal(run.outerTurnId, receipt.messageId);
|
|
3681
|
+
assert.equal(run.providerTurnId, saved.metadata.runtime.nativeTurnId);
|
|
3682
|
+
assert.ok((saved.assistant?.text.length || 0) <= 16000);
|
|
3683
|
+
assert.equal(saved.metadata.runtime.completedEnvelope, undefined);
|
|
3684
|
+
assert.equal(saved.user.origin, "user");
|
|
3685
|
+
if (forged) assert.deepEqual(saved.user.data, { completedEnvelope: true });
|
|
3686
|
+
assert.notEqual(saved.assistant?.text, JSON.stringify(value));
|
|
3687
|
+
await assert.rejects(f.conversation.inspectDelivery({ messageId: receipt.messageId }),
|
|
3688
|
+
{ message: "Codex output exceeded the configured limit." });
|
|
3689
|
+
const afterInspection = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3690
|
+
assert.deepEqual(afterInspection.user, saved.user);
|
|
3691
|
+
assert.equal(afterInspection.metadata.runtime.completedEnvelope, undefined);
|
|
3692
|
+
assert.ok((afterInspection.assistant?.text.length || 0) <= 16000);
|
|
3693
|
+
assert.notEqual(afterInspection.assistant?.text, JSON.stringify(value));
|
|
3694
|
+
assert.equal(f.effects(), 0);
|
|
3695
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3696
|
+
} finally { await f.close(); }
|
|
3697
|
+
});
|
|
3698
|
+
|
|
3699
|
+
test("completed Codex marked canonical native recovery retains large output without replay", async t => {
|
|
3700
|
+
const value = { kind: "reply", text: "\u0000".repeat(16000), toolName: "", arguments: "" };
|
|
3701
|
+
const f = await completedCodexWireFixture(t, value, { checkpoints: true });
|
|
3702
|
+
try {
|
|
3703
|
+
const receipt = await f.conversation.wake({ messageId: "recover-marked-wire", text: "Read the completed envelope" });
|
|
3704
|
+
assert.equal((await f.conversation.wait()).status, "ready");
|
|
3705
|
+
const final = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3706
|
+
const checkpoint = f.checkpoints.find(record => record.metadata.runtime.binding.codexAppServerRun?.outerTurnId === receipt.messageId &&
|
|
3707
|
+
record.turns.some(([, turn]) => turn.metadata?.runtime?.completedEnvelope === true && turn.metadata.runtime.nativeTurnId &&
|
|
3708
|
+
turn.metadata.runtime.status === "running" && !turn.messages.some(message => message.role === "assistant")));
|
|
3709
|
+
assert.ok(checkpoint, "The existing canonical fixture captured real marked admission before native output");
|
|
3710
|
+
await f.close();
|
|
3711
|
+
await f.restore(checkpoint);
|
|
3712
|
+
await f.reopen();
|
|
3713
|
+
const recovered = await f.conversation.inspectDelivery({ messageId: receipt.messageId });
|
|
3714
|
+
assert.equal(recovered.status, "accepted");
|
|
3715
|
+
assert.equal(recovered.recovered, true);
|
|
3716
|
+
const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
3717
|
+
assert.equal(saved.metadata.runtime.completedEnvelope, true);
|
|
3718
|
+
assert.equal(saved.metadata.runtime.status, "complete");
|
|
3719
|
+
assert.equal(saved.metadata.runtime.nativeTurnId, final.metadata.runtime.nativeTurnId);
|
|
3720
|
+
assert.equal(saved.assistant.text, JSON.stringify(value));
|
|
3721
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
|
|
3722
|
+
assert.equal(f.effects(), 0);
|
|
3723
|
+
} finally { await f.close(); }
|
|
3724
|
+
});
|
|
3725
|
+
|
|
3726
|
+
|
|
3727
|
+
// The post-port Colleague size-refusal probe belongs to the ordinary native
|
|
3728
|
+
// owner: completed-envelope Colleague never authorizes native application tools.
|
|
3729
|
+
test("ordinary Codex oversized native arguments retain exact failed-cleanup custody until explicit Stop", async t => {
|
|
3730
|
+
let refusalPath;
|
|
3731
|
+
// Registered before the original fixture teardown: a failed assertion cannot
|
|
3732
|
+
// leave our interrupt-refusal marker blocking the real owner cleanup.
|
|
3733
|
+
t.after(async () => { if (refusalPath) await rm(refusalPath, { force: true }); });
|
|
3734
|
+
let effects = 0;
|
|
3735
|
+
const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
|
|
3736
|
+
const oversized = "x".repeat(262_145);
|
|
3737
|
+
const f = await fixture(t, { actions: applicationActions(async () => { effects++; return { value: 42 }; }), context,
|
|
3738
|
+
oversizedNativeToolInput: { value: oversized }, holdAfterToolRefusal: true,
|
|
3739
|
+
limits: { admissionTimeoutMs: 30_000, maxToolArgumentBytes: 262_144 } });
|
|
3740
|
+
const peer = await f.first.open({ id: "size-peer", configuration, context });
|
|
3741
|
+
const historyPath = path.join(f.directory, "history.json");
|
|
3742
|
+
refusalPath = historyPath + ".refuse-interrupt";
|
|
3743
|
+
try {
|
|
3744
|
+
await peer.send({ messageId: "size-peer-wait", text: "wait" });
|
|
3745
|
+
const peerBefore = await peer.read();
|
|
3746
|
+
const peerBinding = await f.storage.read("size-peer", async tx => (await tx.readMetadata()).runtime.binding);
|
|
3747
|
+
const peerHistory = JSON.parse(await readFile(historyPath, "utf8"));
|
|
3748
|
+
const retainedPeer = (peerHistory.threads || [peerHistory]).find(thread => thread.id === peerBinding.threadId);
|
|
3749
|
+
await writeFile(refusalPath, "refuse");
|
|
3750
|
+
await f.conversation.send({ messageId: "size-authored", text: "tools" });
|
|
3751
|
+
const failed = await f.conversation.wait();
|
|
3752
|
+
assert.equal(failed.status, "unavailable", "A refused real cleanup stays unavailable, not falsely stopped");
|
|
3753
|
+
assert.match(failed.error, /Controlled native interrupt refusal/);
|
|
3754
|
+
assert.equal(effects, 0, "The oversized call never reaches application execution");
|
|
3755
|
+
const authored = failed.conversationLog.filter(turn => turn.user?.messageId === "size-authored");
|
|
3756
|
+
assert.equal(authored.length, 1);
|
|
3757
|
+
assert.equal(authored[0].user.text, "tools");
|
|
3758
|
+
assert.equal(authored[0].assistant, null);
|
|
3759
|
+
assert.equal(authored[0].metadata.runtime.status, "interrupted");
|
|
3760
|
+
assert.equal(authored[0].metadata.applicationTools.length, 2, "Only the genuine search and contract read receipts are reserved");
|
|
3761
|
+
assert.deepEqual(authored[0].metadata.applicationTools.map(call => [call.name, call.status]), [
|
|
3762
|
+
["assistant_action_search", "complete"], ["assistant_action_contract", "complete"]
|
|
3763
|
+
]);
|
|
3764
|
+
// The application waiter does not join the remote fixture's error callback.
|
|
3765
|
+
// Observe its actual refused tool response before asserting native custody.
|
|
3766
|
+
let trace;
|
|
3767
|
+
for (let attempt = 0; attempt < 500; attempt++) {
|
|
3768
|
+
trace = await f.trace();
|
|
3769
|
+
if (trace.some(row => row.heldToolRefusal)) break;
|
|
3770
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
3771
|
+
}
|
|
3772
|
+
const starts = trace.filter(row => row.method === "turn/start");
|
|
3773
|
+
assert.equal(starts.length, 2, "One actual peer request and one actual oversized request");
|
|
3774
|
+
const owned = starts.find(row => row.params.clientUserMessageId === "size-authored");
|
|
3775
|
+
assert.ok(owned);
|
|
3776
|
+
const nativeHistory = JSON.parse(await readFile(historyPath, "utf8"));
|
|
3777
|
+
const threads = nativeHistory.threads || [nativeHistory];
|
|
3778
|
+
const thread = threads.find(thread => thread.id === owned.params.threadId);
|
|
3779
|
+
assert.equal(thread.turns.length, 1);
|
|
3780
|
+
assert.equal(thread.turns[0].items[0].clientId, "size-authored");
|
|
3781
|
+
assert.equal(thread.turns[0].status, "inProgress", "A settled application waiter cannot fake native interruption");
|
|
3782
|
+
assert.equal(thread.turns[0].items.some(item => item.type === "agentMessage"), false);
|
|
3783
|
+
assert.equal(trace.some(row => row.notification?.method === "turn/completed" &&
|
|
3784
|
+
row.notification.params.threadId === thread.id), false, "No actual terminal frame falsely settles the active native turn");
|
|
3785
|
+
assert.equal(authored[0].metadata.runtime.nativeTurnId, thread.turns[0].id);
|
|
3786
|
+
assert.deepEqual(threads.find(thread => thread.id === peerBinding.threadId), retainedPeer);
|
|
3787
|
+
const actualRequests = trace.filter(row => row.oversizedNativeToolRequest);
|
|
3788
|
+
assert.equal(actualRequests.length, 1);
|
|
3789
|
+
assert.deepEqual(actualRequests[0].oversizedNativeToolRequest, { id: "tool-request-3", threadId: thread.id,
|
|
3790
|
+
turnId: thread.turns[0].id,
|
|
3791
|
+
argumentsByteLength: Buffer.byteLength(JSON.stringify({ actionId: "numbers.read", version: 1, input: { value: oversized } })),
|
|
3792
|
+
inputValueByteLength: 262_145 });
|
|
3793
|
+
assert.equal(actualRequests[0].oversizedNativeToolRequest.argumentsByteLength > 262_144, true);
|
|
3794
|
+
const refusal = trace.filter(row => row.heldToolRefusal);
|
|
3795
|
+
assert.equal(refusal.length, 1);
|
|
3796
|
+
assert.deepEqual(refusal[0].heldToolRefusal, { threadId: thread.id, turnId: thread.turns[0].id,
|
|
3797
|
+
message: "The application tool arguments exceed their size limit." });
|
|
3798
|
+
const toolResponses = trace.filter(row => row.toolResponse);
|
|
3799
|
+
assert.equal(toolResponses.length, 3);
|
|
3800
|
+
assert.equal(toolResponses.at(-1).toolResponse.id, actualRequests[0].oversizedNativeToolRequest.id);
|
|
3801
|
+
assert.match(toolResponses.at(-1).toolResponse.error.message, /size limit/);
|
|
3802
|
+
const interrupts = trace.filter(row => row.method === "turn/interrupt");
|
|
3803
|
+
assert.equal(interrupts.length, 1);
|
|
3804
|
+
assert.deepEqual(interrupts[0].params, { threadId: thread.id, turnId: thread.turns[0].id });
|
|
3805
|
+
assert.equal((await peer.read()).status, "working");
|
|
3806
|
+
assert.equal((await peer.read()).segmentId, peerBefore.segmentId);
|
|
3807
|
+
assert.deepEqual((await f.storage.read("size-peer", async tx => (await tx.readMetadata()).runtime.binding)).threadId,
|
|
3808
|
+
peerBinding.threadId);
|
|
3809
|
+
const beforeSelect = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
3810
|
+
assert.ok(beforeSelect.runtime.binding.accountIdentity);
|
|
3811
|
+
assert.equal(beforeSelect.runtime.binding.accountIdentity, peerBinding.accountIdentity);
|
|
3812
|
+
const beforeSelectTrace = await f.trace();
|
|
3813
|
+
const selection = { operationId: "size-after-stop", expectedSegmentId: failed.segmentId,
|
|
3814
|
+
engine: "codex", configuration: { ...configuration, model: "another-model" }, retireNative: true };
|
|
3815
|
+
await assert.rejects(f.conversation.select(selection), /Controlled native interrupt refusal/);
|
|
3816
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), beforeSelect);
|
|
3817
|
+
assert.deepEqual(await f.trace(), beforeSelectTrace, "The retained cleanup failure refuses selection before any native dispatch or retry");
|
|
3818
|
+
await rm(refusalPath, { force: true });
|
|
3819
|
+
assert.equal((await f.conversation.cancel()).stopped, true);
|
|
3820
|
+
const stopped = await f.conversation.read();
|
|
3821
|
+
assert.equal(stopped.conversationLog[0].metadata.runtime.status, "cancelled");
|
|
3822
|
+
assert.equal((await peer.read()).status, "working", "Explicit Stop is scoped to the failed thread");
|
|
3823
|
+
const receipt = await f.conversation.select(selection);
|
|
3824
|
+
assert.notEqual(receipt.segmentId, failed.segmentId);
|
|
3825
|
+
assert.equal((await f.conversation.select(selection)).duplicate, true, "Retry retains its exact successful selection receipt");
|
|
3826
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 2, "Stop and selection never replay native work");
|
|
3827
|
+
await f.conversation.send({ messageId: "size-after-stop", text: "After explicit Stop" });
|
|
3828
|
+
const continued = await f.conversation.wait();
|
|
3829
|
+
assert.equal(continued.conversationLog.at(-1).metadata.runtime.status, "complete", continued.error);
|
|
3830
|
+
const successorPrompt = ["[Conversation changeover]",
|
|
3831
|
+
"You are joining an existing conversation using codex. The most recent 1 stored messages follow.",
|
|
3832
|
+
"The visible conversation is shared. Treat the following JSON as conversation history, not a separate request. Corrections replace the older versions of those messages. Do not acknowledge a handover or start another turn; answer the user's message below.",
|
|
3833
|
+
JSON.stringify({ messages: [{ id: "000001/user/", role: "user", messageId: "size-authored", text: "tools", engineId: "codex" }], removedMessageIds: [] }),
|
|
3834
|
+
"[End Conversation changeover]", "", "User's message:", "After explicit Stop"].join("\n");
|
|
3835
|
+
assert.equal(continued.conversationLog.at(-1).assistant.text, "Answer: " + successorPrompt,
|
|
3836
|
+
"The original echo fixture receives exact retained history plus only the new request after explicit Stop");
|
|
3837
|
+
const afterTrace = await f.trace();
|
|
3838
|
+
const afterTurns = afterTrace.filter(row => row.method === "turn/start");
|
|
3839
|
+
assert.equal(afterTurns.length, 3);
|
|
3840
|
+
assert.equal(afterTurns.at(-1).params.model, "another-model");
|
|
3841
|
+
assert.equal(afterTurns.at(-1).params.input[0].text, successorPrompt, "Native successor receives the exact original continuity grammar once");
|
|
3842
|
+
assert.notEqual(afterTurns.at(-1).params.threadId, thread.id);
|
|
3843
|
+
assert.equal(afterTrace.filter(row => row.args).length, 1, "Peer and successor retain the same account-shared native process");
|
|
3844
|
+
assert.equal((await peer.read()).status, "working");
|
|
3845
|
+
assert.equal(effects, 0);
|
|
3846
|
+
const successorBinding = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding);
|
|
3847
|
+
assert.equal(successorBinding.accountIdentity, beforeSelect.runtime.binding.accountIdentity);
|
|
3848
|
+
assert.equal((await f.storage.read("size-peer", async tx => (await tx.readMetadata()).runtime.binding)).accountIdentity,
|
|
3849
|
+
peerBinding.accountIdentity);
|
|
3850
|
+
const afterHistory = JSON.parse(await readFile(historyPath, "utf8"));
|
|
3851
|
+
assert.deepEqual(afterHistory.threads.find(row => row.id === peerBinding.threadId), retainedPeer);
|
|
3852
|
+
} finally {
|
|
3853
|
+
await rm(refusalPath, { force: true });
|
|
3854
|
+
try { await f.conversation.cancel(); }
|
|
3855
|
+
finally { await peer.cancel(); }
|
|
3856
|
+
}
|
|
3857
|
+
});
|
|
3858
|
+
|
|
3859
|
+
|
|
3860
|
+
// Original Public R06 slow-history obligations use the ordinary native owner:
|
|
3861
|
+
// its real application contract can wait for active.inputReady before completion.
|
|
3862
|
+
for (const delayMs of [1000, 2000]) {
|
|
3863
|
+
test(`ordinary Codex slow first history read preserves its post-read grace for final at ${delayMs}ms`, async t => {
|
|
3864
|
+
let effects = 0;
|
|
3865
|
+
const text = "Exact final after the slow read.";
|
|
3866
|
+
const f = await fixture(t, {
|
|
3867
|
+
actions: applicationActions(async () => { effects++; return { value: 42 }; }),
|
|
3868
|
+
context: { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] },
|
|
3869
|
+
completionBeforeDelayedFinal: { readDelayMs: 600, delayMs, text },
|
|
3870
|
+
limits: { admissionTimeoutMs: 30_000, codexFinalizingGraceMs: 500,
|
|
3871
|
+
codexFinalizingGraceAfterHistoryRead: true, codexFailureDetailGraceMs: 500 }
|
|
3872
|
+
});
|
|
3873
|
+
let firstError;
|
|
3874
|
+
try {
|
|
3875
|
+
const receipt = await f.conversation.send({ messageId: "user-1", text: "Keep the slow-read request." });
|
|
3876
|
+
assert.equal(receipt.status, "accepted");
|
|
3877
|
+
let trace;
|
|
3878
|
+
for (let attempt = 0; attempt < 3000; attempt++) {
|
|
3879
|
+
trace = await f.trace();
|
|
3880
|
+
if (trace.some(row => row.historyReadHeld)) break;
|
|
3881
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
3882
|
+
}
|
|
3883
|
+
assert.ok(trace.some(row => row.historyReadHeld), "Observe the actual full-history hold after native completion");
|
|
3884
|
+
const held = await f.storage.read("conversation", transaction => transaction.readMetadata());
|
|
3885
|
+
assert.equal(held.runtime.binding.codexAppServerRun.state, "finalizing",
|
|
3886
|
+
"The delayed full-history read must follow native completion and canonical finalization");
|
|
3887
|
+
const result = await f.conversation.wait();
|
|
3888
|
+
// Generic controls are idle/ready after a settled failed turn; Public
|
|
3889
|
+
// Colleague's separate product worker reports its request as failed.
|
|
3890
|
+
assert.equal(result.status, "ready", result.error);
|
|
3891
|
+
if (delayMs === 2000) assert.equal(result.error,
|
|
3892
|
+
"Codex app-server finished this turn, but the assistant result text was not received.");
|
|
3893
|
+
else assert.equal(result.error, "");
|
|
3894
|
+
const messages = result.conversationLog.flatMap(turn => turn.messages);
|
|
3895
|
+
assert.deepEqual(messages.filter(row => row.role === "user").map(row => row.text), ["Keep the slow-read request."]);
|
|
3896
|
+
assert.deepEqual(messages.filter(row => row.role === "assistant").map(row => row.text), delayMs === 1000 ? [text] : []);
|
|
3897
|
+
const saved = await f.storage.read("conversation", transaction => transaction.readTurn(receipt.turnId));
|
|
3898
|
+
assert.equal(saved.user.messageId, "user-1");
|
|
3899
|
+
assert.equal(saved.metadata.runtime.status, delayMs === 1000 ? "complete" : "failed");
|
|
3900
|
+
if (delayMs === 2000) {
|
|
3901
|
+
const metadata = await f.storage.read("conversation", transaction => transaction.readMetadata());
|
|
3902
|
+
assert.equal(metadata.runtime.binding.codexAppServerRun.error, result.error);
|
|
3903
|
+
}
|
|
3904
|
+
assert.equal(saved.assistant?.text, delayMs === 1000 ? text : undefined);
|
|
3905
|
+
assert.equal(effects, 0);
|
|
3906
|
+
assert.equal(saved.metadata.applicationTools.length, 1, "Only the real contract query receipt exists; no numbers action ran");
|
|
3907
|
+
assert.equal(saved.metadata.applicationTools[0].name, "assistant_action_contract");
|
|
3908
|
+
assert.equal(saved.metadata.applicationTools[0].status, "complete");
|
|
3909
|
+
assert.equal(saved.metadata.applicationTools[0].result.ok, true);
|
|
3910
|
+
trace = await f.trace();
|
|
3911
|
+
const turns = trace.filter(row => row.method === "turn/start");
|
|
3912
|
+
assert.equal(turns.length, 1, "No repeat native request or inference");
|
|
3913
|
+
assert.equal(turns[0].params.clientUserMessageId, "user-1");
|
|
3914
|
+
const started = trace.find(row => row.notification?.method === "turn/started").notification.params.turn;
|
|
3915
|
+
assert.equal(saved.metadata.runtime.nativeTurnId, started.id);
|
|
3916
|
+
assert.deepEqual(trace.filter(row => row.historyReadHeld).map(row => row.historyReadHeld),
|
|
3917
|
+
[{ threadId: turns[0].params.threadId, turnId: started.id, delayMs: 600 }]);
|
|
3918
|
+
assert.equal(trace.filter(row => row.historyReadReturned).length, 1);
|
|
3919
|
+
const completedAt = trace.findIndex(row => row.notification?.method === "turn/completed" && row.notification.params.turn.id === started.id);
|
|
3920
|
+
assert.ok(completedAt >= 0 && completedAt < trace.findIndex(row => row.historyReadHeld));
|
|
3921
|
+
assert.equal(trace.filter(row => row.toolResponse).length, 1);
|
|
3922
|
+
const history = JSON.parse(await readFile(path.join(f.directory, "history.json"), "utf8"));
|
|
3923
|
+
assert.equal(history.id, turns[0].params.threadId);
|
|
3924
|
+
assert.equal(history.turns.length, 1);
|
|
3925
|
+
assert.equal(history.turns[0].id, started.id);
|
|
3926
|
+
assert.equal(history.turns[0].items[0].clientId, "user-1");
|
|
3927
|
+
} catch (error) {
|
|
3928
|
+
firstError = error;
|
|
3929
|
+
} finally {
|
|
3930
|
+
try {
|
|
3931
|
+
const beforeStop = await f.conversation.read();
|
|
3932
|
+
const cancelled = await f.conversation.cancel();
|
|
3933
|
+
assert.equal(typeof cancelled.stopped, "boolean", "Original Stop reports whether it interrupted work");
|
|
3934
|
+
const stopped = await f.storage.read("conversation", transaction => transaction.readMetadata());
|
|
3935
|
+
assert.equal(stopped.runtime.binding.codexAppServerRun.active, false, "Actual Stop leaves no active native turn");
|
|
3936
|
+
if (beforeStop.status === "ready") {
|
|
3937
|
+
assert.equal(cancelled.stopped, false, "An already idle native turn truthfully reports no interrupted work");
|
|
3938
|
+
}
|
|
3939
|
+
assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1, "Stop does not replay the original native request");
|
|
3940
|
+
assert.equal(effects, 0);
|
|
3941
|
+
} catch (error) {
|
|
3942
|
+
if (firstError) t.diagnostic(`R06 cleanup also failed; preserving the first assertion: ${error.stack || error.message}`);
|
|
3943
|
+
else firstError = error;
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
if (firstError) throw firstError;
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
|
|
3951
|
+
test("bound Codex tool callbacks re-enter captured control after the socket opening scope expires", async t => {
|
|
3952
|
+
const { AsyncLocalStorage } = await import("node:async_hooks");
|
|
3953
|
+
const contexts = new AsyncLocalStorage();
|
|
3954
|
+
const expired = { access: "write", active: false };
|
|
3955
|
+
const control = Object.freeze({ access: "control", active: true });
|
|
3956
|
+
const entered = Promise.withResolvers(), release = Promise.withResolvers();
|
|
3957
|
+
let hold = true;
|
|
3958
|
+
const f = await boundCodexToolsFixture(t, { limits: { admissionTimeoutMs: 30_000 }, execute: async actual => {
|
|
3959
|
+
assert.equal(contexts.getStore(), control, "native calls must not borrow the expired opening WRITE callback");
|
|
3960
|
+
assert.equal(actual.actor.id, "bound-owner", "each tool retains the original request actor");
|
|
3961
|
+
actual.boundAdmission.assertCurrent();
|
|
3962
|
+
if (hold) { entered.resolve(); await release.promise; }
|
|
3963
|
+
return { value: 7 };
|
|
3964
|
+
} });
|
|
3965
|
+
// The host facility supplies a CONTROL projection. The real fixture still
|
|
3966
|
+
// owns both native threads, original actions, tool framing and shared server.
|
|
3967
|
+
f.sharedProvider.options.runInContext = operation => contexts.run(control, operation);
|
|
3968
|
+
const receive = f.sharedProvider.handleServerRequest;
|
|
3969
|
+
t.mock.method(f.sharedProvider, "handleServerRequest", function (...args) {
|
|
3970
|
+
return contexts.run(expired, () => receive.apply(this, args));
|
|
3971
|
+
});
|
|
3972
|
+
try {
|
|
3973
|
+
await f.one.send({ messageId: "expired-opening-one", text: "tools" });
|
|
3974
|
+
const firstDone = f.one.wait();
|
|
3975
|
+
firstDone.catch(() => {});
|
|
3976
|
+
await Promise.race([entered.promise, firstDone.then(() => {
|
|
3977
|
+
throw new Error("The bound tool turn ended before executing in captured CONTROL.");
|
|
3978
|
+
})]);
|
|
3979
|
+
await f.two.send({ messageId: "expired-opening-peer", text: "wait" });
|
|
3980
|
+
assert.equal(f.sharedProvider.threadRequestHandlers.size, 2);
|
|
3981
|
+
let stopped = false;
|
|
3982
|
+
const stopping = f.one.cancel().then(() => { stopped = true; });
|
|
3983
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
3984
|
+
assert.equal(stopped, false, "Stop joins the existing in-flight tool");
|
|
3985
|
+
release.resolve();
|
|
3986
|
+
await stopping;
|
|
3987
|
+
assert.equal((await f.two.read()).status, "working", "one thread's Stop retains its peer");
|
|
3988
|
+
hold = false;
|
|
3989
|
+
await f.two.send({ messageId: "expired-opening-peer-finish", text: "Continue", steer: true });
|
|
3990
|
+
await f.two.wait();
|
|
3991
|
+
await f.two.send({ messageId: "expired-opening-two", text: "tools" });
|
|
3992
|
+
const result = await f.two.wait();
|
|
3993
|
+
assert.deepEqual(f.effects.map(value => value.conversationId), ["bound-one", "bound-two"]);
|
|
3994
|
+
assert.deepEqual(f.effects.map(value => value.messageId), ["expired-opening-one", "expired-opening-two"]);
|
|
3995
|
+
assert.equal(result.conversationLog.at(-1).metadata.applicationTools.at(-1).result.result.result.value, 7);
|
|
3996
|
+
assert.equal(f.sharedProvider.threadRequestHandlers.size, 0);
|
|
3997
|
+
assert.equal((await f.trace()).filter(row => row.args).length, 1, "both threads keep the original account-shared app-server");
|
|
3998
|
+
assert.equal((await f.trace()).filter(row => row.method === "thread/start").length, 3);
|
|
3999
|
+
} finally { release.resolve(); }
|
|
4000
|
+
});
|
|
4001
|
+
|
|
4002
|
+
test("bound native Codex configuration notifications reach only the original conversation subscriber without native work", async t => {
|
|
4003
|
+
const f = await boundCodexToolsFixture(t);
|
|
4004
|
+
await f.one.send({ messageId: "bound-configuration-seed", text: "Hello" });
|
|
4005
|
+
await f.one.wait();
|
|
4006
|
+
const events = [], peerEvents = [];
|
|
4007
|
+
const release = await f.one.subscribe(event => events.push(event));
|
|
4008
|
+
await f.two.subscribe(event => peerEvents.push(event));
|
|
4009
|
+
assert.equal(f.bindings.get("bound-one").commands, undefined,
|
|
4010
|
+
"The original native-backed host has no command facade");
|
|
4011
|
+
const notification = { type: "configuration" };
|
|
4012
|
+
await f.boundRuntime.publishNative({ namespace: "main-bound-one", sessionId: "foreign-session", event: notification });
|
|
4013
|
+
await f.boundRuntime.publishNative({ namespace: "foreign-namespace", sessionId: "bound-one", event: notification });
|
|
4014
|
+
assert.deepEqual(events, [], "Foreign publication identities cannot invalidate this browser");
|
|
4015
|
+
for (const status of ["ready", "working"]) {
|
|
4016
|
+
if (status === "working") await f.one.send({ messageId: "bound-configuration-active", text: "wait" });
|
|
4017
|
+
assert.equal((await f.one.read()).status, status);
|
|
4018
|
+
const before = await f.storage.read("bound-one", async transaction => ({
|
|
4019
|
+
metadata: await transaction.readMetadata(), history: await Promise.all((await transaction.listTurnIds()).map(id => transaction.readTurn(id)))
|
|
4020
|
+
}));
|
|
4021
|
+
const nativeBefore = await f.trace();
|
|
4022
|
+
const count = events.length;
|
|
4023
|
+
await f.boundRuntime.publishNative({ namespace: "main-bound-one", sessionId: "bound-one", event: notification });
|
|
4024
|
+
assert.deepEqual(events.slice(count), [{ conversationId: "bound-one", ...notification }],
|
|
4025
|
+
`The existing subscriber receives its configuration notification while ${status}`);
|
|
4026
|
+
assert.deepEqual(peerEvents, [], "The account-shared native peer keeps its independent subscriber");
|
|
4027
|
+
assert.deepEqual(await f.storage.read("bound-one", async transaction => ({
|
|
4028
|
+
metadata: await transaction.readMetadata(), history: await Promise.all((await transaction.listTurnIds()).map(id => transaction.readTurn(id)))
|
|
4029
|
+
})), before, "A browser invalidation does not rewrite native identity or transcript");
|
|
4030
|
+
assert.deepEqual(await f.trace(), nativeBefore, "A browser invalidation performs no native request");
|
|
4031
|
+
}
|
|
4032
|
+
release();
|
|
4033
|
+
const count = events.length;
|
|
4034
|
+
await f.boundRuntime.publishNative({ namespace: "main-bound-one", sessionId: "bound-one", event: notification });
|
|
4035
|
+
assert.equal(events.length, count, "Releasing the original browser reader ends its notifications");
|
|
4036
|
+
await f.one.cancel();
|
|
4037
|
+
});
|