@jskit-ai/assistant-core 0.1.200 → 0.1.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,2122 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, mkdir, rm, readFile, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { createHash } from "node:crypto";
7
+ import { createConversationRuntime, createFileConversationStorage } from "../src/server/conversation/index.js";
8
+ import { createCodexConversationStore } from "../src/server/conversation/agentRun.js";
9
+ import { createReentrantConversationStorage } from "../src/server/conversation/storage.js";
10
+ import { createConversationTranscript } from "../src/server/conversation/transcript.js";
11
+ import { createConversationStreams } from "../src/server/conversation/streams.js";
12
+ import { createLocalConversationExecution } from "../src/server/conversation/localExecution.js";
13
+ import { createCodexConversationDriver } from "../src/server/conversation/providers/codexDriver.js";
14
+ import { createCodexAppServerProviderOwner } from "../src/server/conversation/codexProviderOwner.js";
15
+ import { CodexAppServerJsonRpcClient } from "../src/server/conversation/codexClient.js";
16
+ import { createActionCatalogue } from "@jskit-ai/kernel/server/actions";
17
+ import { createSchema } from "json-rest-schema";
18
+ import { createAiConnectionResolver } from "../../connectors-catalog/src/server/ai.js";
19
+
20
+ const configuration = { systemPrompt: "Keep these instructions fresh.", model: "test-model", effort: "high" };
21
+ const input = { messageId: "first", text: "Hello" };
22
+
23
+ test("Codex structured output uses the existing native turn setting without changing conversation identity", async t => {
24
+ const outputSchema = { type: "object", additionalProperties: false,
25
+ properties: { answer: { type: "string", maxLength: 32 } }, required: ["answer"] };
26
+ const f = await fixture(t, { configuration: { ...configuration, outputSchema } });
27
+ await f.conversation.send(input);
28
+ const first = await f.conversation.wait();
29
+ assert.equal(first.capabilities.structuredOutput, true);
30
+ assert.deepEqual(first.configuration.outputSchema, outputSchema);
31
+ const turns = () => f.trace().then(rows => rows.filter(row => row.method === "turn/start"));
32
+ assert.deepEqual((await turns())[0].params.outputSchema, outputSchema);
33
+ const threadId = (await f.storage.read("conversation", tx => tx.readMetadata())).runtime.binding.threadId;
34
+ await assert.rejects(f.conversation.configure({ outputSchema: { type: "string" } }), /finite positive maxLength/);
35
+ assert.equal((await turns()).length, 1, "An unbounded schema fails before another provider turn");
36
+ assert.deepEqual((await f.conversation.read()).configuration.outputSchema, outputSchema);
37
+ await f.conversation.configure({ outputSchema: undefined });
38
+ await f.conversation.send({ messageId: "ordinary", text: "Use ordinary output" });
39
+ await f.conversation.wait();
40
+ assert.equal(Object.hasOwn((await turns())[1].params, "outputSchema"), false);
41
+ assert.equal((await f.storage.read("conversation", tx => tx.readMetadata())).runtime.binding.threadId, threadId);
42
+ });
43
+
44
+ test("native binding writes preserve the current original run without copying it to a replacement", async t => {
45
+ const f = await fixture(t);
46
+ const savedRun = { id: "codex_app_server", state: "completed", active: false,
47
+ outerTurnId: "outer-before-open", providerTurnId: "native-before-open", sentinel: { kept: true },
48
+ events: [{ kind: "complete", detail: { kept: true } }] };
49
+ async function writeRun(run) {
50
+ await f.storage.write("conversation", async transaction => {
51
+ const metadata = await transaction.readMetadata();
52
+ metadata.runtime.binding.codexAppServerRun = run;
53
+ await transaction.writeMetadata(metadata);
54
+ });
55
+ }
56
+ const state = () => f.storage.read("conversation", async transaction => (await transaction.readMetadata()).runtime);
57
+ await writeRun(savedRun);
58
+ await f.conversation.send(input);
59
+ await f.conversation.wait();
60
+ function assertAdvanced(run, previous, messageId, threadId) {
61
+ assert.deepEqual(run.sentinel, previous.sentinel);
62
+ assert.deepEqual(run.events.slice(0, previous.events.length), previous.events,
63
+ "Binding writes preserve the exact ordered event prefix from the current store.");
64
+ const kinds = run.events.slice(previous.events.length).map(event => event.kind);
65
+ const claimed = kinds.indexOf("codex-app-server-turn-claimed");
66
+ const active = kinds.indexOf("codex-app-server-turn-active");
67
+ const finalizing = kinds.indexOf("codex-app-server-turn-finalizing");
68
+ const idle = kinds.indexOf("codex-app-server-turn-idle");
69
+ assert.ok(claimed >= 0 && claimed < active && active < finalizing && finalizing < idle);
70
+ assert.equal(run.outerTurnId, messageId);
71
+ assert.equal(run.providerThreadId, threadId);
72
+ assert.ok(run.providerTurnId);
73
+ assert.notEqual(run.providerTurnId, previous.providerTurnId);
74
+ assert.equal(run.state, "completed");
75
+ assert.equal(run.active, false);
76
+ }
77
+ const firstState = await state();
78
+ assertAdvanced(firstState.binding.codexAppServerRun, savedRun, input.messageId, firstState.binding.threadId);
79
+ await f.first.close();
80
+ const reopened = await f.runtime().open({ id: "conversation" });
81
+ const latestRun = { ...savedRun, outerTurnId: "outer-latest", providerTurnId: "native-latest",
82
+ events: [...savedRun.events, { kind: "later-record", unknownField: [1, 2] }] };
83
+ await writeRun(latestRun);
84
+ await reopened.send({ messageId: "after-open", text: "Continue" });
85
+ const completed = await reopened.wait();
86
+ const latestState = await state();
87
+ const advancedRun = latestState.binding.codexAppServerRun;
88
+ assertAdvanced(advancedRun, latestRun, "after-open", latestState.binding.threadId);
89
+ await reopened.replace({ operationId: "fresh-native", expectedSegmentId: completed.segmentId,
90
+ reason: "renewal", briefing: "Continue the saved work." });
91
+ const replacement = await state();
92
+ assert.equal(Object.hasOwn(replacement.binding, "codexAppServerRun"), false);
93
+ assert.deepEqual(replacement.predecessors.at(-1).binding.codexAppServerRun, advancedRun);
94
+ });
95
+
96
+ test("Codex exposes the durable native identity before dispatch and respects a rejected gate", async t => {
97
+ const f = await fixture(t);
98
+ await f.first.close();
99
+ const driver = createCodexConversationDriver(f.driverOptions);
100
+ const readBinding = () => f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding);
101
+ const storage = createReentrantConversationStorage(f.storage);
102
+ const segmentId = await storage.read("conversation", async tx => (await tx.readMetadata()).runtime.segmentId);
103
+ const store = createCodexConversationStore({ storage, scope: "conversation", segmentId, isCurrent: () => true,
104
+ transcript: createConversationTranscript({ storage }), streams: createConversationStreams() });
105
+ const provider = await driver.open({ binding: await readBinding(),
106
+ conversation: { sessionId: "conversation", runtime: { store, getSession: store.getSession }, publish() {}, checkpoint() {} },
107
+ writeBinding: binding => f.storage.write("conversation", async tx => {
108
+ const metadata = await tx.readMetadata();
109
+ const { codexAppServerRun: _stale, ...next } = binding;
110
+ metadata.runtime.binding = { ...next, ...(metadata.runtime.binding.codexAppServerRun
111
+ ? { codexAppServerRun: metadata.runtime.binding.codexAppServerRun } : {}) };
112
+ await tx.writeMetadata(metadata);
113
+ }),
114
+ onFailure() {}
115
+ });
116
+ const blocked = new Error("Dispatch stopped by the persisted delivery gate.");
117
+ let checked = false;
118
+ try {
119
+ await assert.rejects(provider.run({ configuration, input, signal: new AbortController().signal,
120
+ async beforeDispatch(identity) {
121
+ assert.deepEqual(identity, { threadId: (await readBinding()).threadId });
122
+ assert.ok(identity.threadId);
123
+ checked = true;
124
+ throw blocked;
125
+ },
126
+ accept() { assert.fail("A rejected dispatch must not admit the message."); },
127
+ onMessage() {}, onEvent() {}
128
+ }), error => error === blocked);
129
+ assert.equal(checked, true);
130
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 0);
131
+ } finally { await provider.dispose(); }
132
+ });
133
+
134
+ test("Codex goal controls preserve native scheduling without authored command messages", async t => {
135
+ const f = await fixture(t);
136
+ const state = await f.conversation.read();
137
+ assert.deepEqual(state.capabilities.goalCommands, {
138
+ set: { delivery: "control", interruptsTurn: false }, resume: { delivery: "control", interruptsTurn: false },
139
+ pause: { delivery: "control", interruptsTurn: false }, cancel: { delivery: "control", interruptsTurn: false }
140
+ });
141
+ const command = { action: "set", objective: "Finish the goal", expectedSegmentId: state.segmentId, tokenBudget: 40 };
142
+ const events = [];
143
+ await f.conversation.subscribe(event => events.push(event));
144
+ assert.equal((await f.conversation.updateGoal(command)).objective, command.objective);
145
+ const completed = await settledNativeGoal(f.conversation);
146
+ assert.equal(completed.error, "");
147
+ assert.equal(completed.goal.status, "budgetLimited");
148
+ assert.equal(completed.goal.tokensUsed, 45);
149
+ assert.equal(completed.conversationLog.some(turn => turn.user || turn.system), false);
150
+ assert.equal(completed.pendingRequest, null);
151
+ const saved = await f.storage.read("conversation", async transaction => (await transaction.readMetadata()).runtime);
152
+ assert.equal(Object.hasOwn(saved.binding, "goalRequest"), false);
153
+ assert.equal(Object.hasOwn(saved.binding, "goalReceipt"), false);
154
+ assert.equal(saved.binding.codexAppServerRun.providerThreadId, saved.binding.threadId);
155
+ assert.equal(saved.binding.codexAppServerRun.providerGoalStatus, "budgetLimited");
156
+ const trace = await f.trace();
157
+ assert.equal(trace.filter(row => row.method === "thread/goal/set").length, 1);
158
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 0);
159
+ assert.equal(trace.some(row => row.method === "thread/inject_items"), false);
160
+ assert.equal(trace.find(row => row.method === "thread/start").params.config.features.goals, true);
161
+ assert.equal(events.filter(event => event.type === "accepted").length, 0);
162
+ assert.ok(events.some(event => event.type === "goal" && event.goal?.status === "budgetLimited"));
163
+ });
164
+
165
+ test("a goal added during ordinary Codex work preserves its answer, tool receipt and native turn", async t => {
166
+ const entered = Promise.withResolvers(), complete = Promise.withResolvers();
167
+ let calls = 0;
168
+ const authorized = [];
169
+ const context = { actor: { id: "original" }, surface: "app", permissions: ["numbers.read"] };
170
+ const f = await fixture(t, { context, checkpoints: true,
171
+ actions: applicationActions(async () => {
172
+ calls++;
173
+ if (calls === 1) { entered.resolve(); return complete.promise; }
174
+ return { value: calls };
175
+ }),
176
+ authorize: ({ context, operation }) => { if (operation === "tool") authorized.push(context.actor.id); return true; }
177
+ });
178
+ await f.conversation.send({ messageId: "ordinary", text: "tools" });
179
+ await entered.promise;
180
+ const before = await f.conversation.read();
181
+ const other = await f.first.open({ id: "conversation", context: { ...context, actor: { id: "goal-owner" } } });
182
+ const command = { action: "set", messageId: "during", objective: "tools goal", expectedSegmentId: before.segmentId, tokenBudget: 40 };
183
+ await other.updateGoal(command);
184
+ const admitted = await other.read();
185
+ assert.equal(admitted.conversationLog[0].metadata.runtime.nativeTurnId, before.conversationLog[0].metadata.runtime.nativeTurnId);
186
+ assert.equal(admitted.conversationLog[0].metadata.applicationTools.at(-1).status, "running");
187
+ assert.equal(admitted.conversationLog.length, 1);
188
+ assert.equal(admitted.pendingRequest, null);
189
+ complete.resolve({ value: 42 });
190
+ const state = await other.wait();
191
+ assert.equal(state.error, "");
192
+ assert.equal(state.goal.status, "budgetLimited");
193
+ assert.equal(state.conversationLog.filter(turn => turn.user || turn.system).length, 1);
194
+ assert.equal(state.conversationLog[0].metadata.applicationTools.find(call => call.name === "assistant_action_execute").result.result.result.value, 42);
195
+ assert.equal(state.conversationLog[0].metadata.runtime.goalMessageId, undefined);
196
+ assert.equal(state.conversationLog[0].metadata.runtime.nativeTurnId, before.conversationLog[0].metadata.runtime.nativeTurnId);
197
+ assert.equal(state.conversationLog[0].metadata.applicationTools.length, 12);
198
+ assert.equal(calls, 4);
199
+ assert.ok(authorized.includes("original"));
200
+ assert.equal(authorized.every(actor => actor === "original"), true);
201
+ const trace = await f.trace();
202
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 1);
203
+ assert.equal(trace.filter(row => row.method === "thread/goal/set").length, 1);
204
+ assert.equal(trace.filter(row => row.args).length, 1);
205
+ assert.equal(trace.some(row => row.method === "thread/inject_items"), false);
206
+ });
207
+
208
+ test("older uncertain Codex goal-message records remain inspection-only", async t => {
209
+ const f = await fixture(t);
210
+ await f.conversation.send(input);
211
+ const before = await f.conversation.wait();
212
+ const command = { action: "set", expectedGoalId: null, expectedSegmentId: before.segmentId, objective: "Old goal" };
213
+ await f.storage.write("conversation", async transaction => {
214
+ const metadata = await transaction.readMetadata();
215
+ const pending = { messageId: "old-goal", text: command.objective, origin: "user", attachments: [], goal: command,
216
+ at: "2026-01-01T00:00:00.000Z", attempted: true, threadId: metadata.runtime.binding.threadId,
217
+ message: "Old frozen native prompt", seen: metadata.runtime.seen };
218
+ metadata.runtime.request = pending;
219
+ metadata.runtime.binding.goalRequest = { messageId: pending.messageId, afterTurnId: "old-boundary", command };
220
+ metadata.runtime.binding.goalReceipt = { messageId: pending.messageId, afterTurnId: "old-boundary" };
221
+ await transaction.writeMetadata(metadata);
222
+ const turnId = await transaction.nextTurnId();
223
+ await transaction.appendMessage(turnId, { role: "user", ...pending,
224
+ turnMetadata: { runtime: { engine: "codex", segmentId: before.segmentId, status: "interrupted" } } });
225
+ });
226
+ const snapshot = () => f.storage.read("conversation", async transaction => ({ metadata: await transaction.readMetadata(),
227
+ turns: await Promise.all((await transaction.listTurnIds()).map(id => transaction.readTurn(id))) }));
228
+ const saved = await snapshot();
229
+ await assert.rejects(f.conversation.updateGoal(command), { code: "conversation_delivery_uncertain" });
230
+ const inspected = await f.conversation.inspectDelivery({ messageId: "old-goal" });
231
+ assert.equal(inspected.status, "accepted");
232
+ assert.match(inspected.recoveryLimitation, /offline inspection/);
233
+ assert.deepEqual(await snapshot(), saved);
234
+ await f.storage.write("conversation", async transaction => {
235
+ const metadata = await transaction.readMetadata();
236
+ delete metadata.runtime.request;
237
+ await transaction.writeMetadata(metadata);
238
+ });
239
+ const bindingOnly = await snapshot();
240
+ await assert.rejects(f.conversation.updateGoal(command), { code: "conversation_delivery_uncertain" });
241
+ assert.deepEqual(await snapshot(), bindingOnly);
242
+ assert.equal((await f.trace()).filter(row => row.method === "thread/goal/set").length, 0);
243
+ });
244
+
245
+ test("Cancel stops observed Codex work without implicitly pausing its standalone goal", async t => {
246
+ const f = await fixture(t);
247
+ const { segmentId } = await f.conversation.read();
248
+ const goal = await f.conversation.updateGoal({ action: "set", objective: "wait goal", expectedSegmentId: segmentId });
249
+ assert.equal((await f.conversation.read()).status, "working");
250
+ assert.deepEqual(await f.conversation.cancel(), { stopped: true });
251
+ assert.equal((await f.conversation.read()).status, "ready");
252
+ assert.equal((await f.conversation.readGoal()).status, "active");
253
+ assert.equal((await f.conversation.read()).conversationLog.some(turn => turn.user || turn.system), false);
254
+ assert.deepEqual((await f.trace()).filter(row => row.method === "thread/goal/set").map(row => row.params.status), ["active"]);
255
+ await f.conversation.updateGoal({ action: "pause", expectedGoalId: goal.id, expectedSegmentId: segmentId });
256
+ assert.equal((await f.conversation.readGoal()).status, "paused");
257
+ assert.deepEqual(await f.conversation.cancel(), { stopped: false });
258
+ });
259
+
260
+ test("explicit Codex steering joins the original active goal without inventing a starting run", async t => {
261
+ const f = await fixture(t);
262
+ const { segmentId } = await f.conversation.read();
263
+ const goal = await f.conversation.updateGoal({ action: "set", objective: "wait goal", expectedSegmentId: segmentId });
264
+ const original = await f.storage.read("conversation", async transaction => (await transaction.readMetadata()).runtime.binding.codexAppServerRun);
265
+ assert.equal(original.active, true);
266
+ const receipt = await f.conversation.send({ messageId: "steer-control", text: "Finish this part", steer: true });
267
+ assert.equal(receipt.status, "accepted");
268
+ await f.conversation.updateGoal({ action: "pause", expectedGoalId: goal.id, expectedSegmentId: segmentId });
269
+ const completed = await f.conversation.wait();
270
+ assert.equal(completed.error, "");
271
+ const authored = completed.conversationLog.filter(turn => turn.user || turn.system);
272
+ assert.equal(authored.length, 1);
273
+ assert.equal(authored[0].user.messageId, "steer-control");
274
+ assert.equal(authored[0].metadata.runtime.nativeTurnId, original.providerTurnId);
275
+ assert.equal(authored[0].assistant.text, "Steered: Finish this part");
276
+ const stored = await f.storage.read("conversation", async transaction => (await transaction.readMetadata()).runtime.binding.codexAppServerRun);
277
+ assert.equal(stored.outerTurnId, original.outerTurnId);
278
+ assert.equal(stored.events.some(event => event.kind === "codex-app-server-turn-claimed"), false);
279
+ const trace = await f.trace();
280
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 0);
281
+ assert.deepEqual(trace.filter(row => row.method === "turn/steer").map(row => ({
282
+ threadId: row.params.threadId, turnId: row.params.expectedTurnId, messageId: row.params.clientUserMessageId
283
+ })), [{ threadId: original.providerThreadId, turnId: original.providerTurnId, messageId: "steer-control" }]);
284
+ });
285
+
286
+ test("a rejected goal leaves ordinary Codex work running and steerable", async t => {
287
+ const f = await fixture(t);
288
+ await f.conversation.send({ messageId: "ordinary", text: "steering" });
289
+ const { segmentId } = await f.conversation.read();
290
+ await assert.rejects(f.conversation.updateGoal({ action: "set", messageId: "rejected-goal", objective: "rejected goal",
291
+ expectedSegmentId: segmentId }), /Goal rejected/);
292
+ const pending = await f.conversation.read();
293
+ assert.equal(pending.pendingRequest, null);
294
+ assert.equal(pending.conversationLog.length, 1);
295
+ await f.conversation.send({ messageId: "finish", text: "Finish now", steer: true });
296
+ const state = await f.conversation.wait();
297
+ assert.equal(state.error, "");
298
+ assert.equal(state.conversationLog.at(-1).assistant.text, "Steered: Finish now");
299
+ assert.equal((await f.trace()).filter(row => row.args).length, 1);
300
+ });
301
+
302
+ test("a native create_goal retains its authored owner and recovers only the exact tracked result", async t => {
303
+ const f = await fixture(t, { checkpoints: true });
304
+ await f.conversation.send({ messageId: "native", text: "native-goal" });
305
+ const state = await f.conversation.wait();
306
+ assert.equal(state.error, "");
307
+ assert.equal(state.goal.status, "complete");
308
+ assert.deepEqual(state.conversationLog.filter(turn => turn.user || turn.system).map(turn => turn.user.messageId), ["native"]);
309
+ assert.equal(state.conversationLog.some(turn => turn.system?.text === "Continue working on the goal."), false);
310
+ const native = JSON.parse(await readFile(path.join(f.directory, "history.json"), "utf8"));
311
+ const lastTurnId = native.turns.at(-1).id;
312
+ const checkpoint = f.checkpoints.find(record => record.metadata.runtime.binding.codexAppServerRun?.providerTurnId === lastTurnId &&
313
+ record.metadata.runtime.binding.codexAppServerRun.active &&
314
+ !record.turns.some(([, turn]) => turn.messages.some(message => message.role === "assistant" && message.text === "Goal reply 3")));
315
+ assert.ok(checkpoint, "The original owner must persist successor ownership before its output.");
316
+ assert.equal(checkpoint.metadata.runtime.binding.codexAppServerRun.outerTurnId, "native");
317
+ const savedReplies = checkpoint.turns.flatMap(([, turn]) => turn.messages.filter(message => message.role === "assistant"));
318
+ await f.restore(checkpoint);
319
+ const reopened = await f.runtime().open({ id: "conversation" });
320
+ const receipt = await reopened.inspectDelivery({ messageId: "native" });
321
+ assert.equal(receipt.status, "accepted");
322
+ assert.equal(receipt.recoveryLimitation, undefined);
323
+ const recovered = await reopened.read();
324
+ const replies = recovered.conversationLog.flatMap(turn => turn.messages.filter(message => message.role === "assistant"));
325
+ for (const saved of savedReplies) assert.deepEqual(replies.find(message => message.messageId === saved.messageId), saved);
326
+ assert.equal(replies.filter(message => message.text === "Goal reply 3").length, 1);
327
+ assert.deepEqual(recovered.conversationLog.filter(turn => turn.user || turn.system).map(turn => turn.user.messageId), ["native"]);
328
+ await reopened.inspectDelivery({ messageId: "native" });
329
+ assert.deepEqual((await reopened.read()).conversationLog, recovered.conversationLog);
330
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
331
+ assert.equal((await f.trace()).some(row => row.method === "thread/goal/set"), false);
332
+ });
333
+
334
+ test("Codex goal pause, process restart, resume and clear retain the same goal identity", async t => {
335
+ const f = await fixture(t);
336
+ const { segmentId } = await f.conversation.read();
337
+ await f.conversation.updateGoal({ action: "set", messageId: "start", objective: "wait goal", expectedSegmentId: segmentId });
338
+ const goal = await f.conversation.readGoal();
339
+ assert.equal(goal.status, "active");
340
+ await f.conversation.updateGoal({ action: "pause", expectedGoalId: goal.id, expectedSegmentId: segmentId });
341
+ assert.equal((await f.conversation.readGoal()).status, "paused");
342
+ await f.first.close();
343
+ const reopened = await f.runtime().open({ id: "conversation" });
344
+ assert.equal((await reopened.readGoal()).id, goal.id);
345
+ await reopened.updateGoal({ action: "resume", messageId: "resume", expectedGoalId: goal.id, expectedSegmentId: segmentId });
346
+ assert.equal((await reopened.readGoal()).status, "active");
347
+ assert.equal(await reopened.updateGoal({ action: "cancel", expectedGoalId: goal.id, expectedSegmentId: segmentId }), null);
348
+ assert.equal(await reopened.readGoal(), null);
349
+ await reopened.cancel();
350
+ assert.equal((await reopened.wait()).status, "ready");
351
+ });
352
+
353
+ test("reading a Codex goal started by another native client does not pause it", async t => {
354
+ const f = await fixture(t);
355
+ await f.conversation.send(input);
356
+ assert.equal((await f.conversation.wait()).status, "ready");
357
+ const { args } = (await f.trace()).find(row => row.args);
358
+ const external = new CodexAppServerJsonRpcClient({ endpoint: args[args.indexOf("--listen") + 1] });
359
+ await external.connect();
360
+ try {
361
+ await external.initialize();
362
+ const { id: threadId } = JSON.parse(await readFile(path.join(f.directory, "history.json"), "utf8"));
363
+ const { goal } = await external.request("thread/goal/set", { threadId, objective: "wait goal", status: "active" });
364
+ for (let index = 0; index < 2; index++) {
365
+ const observed = await f.conversation.readGoal();
366
+ assert.equal(observed.status, "active");
367
+ assert.equal(observed.objective, goal.objective);
368
+ }
369
+ assert.equal((await external.request("thread/goal/get", { threadId })).goal.status, "active");
370
+ assert.equal((await f.trace()).filter(row => row.method === "thread/goal/set").length, 1);
371
+ } finally { external.close(); }
372
+ });
373
+
374
+ test("goal commands reject stale identity, invalid budgets and unauthorized callers", async t => {
375
+ const f = await fixture(t);
376
+ const { segmentId } = await f.conversation.read();
377
+ const command = { action: "set", objective: "Finish the goal", expectedSegmentId: segmentId };
378
+ await assert.rejects(f.conversation.updateGoal({ ...command, tokenBudget: 0 }), /positive token budget/);
379
+ await assert.rejects(f.conversation.updateGoal({ ...command, expectedSegmentId: "old" }), /conversation changed/);
380
+ await f.conversation.updateGoal(command);
381
+ await f.conversation.wait();
382
+ await assert.rejects(f.conversation.updateGoal(command), /native goal changed/);
383
+ await assert.rejects(f.conversation.updateGoal({ action: "cancel", expectedSegmentId: segmentId, expectedGoalId: "stale" }), /native goal changed/);
384
+ const denied = await fixture(t, { authorize: ({ operation }) => operation !== "goal" });
385
+ await assert.rejects(denied.conversation.updateGoal({ ...command, expectedSegmentId: (await denied.conversation.read()).segmentId }), { statusCode: 403 });
386
+ });
387
+
388
+ test("accepted Codex output is recovered after a restart without resending the request", async t => {
389
+ const f = await fixture(t, { checkpoints: true });
390
+ await f.conversation.send({ ...input, text: "blocks-without-phase" });
391
+ const finished = await f.conversation.wait();
392
+ const checkpoint = f.checkpoints.find(record => record.turns[0]?.[1].metadata?.runtime?.nativeTurnId &&
393
+ record.turns[0][1].metadata.runtime.status === "running" &&
394
+ record.turns[0][1].messages.some(message => message.role === "commentary" && message.text === "Checking the numbers.") &&
395
+ !record.turns[0][1].messages.some(message => message.role === "assistant"));
396
+ assert.ok(checkpoint);
397
+ await f.restore(checkpoint);
398
+ const reopened = await f.runtime().open({ id: "conversation" });
399
+ const receipt = await reopened.inspectDelivery({ messageId: input.messageId });
400
+ assert.equal(receipt.duplicate, true);
401
+ assert.equal(receipt.recovered, true);
402
+ const state = await reopened.read();
403
+ assert.equal(state.conversationLog[0].metadata.runtime.status, "complete");
404
+ assert.equal(state.conversationLog[0].assistant.text, finished.conversationLog[0].assistant.text);
405
+ assert.equal(state.conversationLog[0].commentary[0].text, "Checking the numbers.");
406
+ await reopened.inspectDelivery({ messageId: input.messageId });
407
+ assert.deepEqual((await reopened.read()).conversationLog, state.conversationLog);
408
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
409
+ });
410
+
411
+ test("a native completed goal cannot certify an application tool with an unknown outcome", async t => {
412
+ let calls = 0;
413
+ const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
414
+ const f = await fixture(t, { actions: applicationActions(async () => ({ value: ++calls })), context, checkpoints: true,
415
+ nativeGoalObjective: "tools goal", limits: { maxToolCalls: 9 } });
416
+ await f.conversation.send({ messageId: "uncertain-effect", text: "native-goal" });
417
+ assert.equal((await f.conversation.wait()).goal.status, "complete");
418
+ const checkpoint = f.checkpoints.find(record => record.turns.some(([, turn]) =>
419
+ turn.messages.some(message => message.role === "user" && message.messageId === "uncertain-effect") &&
420
+ turn.metadata?.applicationTools?.length === 3 && turn.metadata.applicationTools[2].status === "running"));
421
+ assert.ok(checkpoint, "The authored effect reservation must be durable before its result.");
422
+ const [turnId, reserved] = checkpoint.turns.find(([, turn]) =>
423
+ turn.messages.some(message => message.role === "user" && message.messageId === "uncertain-effect"));
424
+ assert.equal(checkpoint.metadata.runtime.binding.codexAppServerRun.outerTurnId, "uncertain-effect");
425
+ await f.restore(checkpoint);
426
+ const reopened = await f.runtime().open({ id: "conversation", context });
427
+ const receipt = await reopened.inspectDelivery({ messageId: "uncertain-effect" });
428
+ assert.equal(receipt.status, "accepted");
429
+ assert.equal(receipt.turnId, turnId);
430
+ const state = await reopened.read();
431
+ const authored = state.conversationLog.filter(turn => turn.user || turn.system);
432
+ assert.equal(authored.length, 1);
433
+ assert.equal(authored[0].user.messageId, "uncertain-effect");
434
+ assert.equal(authored[0].metadata.runtime.status, "interrupted");
435
+ assert.match(authored[0].metadata.runtime.error, /tool has no verified result/);
436
+ assert.match(state.error, /tool has no verified result/);
437
+ assert.deepEqual(authored[0].metadata.applicationTools, reserved.metadata.applicationTools);
438
+ await reopened.inspectDelivery({ messageId: "uncertain-effect" });
439
+ assert.deepEqual((await reopened.read()).conversationLog, state.conversationLog);
440
+ assert.equal(calls, 3);
441
+ const trace = await f.trace();
442
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 1);
443
+ assert.equal(trace.filter(row => row.toolResponse).length, 9);
444
+ assert.equal(trace.some(row => row.method === "thread/inject_items" || row.method === "thread/goal/set"), false);
445
+ });
446
+
447
+ test("native history cannot overwrite cancelled work or turn a failed answer into success", async t => {
448
+ const f = await fixture(t);
449
+ await f.conversation.send({ messageId: "cancelled", text: "steering" });
450
+ await f.conversation.cancel();
451
+ await f.conversation.inspectDelivery({ messageId: "cancelled" });
452
+ assert.equal((await f.conversation.read()).conversationLog[0].metadata.runtime.status, "cancelled");
453
+ await f.conversation.send({ messageId: "failed", text: "failed" });
454
+ await f.conversation.wait();
455
+ await f.conversation.inspectDelivery({ messageId: "failed" });
456
+ assert.equal((await f.conversation.read()).conversationLog[1].metadata.runtime.status, "failed");
457
+ });
458
+
459
+ test("automatic goal turns retain all nine application tool receipts on their authored request", async t => {
460
+ const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
461
+ let calls = 0;
462
+ const actions = applicationActions(async () => ({ value: ++calls }));
463
+ const f = await fixture(t, { actions, context, nativeGoalObjective: "tools goal", limits: { maxToolCalls: 9 } });
464
+ await f.conversation.send({ messageId: "tools-goal", text: "native-goal" });
465
+ const state = await f.conversation.wait();
466
+ assert.equal(state.error, "");
467
+ assert.equal(calls, 3);
468
+ const authored = state.conversationLog.filter(turn => turn.user || turn.system);
469
+ assert.equal(authored.length, 1);
470
+ assert.equal(authored[0].user.messageId, "tools-goal");
471
+ assert.equal(authored[0].metadata.applicationTools.length, 9);
472
+ assert.equal(new Set(authored[0].metadata.applicationTools.map(call => call.id)).size, 9);
473
+ assert.ok(authored[0].metadata.applicationTools.every(call => call.status === "complete"));
474
+ assert.deepEqual(authored[0].metadata.applicationTools.filter(call => call.name === "assistant_action_execute")
475
+ .map(call => call.result.result.result.value), [1, 2, 3]);
476
+ for (const turn of state.conversationLog) {
477
+ assert.equal(turn.metadata.runtime.status, "complete");
478
+ if (turn !== authored[0]) assert.equal(turn.metadata.applicationTools, undefined);
479
+ }
480
+ const replies = state.conversationLog.flatMap(turn => turn.messages.filter(message => message.role === "assistant"));
481
+ for (const text of ["Goal reply 1", "Goal reply 2", "Goal reply 3"]) {
482
+ assert.equal(replies.filter(message => message.text === text).length, 1);
483
+ }
484
+ const trace = await f.trace();
485
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 1);
486
+ assert.equal(trace.filter(row => row.toolResponse).length, 9);
487
+ assert.equal(trace.some(row => row.method === "thread/inject_items" || row.method === "thread/goal/set"), false);
488
+ });
489
+
490
+ test("automatic goal turns cannot reset their authored request's application tool budget", async t => {
491
+ const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
492
+ let calls = 0;
493
+ const f = await fixture(t, { actions: applicationActions(async () => ({ value: ++calls })), context,
494
+ nativeGoalObjective: "tools goal", limits: { maxToolCalls: 3 } });
495
+ await f.conversation.send({ messageId: "bounded-goal", text: "native-goal" });
496
+ const state = await f.conversation.wait();
497
+ const trace = await f.trace();
498
+ const responses = trace.filter(row => row.toolResponse);
499
+ assert.equal(responses.length, 4);
500
+ assert.match(responses.at(-1).toolResponse.error.message, /application tool-call limit/);
501
+ assert.equal(state.status, "ready");
502
+ assert.match(state.error, /application tool-call limit/);
503
+ assert.equal(calls, 1);
504
+ const authored = state.conversationLog.filter(turn => turn.user || turn.system);
505
+ assert.equal(authored.length, 1);
506
+ assert.equal(authored[0].user.messageId, "bounded-goal");
507
+ assert.equal(authored[0].metadata.runtime.status, "failed");
508
+ assert.equal(authored[0].metadata.applicationTools.length, 3);
509
+ assert.ok(authored[0].metadata.applicationTools.every(call => call.status === "complete"));
510
+ assert.equal(authored[0].metadata.applicationTools.at(-1).result.result.result.value, 1);
511
+ assert.equal(state.conversationLog.some(turn => turn.messages.some(message => message.role === "assistant" && message.text === "Goal reply 2")), false);
512
+ const stopped = await f.storage.read("conversation", async transaction => (await transaction.readMetadata()).runtime.binding);
513
+ assert.equal(stopped.observationLoss.stopped, true);
514
+ assert.equal(stopped.observationLoss.message, "Codex observation failed. Work must be stopped before it can continue.");
515
+ assert.equal(stopped.codexAppServerRun.state, "interrupted");
516
+ assert.equal(stopped.codexAppServerRun.active, false);
517
+ assert.equal(stopped.codexAppServerRun.providerStatus, "observation_lost");
518
+ assert.equal(stopped.codexAppServerRun.outerTurnId, "bounded-goal");
519
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 1);
520
+ });
521
+
522
+ test("failed tracked Codex history storage stops owned work and recovers the exact result without replay", async t => {
523
+ let rejectHistory = false;
524
+ const f = await fixture(t, { checkpoints: true, storage: disk => ({ read: disk.read,
525
+ write: (scope, callback) => disk.write(scope, async transaction => {
526
+ const result = await callback(transaction);
527
+ if (rejectHistory) for (const id of await transaction.listTurnIds()) {
528
+ const turn = await transaction.readTurn(id);
529
+ if (turn.messages.some(message => message.role === "assistant" && message.text === "Goal reply 3")) {
530
+ throw new Error("Goal storage failed");
531
+ }
532
+ }
533
+ return result;
534
+ })
535
+ }) });
536
+ await f.conversation.send({ messageId: "storage-goal", text: "native-goal" });
537
+ assert.equal((await f.conversation.wait()).error, "");
538
+ const native = JSON.parse(await readFile(path.join(f.directory, "history.json"), "utf8"));
539
+ const nativeTurnId = native.turns.at(-1).id;
540
+ const checkpoint = f.checkpoints.find(record => record.metadata.runtime.binding.codexAppServerRun?.providerTurnId === nativeTurnId &&
541
+ record.metadata.runtime.binding.codexAppServerRun.active &&
542
+ !record.turns.some(([, turn]) => turn.messages.some(message => message.role === "assistant" && message.text === "Goal reply 3")));
543
+ assert.ok(checkpoint, "The original run must own the exact missing result before startup recovery.");
544
+ const previousReplies = checkpoint.turns.flatMap(([, turn]) => turn.messages.filter(message => message.role === "assistant"));
545
+ await f.restore(checkpoint);
546
+ const recoveryTraceStart = (await f.trace()).length;
547
+ const reopened = await f.runtime().open({ id: "conversation" });
548
+ rejectHistory = true;
549
+ try {
550
+ await assert.rejects(reopened.inspectDelivery({ messageId: "storage-goal" }), /Goal storage failed/);
551
+ let stopped;
552
+ for (let attempt = 0; attempt < 500; attempt++) {
553
+ stopped = await f.storage.read("conversation", async transaction => (await transaction.readMetadata()).runtime.binding);
554
+ if (stopped.observationLoss?.stopped && stopped.codexAppServerRun.providerStatus === "observation_lost" &&
555
+ !stopped.codexAppServerRun.active) break;
556
+ await new Promise(resolve => setTimeout(resolve, 10));
557
+ }
558
+ assert.ok(stopped.observationLoss, "The original inspected-turn failure must retain its observation barrier.");
559
+ assert.equal(stopped.observationLoss.stopped, true);
560
+ assert.equal(stopped.codexAppServerRun.active, false);
561
+ assert.equal(stopped.codexAppServerRun.providerStatus, "observation_lost");
562
+ assert.equal(stopped.codexAppServerRun.providerThreadId, native.id);
563
+ assert.equal(stopped.codexAppServerRun.providerTurnId, nativeTurnId);
564
+ assert.equal(stopped.codexAppServerRun.outerTurnId, "storage-goal");
565
+ const failed = await reopened.read();
566
+ assert.deepEqual(failed.conversationLog.flatMap(turn => turn.messages.filter(message => message.role === "assistant")), previousReplies);
567
+ assert.equal(failed.conversationLog.filter(turn => turn.user || turn.system).length, 1);
568
+ } finally { rejectHistory = false; }
569
+ const receipt = await reopened.inspectDelivery({ messageId: "storage-goal" });
570
+ assert.equal(receipt.status, "accepted");
571
+ assert.equal(receipt.recovered, true);
572
+ const recovered = await reopened.read();
573
+ const replies = recovered.conversationLog.flatMap(turn => turn.messages.filter(message => message.role === "assistant"));
574
+ for (const previous of previousReplies) assert.deepEqual(replies.find(message => message.messageId === previous.messageId), previous);
575
+ assert.equal(replies.filter(message => message.text === "Goal reply 3").length, 1);
576
+ assert.equal(recovered.conversationLog[0].user.messageId, "storage-goal");
577
+ assert.equal(recovered.conversationLog[0].metadata.runtime.status, "interrupted");
578
+ await reopened.inspectDelivery({ messageId: "storage-goal" });
579
+ assert.deepEqual((await reopened.read()).conversationLog, recovered.conversationLog);
580
+ const trace = await f.trace();
581
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 1);
582
+ assert.equal(trace.slice(recoveryTraceStart).some(row => row.method === "thread/resume" || row.method === "turn/start"), false);
583
+ });
584
+
585
+ test("goal continuations stop when authorization changes at the next native turn", async t => {
586
+ let allowed = true;
587
+ let calls = 0;
588
+ let firstToolTurnId;
589
+ const denied = [];
590
+ const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
591
+ const f = await fixture(t, { actions: applicationActions(async () => ({ value: ++calls })), context,
592
+ nativeGoalObjective: "tools goal", limits: { maxToolCalls: 9 },
593
+ authorize: input => {
594
+ if (input.operation !== "send" || allowed) return true;
595
+ denied.push({ operation: input.operation, actorId: input.context.actor.id, conversationId: input.conversationId });
596
+ return false;
597
+ },
598
+ storage: disk => ({ read: disk.read, write: (scope, callback) => disk.write(scope, async transaction => {
599
+ const result = await callback(transaction);
600
+ const run = (await transaction.readMetadata()).runtime.binding.codexAppServerRun;
601
+ if (!firstToolTurnId) for (const id of await transaction.listTurnIds()) {
602
+ const calls = (await transaction.readTurn(id)).metadata?.applicationTools;
603
+ if (calls?.length === 3 && calls.every(call => call.status === "complete")) {
604
+ firstToolTurnId = run.providerTurnId;
605
+ assert.ok(calls.every(call => call.id.startsWith(`${firstToolTurnId}-`)));
606
+ }
607
+ }
608
+ // The saved first effect precedes its native response. Revoke when the
609
+ // original owner adopts that exact turn's successor, before publication.
610
+ if (firstToolTurnId && run.events.some(event => event.kind === "codex-app-server-turn-continued" &&
611
+ event.previousProviderTurnId === firstToolTurnId)) allowed = false;
612
+ return result;
613
+ }) }) });
614
+ await f.conversation.send({ messageId: "revoked-goal", text: "native-goal" });
615
+ const state = await f.conversation.wait();
616
+ assert.ok(denied.length, "The next native turn must recheck its admitted actor.");
617
+ for (const request of denied) assert.deepEqual(request, { operation: "send", actorId: "owner", conversationId: "conversation" });
618
+ const authored = state.conversationLog.filter(turn => turn.user || turn.system);
619
+ assert.equal(authored.length, 1);
620
+ assert.equal(authored[0].user.messageId, "revoked-goal");
621
+ assert.equal(authored[0].metadata.applicationTools.length, 3);
622
+ assert.ok(authored[0].metadata.applicationTools.every(call => call.status === "complete"));
623
+ assert.equal(authored[0].metadata.applicationTools.at(-1).result.result.result.value, 1);
624
+ assert.equal(calls, 1);
625
+ const replies = state.conversationLog.flatMap(turn => turn.messages.filter(message => message.role === "assistant"));
626
+ assert.equal(replies.filter(message => message.text === "Goal reply 1").length, 1);
627
+ assert.equal(replies.some(message => message.text === "Goal reply 2"), false);
628
+ assert.match(state.error, /not available to this identity/);
629
+ const trace = await f.trace();
630
+ const responses = trace.filter(row => row.toolResponse);
631
+ assert.equal(responses.length, 4);
632
+ assert.match(responses.at(-1).toolResponse.error.message, /not available to this identity/);
633
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 1);
634
+ assert.equal(trace.some(row => row.method === "thread/inject_items"), false);
635
+ const stopped = await f.storage.read("conversation", async transaction => (await transaction.readMetadata()).runtime.binding);
636
+ assert.deepEqual(trace.filter(row => row.method === "thread/goal/set").map(row => row.params), [
637
+ { threadId: stopped.threadId, status: "paused" }
638
+ ]);
639
+ assert.equal(stopped.observationLoss.stopped, true);
640
+ assert.equal(stopped.codexAppServerRun.active, false);
641
+ assert.equal(stopped.codexAppServerRun.providerStatus, "observation_lost");
642
+ assert.equal(stopped.codexAppServerRun.outerTurnId, "revoked-goal");
643
+ });
644
+
645
+ test("a paused goal stays with its native conversation when another engine is selected", async t => {
646
+ const f = await fixture(t, apiOptions());
647
+ const { segmentId } = await f.conversation.read();
648
+ await f.conversation.updateGoal({ action: "set", messageId: "retained-goal", objective: "wait goal", expectedSegmentId: segmentId });
649
+ const goal = await f.conversation.readGoal();
650
+ await f.conversation.updateGoal({ action: "pause", expectedGoalId: goal.id, expectedSegmentId: segmentId });
651
+ assert.equal((await f.conversation.read()).status, "working");
652
+ assert.equal((await f.trace()).some(row => row.method === "turn/interrupt"), false);
653
+ await f.conversation.cancel();
654
+ await select(f.conversation, "api", "to-api");
655
+ assert.equal((await f.conversation.read()).goal, null);
656
+ assert.equal(await f.conversation.readGoal(), null);
657
+ await select(f.conversation, "codex", "back-to-codex");
658
+ assert.equal((await f.conversation.readGoal()).id, goal.id);
659
+ assert.equal((await f.conversation.readGoal()).status, "paused");
660
+ });
661
+
662
+ test("the host can grant and revoke native Codex coding tools without enabling ambient integrations", async t => {
663
+ const f = await fixture(t, { nativeTools: true });
664
+ assert.equal(f.conversation.capabilities.nativeTools, true);
665
+ await f.conversation.send(input);
666
+ await f.conversation.wait();
667
+ const params = (await f.trace()).find(row => row.method === "thread/start").params;
668
+ assert.equal(params.sandbox, "danger-full-access");
669
+ assert.equal(params.config.features.unified_exec, true);
670
+ assert.equal(params.config.features.shell_tool, true);
671
+ assert.equal(params.config.features.hooks, false);
672
+ assert.equal(params.config.features.plugins, false);
673
+ assert.equal(params.environments, undefined);
674
+ assert.equal(params.runtimeWorkspaceRoots, undefined);
675
+ assert.equal(params.selectedCapabilityRoots, undefined);
676
+ assert.equal(params.config.shell_environment_policy.set.TEST_ACCOUNT, f.account);
677
+ await f.first.close();
678
+ const revoked = await f.runtime({ nativeTools: false }).open({ id: "conversation", configuration });
679
+ await revoked.send({ messageId: "second", text: "No tools now" });
680
+ await revoked.wait();
681
+ const resumed = (await f.trace()).find(row => row.method === "thread/resume").params;
682
+ assert.equal(resumed.sandbox, "read-only");
683
+ assert.equal(resumed.config.features.shell_tool, false);
684
+ assert.deepEqual(resumed.environments, []);
685
+ assert.deepEqual(Object.keys(resumed.config.shell_environment_policy.set).sort(), ["JSKIT_CONVERSATION_INSTRUCTIONS", "PATH"]);
686
+ });
687
+
688
+ test("Codex enables only the host command hook and disables ambient hooks", async t => {
689
+ const f = await fixture(t, { nativeTools: true, commandWrapper: "/host/command wrapper" });
690
+ await f.conversation.send(input);
691
+ const state = await f.conversation.wait();
692
+ assert.equal(state.conversationLog[0].metadata.runtime.status, "complete", state.error);
693
+ const params = (await f.trace()).find(row => row.method === "thread/start").params;
694
+ assert.equal(params.config.features.hooks, true);
695
+ assert.deepEqual(params.config.hooks.state, { ambient: { enabled: false }, command: { enabled: true, trusted_hash: "sha256:owned" } });
696
+ const trace = await f.trace();
697
+ assert.equal(trace.some(row => row.method === "config/batchWrite"), false);
698
+ assert.equal(trace.filter(row => row.args).length, 1);
699
+ assert.match(params.config.hooks.PreToolUse[0].hooks[0].command, /commandHook.js/);
700
+ });
701
+
702
+ test("Codex refuses dispatch when its required command hook is missing", async t => {
703
+ const f = await fixture(t, { nativeTools: true, commandWrapper: "/host/wrapper", missingCommandHook: true });
704
+ await assert.rejects(f.conversation.send(input), /required command wrapper/);
705
+ assert.equal((await f.trace()).some(row => row.method === "thread/start"), false);
706
+ });
707
+
708
+ test("Codex receives authorized image content and retains receipts through uncertain admission", async t => {
709
+ const image = Buffer.from("authorized-image");
710
+ const receipt = { attachmentId: "picture", fileName: "picture.png", size: image.length };
711
+ let reads = 0;
712
+ const f = await fixture(t, { attachments: { resolve: async () => {
713
+ reads++;
714
+ return { attachments: [receipt], content: [{ type: "image", image, mediaType: "image/png" }] };
715
+ } } });
716
+ await f.conversation.send({ ...input, attachmentIds: [receipt.attachmentId] });
717
+ const first = await f.conversation.wait();
718
+ assert.equal(first.conversationLog[0].metadata.runtime.status, "complete", first.error);
719
+ assert.deepEqual((await f.trace()).find(row => row.method === "turn/start").params.input, [
720
+ { type: "text", text: input.text, text_elements: [] }, { type: "image", url: `data:image/png;base64,${image.toString("base64")}` }
721
+ ]);
722
+ const missing = { messageId: "lost-image", text: "lost", attachmentIds: [receipt.attachmentId] };
723
+ await assert.rejects(f.conversation.send(missing));
724
+ const uncertain = await f.conversation.read();
725
+ assert.deepEqual(uncertain.pendingRequest.attachments, [receipt]);
726
+ assert.doesNotMatch(JSON.stringify(uncertain), /YXV0aG9yaXplZC1pbWFnZQ==/);
727
+ assert.equal((await f.conversation.inspectDelivery({ messageId: missing.messageId })).status, "accepted");
728
+ assert.deepEqual((await f.conversation.read()).conversationLog.at(-1).user.attachments, [receipt]);
729
+ assert.equal(reads, 2);
730
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 2);
731
+ });
732
+
733
+ function apiOptions() {
734
+ return { connections: createAiConnectionResolver({ configuration: { schemaVersion: 1, registrations: {}, integrations: {
735
+ assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/big-pickle" } }
736
+ } }, authorize: () => ({ applicationId: "test", subjectId: "owner" }) }), fetch: async () => {
737
+ const chunk = { choices: [{ index: 0, delta: { content: "API answer" }, finish_reason: "stop" }] };
738
+ return new Response(`data: ${JSON.stringify(chunk)}\n\n`, { headers: { "content-type": "text/event-stream" } });
739
+ } };
740
+ }
741
+
742
+ async function settledNativeGoal(conversation) {
743
+ for (let attempt = 0; attempt < 500; attempt++) {
744
+ const state = await conversation.read();
745
+ if (state.goal && state.goal.status !== "active" && state.status !== "working") return state;
746
+ await new Promise(resolve => setTimeout(resolve, 10));
747
+ }
748
+ assert.fail("The original native goal owner did not publish its settled state.");
749
+ }
750
+
751
+ async function select(conversation, engine, operationId) {
752
+ return conversation.select({ operationId, expectedSegmentId: (await conversation.read()).segmentId,
753
+ engine, configuration: engine === "codex" ? configuration : { systemPrompt: "Continue this conversation.", integrationId: "assistant" } });
754
+ }
755
+
756
+ async function fixture(t, options = {}) {
757
+ const directory = await mkdtemp(path.join(tmpdir(), "jskit-codex-conversation-"));
758
+ const command = path.join(directory, "codex.mjs");
759
+ const account = path.join(directory, "account.txt");
760
+ const trace = path.join(directory, "trace.jsonl");
761
+ await writeFile(account, "owner@example.test");
762
+ await writeFile(command, `#!${process.execPath}
763
+ import { createServer } from "node:http";
764
+ import { appendFileSync, readFileSync, writeFileSync, existsSync } from "node:fs";
765
+ import { randomUUID } from "node:crypto";
766
+ import { execFileSync } from "node:child_process";
767
+ import { WebSocketServer } from ${JSON.stringify(import.meta.resolve("ws"))};
768
+ const args = process.argv.slice(2);
769
+ if (args.includes("debug") && args.includes("models")) {
770
+ process.stdout.write(JSON.stringify({ models: [{ slug: "test-model", priority: 0 }] }));
771
+ process.exit(0);
772
+ }
773
+ const socket = args[args.indexOf("--listen") + 1].slice(7);
774
+ const log = value => appendFileSync(process.env.TEST_TRACE, JSON.stringify(value) + "\\n");
775
+ log({ args });
776
+ const file = process.env.TEST_HISTORY;
777
+ const saved = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : null;
778
+ const threads = new Map((saved?.threads || (saved ? [saved] : []))
779
+ .map(thread => [thread.id, { thread, loaded: false, runningTurn: null }]));
780
+ const save = () => {
781
+ const histories = [...threads.values()].map(state => state.thread);
782
+ writeFileSync(file, JSON.stringify(histories.length === 1 ? histories[0] : { threads: histories }));
783
+ };
784
+ const server = createServer();
785
+ const wss = new WebSocketServer({ server });
786
+ const toolRequests = new Map();
787
+ let requestId = 0;
788
+ wss.on("connection", ws => ws.on("message", async data => {
789
+ const { id, method, params = {}, result, error } = JSON.parse(data);
790
+ if (!method) {
791
+ log({ toolResponse: { id, result, error } });
792
+ const pending = toolRequests.get(id); toolRequests.delete(id);
793
+ if (error) pending.reject(new Error(error.message)); else pending.resolve(result);
794
+ return;
795
+ }
796
+ const state = threads.get(params.threadId) || { thread: null, loaded: false, runningTurn: null };
797
+ let thread = state.thread;
798
+ log({ method, params });
799
+ const reply = result => ws.send(JSON.stringify({ id, result }));
800
+ if (["thread/read", "thread/turns/list"].includes(method) && existsSync(file + ".unavailable")) {
801
+ ws.send(JSON.stringify({ id, error: { code: -32000, message: "Native history is temporarily unavailable" } }));
802
+ return;
803
+ }
804
+ const emit = (method, params) => ws.send(JSON.stringify({ method, params: { threadId: thread.id, ...params } }));
805
+ const callTool = (turn, tool, arguments_, foreign = false) => new Promise((resolve, reject) => {
806
+ const id = "tool-request-" + (++requestId);
807
+ toolRequests.set(id, { resolve, reject });
808
+ ws.send(JSON.stringify({ id, method: "item/tool/call", params: {
809
+ threadId: foreign ? "another-thread" : thread.id, turnId: turn.id,
810
+ callId: turn.id + "-" + requestId, tool, arguments: arguments_
811
+ } }));
812
+ });
813
+ async function goalTurns() {
814
+ if (!thread.goalsEnabled) return;
815
+ const previous = state.runningTurn;
816
+ while (previous?.status === "inProgress" && ws.readyState === 1) await new Promise(resolve => setTimeout(resolve, 5));
817
+ for (let index = 0; index < 3; index++) {
818
+ if (ws.readyState !== 1 || thread.goal?.status !== "active") return;
819
+ const turn = { id: randomUUID(), status: "inProgress", items: [] };
820
+ state.runningTurn = turn;
821
+ thread.turns.push(turn); save(); emit("turn/started", { turn });
822
+ if (thread.goal.objective === "tools goal") {
823
+ try {
824
+ await callTool(turn, "assistant_action_search", { query: "numbers" });
825
+ await callTool(turn, "assistant_action_contract", { actionId: "numbers.read", version: 1 });
826
+ await callTool(turn, "assistant_action_execute", { actionId: "numbers.read", version: 1, input: {} });
827
+ } catch (error) {
828
+ turn.status = "failed"; save();
829
+ if (ws.readyState === 1) emit("turn/completed", { turnId: turn.id, turn: { ...turn, error: { message: error.message } } });
830
+ return;
831
+ }
832
+ }
833
+ const item = { id: randomUUID(), type: "agentMessage", phase: "final_answer", text: "Goal reply " + (index + 1) };
834
+ turn.items.push(item); save();
835
+ emit("item/started", { turnId: turn.id, item });
836
+ emit("item/completed", { turnId: turn.id, item });
837
+ if (thread.goal.objective === "wait goal") return;
838
+ thread.goal.tokensUsed += 15;
839
+ if (index === 2) thread.goal.status = thread.goal.tokenBudget ? "budgetLimited" : "complete";
840
+ save(); emit("thread/goal/updated", { turnId: turn.id, goal: thread.goal });
841
+ turn.status = "completed"; save(); emit("turn/completed", { turn });
842
+ }
843
+ }
844
+ if (method === "initialize") {
845
+ while (process.env.TEST_STARTUP_WAIT && !existsSync(process.env.TEST_STARTUP_WAIT) && ws.readyState === 1) {
846
+ await new Promise(resolve => setTimeout(resolve, 20));
847
+ }
848
+ if (ws.readyState === 1) reply({});
849
+ return;
850
+ }
851
+ if (method === "initialized") return;
852
+ if (method === "account/read") return reply({ account: { type: "chatgpt", email: readFileSync(process.env.TEST_ACCOUNT, "utf8") } });
853
+ if (method === "config/read") return reply({ config: { mcp_servers: { ambient: {} } } });
854
+ if (method === "hooks/list") {
855
+ const flag = args.find(arg => arg.startsWith("hooks.PreToolUse="));
856
+ const trusted = args.some(arg => arg.startsWith("hooks.state="));
857
+ const hooks = [{ key: "ambient", source: "project", enabled: !trusted }];
858
+ if (flag && !${Boolean(options.missingCommandHook)}) hooks.push({ key: "command", source: "sessionFlags",
859
+ eventName: "preToolUse", handlerType: "command", currentHash: "sha256:owned", enabled: true,
860
+ trustStatus: trusted ? "trusted" : "untrusted", command: JSON.parse(flag.match(/command=(.*),timeout=30/u)[1]) });
861
+ return reply({ data: [{ cwd: params.cwds[0], hooks, errors: [] }] });
862
+ }
863
+ if (method === "thread/start") {
864
+ thread = { id: randomUUID(), historyMode: "paginated", modelProvider: params.modelProvider,
865
+ environment: params.config.shell_environment_policy.set,
866
+ goalsEnabled: params.config.features.goals, turns: [] };
867
+ state.thread = thread; state.loaded = true; threads.set(thread.id, state); save();
868
+ return reply({ thread, modelProvider: thread.modelProvider });
869
+ }
870
+ if (method === "thread/read") return reply({ thread: { ...thread,
871
+ status: { type: !state.loaded ? "notLoaded" : state.runningTurn?.status === "inProgress" ? "active" : "idle" } } });
872
+ if (method === "thread/turns/list") return reply({ data: [...thread.turns].reverse(), nextCursor: null });
873
+ if (method === "thread/resume") { state.loaded = true; thread.modelProvider = params.modelProvider;
874
+ thread.environment = params.config.shell_environment_policy.set;
875
+ thread.goalsEnabled = params.config.features.goals; save(); return reply({ thread, modelProvider: thread.modelProvider }); }
876
+ if (method === "thread/shellCommand") {
877
+ const turn = { id: randomUUID(), status: "completed", items: [] };
878
+ const item = { id: randomUUID(), type: "commandExecution", command: params.command, exitCode: 0,
879
+ aggregatedOutput: execFileSync("/bin/sh", ["-c", params.command], {
880
+ env: { ...process.env, ...thread.environment }, encoding: "utf8"
881
+ }) };
882
+ turn.items.push(item);
883
+ thread.turns.push(turn); save();
884
+ emit("turn/started", { turn });
885
+ emit("item/started", { turnId: turn.id, item });
886
+ emit("item/completed", { turnId: turn.id, item });
887
+ emit("turn/completed", { turn });
888
+ return reply({});
889
+ }
890
+ if (method === "thread/goal/get") return reply({ goal: thread?.goal || null });
891
+ if (method === "thread/unsubscribe") { state.loaded = false; return reply({ status: "unsubscribed" }); }
892
+ if (method === "turn/interrupt") {
893
+ if (state.runningTurn?.id === params.turnId) state.runningTurn.status = "interrupted";
894
+ save();
895
+ reply({});
896
+ if (state.runningTurn?.id === params.turnId) emit("turn/completed", { turn: state.runningTurn });
897
+ return;
898
+ }
899
+ if (method === "thread/goal/clear") {
900
+ thread.goal = null; save(); reply({}); emit("thread/goal/cleared", {}); return;
901
+ }
902
+ if (method === "thread/goal/set") {
903
+ if (params.objective === "rejected goal") return ws.send(JSON.stringify({ id, error: { code: -32602, message: "Goal rejected" } }));
904
+ const now = Math.floor(Date.now() / 1000);
905
+ thread.goal = params.objective ? { threadId: thread.id, objective: params.objective, status: params.status,
906
+ createdAt: now, updatedAt: now, tokensUsed: 0, timeUsedSeconds: 0, tokenBudget: params.tokenBudget ?? null }
907
+ : { ...thread.goal, status: params.status, updatedAt: now };
908
+ save();
909
+ if (params.objective !== "lost goal") reply({ goal: thread.goal });
910
+ emit("thread/goal/updated", { goal: thread.goal });
911
+ if (params.status !== "active") return;
912
+ await goalTurns();
913
+ return;
914
+ }
915
+ if (method === "thread/inject_items" || method === "thread/name/set") return reply({});
916
+ if (method === "turn/steer") {
917
+ const turn = thread.turns.at(-1);
918
+ const text = params.input[0].text;
919
+ if (text === "rejected-steer" || turn.status !== "inProgress" || params.expectedTurnId !== turn.id) {
920
+ return ws.send(JSON.stringify({ id, error: { code: -32602, message: "Steering rejected" } }));
921
+ }
922
+ turn.items.push({ id: "steering-user", type: "userMessage", clientId: params.clientUserMessageId, content: params.input });
923
+ save();
924
+ if (text === "lost-steer") return;
925
+ reply({ turnId: turn.id });
926
+ if (turn.items.some(item => item.id === "before-steer")) emit("item/agentMessage/delta", { turnId: turn.id, itemId: "before-steer", delta: " late old text" });
927
+ const answer = { id: "after-steer", type: "agentMessage", phase: "final_answer", text: "Steered: " + text };
928
+ turn.items.push(answer);
929
+ emit("item/started", { turnId: turn.id, item: answer });
930
+ emit("item/agentMessage/delta", { turnId: turn.id, itemId: answer.id, delta: answer.text });
931
+ emit("item/completed", { turnId: turn.id, item: answer });
932
+ turn.status = "completed"; save();
933
+ emit("turn/completed", { turnId: turn.id, turn });
934
+ return;
935
+ }
936
+ if (method === "turn/start") {
937
+ const text = params.input[0].text;
938
+ if (text === "rejected") return ws.send(JSON.stringify({ id, error: { code: -32602, message: "Turn rejected" } }));
939
+ const turn = { id: randomUUID(), status: "inProgress", items: [{ id: "user", type: "userMessage", clientId: params.clientUserMessageId, content: params.input }] };
940
+ state.runningTurn = turn;
941
+ thread.turns.push(turn); save();
942
+ emit("turn/started", { turn });
943
+ if (text === "lost") return;
944
+ reply({ turn });
945
+ const emitTurn = (method, value) => emit(method, { turnId: turn.id, ...value });
946
+ if (text === "native-goal") {
947
+ const now = Math.floor(Date.now() / 1000);
948
+ thread.goal = { threadId: thread.id, objective: ${JSON.stringify(options.nativeGoalObjective || "A goal from native create_goal")}, status: "active",
949
+ createdAt: now, updatedAt: now, tokensUsed: 0, timeUsedSeconds: 0, tokenBudget: null };
950
+ save(); emit("thread/goal/updated", { goal: thread.goal });
951
+ }
952
+ if (text === "wait") return;
953
+ if (text === "steering") {
954
+ const answer = { id: "before-steer", type: "agentMessage", phase: "final_answer", text: "Initial progress" };
955
+ turn.items.push(answer); save();
956
+ emitTurn("item/started", { item: answer });
957
+ emitTurn("item/agentMessage/delta", { itemId: answer.id, delta: answer.text });
958
+ return;
959
+ }
960
+ if (text === "phases") {
961
+ emitTurn("item/started", { item: { id: "compact", type: "contextCompaction" } });
962
+ emitTurn("item/completed", { item: { id: "compact", type: "contextCompaction" } });
963
+ emitTurn("error", { error: { message: "Temporarily unavailable" }, willRetry: true });
964
+ }
965
+ if (text === "failed") { turn.status = "failed"; save(); emitTurn("turn/completed", { turn: { ...turn, error: { message: "Model unavailable" } } }); return; }
966
+ if (text === "disconnect") { setTimeout(() => ws.close(), 50); return; }
967
+ let toolAnswer;
968
+ if (text === "tools" || text === "foreign-tool") {
969
+ const invoke = (tool, arguments_) => callTool(turn, tool, arguments_, text === "foreign-tool");
970
+ try {
971
+ await invoke("assistant_action_search", { query: "numbers" });
972
+ await invoke("assistant_action_contract", { actionId: "numbers.read", version: 1 });
973
+ const result = await invoke("assistant_action_execute", { actionId: "numbers.read", version: 1, input: {} });
974
+ toolAnswer = result.contentItems[0].text;
975
+ } catch (error) {
976
+ turn.status = "failed"; save();
977
+ if (ws.readyState === 1) emitTurn("turn/completed", { turn: { ...turn, error: { message: error.message } } });
978
+ return;
979
+ }
980
+ }
981
+ const split = text.startsWith("blocks");
982
+ const phase = text === "blocks-without-phase" ? "" : "final_answer";
983
+ if (split) {
984
+ const progress = { id: "progress", type: "agentMessage", text: "Checking the numbers." };
985
+ turn.items.push(progress);
986
+ emitTurn("item/started", { item: progress });
987
+ emitTurn("item/agentMessage/delta", { itemId: progress.id, delta: progress.text });
988
+ emitTurn("item/completed", { item: progress });
989
+ const tool = { id: "tool", type: "commandExecution" };
990
+ turn.items.push(tool); emitTurn("item/started", { item: tool });
991
+ }
992
+ emitTurn("item/started", { item: { id: "answer", type: "agentMessage", phase } });
993
+ emitTurn("item/reasoning/summaryTextDelta", { itemId: "reasoning", delta: "Reasoning" });
994
+ emitTurn("item/reasoning/summaryTextDelta", { itemId: "reasoning", delta: " summary" });
995
+ emitTurn("item/agentMessage/delta", { itemId: "answer", delta: "Answer: " });
996
+ const answer = { id: "answer", type: "agentMessage", phase, text: toolAnswer || "Answer: " + text };
997
+ turn.items.push(answer);
998
+ emitTurn("item/completed", { item: answer });
999
+ if (split) {
1000
+ const tail = { id: "tail", type: "agentMessage", phase, text: "The second paragraph." };
1001
+ turn.items.push(tail);
1002
+ emitTurn("item/started", { item: tail });
1003
+ emitTurn("item/agentMessage/delta", { itemId: tail.id, delta: tail.text });
1004
+ emitTurn("item/completed", { item: tail });
1005
+ }
1006
+ turn.status = "completed"; save();
1007
+ if (split) emitTurn("task_complete", { last_agent_message: answer.text + "\\n\\nThe second paragraph." });
1008
+ emitTurn("turn/completed", { turn });
1009
+ if (text === "native-goal") await goalTurns();
1010
+ return;
1011
+ }
1012
+ ws.send(JSON.stringify({ id, error: { code: -32601, message: "Unsupported method: " + method } }));
1013
+ }));
1014
+ server.listen(socket);
1015
+ `, { mode: 0o700 });
1016
+ const disk = createFileConversationStorage({ directory: path.join(directory, "storage") });
1017
+ const storageFile = path.join(directory, "storage", `${createHash("sha256").update("conversation").digest("hex")}.json`);
1018
+ const checkpoints = [];
1019
+ const backing = options.storage ? options.storage(disk) : disk;
1020
+ const storage = options.checkpoints ? { read: backing.read, async write(scope, callback) {
1021
+ const result = await backing.write(scope, callback);
1022
+ checkpoints.push(JSON.parse(await readFile(storageFile, "utf8")));
1023
+ return result;
1024
+ } } : backing;
1025
+ const runtimes = [];
1026
+ const execution = options.execution || createLocalConversationExecution();
1027
+ t.after(async () => {
1028
+ try { for (const runtime of runtimes) await runtime.close(); }
1029
+ finally {
1030
+ if (!options.execution) await execution.close();
1031
+ await rm(directory, { recursive: true, force: true });
1032
+ }
1033
+ });
1034
+ const env = { ...process.env, CODEX_HOME: path.join(directory, "codex"), TEST_TRACE: trace, TEST_ACCOUNT: account,
1035
+ TEST_HISTORY: path.join(directory, "history.json"), ...options.environment };
1036
+ function runtime({ nativeTools = options.nativeTools } = {}) {
1037
+ const value = createConversationRuntime({ engine: "codex", storage, authorize: options.authorize || (() => true),
1038
+ actions: options.actions, connections: options.connections, fetch: options.fetch, attachments: options.attachments,
1039
+ host: { workdir: directory, runtimeDirectory: directory, env, commands: { codex: command }, execution, nativeTools, commandWrapper: options.commandWrapper },
1040
+ limits: { admissionTimeoutMs: 2000, ...options.limits } });
1041
+ runtimes.push(value);
1042
+ return value;
1043
+ }
1044
+ const first = runtime();
1045
+ const conversation = await first.open({ id: "conversation", configuration: options.configuration || configuration, context: options.context });
1046
+ return { directory, storage, account, runtime, first, conversation, checkpoints,
1047
+ driverOptions: { connections: options.connections,
1048
+ host: { workdir: directory, runtimeDirectory: directory, env, commands: { codex: command }, execution, nativeTools: options.nativeTools, commandWrapper: options.commandWrapper },
1049
+ limits: { admissionTimeoutMs: 2000, ...options.limits } },
1050
+ async restore(checkpoint) {
1051
+ assert.ok(checkpoint);
1052
+ await first.close();
1053
+ // Restore application persistence while retaining native history.
1054
+ await writeFile(storageFile, JSON.stringify(checkpoint));
1055
+ },
1056
+ async trace() { return (await readFile(trace, "utf8")).trim().split("\n").map(line => JSON.parse(line)); } };
1057
+ }
1058
+
1059
+ test("Codex admits, streams, persists reasoning and deduplicates using native receipts", async t => {
1060
+ const f = await fixture(t);
1061
+ const events = [];
1062
+ await f.conversation.subscribe(event => events.push(event));
1063
+ assert.equal((await f.conversation.send(input)).status, "accepted");
1064
+ const state = await f.conversation.wait();
1065
+ assert.equal(state.conversationLog[0].metadata.runtime.status, "complete");
1066
+ assert.equal(state.conversationLog[0].assistant.text, "Answer: Hello");
1067
+ assert.equal(state.conversationLog[0].thinking[0].text, "Reasoning summary");
1068
+ assert.ok(events.some(event => event.type === "message" && event.text === "Answer: "));
1069
+ const replies = events.filter(event => event.type === "message" && event.role === "assistant");
1070
+ assert.equal(new Set(replies.map(event => event.messageId)).size, 1);
1071
+ assert.equal(state.conversationLog[0].assistant.messageId, replies[0].messageId);
1072
+ assert.equal((await f.conversation.send(input)).duplicate, true);
1073
+ const trace = await f.trace();
1074
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 1);
1075
+ assert.equal(trace.find(row => row.method === "thread/start").params.config.mcp_servers.ambient.enabled, false);
1076
+ });
1077
+
1078
+ test("Codex reports common compaction and retry phases without exposing native status values", async t => {
1079
+ const f = await fixture(t);
1080
+ const phases = [];
1081
+ await f.conversation.subscribe(event => { if (event.type === "phase") phases.push(event.phase); });
1082
+ await f.conversation.send({ messageId: "phases", text: "phases" });
1083
+ assert.equal((await f.conversation.wait()).phase, "");
1084
+ assert.deepEqual(phases, ["preparing", "working", "compacting", "working", "retrying", "working", ""]);
1085
+ });
1086
+
1087
+ test("Codex steering admits once and preserves original unfinished-reply grouping and identities", async t => {
1088
+ const f = await fixture(t);
1089
+ const started = Promise.withResolvers();
1090
+ const events = [];
1091
+ await f.conversation.subscribe(event => {
1092
+ events.push(event);
1093
+ if (event.type === "message" && event.text === "Initial progress") started.resolve();
1094
+ });
1095
+ await f.conversation.send({ messageId: "before", text: "steering" });
1096
+ await started.promise;
1097
+ const steering = { messageId: "after", text: "New instruction", steer: true };
1098
+ const [one, two] = await Promise.all([f.conversation.send(steering), f.conversation.send(steering)]);
1099
+ assert.equal(one.turnId, two.turnId);
1100
+ const state = await f.conversation.wait();
1101
+ assert.equal(state.error, "");
1102
+ assert.equal(state.conversationLog.length, 3);
1103
+ assert.deepEqual(state.conversationLog.filter(turn => turn.user).map(turn => turn.user.messageId), ["before", "after"]);
1104
+ assert.equal(state.conversationLog[0].assistant, null);
1105
+ assert.equal(state.conversationLog[0].metadata.runtime.continuedBy, "after");
1106
+ assert.equal(state.conversationLog[1].assistant.text, "Initial progress");
1107
+ assert.equal(state.conversationLog[2].user, null);
1108
+ assert.equal(state.conversationLog[2].assistant.text, "Steered: New instruction");
1109
+ assert.equal(state.conversationLog[2].metadata.runtime.origin, "user");
1110
+ assert.equal(events.some(event => event.status === "inProgress" && event.text === "Initial progress late old text"), true);
1111
+ const live = events.filter(event => event.type === "message" && event.status === "inProgress");
1112
+ assert.ok(live.length);
1113
+ assert.equal(live.every(event => event.origin === "user" && event.turnId === undefined), true);
1114
+ const replies = state.conversationLog.filter(turn => turn.assistant);
1115
+ assert.equal(new Set(replies.map(turn => turn.assistant.messageId)).size, 2);
1116
+ for (const turn of replies) {
1117
+ const publications = events.filter(event => event.type === "message" && event.status === "complete" &&
1118
+ event.messageId === turn.assistant.messageId);
1119
+ assert.ok(publications.length);
1120
+ assert.equal(publications.every(event => event.turnId === turn.turnId && event.text === turn.assistant.text), true);
1121
+ assert.equal(publications.every(event => event.origin === "user"), true);
1122
+ }
1123
+ assert.deepEqual(events.filter(event => event.type === "accepted").map(event => event.messageId), ["before", "after"]);
1124
+ const trace = await f.trace();
1125
+ assert.equal(trace.filter(row => row.method === "turn/start").length, 1);
1126
+ assert.equal(trace.filter(row => row.method === "turn/steer").length, 1);
1127
+ assert.equal(trace.find(row => row.method === "turn/steer").params.clientUserMessageId, "after");
1128
+ assert.equal((await f.conversation.send(steering)).duplicate, true);
1129
+ await assert.rejects(f.conversation.send({ ...steering, messageId: "idle" }), { code: "conversation_not_steerable" });
1130
+ });
1131
+
1132
+ test("Codex steering rejection leaves the active turn available and checks authorization before dispatch", async t => {
1133
+ let allowed = true;
1134
+ const f = await fixture(t, { authorize: ({ operation }) => operation !== "steer" || allowed });
1135
+ await f.conversation.send({ messageId: "before", text: "wait" });
1136
+ await assert.rejects(f.conversation.send({ messageId: "rejected", text: "rejected-steer", steer: true }), /Steering rejected/);
1137
+ assert.equal((await f.conversation.read()).pendingRequest, null);
1138
+ allowed = false;
1139
+ await assert.rejects(f.conversation.send({ messageId: "forbidden", text: "New instruction", steer: true }), { code: "conversation_forbidden" });
1140
+ assert.equal((await f.trace()).filter(row => row.method === "turn/steer").length, 1);
1141
+ allowed = true;
1142
+ await f.conversation.send({ messageId: "accepted", text: "Continue", steer: true });
1143
+ assert.equal((await f.conversation.wait()).conversationLog.length, 2);
1144
+ });
1145
+
1146
+ test("lost Codex steering acknowledgement keeps its own durable request without resending", async t => {
1147
+ const f = await fixture(t);
1148
+ await f.conversation.send({ messageId: "before", text: "wait" });
1149
+ const input = { messageId: "lost-steering", text: "lost-steer", steer: true };
1150
+ await assert.rejects(f.conversation.send(input), /timed out/);
1151
+ const active = await f.conversation.read();
1152
+ assert.equal(active.status, "working");
1153
+ assert.equal(active.pendingRequest.messageId, input.messageId);
1154
+ assert.equal((await f.trace()).filter(row => row.method === "turn/interrupt").length, 0);
1155
+ assert.deepEqual(await f.conversation.cancel(), { stopped: true });
1156
+ const state = await f.conversation.wait();
1157
+ assert.equal(state.status, "unconfirmed");
1158
+ assert.equal(state.pendingRequest.messageId, input.messageId);
1159
+ assert.equal(state.pendingRequest.steering, true);
1160
+ assert.equal((await f.conversation.inspectDelivery({ messageId: input.messageId })).status, "accepted");
1161
+ assert.equal((await f.conversation.send(input)).duplicate, true);
1162
+ assert.equal((await f.trace()).filter(row => row.method === "turn/steer").length, 1);
1163
+ assert.equal((await f.conversation.read()).conversationLog.length, 2);
1164
+ });
1165
+
1166
+ for (const text of ["blocks", "blocks-without-phase"]) test(`Codex preserves speech identities across progress and ${text}`, async t => {
1167
+ const f = await fixture(t);
1168
+ const events = [];
1169
+ await f.conversation.subscribe(event => { if (event.type === "message") events.push(event); });
1170
+ await f.conversation.send({ messageId: "blocks", text });
1171
+ const state = await f.conversation.wait();
1172
+ const turn = state.conversationLog[0];
1173
+ const replies = state.conversationLog.filter(row => row.assistant);
1174
+ assert.equal(replies.map(row => row.assistant.text).join("\n\n"), `Answer: ${text}\n\nThe second paragraph.`);
1175
+ assert.deepEqual(replies.map(row => row.assistant.text), [`Answer: ${text}`, "The second paragraph."]);
1176
+ const binding = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding);
1177
+ const nativeMessageId = itemId => `codex-${createHash("sha256").update([
1178
+ binding.threadId, turn.metadata.runtime.nativeTurnId, "assistant-item", itemId
1179
+ ].join("\u0000")).digest("hex")}`;
1180
+ assert.deepEqual(replies.map(row => row.assistant.messageId), ["answer", "tail"].map(nativeMessageId));
1181
+ assert.deepEqual(replies.map(row => row.assistant.outputId), ["answer", "tail"].map(nativeMessageId));
1182
+ assert.equal(turn.commentary[0].text, "Checking the numbers.");
1183
+ const progress = events.filter(message => message.text === "Checking the numbers.");
1184
+ assert.equal(new Set(progress.map(message => message.outputId)).size, 1);
1185
+ assert.equal(progress.at(-1).role, "commentary");
1186
+ assert.equal(progress.at(-1).status, "complete");
1187
+ assert.equal(turn.commentary[0].outputId, nativeMessageId("progress"));
1188
+ assert.notEqual(turn.commentary[0].messageId, progress.find(message => message.status === "inProgress").messageId);
1189
+ const answers = events.filter(message => message.role === "assistant" && message.text.startsWith("Answer:"));
1190
+ assert.equal(new Set(answers.map(message => message.messageId)).size, 1);
1191
+ assert.equal(answers.at(-1).messageId, turn.assistant.messageId);
1192
+ assert.notEqual(turn.assistant.messageId, turn.commentary[0].messageId);
1193
+ for (let index = 1; index < answers.length; index++) assert.ok(answers[index].text.startsWith(answers[index - 1].text));
1194
+ for (const itemId of ["answer", "tail"]) {
1195
+ const output = events.filter(message => message.role === "assistant" && message.messageId === nativeMessageId(itemId));
1196
+ // Explicit final history can retire the tail before its queued live events.
1197
+ assert.equal(output.some(message => message.status === "inProgress"), itemId === "answer" || text === "blocks-without-phase");
1198
+ const completed = output.findIndex(message => message.status === "complete");
1199
+ assert.ok(completed >= 0);
1200
+ assert.equal(output.slice(completed).some(message => message.status === "inProgress"), false);
1201
+ assert.deepEqual([...new Set(output.map(message => message.outputId))], [nativeMessageId(itemId)]);
1202
+ }
1203
+ });
1204
+
1205
+ test("Codex changes instructions on its existing shared process and native history", async t => {
1206
+ const f = await fixture(t);
1207
+ await f.conversation.send(input);
1208
+ await f.conversation.wait();
1209
+ await f.conversation.send({ messageId: "unchanged", text: "Again" });
1210
+ await f.conversation.wait();
1211
+ assert.equal((await f.trace()).filter(row => row.args).length, 1);
1212
+ const beforeChange = (await f.trace()).length;
1213
+ await f.conversation.configure({ systemPrompt: "New instructions", model: "new-model" });
1214
+ await f.conversation.send({ messageId: "changed", text: "Continue" });
1215
+ await f.conversation.wait();
1216
+ const trace = await f.trace();
1217
+ assert.equal(trace.filter(row => row.args).length, 1);
1218
+ assert.equal(trace.filter(row => row.method === "thread/start").length, 1);
1219
+ assert.equal(trace.slice(beforeChange).find(row => row.method === "thread/resume").params.developerInstructions, "New instructions");
1220
+ assert.equal(trace.filter(row => row.method === "thread/inject_items").length, 1);
1221
+ });
1222
+
1223
+ test("Codex shutdown and reopen retain native identity, settings and accepted messages", async t => {
1224
+ const f = await fixture(t);
1225
+ await f.conversation.send(input);
1226
+ await f.conversation.wait();
1227
+ await f.first.close();
1228
+ const binding = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding);
1229
+ assert.ok(binding.executionId);
1230
+ const reopened = await f.runtime().open({ id: "conversation" });
1231
+ assert.equal((await reopened.send(input)).duplicate, true);
1232
+ await reopened.send({ messageId: "restart", text: "Continue" });
1233
+ assert.equal((await reopened.wait()).conversationLog.length, 2);
1234
+ assert.equal((await f.trace()).find(row => row.method === "thread/resume").params.threadId, binding.threadId);
1235
+ // The production owner stops the service when its last consumer closes.
1236
+ // Reopening starts a service, while retaining the same native history.
1237
+ assert.equal((await f.trace()).filter(row => row.args).length, 2);
1238
+ });
1239
+
1240
+ for (const closeFirst of [true, false]) test(`closing one common runtime retains its working Codex peer (first: ${closeFirst})`, async t => {
1241
+ const f = await fixture(t);
1242
+ const peerRuntime = f.runtime();
1243
+ const peer = await peerRuntime.open({ id: "peer", configuration });
1244
+ const closing = closeFirst ? f.conversation : peer;
1245
+ const working = closeFirst ? peer : f.conversation;
1246
+ await closing.send(input);
1247
+ await closing.wait();
1248
+ await working.send({ messageId: "working", text: "wait" });
1249
+ await (closeFirst ? f.first : peerRuntime).close();
1250
+ assert.equal((await working.read()).status, "working");
1251
+ await working.send({ messageId: "steer", text: "Continue", steer: true });
1252
+ assert.equal((await working.wait()).conversationLog.at(-1).assistant.text, "Steered: Continue");
1253
+ assert.equal((await f.trace()).filter(row => row.args).length, 1);
1254
+ });
1255
+
1256
+ test("concurrent final common-runtime closes stop the shared Codex service once", async t => {
1257
+ const execution = createLocalConversationExecution();
1258
+ let stops = 0;
1259
+ const f = await fixture(t, { execution: { ...execution, async stop(options) {
1260
+ stops++;
1261
+ return execution.stop(options);
1262
+ } } });
1263
+ t.after(() => execution.close());
1264
+ const peerRuntime = f.runtime();
1265
+ const peer = await peerRuntime.open({ id: "peer", configuration });
1266
+ await f.conversation.send(input);
1267
+ await f.conversation.wait();
1268
+ await peer.send(input);
1269
+ await peer.wait();
1270
+ const before = stops;
1271
+ await Promise.all([f.first.close(), peerRuntime.close()]);
1272
+ assert.equal(stops - before, 1);
1273
+ });
1274
+
1275
+ for (const loseFirst of [true, false]) test(`shared Codex fallback records both common-runtime barriers before stop (first: ${loseFirst})`, async t => {
1276
+ const providers = new Map();
1277
+ const startThread = CodexAppServerAgentProvider.prototype.startThread;
1278
+ t.mock.method(CodexAppServerAgentProvider.prototype, "startThread", async function (...args) {
1279
+ const thread = await startThread.apply(this, args);
1280
+ providers.set(thread.id, this);
1281
+ return thread;
1282
+ });
1283
+ const execution = createLocalConversationExecution();
1284
+ const barriers = [];
1285
+ let storage;
1286
+ const f = await fixture(t, { execution: { ...execution, async stop(options) {
1287
+ if (storage) barriers.push(await Promise.all(["conversation", "peer"].map(id => storage.read(id,
1288
+ async transaction => (await transaction.readMetadata()).runtime.binding.observationLoss))));
1289
+ return execution.stop(options);
1290
+ } } });
1291
+ t.after(() => execution.close());
1292
+ storage = f.storage;
1293
+ const peer = await f.runtime().open({ id: "peer", configuration });
1294
+ await f.conversation.send({ messageId: "first-working", text: "wait" });
1295
+ await peer.send({ messageId: "peer-working", text: "wait" });
1296
+ const binding = await storage.read(loseFirst ? "conversation" : "peer",
1297
+ async transaction => (await transaction.readMetadata()).runtime.binding);
1298
+ const source = providers.get(binding.threadId);
1299
+ assert.ok(source);
1300
+ source.client.close();
1301
+ await source.failObservation(new Error("Forced observation loss"));
1302
+ const states = await Promise.all([f.conversation.wait(), peer.wait()]);
1303
+ assert.ok(barriers.some(values => values.every(value => value?.stopped === false)),
1304
+ "both durable admission barriers must precede shared process stop");
1305
+ assert.ok(states.every(state => state.status === "ready"));
1306
+ for (const id of ["conversation", "peer"]) {
1307
+ const saved = await storage.read(id, async transaction => (await transaction.readMetadata()).runtime.binding);
1308
+ assert.equal(saved.observationLoss.stopped, true);
1309
+ }
1310
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 2, "recovery must not replay work");
1311
+ });
1312
+
1313
+ test("account invalidation reaches common consumers without stopping another runtime scope", async t => {
1314
+ const f = await fixture(t);
1315
+ const other = await fixture(t);
1316
+ const peer = await f.runtime().open({ id: "peer", configuration });
1317
+ await f.conversation.send({ messageId: "first-working", text: "wait" });
1318
+ await peer.send({ messageId: "peer-working", text: "wait" });
1319
+ await other.conversation.send({ messageId: "other-working", text: "wait" });
1320
+ const owner = createCodexAppServerProviderOwner({ runtimeRoot: f.directory });
1321
+ const result = await owner.invalidateRuntimes({ includeOwned: true, requireVerifiedExit: true, stopOwnedRuntimes: true });
1322
+ assert.equal(result.ok, true, JSON.stringify(result));
1323
+ assert.equal(result.providerCount, 2);
1324
+ assert.equal(result.stopped, 1);
1325
+ const states = await Promise.race([
1326
+ Promise.all([f.conversation.wait(), peer.wait()]),
1327
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Invalidated consumers did not settle")), 2000))
1328
+ ]);
1329
+ assert.ok(states.every(state => state.status === "ready"));
1330
+ assert.equal((await other.conversation.read()).status, "working");
1331
+ await other.conversation.cancel();
1332
+ });
1333
+
1334
+ test("unverified account invalidation keeps both common cleanup owners and settles their waiters unavailable", async t => {
1335
+ const execution = createLocalConversationExecution();
1336
+ let allowStop = true;
1337
+ const f = await fixture(t, { execution: { ...execution, authoritative: true, async stop(...args) {
1338
+ if (!allowStop) return { ok: false, scopeEmpty: false };
1339
+ return { ok: true, ...await execution.stop(...args) };
1340
+ } } });
1341
+ t.after(() => execution.close());
1342
+ const other = await fixture(t);
1343
+ const peer = await f.runtime().open({ id: "peer", configuration });
1344
+ await f.conversation.send({ messageId: "first-working", text: "wait" });
1345
+ await peer.send({ messageId: "peer-working", text: "wait" });
1346
+ await other.conversation.send({ messageId: "other-working", text: "wait" });
1347
+ const owner = createCodexAppServerProviderOwner({ runtimeRoot: f.directory });
1348
+ allowStop = false;
1349
+ try {
1350
+ const result = await owner.invalidateRuntimes({ includeOwned: true, requireVerifiedExit: true, stopOwnedRuntimes: true });
1351
+ assert.equal(result.ok, false);
1352
+ assert.equal(result.providerCount, 2);
1353
+ assert.equal(result.stopped, 0);
1354
+ assert.ok(result.failed.some(failure => failure.code === "codex_runtime_exit_unverified" && failure.retryable));
1355
+ const states = await Promise.race([
1356
+ Promise.all([f.conversation.wait(), peer.wait()]),
1357
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Unverified consumers did not settle")), 2000))
1358
+ ]);
1359
+ assert.ok(states.every(state => state.status === "unavailable"));
1360
+ for (const id of ["conversation", "peer"]) {
1361
+ const saved = await f.storage.read(id, async transaction => (await transaction.readMetadata()).runtime.binding);
1362
+ assert.equal(saved.observationLoss.stopped, false);
1363
+ assert.ok(saved.observationLoss.runtime.executionId);
1364
+ assert.equal(saved.codexAppServerRun.providerStatus, "observation_lost");
1365
+ assert.equal(saved.codexAppServerRun.active, true);
1366
+ }
1367
+ await assert.rejects(f.conversation.send(input), { code: "codex_runtime_invalidated" });
1368
+ assert.equal((await other.conversation.read()).status, "working");
1369
+ } finally { allowStop = true; }
1370
+ await f.conversation.cancel();
1371
+ await peer.cancel();
1372
+ assert.ok((await Promise.all([f.conversation.read(), peer.read()])).every(state => state.status === "ready"));
1373
+ assert.equal((await f.trace()).filter(row => row.args).length, 1, "cleanup cannot start another native process");
1374
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 2, "cleanup cannot replay either request");
1375
+ assert.equal((await other.conversation.read()).status, "working");
1376
+ await other.conversation.cancel();
1377
+ });
1378
+
1379
+ for (const stage of ["preparation", "completion"]) test(`account invalidation retains durable retry ownership after failed ${stage} storage`, async t => {
1380
+ let rejectStorage = false;
1381
+ const failure = new Error(`Invalidation ${stage} could not be persisted`);
1382
+ const f = await fixture(t, { storage: disk => ({ read: disk.read,
1383
+ write: (scope, callback) => disk.write(scope, transaction => callback({
1384
+ ...transaction,
1385
+ async writeMetadata(metadata) {
1386
+ const binding = metadata.runtime?.binding;
1387
+ const rejected = stage === "preparation"
1388
+ ? binding?.codexAppServerRun?.providerStatus === "observation_lost"
1389
+ : binding?.observationLoss?.stopped;
1390
+ if (rejectStorage && rejected) throw failure;
1391
+ return transaction.writeMetadata(metadata);
1392
+ }
1393
+ }))
1394
+ }) });
1395
+ const peer = await f.runtime().open({ id: "peer", configuration });
1396
+ await f.conversation.send({ messageId: "first-working", text: "wait" });
1397
+ await peer.send({ messageId: "peer-working", text: "wait" });
1398
+ const owner = createCodexAppServerProviderOwner({ runtimeRoot: f.directory });
1399
+ rejectStorage = true;
1400
+ try {
1401
+ const result = await owner.invalidateRuntimes({ includeOwned: true, requireVerifiedExit: true, stopOwnedRuntimes: true });
1402
+ assert.equal(result.ok, false);
1403
+ assert.equal(result.providerCount, 2);
1404
+ assert.equal(result.stopped, 1);
1405
+ assert.equal(result.failed.filter(value => value.error === failure.message && value.retryable).length, 2);
1406
+ const states = await Promise.race([
1407
+ Promise.all([f.conversation.wait(), peer.wait()]),
1408
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Failed invalidation consumers did not settle")), 2000))
1409
+ ]);
1410
+ assert.ok(states.every(state => state.status === "unavailable"));
1411
+ for (const id of ["conversation", "peer"]) {
1412
+ const saved = await f.storage.read(id, async transaction => (await transaction.readMetadata()).runtime.binding);
1413
+ assert.equal(saved.observationLoss.stopped, false);
1414
+ assert.ok(saved.observationLoss.runtime.executionId);
1415
+ assert.equal(saved.codexAppServerRun.active, true);
1416
+ }
1417
+ } finally { rejectStorage = false; }
1418
+ await f.conversation.cancel();
1419
+ await peer.cancel();
1420
+ assert.ok((await Promise.all([f.conversation.read(), peer.read()])).every(state => state.status === "ready"));
1421
+ for (const id of ["conversation", "peer"]) {
1422
+ const saved = await f.storage.read(id, async transaction => (await transaction.readMetadata()).runtime.binding);
1423
+ assert.equal(saved.observationLoss.stopped, true);
1424
+ assert.equal(saved.codexAppServerRun.active, false);
1425
+ }
1426
+ assert.equal((await f.trace()).filter(row => row.args).length, 1, "storage retry cannot start another native process");
1427
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 2, "storage retry cannot repeat inference");
1428
+ });
1429
+
1430
+ test("failed stopped-state persistence retains Codex recovery across reopening without starting work", async t => {
1431
+ let source;
1432
+ let rejectStopped = false;
1433
+ const startThread = CodexAppServerAgentProvider.prototype.startThread;
1434
+ t.mock.method(CodexAppServerAgentProvider.prototype, "startThread", async function (...args) {
1435
+ source = this;
1436
+ return startThread.apply(this, args);
1437
+ });
1438
+ const f = await fixture(t, { checkpoints: true, storage: disk => ({
1439
+ read: disk.read,
1440
+ write: (scope, callback) => disk.write(scope, transaction => callback({
1441
+ ...transaction,
1442
+ async writeMetadata(metadata) {
1443
+ if (rejectStopped && metadata.runtime?.binding?.observationLoss?.stopped) {
1444
+ throw new Error("Stopped state could not be persisted");
1445
+ }
1446
+ return transaction.writeMetadata(metadata);
1447
+ }
1448
+ }))
1449
+ }) });
1450
+ await f.conversation.send({ messageId: "unfinished", text: "wait" });
1451
+ rejectStopped = true;
1452
+ source.client.close();
1453
+ await assert.rejects(source.failObservation(new Error("Lost observation")), /Stopped state could not be persisted/);
1454
+ await f.conversation.wait();
1455
+ assert.equal((await f.conversation.read()).status, "unavailable");
1456
+ const checkpoint = f.checkpoints.findLast(record => record.metadata.runtime?.binding?.observationLoss?.stopped === false);
1457
+ assert.ok(checkpoint, "the pending barrier and exact runtime owner must remain durable");
1458
+ rejectStopped = false;
1459
+ await f.restore(checkpoint);
1460
+ const starts = (await f.trace()).filter(row => row.args).length;
1461
+ const reopened = await f.runtime().open({ id: "conversation", configuration });
1462
+ assert.equal((await reopened.read()).status, "unavailable");
1463
+ await reopened.cancel();
1464
+ const saved = await f.storage.read("conversation", async transaction => (await transaction.readMetadata()).runtime.binding);
1465
+ assert.equal(saved.observationLoss.stopped, true);
1466
+ assert.equal((await f.trace()).filter(row => row.args).length, starts, "cleanup cannot launch a new native server");
1467
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1, "cleanup cannot replay the user message");
1468
+ });
1469
+
1470
+ test("shared provider invalidation selects the exact account and workspace", async t => {
1471
+ const root = await mkdtemp(path.join(tmpdir(), "jskit-codex-owner-"));
1472
+ const main = createCodexAppServerProviderOwner({ runtimeRoot: root });
1473
+ const common = createCodexAppServerProviderOwner();
1474
+ const calls = [];
1475
+ t.after(async () => {
1476
+ for (const owner of [main, common]) await owner.invalidateRuntimes({ includeOwned: true, stopOwnedRuntimes: true });
1477
+ await rm(root, { recursive: true, force: true });
1478
+ });
1479
+ for (const [owner, key, runtimeDir, toolHomeSource] of [
1480
+ [main, "main-a", path.join(root, "a"), "account-a"],
1481
+ [common, "common-a", path.join(root, "a"), "account-a"],
1482
+ [common, "common-b", path.join(root, "b"), "account-b"],
1483
+ [common, "other-workspace", `${root}-other/a`, "account-a"]
1484
+ ]) {
1485
+ const providerOptions = { runtimeDir, toolHomeSource };
1486
+ const provider = owner.createProvider({ providerKey: key, providerOptions, create: () => ({
1487
+ close() { calls.push([key, "close"]); },
1488
+ async stopRuntime() { calls.push([key, "stop"]); return { stopped: true }; }
1489
+ }) });
1490
+ await owner.acquireRuntime({ providerKey: key, providerOptions, provider, operation: async () => {} });
1491
+ }
1492
+ const result = await main.invalidateRuntimes({ includeOwned: true, requireVerifiedExit: true,
1493
+ stopOwnedRuntimes: true, toolHomeSource: "account-a" });
1494
+ assert.equal(result.ok, true);
1495
+ assert.equal(result.providerCount, 2);
1496
+ assert.equal(result.stopped, 1);
1497
+ assert.deepEqual(calls, [["main-a", "close"], ["common-a", "stop"]]);
1498
+ assert.ok(common.providers.get("common-b"));
1499
+ assert.ok(common.providers.get("other-workspace"));
1500
+ });
1501
+
1502
+ test("Codex cancellation drains owned work, and a deadline saves a failed turn", async t => {
1503
+ const f = await fixture(t, { limits: { timeoutMs: 2000 } });
1504
+ await f.conversation.send({ messageId: "cancel", text: "wait" });
1505
+ assert.deepEqual(await f.conversation.cancel(), { stopped: true });
1506
+ await f.conversation.send({ messageId: "timeout", text: "wait" });
1507
+ const state = await f.conversation.wait();
1508
+ assert.equal(state.conversationLog[0].metadata.runtime.status, "cancelled");
1509
+ assert.equal(state.conversationLog[1].metadata.runtime.status, "failed");
1510
+ assert.match(state.error, /time limit/);
1511
+ const trace = await f.trace();
1512
+ assert.equal(trace.filter(row => row.method === "turn/interrupt").length, 2);
1513
+ assert.equal(trace.filter(row => row.args).length, 1);
1514
+ });
1515
+
1516
+ test("lost Codex acknowledgement stays uncertain until native history proves the exact message", async t => {
1517
+ const f = await fixture(t);
1518
+ const lost = { messageId: "lost-id", text: "lost" };
1519
+ await assert.rejects(f.conversation.send(lost), /timed out/);
1520
+ assert.equal((await f.conversation.read()).status, "unconfirmed");
1521
+ await writeFile(path.join(f.directory, "history.json.unavailable"), "");
1522
+ try { await assert.rejects(f.conversation.send(lost), /uncertain/); }
1523
+ finally { await rm(path.join(f.directory, "history.json.unavailable")); }
1524
+ const recovered = await f.conversation.inspectDelivery({ messageId: lost.messageId });
1525
+ assert.equal(recovered.status, "accepted");
1526
+ assert.equal((await f.conversation.send(lost)).duplicate, true);
1527
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
1528
+ });
1529
+
1530
+ test("explicit rejection and a failed native turn do not become successful answers", async t => {
1531
+ const f = await fixture(t);
1532
+ await assert.rejects(f.conversation.send({ messageId: "rejected", text: "rejected" }), /Turn rejected/);
1533
+ assert.equal((await f.conversation.read()).status, "ready");
1534
+ await f.conversation.send({ messageId: "failed", text: "failed" });
1535
+ const state = await f.conversation.wait();
1536
+ assert.equal(state.conversationLog[0].metadata.runtime.status, "failed");
1537
+ assert.equal(state.conversationLog[0].assistant, null);
1538
+ });
1539
+
1540
+ test("changing the native account cannot submit more work to the previous account's thread", async t => {
1541
+ const f = await fixture(t);
1542
+ await f.conversation.send(input);
1543
+ await f.conversation.wait();
1544
+ await writeFile(f.account, "another@example.test");
1545
+ await assert.rejects(f.conversation.send({ messageId: "different", text: "Continue" }), /another Codex account/);
1546
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
1547
+ });
1548
+
1549
+ test("Codex distinguishes native account IDs sharing an email without treating token refresh as a new account", async t => {
1550
+ const f = await fixture(t);
1551
+ const root = path.join(f.directory, "codex");
1552
+ await mkdir(root);
1553
+ const credential = accountId => writeFile(path.join(root, "auth.json"), JSON.stringify({ tokens: {
1554
+ account_id: accountId, access_token: Math.random().toString()
1555
+ } }));
1556
+ await credential("first-account");
1557
+ await f.conversation.send(input);
1558
+ await f.conversation.wait();
1559
+ await credential("first-account");
1560
+ await f.conversation.send({ messageId: "refreshed", text: "Continue" });
1561
+ await f.conversation.wait();
1562
+ await credential("different-account");
1563
+ await assert.rejects(f.conversation.send({ messageId: "different", text: "Continue" }), /another Codex account/);
1564
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 2);
1565
+ });
1566
+
1567
+ test("losing the Codex socket stops its process and preserves the failed accepted turn", async t => {
1568
+ const f = await fixture(t);
1569
+ await f.conversation.send({ messageId: "disconnect", text: "disconnect" });
1570
+ const state = await f.conversation.wait();
1571
+ assert.equal(state.conversationLog[0].metadata.runtime.status, "failed");
1572
+ const binding = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding);
1573
+ assert.equal(binding.executionId, "");
1574
+ await f.conversation.send(input);
1575
+ assert.equal((await f.conversation.wait()).conversationLog[1].assistant.text, "Answer: Hello");
1576
+ });
1577
+
1578
+ test("cancel during a shared Codex handshake releases its caller and leaves startup available to a peer", async t => {
1579
+ const gateDirectory = await mkdtemp(path.join(tmpdir(), "jskit-codex-startup-gate-"));
1580
+ t.after(() => rm(gateDirectory, { recursive: true, force: true }));
1581
+ const gate = path.join(gateDirectory, "ready");
1582
+ const f = await fixture(t, { environment: { TEST_STARTUP_WAIT: gate }, limits: { admissionTimeoutMs: 30_000 } });
1583
+ const sending = f.conversation.send(input);
1584
+ sending.catch(() => {});
1585
+ const deadline = Date.now() + 3000;
1586
+ while (!(await f.trace().catch(() => [])).some(row => row.method === "initialize")) {
1587
+ if (Date.now() > deadline) throw new Error("Codex did not begin its initialization.");
1588
+ await new Promise(resolve => setTimeout(resolve, 20));
1589
+ }
1590
+ const started = Date.now();
1591
+ await f.conversation.cancel();
1592
+ await assert.rejects(sending);
1593
+ assert.ok(Date.now() - started < 1500, "cancel must not wait for the 30-second handshake timeout");
1594
+ assert.equal((await f.conversation.read()).conversationLog.length, 0);
1595
+ await writeFile(gate, "ready");
1596
+ const peer = await f.first.open({ id: "peer", configuration });
1597
+ await peer.send({ messageId: "peer-first", text: "Hello from the peer" });
1598
+ assert.equal((await peer.wait()).conversationLog[0].assistant.text, "Answer: Hello from the peer");
1599
+ const trace = await f.trace();
1600
+ assert.equal(trace.filter(row => row.args).length, 1);
1601
+ assert.deepEqual(trace.filter(row => row.method === "turn/start").map(row => row.params.clientUserMessageId), ["peer-first"]);
1602
+ });
1603
+
1604
+ test("an unconfirmed Codex stop blocks new work and supports an explicit cleanup retry", async t => {
1605
+ const execution = createLocalConversationExecution();
1606
+ let allowStop = false;
1607
+ const f = await fixture(t, { execution: { ...execution, authoritative: true, async stop(...args) { if (!allowStop) return { ok: false, scopeEmpty: false }; return { ok: true, ...await execution.stop(...args) }; } } });
1608
+ t.after(() => execution.close());
1609
+ await f.conversation.send({ messageId: "waiting", text: "disconnect" });
1610
+ await f.conversation.wait();
1611
+ assert.equal((await f.conversation.read()).status, "unavailable");
1612
+ await assert.rejects(f.conversation.send(input), { code: "codex_runtime_exit_unverified" });
1613
+ allowStop = true;
1614
+ await f.conversation.cancel();
1615
+ await f.conversation.send(input);
1616
+ assert.equal((await f.conversation.wait()).status, "ready");
1617
+ });
1618
+
1619
+ test("access revoked during native preparation prevents the user message from being sent", async t => {
1620
+ let allowed = true;
1621
+ const execution = createLocalConversationExecution();
1622
+ const f = await fixture(t, { authorize: () => allowed,
1623
+ execution: { ...execution, async run(params) { const native = await execution.run(params); if (params.mode === "detached") allowed = false; return native; } } });
1624
+ t.after(() => execution.close());
1625
+ await assert.rejects(f.conversation.send(input), /not available/);
1626
+ assert.equal((await f.trace()).some(row => row.method === "turn/start"), false);
1627
+ allowed = true;
1628
+ });
1629
+
1630
+ function applicationActions(execute) {
1631
+ const actions = createActionCatalogue();
1632
+ actions.register({ contributorId: "test.tools", domain: "numbers", actions: [{
1633
+ id: "numbers.read", version: 1, kind: "query", channels: ["automation"], surfaces: ["app"],
1634
+ permission: { require: "all", permissions: ["numbers.read"] }, idempotency: "none",
1635
+ input: { schema: createSchema({}), mode: "replace" },
1636
+ output: { schema: createSchema({ value: { type: "number", required: true } }), mode: "replace" }, execute
1637
+ }] });
1638
+ return actions;
1639
+ }
1640
+
1641
+ test("Codex uses shared discovery and durable application results across native resume", async t => {
1642
+ let executions = 0;
1643
+ const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
1644
+ const actions = applicationActions(async () => { executions++; return { value: 42 }; });
1645
+ const f = await fixture(t, { actions, context });
1646
+ await f.conversation.send({ messageId: "tool-turn", text: "tools" });
1647
+ const first = await f.conversation.wait();
1648
+ assert.equal(first.conversationLog[0].metadata.runtime.status, "complete");
1649
+ assert.equal(first.conversationLog[0].metadata.applicationTools.length, 3);
1650
+ assert.equal(JSON.parse(first.conversationLog[0].assistant.text).result.result.value, 42);
1651
+ assert.equal(executions, 1);
1652
+ const started = (await f.trace()).find(row => row.method === "thread/start");
1653
+ assert.deepEqual(started.params.dynamicTools.map(tool => tool.name), ["assistant_action_search", "assistant_action_contract", "assistant_action_execute"]);
1654
+ assert.equal(started.params.config.features.shell_tool, false);
1655
+ await f.first.close();
1656
+ const resumed = await f.runtime().open({ id: "conversation", context });
1657
+ await resumed.send({ messageId: "resumed-tools", text: "tools" });
1658
+ assert.equal((await resumed.wait()).conversationLog[1].metadata.runtime.status, "complete");
1659
+ assert.equal(executions, 2);
1660
+ context.permissions = [];
1661
+ await resumed.send({ messageId: "revoked-tools", text: "tools" });
1662
+ const revoked = await resumed.wait();
1663
+ assert.equal(revoked.conversationLog[2].metadata.applicationTools.at(-1).result.ok, false);
1664
+ assert.equal(executions, 2);
1665
+ assert.equal((await f.trace()).filter(row => row.method === "thread/start").length, 1, "Permission changes do not replace native history");
1666
+ });
1667
+
1668
+ test("a native tool request cannot target another application conversation", async t => {
1669
+ let executions = 0;
1670
+ const actions = applicationActions(async () => { executions++; return { value: 42 }; });
1671
+ const f = await fixture(t, { actions, context: { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] } });
1672
+ await f.conversation.send({ messageId: "foreign", text: "foreign-tool" });
1673
+ const result = await f.conversation.wait();
1674
+ assert.equal(executions, 0);
1675
+ assert.equal(result.conversationLog[0].metadata.runtime.status, "failed");
1676
+ assert.match(result.error, /does not authorize/);
1677
+ });
1678
+
1679
+ test("Codex cancellation waits for an application effect after closing the native process", async t => {
1680
+ const entered = Promise.withResolvers(), complete = Promise.withResolvers();
1681
+ const actions = applicationActions(async () => { entered.resolve(); return complete.promise; });
1682
+ const f = await fixture(t, { actions, context: { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] } });
1683
+ await f.conversation.send({ messageId: "cancel-tools", text: "tools" });
1684
+ await entered.promise;
1685
+ let finished = false;
1686
+ const stopping = f.conversation.cancel().then(() => { finished = true; });
1687
+ await new Promise(resolve => setTimeout(resolve, 50));
1688
+ assert.equal(finished, false);
1689
+ complete.resolve({ value: 42 });
1690
+ await stopping;
1691
+ const result = await f.conversation.read();
1692
+ assert.equal(result.conversationLog[0].metadata.runtime.status, "cancelled");
1693
+ assert.equal(result.conversationLog[0].metadata.applicationTools.at(-1).result.result.result.value, 42);
1694
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
1695
+ });
1696
+
1697
+ test("steering keeps an in-flight application effect and its receipt on the original message", async t => {
1698
+ const entered = Promise.withResolvers(), complete = Promise.withResolvers();
1699
+ let executions = 0;
1700
+ const actions = applicationActions(async () => { executions++; entered.resolve(); return complete.promise; });
1701
+ const f = await fixture(t, { actions, context: { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] } });
1702
+ await f.conversation.send({ messageId: "original-tools", text: "tools" });
1703
+ await entered.promise;
1704
+ await f.conversation.send({ messageId: "new-instruction", text: "Continue differently", steer: true });
1705
+ const pending = await f.conversation.read();
1706
+ assert.equal(pending.conversationLog[0].metadata.applicationTools.at(-1).status, "running");
1707
+ complete.resolve({ value: 42 });
1708
+ const state = await f.conversation.wait();
1709
+ assert.equal(state.error, "");
1710
+ assert.equal(state.conversationLog[0].metadata.applicationTools.at(-1).result.result.result.value, 42);
1711
+ assert.deepEqual(state.conversationLog[1].metadata.applicationTools, []);
1712
+ assert.equal(state.conversationLog[1].assistant.text, "Steered: Continue differently");
1713
+ assert.equal(executions, 1);
1714
+ });
1715
+
1716
+ test("steering resolves current attachment access and saves only its authorized receipt", async t => {
1717
+ const image = Buffer.from("authorized-steering-image");
1718
+ const receipt = { attachmentId: "picture", fileName: "picture.png", size: image.length };
1719
+ let reads = 0;
1720
+ const f = await fixture(t, { attachments: { resolve: async () => {
1721
+ reads++;
1722
+ return { attachments: [receipt], content: [{ type: "image", image, mediaType: "image/png" }] };
1723
+ } } });
1724
+ await f.conversation.send({ messageId: "before", text: "wait" });
1725
+ const instruction = { messageId: "image-steer", text: "Use this image", attachmentIds: [receipt.attachmentId], steer: true };
1726
+ await f.conversation.send(instruction);
1727
+ const state = await f.conversation.wait();
1728
+ assert.equal(state.error, "");
1729
+ assert.deepEqual(state.conversationLog[1].user.attachments, [receipt]);
1730
+ assert.equal((await f.trace()).find(row => row.method === "turn/steer").params.input[1].url,
1731
+ `data:image/png;base64,${image.toString("base64")}`);
1732
+ assert.equal((await f.conversation.send(instruction)).duplicate, true);
1733
+ assert.equal(reads, 1);
1734
+ });
1735
+
1736
+ test("an uncertain application effect cannot become successful when steering has already produced an answer", async t => {
1737
+ const entered = Promise.withResolvers(), complete = Promise.withResolvers();
1738
+ const actions = applicationActions(async () => { entered.resolve(); return complete.promise; });
1739
+ const f = await fixture(t, { actions, context: { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] } });
1740
+ await f.conversation.send({ messageId: "original-tools", text: "tools" });
1741
+ await entered.promise;
1742
+ await f.conversation.send({ messageId: "new-instruction", text: "Continue differently", steer: true });
1743
+ complete.reject(new Error("The external effect could not be confirmed."));
1744
+ const state = await f.conversation.wait();
1745
+ assert.equal(state.conversationLog[0].metadata.applicationTools.at(-1).status, "unknown");
1746
+ assert.equal(state.conversationLog[1].metadata.runtime.status, "failed");
1747
+ assert.match(state.error, /did not return a verified result/);
1748
+ });
1749
+
1750
+ test("a failed write while recording rejected steering can be recovered when the active run is saved", async t => {
1751
+ let rejectedPending = false;
1752
+ let failureInjected = false;
1753
+ const f = await fixture(t, { storage: disk => ({ ...disk, write(id, callback) {
1754
+ return disk.write(id, transaction => callback({ ...transaction, async writeMetadata(metadata) {
1755
+ if (metadata.runtime.request?.messageId === "rejected" && metadata.runtime.request.attempted) rejectedPending = true;
1756
+ if (rejectedPending && !failureInjected && metadata.runtime.request?.messageId === "rejected" && metadata.runtime.request.attempted === false) {
1757
+ failureInjected = true;
1758
+ throw new Error("Disk temporarily unavailable");
1759
+ }
1760
+ await transaction.writeMetadata(metadata);
1761
+ } }));
1762
+ } }) });
1763
+ await f.conversation.send({ messageId: "before", text: "wait" });
1764
+ await assert.rejects(f.conversation.send({ messageId: "rejected", text: "rejected-steer", steer: true }), /Disk temporarily/);
1765
+ await f.conversation.cancel();
1766
+ const state = await f.conversation.read();
1767
+ assert.equal(state.pendingRequest, null);
1768
+ assert.equal(state.status, "ready");
1769
+ assert.equal(state.conversationLog.length, 1);
1770
+ });
1771
+
1772
+ test("one logical conversation switches between Codex and API across restart without duplicating the displayed history", async t => {
1773
+ const requests = [];
1774
+ const connections = createAiConnectionResolver({ configuration: { schemaVersion: 1, registrations: {}, integrations: {
1775
+ assistant: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, settings: { model: "opencode/big-pickle" } }
1776
+ } }, authorize: () => ({ applicationId: "test", subjectId: "owner" }) });
1777
+ const f = await fixture(t, { connections, fetch: async (_url, init) => {
1778
+ requests.push(JSON.parse(init.body));
1779
+ const chunk = { choices: [{ index: 0, delta: { content: "API answer" }, finish_reason: "stop" }] };
1780
+ return new Response(`data: ${JSON.stringify(chunk)}\n\n`, { headers: { "content-type": "text/event-stream" } });
1781
+ } });
1782
+ await f.conversation.send(input);
1783
+ const initial = await f.conversation.wait();
1784
+ await f.conversation.replace({ operationId: "to-api", expectedSegmentId: initial.segmentId, reason: "engine-change", engine: "api",
1785
+ configuration: { systemPrompt: "Continue this conversation.", integrationId: "assistant" } });
1786
+ assert.equal((await f.conversation.read()).engine, "api");
1787
+ const departure = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime);
1788
+ assert.ok(departure.predecessors[0].preparedAt, "Initialized native departure uses the original prepared replacement");
1789
+ assert.ok(departure.predecessors[0].binding.threadId);
1790
+ assert.equal(requests.length, 0, "Preparing an API successor performs no inference");
1791
+ assert.equal((await f.trace()).filter(row => row.method === "thread/start").length, 1);
1792
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
1793
+ await f.conversation.send({ messageId: "api", text: "Continue through API" });
1794
+ await f.conversation.wait();
1795
+ assert.match(requests[0].messages[1].content, /Hello/);
1796
+ await f.first.close();
1797
+ const reopened = await f.runtime().open({ id: "conversation" });
1798
+ const saved = await reopened.read();
1799
+ assert.equal(saved.engine, "api", "Reopen uses the saved selection, not the runtime default");
1800
+ await reopened.replace({ operationId: "to-codex", expectedSegmentId: saved.segmentId, reason: "engine-change", engine: "codex", configuration });
1801
+ await reopened.send({ messageId: "native-again", text: "Continue native" });
1802
+ await reopened.wait();
1803
+ await reopened.send({ messageId: "native-next", text: "Next" });
1804
+ const result = await reopened.wait();
1805
+ assert.deepEqual(result.conversationLog.map(turn => turn.user.text), ["Hello", "Continue through API", "Continue native", "Next"]);
1806
+ const trace = await f.trace();
1807
+ const turns = trace.filter(row => row.method === "turn/start");
1808
+ assert.equal(trace.filter(row => row.method === "thread/start").length, 2);
1809
+ assert.match(turns[1].params.input[0].text, /API answer/);
1810
+ assert.equal(turns[2].params.input[0].text, "Next", "The successor receives its briefing only once");
1811
+ const metadata = await f.storage.read("conversation", tx => tx.readMetadata());
1812
+ assert.equal(metadata.runtime.predecessors.length, 2);
1813
+ assert.ok(metadata.runtime.predecessors[0].binding.executionId, "Retain the production execution reference as exit-proof identity.");
1814
+ assert.notEqual(metadata.runtime.predecessors[0].binding.threadId, metadata.runtime.binding.threadId);
1815
+ });
1816
+
1817
+ test("replacement retains the predecessor and blocks a successor until native cleanup is confirmed", async t => {
1818
+ const execution = createLocalConversationExecution();
1819
+ let allowStop = false;
1820
+ const f = await fixture(t, { execution: { ...execution, authoritative: true, async stop(...args) { if (!allowStop) return { ok: false, scopeEmpty: false }; return { ok: true, ...await execution.stop(...args) }; } } });
1821
+ t.after(() => execution.close());
1822
+ await f.conversation.send(input);
1823
+ const state = await f.conversation.wait();
1824
+ const operation = { operationId: "renew", expectedSegmentId: state.segmentId, reason: "renewal", briefing: "Continue from the saved work." };
1825
+ await assert.rejects(f.conversation.replace(operation), { code: "codex_runtime_exit_unverified" });
1826
+ const preparing = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime);
1827
+ assert.ok(preparing.replacement.preparedAt);
1828
+ assert.equal(preparing.segmentId, state.segmentId);
1829
+ assert.equal(preparing.predecessors.length, 0);
1830
+ assert.equal(Object.hasOwn(preparing.replacement, "binding"), false, "Successor allocation waits for confirmed native cleanup");
1831
+ await assert.rejects(f.conversation.send({ messageId: "blocked", text: "Too early" }), { code: "codex_runtime_exit_unverified" });
1832
+ assert.equal((await f.trace()).filter(row => row.method === "thread/start").length, 1);
1833
+ allowStop = true;
1834
+ await f.conversation.replace(operation);
1835
+ await f.conversation.send({ messageId: "renewed", text: "Continue" });
1836
+ const result = await f.conversation.wait();
1837
+ assert.equal(result.conversationLog.length, 2);
1838
+ assert.equal((await f.trace()).filter(row => row.method === "thread/start").length, 2);
1839
+ });
1840
+
1841
+ test("one runtime starts conversations in separate worktrees and account homes", async t => {
1842
+ const f = await fixture(t);
1843
+ const otherDirectory = path.join(f.directory, "another-worktree");
1844
+ await mkdir(otherDirectory);
1845
+ const trace = path.join(otherDirectory, "trace.jsonl");
1846
+ const account = path.join(otherDirectory, "account.txt");
1847
+ await writeFile(account, "another@example.test");
1848
+ const host = { workdir: otherDirectory, commands: { codex: path.join(f.directory, "codex.mjs") },
1849
+ env: { ...process.env, CODEX_HOME: path.join(otherDirectory, "credentials"), TEST_ACCOUNT: account,
1850
+ TEST_TRACE: trace, TEST_HISTORY: path.join(otherDirectory, "history.json") } };
1851
+ const other = await f.first.open({ id: "another", engine: "codex", host, configuration });
1852
+ await f.conversation.send(input);
1853
+ await f.conversation.wait();
1854
+ await other.send({ messageId: "other", text: "Other worktree" });
1855
+ await other.wait();
1856
+ const one = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding);
1857
+ const two = await f.storage.read("another", async tx => (await tx.readMetadata()).runtime.binding);
1858
+ assert.equal(one.workdir, f.directory);
1859
+ assert.equal(two.workdir, otherDirectory);
1860
+ assert.notEqual(one.configRoot, two.configRoot);
1861
+ assert.notEqual(one.accountIdentity, two.accountIdentity);
1862
+ assert.notEqual(one.executionId, two.executionId);
1863
+ await f.conversation.dispose();
1864
+ await other.send({ messageId: "other-next", text: "Still active" });
1865
+ assert.equal((await other.wait()).conversationLog.length, 2);
1866
+ });
1867
+
1868
+ test("an application wake uses native admission without becoming a user message", async t => {
1869
+ const f = await fixture(t);
1870
+ const events = [];
1871
+ await f.conversation.subscribe(event => events.push(event));
1872
+ const input = { messageId: "watch", text: "The watched session completed." };
1873
+ await f.conversation.wake(input);
1874
+ const result = await f.conversation.wait();
1875
+ assert.equal(result.conversationLog[0].user, null);
1876
+ assert.equal(result.conversationLog[0].system.text, input.text);
1877
+ assert.equal(result.conversationLog[0].metadata.runtime.origin, "application");
1878
+ const replies = events.filter(event => event.type === "message" && event.role === "assistant");
1879
+ assert.ok(replies.some(event => event.status === "inProgress"));
1880
+ assert.ok(replies.some(event => event.status === "complete"));
1881
+ assert.equal(replies.every(event => event.origin === "application"), true);
1882
+ assert.equal(replies.filter(event => event.status === "inProgress").every(event => event.turnId === undefined), true);
1883
+ assert.equal(replies.filter(event => event.status === "complete").every(event => event.turnId === result.conversationLog[0].turnId), true);
1884
+ assert.equal(JSON.stringify(replies).includes("nativeIdentity"), false);
1885
+ assert.equal((await f.conversation.wake(input)).duplicate, true);
1886
+ const calls = (await f.trace()).filter(row => row.method === "turn/start");
1887
+ assert.equal(calls.length, 1);
1888
+ assert.match(calls[0].params.input[0].text, /\[Application event\]/);
1889
+ assert.equal(calls[0].params.clientUserMessageId, "watch");
1890
+ });
1891
+
1892
+ test("select returns to retained native history after restart and sends all missed, edited and removed messages once", async t => {
1893
+ const f = await fixture(t, apiOptions());
1894
+ await f.conversation.send(input);
1895
+ const original = await f.conversation.wait();
1896
+ await f.conversation.send({ messageId: "remove", text: "This turn will be removed" });
1897
+ const second = await f.conversation.wait();
1898
+ await select(f.conversation, "api", "choose-api");
1899
+ for (let i = 0; i < 31; i++) {
1900
+ await f.conversation.send({ messageId: `api-${i}`, text: `Missed message ${i}` });
1901
+ await f.conversation.wait();
1902
+ }
1903
+ await f.storage.write("conversation", async tx => {
1904
+ await tx.replaceAssistant(original.conversationLog[0].turnId, { role: "assistant", text: "Corrected original answer" });
1905
+ const removed = second.conversationLog[1];
1906
+ await tx.updateTurnMetadata(removed.turnId, { runtime: { ...removed.metadata.runtime, supersededBy: "application-edit" } });
1907
+ });
1908
+ await f.first.close();
1909
+ const reopened = await f.runtime().open({ id: "conversation" });
1910
+ const operation = { operationId: "return-codex", expectedSegmentId: (await reopened.read()).segmentId, engine: "codex", configuration };
1911
+ const receipt = await reopened.select(operation);
1912
+ assert.equal(receipt.segmentId, original.segmentId);
1913
+ assert.equal((await reopened.select(operation)).duplicate, true);
1914
+ await reopened.send({ messageId: "return", text: "Resume here" });
1915
+ await reopened.wait();
1916
+ const calls = (await f.trace()).filter(row => row.method === "turn/start");
1917
+ const catchup = calls[2].params.input[0].text;
1918
+ assert.match(catchup, /Corrected original answer/);
1919
+ assert.match(catchup, /"corrected":true/);
1920
+ assert.match(catchup, /Missed message 0/);
1921
+ assert.match(catchup, /Missed message 30/);
1922
+ assert.match(catchup, /"removedMessageIds":\["000002\/user\/","000002\/assistant\/"\]/);
1923
+ assert.doesNotMatch(catchup, /This turn will be removed/);
1924
+ assert.equal((await f.trace()).filter(row => row.method === "thread/start").length, 1);
1925
+ await reopened.send({ messageId: "next", text: "Next" });
1926
+ await reopened.wait();
1927
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").at(-1).params.input[0].text, "Next");
1928
+ });
1929
+
1930
+ test("an uncertain native receipt stays with its engine while another engine can work", async t => {
1931
+ const f = await fixture(t, apiOptions());
1932
+ const lost = { messageId: "lost-id", text: "lost" };
1933
+ await assert.rejects(f.conversation.send(lost), /timed out/);
1934
+ await select(f.conversation, "api", "choose-api");
1935
+ await assert.rejects(f.conversation.send(lost), { code: "conversation_delivery_uncertain" });
1936
+ await f.conversation.send({ messageId: "api-work", text: "Different work" });
1937
+ await f.conversation.wait();
1938
+ await f.first.close();
1939
+ const reopened = await f.runtime().open({ id: "conversation" });
1940
+ await select(reopened, "codex", "return-codex");
1941
+ assert.equal((await reopened.read()).status, "unconfirmed");
1942
+ await writeFile(path.join(f.directory, "history.json.unavailable"), "");
1943
+ try { await assert.rejects(reopened.send({ messageId: "new", text: "Too early" }), { code: "conversation_delivery_uncertain" }); }
1944
+ finally { await rm(path.join(f.directory, "history.json.unavailable")); }
1945
+ assert.equal((await reopened.inspectDelivery({ messageId: lost.messageId })).recovered, true);
1946
+ assert.equal((await reopened.send(lost)).duplicate, true);
1947
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
1948
+ await reopened.send({ messageId: "confirmed", text: "Now continue" });
1949
+ await reopened.wait();
1950
+ const sent = (await f.trace()).filter(row => row.method === "turn/start");
1951
+ assert.match(sent[1].params.input[0].text, /Different work/);
1952
+ });
1953
+
1954
+ test("selecting another model on the current engine preserves its native identity", async t => {
1955
+ const f = await fixture(t);
1956
+ await f.conversation.send(input);
1957
+ const original = await f.conversation.wait();
1958
+ const receipt = await f.conversation.select({ operationId: "model", expectedSegmentId: original.segmentId,
1959
+ engine: "codex", configuration: { ...configuration, model: "another-model" } });
1960
+ assert.equal(receipt.segmentId, original.segmentId);
1961
+ await f.conversation.send({ messageId: "next", text: "Continue" });
1962
+ await f.conversation.wait();
1963
+ const trace = await f.trace();
1964
+ assert.equal(trace.filter(row => row.method === "thread/start").length, 1);
1965
+ assert.equal(trace.filter(row => row.method === "turn/start").at(-1).params.model, "another-model");
1966
+ });
1967
+
1968
+ test("Codex uses an authorized foreign connection, exact catalogue IDs and recovery without replacing native history", async t => {
1969
+ let apiKey = "test-foreign-key";
1970
+ const context = { user: "owner" };
1971
+ const f = await fixture(t, { context,
1972
+ connections: { async resolve(request) {
1973
+ assert.equal(request.context, context);
1974
+ assert.equal(request.integrationId, "foreign");
1975
+ return { providerId: "deepseek", model: "deepseek-flash", apiKey };
1976
+ } }
1977
+ });
1978
+ await f.conversation.send(input);
1979
+ await f.conversation.wait();
1980
+ const original = await f.storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding.threadId);
1981
+ await f.conversation.select({ operationId: "foreign-model", expectedSegmentId: (await f.conversation.read()).segmentId,
1982
+ engine: "codex", configuration: { systemPrompt: configuration.systemPrompt, integrationId: "foreign", effort: "low" } });
1983
+ await f.conversation.send({ messageId: "foreign", text: "Continue" });
1984
+ await f.conversation.wait();
1985
+ const trace = await f.trace();
1986
+ assert.equal(trace.filter(row => row.method === "thread/start").length, 1);
1987
+ const resumed = trace.find(row => row.method === "thread/resume").params;
1988
+ assert.equal(resumed.threadId, original);
1989
+ assert.equal(resumed.model, "deepseek-flash");
1990
+ assert.equal(resumed.modelProvider, "deepseek");
1991
+ const routing = resumed.config["model_providers.deepseek"];
1992
+ assert.equal(routing.experimental_bearer_token, apiKey);
1993
+ assert.equal(routing.wire_api, "responses");
1994
+ assert.equal(routing.requires_openai_auth, false);
1995
+ // The moved native service owns its history proxy. HTTP reconstruction is
1996
+ // covered by the original history-adapter tests; this boundary proves routing.
1997
+ assert.match(routing.base_url, /^http:\/\/127\.0\.0\.1:\d+\/[^/]+\/v2\/deepseek(?:\/history\/[^/]+)?$/u);
1998
+ assert.equal((await fetch(`${routing.base_url}/unsupported-route`)).status, 404);
1999
+ const args = trace.filter(row => row.args).at(-1).args;
2000
+ const cataloguePath = JSON.parse(args.find(value => value.startsWith("model_catalog_json=")).slice("model_catalog_json=".length));
2001
+ const catalogue = JSON.parse(await readFile(cataloguePath, "utf8"));
2002
+ const foreignModel = catalogue.models.find(model => model.slug === "deepseek-flash");
2003
+ assert.ok(foreignModel, "The production catalogue retains native and curated models together.");
2004
+ assert.equal(foreignModel.context_window, 1048576);
2005
+ assert.ok(!JSON.stringify(await f.storage.read("conversation", tx => tx.readMetadata())).includes(apiKey));
2006
+ apiKey = "different-account";
2007
+ await assert.rejects(f.conversation.send({ messageId: "wrong-account", text: "No" }), /another Codex provider account/);
2008
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 2);
2009
+ apiKey = "test-foreign-key";
2010
+ await f.conversation.select({ operationId: "return-native", expectedSegmentId: (await f.conversation.read()).segmentId,
2011
+ engine: "codex", configuration });
2012
+ await f.conversation.send({ messageId: "back", text: "Back" });
2013
+ await f.conversation.wait();
2014
+ assert.equal((await f.trace()).filter(row => row.method === "thread/resume").at(-1).params.threadId, original);
2015
+ });
2016
+
2017
+ test("Codex rejects mismatched model identities and unsupported effort before dispatch", async t => {
2018
+ const f = await fixture(t, { connections: { resolve: async () => ({ providerId: "deepseek", model: "deepseek-flash", apiKey: "test-key" }) } });
2019
+ for (const [override, message] of [[{ model: "deepseek-v4-pro" }, /differs from/], [{ effort: "ultra" }, /does not support/]]) {
2020
+ await f.conversation.select({ operationId: `unsupported-${override.model || override.effort}`,
2021
+ expectedSegmentId: (await f.conversation.read()).segmentId, engine: "codex",
2022
+ configuration: { systemPrompt: configuration.systemPrompt, integrationId: "foreign", ...override } });
2023
+ await assert.rejects(f.conversation.send(input), message);
2024
+ assert.equal((await f.conversation.read()).conversationLog.length, 0);
2025
+ }
2026
+ await assert.rejects(readFile(path.join(f.directory, "trace.jsonl")), { code: "ENOENT" });
2027
+ });
2028
+ import { CodexAppServerAgentProvider } from "../src/server/conversation/codexProvider.js";
2029
+
2030
+ const nativeAttachmentFiles = [
2031
+ { attachmentId: "image", fileName: "a b.png", path: "/session/artifacts/a b.png", contentType: "image/png", size: 16, reference: "[Image #1]" },
2032
+ { attachmentId: "file", fileName: "data.csv", path: "/session/artifacts/data.csv", contentType: "application/octet-stream", size: 24, reference: "[File #1]" }
2033
+ ];
2034
+ const nativeAttachmentReceipt = ({ attachmentId, fileName, size, reference }) => ({ attachmentId, fileName, size, reference });
2035
+ const nativeAttachmentResolver = ({ attachmentIds }) => {
2036
+ const localFiles = attachmentIds.map(id => nativeAttachmentFiles.find(file => file.attachmentId === id));
2037
+ return { attachments: localFiles.map(nativeAttachmentReceipt), localFiles };
2038
+ };
2039
+ const nativeAttachmentManifest = '\n\nAttached files:\n[Image #1] "a b.png": "/session/artifacts/a b.png"\n[File #1] "data.csv": "/session/artifacts/data.csv"';
2040
+
2041
+ test("Codex common dispatch preserves the original local image and file manifest with authored receipts", async t => {
2042
+ const f = await fixture(t, { attachments: { resolve: nativeAttachmentResolver }, checkpoints: true });
2043
+ const request = { messageId: "local-files", text: "Inspect [Image #1] and [File #1]", attachmentIds: ["image", "file"] };
2044
+ await f.conversation.send({ ...request, localFiles: [{ path: "/etc/passwd" }], attachmentManifest: "forged" });
2045
+ const state = await f.conversation.wait();
2046
+ assert.equal(state.error, "");
2047
+ assert.deepEqual((await f.trace()).find(row => row.method === "turn/start").params.input, [
2048
+ { type: "text", text: request.text + nativeAttachmentManifest, text_elements: [] },
2049
+ { type: "localImage", path: nativeAttachmentFiles[0].path }
2050
+ ]);
2051
+ assert.equal(state.conversationLog[0].user.text, request.text);
2052
+ assert.deepEqual(state.conversationLog[0].user.attachments, nativeAttachmentFiles.map(nativeAttachmentReceipt));
2053
+ assert.doesNotMatch(JSON.stringify(state.conversationLog[0].user), /session\/artifacts|contentType|forged|etc\/passwd/);
2054
+ const frozen = f.checkpoints.find(record => record.metadata.runtime.request?.attachmentManifest);
2055
+ assert.equal(frozen.metadata.runtime.request.attachmentManifest, nativeAttachmentManifest);
2056
+ assert.equal(Object.hasOwn(frozen.metadata.runtime.request, "localFiles"), false);
2057
+ assert.equal((await f.conversation.send(request)).duplicate, true);
2058
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
2059
+ });
2060
+
2061
+ test("Codex common steering uses the original local image input on its exact active turn", async t => {
2062
+ const f = await fixture(t, { attachments: { resolve: nativeAttachmentResolver } });
2063
+ await f.conversation.send({ messageId: "local-before", text: "wait" });
2064
+ const request = { messageId: "local-steer", text: "Inspect [Image #1] and [File #1]", attachmentIds: ["image", "file"], steer: true };
2065
+ await f.conversation.send(request);
2066
+ const state = await f.conversation.wait();
2067
+ assert.equal(state.error, "");
2068
+ assert.deepEqual((await f.trace()).find(row => row.method === "turn/steer").params.input, [
2069
+ { type: "text", text: request.text + nativeAttachmentManifest, text_elements: [] },
2070
+ { type: "localImage", path: nativeAttachmentFiles[0].path }
2071
+ ]);
2072
+ assert.deepEqual(state.conversationLog[1].user.attachments, nativeAttachmentFiles.map(nativeAttachmentReceipt));
2073
+ assert.equal(state.conversationLog[1].user.text, request.text);
2074
+ assert.equal((await f.conversation.send(request)).duplicate, true);
2075
+ assert.equal((await f.trace()).filter(row => row.method === "turn/start").length, 1);
2076
+ assert.equal((await f.trace()).filter(row => row.method === "turn/steer").length, 1);
2077
+ });
2078
+
2079
+ test("Codex local attachment context and current files form one ordered manifest", async t => {
2080
+ const f = await fixture(t, { attachments: { resolve: nativeAttachmentResolver } });
2081
+ await f.storage.write("conversation", transaction => transaction.appendMessage("000001", {
2082
+ role: "user", messageId: "old-image", text: "The original picture", at: new Date().toISOString(),
2083
+ attachments: [nativeAttachmentReceipt(nativeAttachmentFiles[0])],
2084
+ turnMetadata: { runtime: { engine: "api", segmentId: "previous", origin: "user", status: "complete" } }
2085
+ }));
2086
+ await f.conversation.send({ messageId: "new-file", text: "Inspect the data", attachmentIds: ["file"] });
2087
+ const state = await f.conversation.wait();
2088
+ assert.equal(state.error, "");
2089
+ const parts = (await f.trace()).find(row => row.method === "turn/start").params.input;
2090
+ assert.equal(parts[0].text.endsWith(nativeAttachmentManifest), true);
2091
+ assert.equal(parts[0].text.match(/Attached files:/gu).length, 1);
2092
+ assert.deepEqual(parts.slice(1), [{ type: "localImage", path: nativeAttachmentFiles[0].path }]);
2093
+ assert.deepEqual(state.conversationLog.at(-1).user.attachments, [nativeAttachmentReceipt(nativeAttachmentFiles[1])]);
2094
+ });
2095
+
2096
+ test("Codex retries retain their frozen local file manifest and cannot retarget authorized paths", async t => {
2097
+ let connectionReads = 0;
2098
+ let files = nativeAttachmentFiles;
2099
+ const f = await fixture(t, { configuration: { ...configuration, integrationId: "assistant" },
2100
+ attachments: { resolve: async () => ({ attachments: files.map(nativeAttachmentReceipt), localFiles: files }) },
2101
+ connections: { resolve: async () => { connectionReads++; throw new Error("Connection temporarily unavailable"); } }
2102
+ });
2103
+ const request = { messageId: "retry-local", text: "Inspect", attachmentIds: ["image", "file"] };
2104
+ await assert.rejects(f.conversation.send(request), /Connection temporarily unavailable/);
2105
+ const pending = (await f.storage.read("conversation", transaction => transaction.readMetadata())).runtime.request;
2106
+ assert.equal(pending.attachmentManifest, nativeAttachmentManifest);
2107
+ assert.equal(Object.hasOwn(pending, "localFiles"), false);
2108
+ assert.doesNotMatch(JSON.stringify((await f.conversation.read()).pendingRequest), /session\/artifacts|attachmentManifest|localFiles/);
2109
+ files = nativeAttachmentFiles.map(file => ({ ...file, path: file.path + ".changed" }));
2110
+ await assert.rejects(f.conversation.send(request), { code: "conversation_attachment_manifest_changed" });
2111
+ assert.equal(connectionReads, 1);
2112
+ assert.equal((await f.conversation.read()).conversationLog.length, 0);
2113
+ await assert.rejects(readFile(path.join(f.directory, "trace.jsonl")), { code: "ENOENT" });
2114
+ });
2115
+
2116
+ test("Codex goal controls reject local attachments before native work", async t => {
2117
+ const f = await fixture(t, { attachments: { resolve: nativeAttachmentResolver } });
2118
+ await assert.rejects(f.conversation.updateGoal({ action: "set", messageId: "local-goal", objective: "Inspect the files",
2119
+ attachmentIds: ["image", "file"], expectedSegmentId: (await f.conversation.read()).segmentId }), { code: "conversation_unsupported" });
2120
+ assert.equal((await f.conversation.read()).conversationLog.length, 0);
2121
+ await assert.rejects(readFile(path.join(f.directory, "trace.jsonl")), { code: "ENOENT" });
2122
+ });