@jskit-ai/assistant-core 0.1.199 → 0.1.201

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,2158 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import test from "node:test";
4
+ import { createMemoryConversationStorage } from "../src/server/conversation/memoryStorage.js";
5
+ import { createConversationTranscript } from "../src/server/conversation/transcript.js";
6
+ import { createConversationStreams } from "../src/server/conversation/streams.js";
7
+ import { sendCodexAppServerPrompt } from "../src/server/conversation/codexProvider.js";
8
+ import {
9
+ waitForCodexAppServerTurn,
10
+ createCodexAppServerRunOwner,
11
+ createCodexAppServerNotificationQueue,
12
+ codexAppServerAgentRunPatch,
13
+ codexAppServerPendingUserMessageOwnership,
14
+ codexAppServerTurnState,
15
+ CODEX_APP_SERVER_RUN_STATE
16
+ } from "../src/server/conversation/codexTurn.js";
17
+
18
+ // Original stable-outer-turn source assertions follow their native owners.
19
+ test("a new chat turn claims its client message id and preserves the stable outer identity", async () => {
20
+ const messageSource = await readFile(new URL(
21
+ "../src/server/conversation/codexMessageCommands.js", import.meta.url
22
+ ), "utf8");
23
+ const journalSource = await readFile(new URL(
24
+ "../src/server/conversation/codexRunJournal.js", import.meta.url
25
+ ), "utf8");
26
+
27
+ assert.match(messageSource, /claimCodexAppServerMessageStart\(runtime, sessionId, messageId\)/u);
28
+ assert.match(journalSource, /outerTurnId: normalizedOuterTurnId/u);
29
+ });
30
+
31
+ // The original public renewal tests retain the application retry, accepted
32
+ // handover, saved identity and structured acknowledgement assertions. These
33
+ // cases exercise the native wait sequence carried from that implementation.
34
+ function turn(id = "turn-1", status = "inProgress", text = "") {
35
+ return {
36
+ id,
37
+ status,
38
+ items: text ? [{ id: `answer-${id}`, type: "agentMessage", phase: "final_answer", text }] : []
39
+ };
40
+ }
41
+
42
+ function fixture(turns = []) {
43
+ const listeners = new Set();
44
+ const operations = [];
45
+ const provider = {
46
+ subscribe(listener) {
47
+ operations.push("subscribe");
48
+ listeners.add(listener);
49
+ return () => {
50
+ operations.push("unsubscribe");
51
+ listeners.delete(listener);
52
+ };
53
+ },
54
+ async readThread(threadId) {
55
+ assert.equal(threadId, "thread-1");
56
+ operations.push("read");
57
+ return { raw: { turns } };
58
+ }
59
+ };
60
+ return { provider, listeners, operations };
61
+ }
62
+
63
+ test("an already completed exact turn returns its saved answer without subscribing or rereading", async () => {
64
+ const f = fixture();
65
+ const result = await waitForCodexAppServerTurn(f.provider, " thread-1 ", turn("turn-1", "completed", "Approved handover."));
66
+ assert.equal(result.turnId, "turn-1");
67
+ assert.deepEqual(result, {
68
+ status: "completed",
69
+ text: "Approved handover.",
70
+ threadId: "thread-1",
71
+ turnId: "turn-1",
72
+ usage: null
73
+ });
74
+ assert.deepEqual(f.operations, []);
75
+ });
76
+
77
+ test("missing native thread or turn identity fails before observation", async () => {
78
+ for (const [threadId, nativeTurn, expectedTurnId] of [["", turn(), "turn-1"], [0, turn(), "turn-1"], [false, turn(), "turn-1"], ["thread-1", {}, ""]]) {
79
+ const f = fixture();
80
+ await assert.rejects(waitForCodexAppServerTurn(f.provider, threadId, nativeTurn), {
81
+ code: "codex_app_server_turn_identity_missing",
82
+ retryable: false,
83
+ details: { threadId: String(threadId || "").trim(), turnId: expectedTurnId, retryable: false }
84
+ });
85
+ assert.deepEqual(f.operations, []);
86
+ }
87
+ });
88
+
89
+ test("an already failed or interrupted exact turn retains its provider error and identity", async () => {
90
+ for (const status of ["failed", "interrupted"]) {
91
+ const f = fixture();
92
+ await assert.rejects(waitForCodexAppServerTurn(f.provider, "thread-1", {
93
+ ...turn("turn-1", status), error: { message: "Native failure detail." }
94
+ }), {
95
+ code: "codex_app_server_turn_failed",
96
+ message: "Native failure detail.",
97
+ retryable: true,
98
+ details: { status, threadId: "thread-1", turnId: "turn-1", retryable: true }
99
+ });
100
+ assert.deepEqual(f.operations, []);
101
+ }
102
+ });
103
+
104
+ test("observation subscribes before rereading and recovers only the exact completed turn", async () => {
105
+ const f = fixture([
106
+ turn("foreign-turn", "failed", "Unrelated response."),
107
+ turn("turn-1", "completed", "Approved handover."),
108
+ turn("later-turn", "completed", "A later response.")
109
+ ]);
110
+ const result = await waitForCodexAppServerTurn(f.provider, "thread-1", turn(), { timeoutMs: 1000 });
111
+ assert.equal(result.turnId, "turn-1");
112
+ assert.equal(result.text, "Approved handover.");
113
+ assert.deepEqual(f.operations, ["subscribe", "read", "read", "unsubscribe"]);
114
+ assert.equal(f.listeners.size, 0);
115
+ });
116
+
117
+ test("completed receipts without text recover their exact response through the subscribed watcher", async () => {
118
+ const f = fixture([turn("turn-1", "completed", "Approved handover.")]);
119
+ const result = await waitForCodexAppServerTurn(f.provider, "thread-1", turn("turn-1", "completed"), { timeoutMs: 1000 });
120
+ assert.equal(result.text, "Approved handover.");
121
+ assert.deepEqual(f.operations, ["subscribe", "read", "unsubscribe"]);
122
+ assert.equal(f.listeners.size, 0);
123
+ });
124
+
125
+ test("a failed exact reread uses the host error factory and releases the original watcher", async () => {
126
+ const f = fixture([{ ...turn("turn-1", "failed"), error: { message: "Native failure detail." } }]);
127
+ const expected = new Error("Application failure.");
128
+ const errors = [];
129
+ await assert.rejects(waitForCodexAppServerTurn(f.provider, "thread-1", turn(), {
130
+ timeoutMs: 1000,
131
+ createError(...args) { errors.push(args); return expected; }
132
+ }), error => error === expected);
133
+ assert.deepEqual(errors, [["failed", {
134
+ status: "failed", threadId: "thread-1", turnId: "turn-1"
135
+ }, "Native failure detail."]]);
136
+ assert.deepEqual(f.operations, ["subscribe", "read", "unsubscribe"]);
137
+ assert.equal(f.listeners.size, 0);
138
+ });
139
+
140
+ test("completion delivered during the latest read is retained by the already registered observer", async () => {
141
+ const f = fixture([turn("turn-1", "completed", "Approved handover.")]);
142
+ const readThread = f.provider.readThread;
143
+ let delivered = false;
144
+ f.provider.readThread = async threadId => {
145
+ if (!delivered) {
146
+ delivered = true;
147
+ assert.equal(f.listeners.size, 1);
148
+ for (const listener of [...f.listeners]) {
149
+ listener({ method: "turn/completed", params: { threadId, turn: turn("turn-1", "completed") } });
150
+ }
151
+ }
152
+ return readThread(threadId);
153
+ };
154
+ const result = await waitForCodexAppServerTurn(f.provider, "thread-1", turn(), { timeoutMs: 1000 });
155
+ assert.equal(result.turnId, "turn-1");
156
+ assert.equal(result.text, "Approved handover.");
157
+ assert.equal(f.operations[0], "subscribe");
158
+ assert.equal(f.operations.filter(operation => operation === "unsubscribe").length, 1);
159
+ assert.equal(f.listeners.size, 0);
160
+ });
161
+
162
+ test("a native wait deadline releases its subscription", async () => {
163
+ const f = fixture([turn()]);
164
+ await assert.rejects(waitForCodexAppServerTurn(f.provider, "thread-1", turn(), { timeoutMs: 1 }), {
165
+ message: "Timed out waiting for Codex app-server response."
166
+ });
167
+ assert.deepEqual(f.operations, ["subscribe", "read", "unsubscribe"]);
168
+ assert.equal(f.listeners.size, 0);
169
+ });
170
+
171
+ // The production public lifecycle cases remain unchanged. This small store
172
+ // fixture records the moved mutation boundary; public tests still prove the
173
+ // real lease, run file, receipt admission, publication and checkpoint behavior.
174
+ function runFixture(initial = {}) {
175
+ let run = structuredClone({ id: "codex_app_server", state: "active", providerStatus: "inProgress",
176
+ providerThreadId: "thread-1", providerTurnId: "turn-1", inputSource: "chat", outerTurnId: "request-1",
177
+ startedAt: "2026-09-30T01:00:00.000Z", updatedAt: "2026-09-30T01:00:00.000Z", events: [{ kind: "claimed" }], ...initial });
178
+ const operations = [];
179
+ const writes = [];
180
+ let locked = false;
181
+ const runtime = {
182
+ async getSession(sessionId) {
183
+ assert.equal(sessionId, "session-1");
184
+ assert.equal(locked, true, "The existing mutation lock owns each current-run read");
185
+ operations.push("read");
186
+ return { sessionId, agentRuns: [structuredClone(run)] };
187
+ },
188
+ store: {
189
+ async mutateSession(sessionId, operation) {
190
+ assert.equal(sessionId, "session-1");
191
+ assert.equal(locked, false);
192
+ locked = true;
193
+ operations.push("lock");
194
+ try { return await operation(); }
195
+ finally { locked = false; operations.push("unlock"); }
196
+ },
197
+ async writeAgentRunEvent(sessionId, runId, { event, patch }) {
198
+ assert.equal(locked, true);
199
+ assert.equal(sessionId, "session-1");
200
+ assert.equal(runId, "codex_app_server");
201
+ operations.push("write");
202
+ writes.push(structuredClone({ event, patch }));
203
+ run = { ...run, ...structuredClone(patch), events: [...run.events, structuredClone(event)] };
204
+ return structuredClone(run);
205
+ },
206
+ clearConversationStream(sessionId) {
207
+ assert.equal(locked, true);
208
+ assert.equal(sessionId, "session-1");
209
+ operations.push("clear-stream");
210
+ return { cleared: true };
211
+ },
212
+ async readAgentRun(sessionId, runId) {
213
+ assert.equal(sessionId, "session-1");
214
+ assert.equal(runId, "codex_app_server");
215
+ return structuredClone(run);
216
+ }
217
+ }
218
+ };
219
+ return { runtime, operations, writes, read: () => structuredClone(run),
220
+ replace: value => { run = structuredClone(value); } };
221
+ }
222
+
223
+ test("native run claims retain the original lock/read/write/read sequence and clear former goal ownership", async () => {
224
+ const f = runFixture({ state: "completed", providerGoalStatus: "active", providerGoalThreadId: "old-thread",
225
+ providerGoalUpdatedAt: "old-goal-time", pendingUserMessageClientIds: ["old-message"] });
226
+ const owner = createCodexAppServerRunOwner();
227
+ const result = await owner.claimTurnStart(f.runtime, " session-1 ", " request-2 ");
228
+ assert.equal(result.claimed, true);
229
+ const run = result.session.agentRuns[0];
230
+ assert.equal(run.state, CODEX_APP_SERVER_RUN_STATE.STARTING);
231
+ assert.equal(run.outerTurnId, "request-2");
232
+ assert.equal(run.inputSource, "chat");
233
+ assert.equal(run.providerGoalStatus, "");
234
+ assert.equal(run.providerGoalThreadId, "");
235
+ assert.equal(run.providerGoalUpdatedAt, "");
236
+ assert.deepEqual(run.pendingUserMessageClientIds, []);
237
+ assert.deepEqual(f.operations, ["lock", "read", "write", "read", "unlock"]);
238
+ assert.equal(f.writes[0].event.kind, "codex-app-server-turn-claimed");
239
+ assert.equal((await owner.claimTurnStart(f.runtime, "session-1", "request-3")).claimed, false);
240
+ assert.equal(f.writes.length, 1);
241
+ assert.equal(f.read().outerTurnId, "request-2");
242
+ });
243
+
244
+ test("the moved native run guard preserves Stop when an earlier provider read resolves", async () => {
245
+ const f = runFixture();
246
+ const owner = createCodexAppServerRunOwner();
247
+ const observedRun = f.read();
248
+ await owner.writeAgentRun(f.runtime, "session-1", { runState: CODEX_APP_SERVER_RUN_STATE.INTERRUPTED,
249
+ status: "interrupted", threadId: "thread-1", turnId: "turn-1", publishReason: "codex-app-server-turn-interrupted" });
250
+ const store = f.runtime.store;
251
+ const sessionId = "session-1";
252
+ // Same original assertions as the public pending-activity-read/Stop case.
253
+ assert.equal((await store.readAgentRun(sessionId, "codex_app_server")).state,
254
+ CODEX_APP_SERVER_RUN_STATE.INTERRUPTED);
255
+ const stopped = f.read();
256
+ const result = await owner.writeAgentRun(f.runtime, sessionId, { observedRun,
257
+ runState: CODEX_APP_SERVER_RUN_STATE.ACTIVE, status: "inProgress", threadId: "thread-1", turnId: "turn-1" });
258
+ assert.equal((await store.readAgentRun(sessionId, "codex_app_server")).state,
259
+ CODEX_APP_SERVER_RUN_STATE.INTERRUPTED);
260
+ assert.equal(result.wrote, false);
261
+ assert.deepEqual(f.read(), stopped);
262
+ assert.equal(f.writes.length, 1);
263
+ assert.deepEqual(f.operations, ["lock", "read", "write", "clear-stream", "read", "unlock", "lock", "read", "unlock"]);
264
+ });
265
+
266
+ test("native recovery requires the exact stopped revision and keeps its original start and outer owner", async () => {
267
+ for (const state of ["failed", "interrupted"]) {
268
+ const f = runFixture({ state, providerStatus: state });
269
+ const observedRun = f.read();
270
+ const result = await createCodexAppServerRunOwner().writeAgentRun(f.runtime, "session-1", { observedRun,
271
+ runState: CODEX_APP_SERVER_RUN_STATE.ACTIVE, status: "inProgress", threadId: "thread-1", turnId: "turn-1",
272
+ updatedAt: "2026-10-01T01:00:00.000Z" });
273
+ assert.equal(result.wrote, true);
274
+ assert.equal(result.runPatch.startedAt, observedRun.startedAt);
275
+ assert.equal(result.runPatch.outerTurnId, "request-1");
276
+ assert.equal(f.read().inputSource, "chat");
277
+ assert.equal(f.read().providerThreadId, "thread-1");
278
+ assert.equal(f.read().providerTurnId, "turn-1");
279
+ assert.equal(f.read().state, CODEX_APP_SERVER_RUN_STATE.ACTIVE);
280
+ assert.deepEqual(f.operations, ["lock", "read", "write", "read", "unlock"]);
281
+ }
282
+ });
283
+
284
+ test("durable processed results and observation loss prevent revival of an exact stopped native turn", async () => {
285
+ for (const barrier of ["durable", "memory", "observation_lost"]) {
286
+ const f = runFixture({ state: "interrupted", providerStatus: barrier === "observation_lost" ? barrier : "interrupted" });
287
+ const owner = createCodexAppServerRunOwner({ namespace: sessionId => `project:${sessionId}` });
288
+ if (barrier === "durable") {
289
+ const run = f.read();
290
+ run.events.push({ kind: "codex-app-server-result-processed", providerThreadId: "thread-1", providerTurnId: "turn-1" });
291
+ f.replace(run);
292
+ }
293
+ if (barrier === "memory") owner.processedTurns.add(owner.resultFinalizationKey("session-1", "thread-1", "turn-1"));
294
+ const observedRun = f.read();
295
+ const result = await owner.writeAgentRun(f.runtime, "session-1", { observedRun,
296
+ runState: CODEX_APP_SERVER_RUN_STATE.ACTIVE, status: "inProgress", threadId: "thread-1", turnId: "turn-1" });
297
+ assert.equal(result.wrote, false);
298
+ assert.deepEqual(f.read(), observedRun);
299
+ assert.deepEqual(f.writes, []);
300
+ assert.equal(owner.resultFinalizationKey("session-1", "thread-1", "turn-1"), "project:session-1:thread-1:turn-1");
301
+ }
302
+ });
303
+
304
+ test("native phase changes require the current exact turn and retain its active start", async () => {
305
+ const f = runFixture({ providerPhase: "reasoning" });
306
+ const owner = createCodexAppServerRunOwner();
307
+ for (const turnId of ["", "foreign-turn"]) {
308
+ assert.equal((await owner.writeAgentRun(f.runtime, "session-1", { phase: "compacting",
309
+ runState: CODEX_APP_SERVER_RUN_STATE.ACTIVE, status: "inProgress", threadId: "thread-1", turnId })).wrote, false);
310
+ }
311
+ const changed = await owner.writeAgentRun(f.runtime, "session-1", { phase: "compacting",
312
+ runState: CODEX_APP_SERVER_RUN_STATE.ACTIVE, status: "inProgress", threadId: "thread-1", turnId: "turn-1" });
313
+ assert.equal(changed.runPatch.providerPhase, "compacting");
314
+ assert.equal(changed.runPatch.startedAt, "2026-09-30T01:00:00.000Z");
315
+ assert.equal(f.writes.length, 1);
316
+ });
317
+
318
+ test("original pending ownership and terminal outer identity are preserved without weakening run-state errors", () => {
319
+ const run = { inputSource: "chat", pendingUserMessageClientIds: [0, false, " first ", "second"] };
320
+ assert.deepEqual(codexAppServerPendingUserMessageOwnership(run, "first"), { clientId: "first", inputSource: "chat" });
321
+ assert.equal(codexAppServerPendingUserMessageOwnership(run, "other"), null);
322
+ const patch = codexAppServerAgentRunPatch({ inputSource: "terminal", runState: "active",
323
+ session: { agentRuns: [{ id: "codex_app_server", outerTurnId: "old-owner", providerThreadId: "thread-1", providerTurnId: "old-turn" }] },
324
+ threadId: "thread-1", turnId: "turn-2", status: "inProgress" });
325
+ assert.equal(patch.outerTurnId, "codex:thread-1:turn-2");
326
+ assert.deepEqual(patch.pendingUserMessageClientIds, []);
327
+ const hostError = new Error("Existing application run-state error");
328
+ assert.throws(() => codexAppServerTurnState({ agentRuns: [{ id: "codex_app_server", state: "invalid" }] }, () => { throw hostError; }),
329
+ error => error === hostError);
330
+ });
331
+
332
+ test("native notification queues merge only waiting stream deltas and retain receipt order", async () => {
333
+ const owner = createCodexAppServerNotificationQueue();
334
+ const context = { sessionId: "session-1", sessionKey: "session-1", threadId: "thread-1", turnId: "turn-1" };
335
+ const started = Promise.withResolvers();
336
+ const release = Promise.withResolvers();
337
+ const output = [];
338
+ const stream = delta => ({ kind: "assistant_delta", itemId: "item-1", threadId: "thread-1", turnId: "turn-1", delta });
339
+ const deliver = async (sessionId, classification) => {
340
+ assert.equal(sessionId, "session-1");
341
+ output.push(classification);
342
+ if (output.length === 1) { started.resolve(); await release.promise; }
343
+ };
344
+ owner.stream(context, stream("First"), deliver);
345
+ await started.promise;
346
+ owner.stream(context, stream("Second"), deliver);
347
+ owner.stream(context, stream(" part"), deliver);
348
+ owner.run(context, () => { output.push({ receipt: "durable" }); });
349
+ owner.stream(context, stream("After receipt"), deliver);
350
+ assert.equal(output[0].delta, "First", "The running batch is immutable while its sink is pending");
351
+ const drained = owner.drain("session-1");
352
+ release.resolve();
353
+ await drained;
354
+ assert.deepEqual(output.map(value => value.delta || value.receipt), ["First", "Second part", "durable", "After receipt"]);
355
+ assert.equal(owner.tasks.size, 0);
356
+ assert.equal(owner.pendingStreams.size, 0);
357
+ });
358
+
359
+ test("native reasoning batches retain the original thousand-fragment commentary boundary", async () => {
360
+ const owner = createCodexAppServerNotificationQueue();
361
+ const context = { sessionId: "session-1", sessionKey: "session-1", threadId: "thread-1", turnId: "turn-1" };
362
+ const entered = Promise.withResolvers();
363
+ const release = Promise.withResolvers();
364
+ const writes = [];
365
+ const delivered = [];
366
+ const deliver = async (sessionId, threadId, notifications) => {
367
+ assert.equal(sessionId, "session-1");
368
+ assert.equal(threadId, "thread-1");
369
+ writes.push(notifications);
370
+ delivered.push(notifications.map(({ delta }) => delta).join(""));
371
+ if (writes.length === 1) { entered.resolve(); await release.promise; }
372
+ };
373
+ owner.reasoning(context, { delta: "Start " }, deliver);
374
+ await entered.promise;
375
+ for (let index = 0; index < 1_000; index++) owner.reasoning(context, { delta: "word " }, deliver);
376
+ owner.run(context, () => { delivered.push("Checking the files."); });
377
+ for (let index = 0; index < 1_000; index++) owner.reasoning(context, { delta: "next " }, deliver);
378
+ assert.equal(writes[0].length, 1, "Waiting fragments cannot mutate the batch already in its sink");
379
+ release.resolve();
380
+ await owner.drain("session-1");
381
+ assert.deepEqual(delivered, ["Start ", "word ".repeat(1_000), "Checking the files.", "next ".repeat(1_000)]);
382
+ assert.equal(writes.length, 3, "queued fragments share a write; commentary remains between the two reasoning items");
383
+ assert.equal(owner.pendingReasoning.size, 0);
384
+ });
385
+
386
+ test("native notification failure stops observation before reporting and preserves later queued work", async () => {
387
+ const output = [];
388
+ const failure = new Error("Original delivery failed");
389
+ const projectContext = { targetRoot: "/authorized/project" };
390
+ const owner = createCodexAppServerNotificationQueue({ namespace: id => `project:${id}`,
391
+ runInContext(context, operation) { assert.equal(context, projectContext); return operation(); },
392
+ reportError(error, context) { assert.equal(error, failure); assert.equal(context.method, "item/completed"); output.push("report"); }
393
+ });
394
+ const context = { sessionId: "session-1", projectContext, method: "item/completed",
395
+ provider: { failObservation(error) { assert.equal(error, failure); output.push("stop-observation"); } } };
396
+ owner.run(context, () => { throw failure; });
397
+ owner.run(context, () => { output.push("next"); });
398
+ await owner.drain("session-1");
399
+ assert.deepEqual(output, ["stop-observation", "report", "next"]);
400
+ assert.equal(owner.tasks.size, 0);
401
+ });
402
+
403
+ test("native notification draining follows same-scope successors while another scope remains independent", async () => {
404
+ const owner = createCodexAppServerNotificationQueue();
405
+ const entered = Promise.withResolvers();
406
+ const release = Promise.withResolvers();
407
+ const output = [];
408
+ owner.run({ sessionId: "first" }, async () => {
409
+ entered.resolve();
410
+ await release.promise;
411
+ output.push("first");
412
+ owner.run({ sessionId: "first" }, () => { output.push("successor"); });
413
+ });
414
+ await entered.promise;
415
+ owner.run({ sessionId: "second" }, () => { output.push("independent"); });
416
+ await owner.drain("second");
417
+ assert.deepEqual(output, ["independent"]);
418
+ const draining = owner.drain("first");
419
+ release.resolve();
420
+ await draining;
421
+ assert.deepEqual(output, ["independent", "first", "successor"]);
422
+ assert.equal(owner.tasks.size, 0);
423
+ });
424
+
425
+ test("closing prevents new native notification batches while previously admitted queue work drains", async () => {
426
+ let closing = false;
427
+ const owner = createCodexAppServerNotificationQueue({ isClosing: () => closing });
428
+ const context = { sessionId: "session-1", sessionKey: "session-1" };
429
+ const output = [];
430
+ owner.run(context, () => { output.push("accepted"); });
431
+ closing = true;
432
+ owner.run(context, () => { output.push("late-task"); });
433
+ owner.stream(context, { itemId: "item-1", turnId: "turn-1" }, () => { output.push("late-stream"); });
434
+ owner.reasoning(context, {}, () => { output.push("late-reasoning"); });
435
+ await owner.drain("session-1");
436
+ assert.deepEqual(output, ["accepted"]);
437
+ assert.equal(owner.tasks.size, 0);
438
+ assert.equal(owner.pendingStreams.size, 0);
439
+ assert.equal(owner.pendingReasoning.size, 0);
440
+ });
441
+
442
+ // Native output assertions are carried from the original public lifecycle cases.
443
+ // The existing shared transcript/stream implementations replace only the public
444
+ // filesystem fixture; public tests retain subscription, Stop and restart coverage.
445
+ async function outputFixture() {
446
+ const sessionId = "session-1";
447
+ const threadId = "thread-1";
448
+ const turnId = "turn-1";
449
+ const storage = createMemoryConversationStorage();
450
+ const transcript = createConversationTranscript({ storage });
451
+ const streams = createConversationStreams();
452
+ const f = {
453
+ sessionId, threadId, turnId, hydrations: 0, publications: [], pages: [], history: [],
454
+ run: { id: "codex_app_server", state: "active", providerStatus: "inProgress",
455
+ providerThreadId: threadId, providerTurnId: turnId, inputSource: "chat", outerTurnId: "request-1", events: [] },
456
+ onPublish: null,
457
+ historyError: null
458
+ };
459
+ let mutation = Promise.resolve();
460
+ const store = {
461
+ ...transcript,
462
+ async readAgentRun(id, runId) {
463
+ assert.equal(id, sessionId);
464
+ assert.equal(runId, "codex_app_server");
465
+ return structuredClone(f.run);
466
+ },
467
+ mutateSession(id, operation) {
468
+ assert.equal(id, sessionId);
469
+ const next = mutation.catch(() => null).then(operation);
470
+ mutation = next;
471
+ return next;
472
+ },
473
+ updateConversationStream: streams.update,
474
+ completeConversationStreamMessage: streams.complete,
475
+ clearConversationStream: streams.clear,
476
+ readConversationStream: streams.read
477
+ };
478
+ const runtime = { store, async getSession(id) {
479
+ assert.equal(id, sessionId);
480
+ return { sessionId, agentRuns: [structuredClone(f.run)] };
481
+ } };
482
+ const provider = {
483
+ async listThreadTurns(id, options) {
484
+ assert.equal(id, threadId);
485
+ f.pages.push(structuredClone(options));
486
+ if (f.historyError) throw f.historyError;
487
+ const offset = Number(options.cursor || 0);
488
+ return { data: structuredClone(f.history.slice(offset, offset + 1)),
489
+ nextCursor: offset + 1 < f.history.length ? String(offset + 1) : null };
490
+ },
491
+ async resumeThread() { throw new Error("History recovery must not resume native work"); },
492
+ async startTurn() { throw new Error("History reads must not resend the prompt"); }
493
+ };
494
+ f.store = store;
495
+ f.provider = provider;
496
+ f.createOwner = () => createCodexAppServerRunOwner({
497
+ createRuntime: async () => { f.hydrations += 1; return runtime; },
498
+ createStore: async id => { assert.equal(id, sessionId); return store; },
499
+ async acquireProvider(input) {
500
+ assert.equal(input.sessionId, sessionId);
501
+ assert.equal(input.runtime, runtime);
502
+ assert.equal(input.session.sessionId, sessionId);
503
+ return provider;
504
+ },
505
+ async publish(id, event) {
506
+ assert.equal(id, sessionId);
507
+ await f.onPublish?.(id, event);
508
+ f.publications.push(structuredClone(event));
509
+ }
510
+ });
511
+ f.owner = f.createOwner();
512
+ f.replies = async () => (await store.readConversationLog(sessionId)).filter(row => row.assistant);
513
+ f.final = (itemId, text) => f.owner.recordFinalAssistantResult({
514
+ sessionId, threadId, turnId, itemId, text, source: "notification"
515
+ });
516
+ await store.writeConversationUserMessage(sessionId, { messageId: "request-1", text: "Work" });
517
+ return f;
518
+ }
519
+
520
+ function outputItem(id, text, phase = "final_answer") {
521
+ return { id, type: "agentMessage", phase, text };
522
+ }
523
+
524
+ test("original output owner keeps chunk reads bounded and rejects output after Stop", async () => {
525
+ const f = await outputFixture();
526
+ const { sessionId, threadId, turnId, owner, store } = f;
527
+ for (const delta of ["Hello", " ", "world"]) {
528
+ await owner.writeStream(sessionId, { threadId, turnId, itemId: "stream-answer", role: "assistant", delta });
529
+ }
530
+ assert.equal(store.readConversationStream(sessionId).messages[0].text, "Hello world");
531
+ assert.equal(f.hydrations, 0, "text chunks must not hydrate the session runtime");
532
+ await owner.recordReasoningForSession(sessionId, threadId, [{ method: "item/reasoning/summaryTextDelta",
533
+ params: { threadId, turnId, itemId: "stream-reasoning", delta: "**Checking the answer**" } }]);
534
+ assert.equal(f.hydrations, 0, "reasoning chunks must not hydrate the session runtime");
535
+ assert.deepEqual((await store.readConversationLog(sessionId)).flatMap(row => row.thinking || []).map(message => message.text),
536
+ ["Checking the answer"]);
537
+ assert.equal((await store.readConversationLog(sessionId)).some(row => row.assistant), false);
538
+ const messageId = store.readConversationStream(sessionId).messages[0].messageId;
539
+ f.history = [{ id: turnId, status: "inProgress", items: [outputItem("stream-answer", "Hello world!")] }];
540
+ await f.final("stream-answer", "Hello world!");
541
+ assert.equal((await f.replies()).at(-1).assistant.messageId, messageId);
542
+ assert.deepEqual(store.readConversationStream(sessionId).messages, []);
543
+ await owner.writeStream(sessionId, { threadId, turnId, itemId: "stream-answer", role: "assistant", delta: "late" });
544
+ assert.deepEqual(store.readConversationStream(sessionId).messages, []);
545
+ f.run = { ...f.run, state: "interrupted", providerStatus: "interrupted" };
546
+ store.clearConversationStream(sessionId);
547
+ await owner.writeStream(sessionId, { threadId, turnId, itemId: "after-stop", role: "assistant", delta: "Wrong" });
548
+ assert.deepEqual(store.readConversationStream(sessionId).messages, []);
549
+ assert.equal((await store.readConversationLog(sessionId)).filter(row => row.assistant).length, 1);
550
+ });
551
+
552
+ test("original output owner retains readable reasoning channels and completed-item deduplication", async t => {
553
+ t.mock.timers.enable({ apis: ["Date"], now: Date.parse("2026-10-01T00:00:00Z") });
554
+ const f = await outputFixture();
555
+ const { owner, sessionId, threadId, turnId, store } = f;
556
+ const params = { threadId, turnId, itemId: "raw-thought", contentIndex: 0 };
557
+ const record = async notification => {
558
+ t.mock.timers.tick(1);
559
+ await owner.recordReasoningForSession(sessionId, threadId, [notification]);
560
+ };
561
+ await record({ method: "item/reasoning/summaryPartAdded", params: { ...params, summaryIndex: 0 } });
562
+ await record({ method: "item/reasoning/textDelta", params: { ...params, delta: "First " } });
563
+ await record({ method: "item/reasoning/textDelta", params: { ...params, delta: "step." } });
564
+ await record({ method: "item/reasoning/textDelta", params: { ...params, contentIndex: 1, delta: "Second step." } });
565
+ await record({ method: "item/reasoning/summaryTextDelta", params: { ...params, delta: "Duplicate summary" } });
566
+ await record({ method: "item/completed", params: {
567
+ threadId, turnId, item: { id: params.itemId, type: "reasoning", summary: [], content: ["First step.", "Second step."] }
568
+ } });
569
+ await owner.writeLiveProgress(sessionId, threadId, { method: "item/completed", params: {
570
+ threadId, turnId, item: outputItem("progress", "Checking the sources.", "commentary")
571
+ } });
572
+ await record({ method: "item/completed", params: {
573
+ threadId, turnId, item: { id: "completed-thought", type: "reasoning", summary: [], content: ["Final check."] }
574
+ } });
575
+ assert.deepEqual((await store.readConversationLog(sessionId)).flatMap(row => row.thinking || []).map(({ text }) => text),
576
+ ["First step.", "Second step.", "Final check."]);
577
+ assert.deepEqual((await store.readConversationLog(sessionId)).flatMap(row => row.commentary || []).map(({ text }) => text),
578
+ ["Checking the sources."]);
579
+ assert.equal(f.hydrations, 0);
580
+ });
581
+
582
+ test("original output owner publishes native history identities once through correction and restart", async () => {
583
+ const f = await outputFixture();
584
+ const { sessionId, threadId, turnId, store } = f;
585
+ const historyIds = { "msg-first": "item-4040", "msg-second": "item-4041" };
586
+ const items = [];
587
+ for (const [index, itemId] of Object.keys(historyIds).entries()) {
588
+ await f.owner.writeStream(sessionId, { threadId, turnId, itemId, role: "assistant", delta: "Same answer." });
589
+ const live = store.readConversationStream(sessionId);
590
+ items.push(outputItem(historyIds[itemId], "Same answer."));
591
+ f.history = [{ id: "successor", status: "inProgress", items: [outputItem("foreign", "Not this reply.")] },
592
+ { id: turnId, status: "completed", items }];
593
+ f.publications.length = 0;
594
+ await f.final(itemId, "Same answer.");
595
+ const saved = await f.replies();
596
+ assert.equal(saved.length, index + 1);
597
+ const savedMessageId = saved.at(-1).assistant.messageId;
598
+ assert.notEqual(savedMessageId, live.messages[0].messageId);
599
+ assert.deepEqual(store.readConversationStream(sessionId).messages, []);
600
+ const bundles = f.publications.filter(event => event.payload?.conversationLogPatch);
601
+ for (const event of bundles.filter(event => !event.payload.conversationStream.messages.length)) {
602
+ assert.equal(event.payload.conversationLogPatch.turn.assistant.messageId, savedMessageId,
603
+ "The stream must be retired with its saved reply, not an earlier reply");
604
+ }
605
+ }
606
+ const ids = (await f.replies()).map(row => row.assistant.messageId);
607
+ items[0] = outputItem("item-4040", "Corrected first answer.");
608
+ await f.final("msg-first", "Corrected first answer.");
609
+ assert.equal((await f.replies())[0].assistant.text, "Corrected first answer.");
610
+ f.owner = f.createOwner();
611
+ for (const [itemId, answer] of [["msg-first", "Corrected first answer."], ["msg-second", "Same answer."]]) {
612
+ await f.final(itemId, answer);
613
+ }
614
+ assert.deepEqual((await f.replies()).map(row => row.assistant.messageId), ids);
615
+ assert.deepEqual((await f.replies()).map(row => row.assistant.text), ["Corrected first answer.", "Same answer."]);
616
+ assert.ok(f.pages.length >= 2);
617
+ assert.deepEqual(f.pages.slice(0, 2), [
618
+ { limit: 1, itemsView: "full", sortDirection: "desc" },
619
+ { limit: 1, itemsView: "full", sortDirection: "desc", cursor: "1" }
620
+ ]);
621
+ });
622
+
623
+ test("original output owner carries proven live identity across distinct native history IDs", async () => {
624
+ const f = await outputFixture();
625
+ const { sessionId, threadId, turnId, store } = f;
626
+ await f.owner.writeStream(sessionId, { threadId, turnId, itemId: "live-progress", role: "commentary", delta: "Checking the source." });
627
+ const progress = store.readConversationStream(sessionId).messages[0];
628
+ await f.owner.writeLiveProgress(sessionId, threadId, { method: "item/completed", params: {
629
+ threadId, turnId, item: outputItem("live-progress", "Checking the source.", "commentary")
630
+ } });
631
+ const savedProgress = (await store.readConversationLog(sessionId))[0].commentary[0];
632
+ assert.notEqual(savedProgress.messageId, progress.messageId);
633
+ assert.equal(progress.outputId, progress.messageId);
634
+ assert.equal(savedProgress.outputId, progress.outputId);
635
+
636
+ await f.owner.writeStream(sessionId, { threadId, turnId, itemId: "live-answer", role: "assistant", delta: "The answer." });
637
+ const live = store.readConversationStream(sessionId).messages[0];
638
+ f.history = [{ id: turnId, status: "completed", items: [outputItem("history-answer", "The answer.")] }];
639
+ await f.final("live-answer", "The answer.");
640
+ const saved = (await f.replies())[0].assistant;
641
+ assert.notEqual(saved.messageId, live.messageId);
642
+ assert.equal(live.outputId, live.messageId);
643
+ assert.equal(saved.outputId, live.outputId);
644
+ assert.deepEqual(store.readConversationStream(sessionId).messages, []);
645
+
646
+ f.history[0].items[0] = outputItem("history-answer", "The corrected answer.");
647
+ await f.final("live-answer", "The corrected answer.");
648
+ f.owner = f.createOwner();
649
+ await f.final("live-answer", "The corrected answer.");
650
+ const reopened = (await f.replies())[0].assistant;
651
+ assert.equal(reopened.messageId, saved.messageId);
652
+ assert.equal(reopened.outputId, live.outputId);
653
+ assert.equal(reopened.text, "The corrected answer.");
654
+ });
655
+
656
+ for (const failure of ["storage", "realtime", "history"]) {
657
+ test(`original output owner preserves a final reply through ${failure} failure and exact recovery`, async () => {
658
+ const f = await outputFixture();
659
+ const { owner, sessionId, threadId, turnId, store } = f;
660
+ f.history = [{ id: turnId, status: "completed", items: [outputItem("recover-final", "The complete reply.")] }];
661
+ const write = store.writeConversationAssistantMessage;
662
+ let unavailable = true;
663
+ if (failure === "history") f.historyError = new Error("Native history temporarily unavailable");
664
+ store.writeConversationAssistantMessage = (...args) => {
665
+ if (unavailable && failure === "storage") throw new Error("Transcript unavailable");
666
+ return write(...args);
667
+ };
668
+ f.onPublish = (_id, event) => {
669
+ if (unavailable && failure === "realtime" && event.payload?.conversationLogPatch?.turn?.assistant) {
670
+ throw new Error("Realtime unavailable");
671
+ }
672
+ };
673
+ await assert.rejects(f.final("live-final", "The complete reply."));
674
+ assert.equal((await store.readConversationLog(sessionId)).filter(row => row.assistant).length,
675
+ failure === "realtime" ? 1 : 0);
676
+ assert.equal(owner.readFinalAssistantResult(sessionId, threadId, turnId), null);
677
+ unavailable = false;
678
+ f.historyError = null;
679
+ f.run = { ...f.run, state: "interrupted", providerStatus: "observation_lost" };
680
+ assert.equal((await owner.submitAssistantResult(sessionId, threadId, turnId, { recoverFromProvider: true })).processed, true);
681
+ assert.equal((await owner.submitAssistantResult(sessionId, threadId, turnId, { recoverFromProvider: true })).processed, true);
682
+ assert.deepEqual((await store.readConversationLog(sessionId)).flatMap(row => row.assistant ? [row.assistant.text] : []),
683
+ ["The complete reply."]);
684
+ assert.equal(f.publications.filter(event => event.payload?.conversationLogPatch?.turn?.assistant).length, 1);
685
+ });
686
+ }
687
+
688
+ async function settlementFixture() {
689
+ const f = await outputFixture();
690
+ f.operations = [];
691
+ f.checkpoints = [];
692
+ f.store.writeAgentRunEvent = async (sessionId, runId, { event, patch }) => {
693
+ assert.equal(sessionId, f.sessionId);
694
+ assert.equal(runId, "codex_app_server");
695
+ f.operations.push(`event:${event.kind}`);
696
+ f.run = { ...f.run, ...structuredClone(patch), events: [...f.run.events, structuredClone(event)] };
697
+ return structuredClone(f.run);
698
+ };
699
+ const runtime = { store: f.store, getSession: async sessionId => {
700
+ assert.equal(sessionId, f.sessionId);
701
+ return { sessionId, agentRuns: [structuredClone(f.run)] };
702
+ } };
703
+ f.owner = createCodexAppServerRunOwner({
704
+ namespace: sessionId => `project:${sessionId}`,
705
+ createRuntime: async () => runtime,
706
+ createStore: async () => f.store,
707
+ acquireProvider: async () => f.provider,
708
+ idlePublishPayload: { refreshOutputs: true },
709
+ async publish(sessionId, event) {
710
+ assert.equal(sessionId, f.sessionId);
711
+ await f.onPublish?.(sessionId, event);
712
+ f.operations.push(event.reason);
713
+ f.publications.push(structuredClone(event));
714
+ },
715
+ async checkpoint(sessionId, input) {
716
+ f.operations.push("checkpoint");
717
+ f.checkpoints.push({ sessionId, ...structuredClone(input) });
718
+ return { outcome: input.status, outerTurnId: f.run.outerTurnId };
719
+ }
720
+ });
721
+ return f;
722
+ }
723
+
724
+ test("original settlement shares finalization and persists its receipt before idle publication and checkpoint", async () => {
725
+ const f = await settlementFixture();
726
+ const { owner, sessionId, threadId, turnId, store } = f;
727
+ f.history = [{ id: turnId, status: "completed", items: [outputItem("final", "The complete reply.")] }];
728
+ const writing = Promise.withResolvers();
729
+ const release = Promise.withResolvers();
730
+ const write = store.writeConversationAssistantMessage;
731
+ store.writeConversationAssistantMessage = async (...args) => {
732
+ writing.resolve();
733
+ await release.promise;
734
+ return write(...args);
735
+ };
736
+ const first = owner.finalizeAssistantResult(sessionId, threadId, turnId, { recoverFromProvider: true });
737
+ await writing.promise;
738
+ const second = owner.finalizeAssistantResult(sessionId, threadId, turnId, { recoverFromProvider: true });
739
+ assert.equal(owner.resultFinalizations.size, 1);
740
+ release.resolve();
741
+ assert.deepEqual(await first, { ok: true, processed: true, reason: "assistant_response" });
742
+ assert.deepEqual(await second, await first);
743
+ assert.deepEqual(f.operations, ["assistant-response-bundle", "event:codex-app-server-result-processed",
744
+ "event:codex-app-server-turn-idle", "codex-app-server-turn-idle", "checkpoint"]);
745
+ assert.equal(f.run.events.filter(event => event.kind === "codex-app-server-result-processed").length, 1);
746
+ assert.equal(f.run.state, "completed");
747
+ assert.equal(f.run.outerTurnId, "request-1");
748
+ assert.equal(f.run.inputSource, "chat");
749
+ assert.equal((await f.replies()).length, 1);
750
+ assert.deepEqual(f.checkpoints, [{ sessionId, status: "completed", turnOutcome: "", threadId, turnId }]);
751
+ const idle = f.publications.find(event => event.reason === "codex-app-server-turn-idle");
752
+ assert.equal(idle.payload.refreshOutputs, true);
753
+ assert.equal(idle.payload.agentSession.turn.outerTurnId, "request-1");
754
+ assert.equal(idle.payload.agentSession.turn.active, false);
755
+ assert.equal(owner.resultFinalizations.size, 0);
756
+ assert.equal(owner.processedTurns.size, 0);
757
+ assert.deepEqual(await owner.finalizeAssistantResult(sessionId, threadId, turnId),
758
+ { ok: true, processed: true, reason: "already_finalized" });
759
+ assert.equal(f.checkpoints.length, 1);
760
+ });
761
+
762
+ test("original settlement uses durable processed-result evidence after restart without rereading or republishing output", async () => {
763
+ const f = await settlementFixture();
764
+ const { owner, sessionId, threadId, turnId, store } = f;
765
+ await store.writeConversationAssistantMessage(sessionId, { messageId: "already-saved", text: "Saved before restart." });
766
+ f.run = { ...f.run, state: "finalizing", providerStatus: "completed", events: [
767
+ { kind: "codex-app-server-result-processed", providerThreadId: threadId, providerTurnId: turnId,
768
+ resultReason: "assistant_response" }
769
+ ] };
770
+ f.historyError = new Error("Finalization must use its durable processed receipt");
771
+ assert.deepEqual(await owner.finalizeAssistantResult(sessionId, threadId, turnId, { recoverFromProvider: true }),
772
+ { ok: true, processed: true, reason: "assistant_response" });
773
+ assert.deepEqual((await f.replies()).map(row => row.assistant.text), ["Saved before restart."]);
774
+ assert.equal(f.pages.length, 0);
775
+ assert.equal(f.publications.filter(event => event.reason === "assistant-response-bundle").length, 0);
776
+ assert.equal(f.run.events.filter(event => event.kind === "codex-app-server-result-processed").length, 1);
777
+ assert.equal(f.run.state, "completed");
778
+ assert.equal(f.checkpoints.length, 1);
779
+ });
780
+
781
+ test("original settlement rereads the native owner after output and cannot release an adopted successor", async () => {
782
+ const f = await settlementFixture();
783
+ const { owner, sessionId, threadId, turnId } = f;
784
+ f.history = [{ id: turnId, status: "completed", items: [outputItem("final", "Predecessor reply.")] }];
785
+ const publishing = Promise.withResolvers();
786
+ const release = Promise.withResolvers();
787
+ f.onPublish = async (_sessionId, event) => {
788
+ if (event.reason !== "assistant-response-bundle") return;
789
+ publishing.resolve();
790
+ await release.promise;
791
+ };
792
+ const pending = owner.finalizeAssistantResult(sessionId, threadId, turnId, { recoverFromProvider: true });
793
+ await publishing.promise;
794
+ f.run = { ...f.run, providerTurnId: "successor", state: "active", providerStatus: "inProgress" };
795
+ release.resolve();
796
+ assert.deepEqual(await pending, { ok: true, processed: true, reason: "stale_turn_state" });
797
+ assert.equal(f.run.providerTurnId, "successor");
798
+ assert.equal(f.run.state, "active");
799
+ assert.equal(f.run.outerTurnId, "request-1");
800
+ assert.deepEqual(f.checkpoints, []);
801
+ assert.equal(f.publications.some(event => event.reason === "codex-app-server-turn-idle"), false);
802
+ assert.deepEqual((await f.replies()).map(row => row.assistant.text), ["Predecessor reply."]);
803
+ assert.equal(owner.processedTurns.size, 0);
804
+ assert.equal(owner.finalizedTurns.has(owner.resultFinalizationKey(sessionId, threadId, turnId)), true);
805
+ });
806
+
807
+ test("original settlement clears the same recovery timers and skips stale idle checkpoints", async t => {
808
+ const f = await settlementFixture();
809
+ const { owner, sessionId, threadId, turnId } = f;
810
+ const timers = [];
811
+ const timer = () => { const value = setTimeout(() => {}, 60_000); value.unref(); timers.push(value); return value; };
812
+ t.after(() => timers.forEach(clearTimeout));
813
+ owner.activeTimers.set(`project:${sessionId}`, timer());
814
+ owner.activeTimers.set("project:other", timer());
815
+ owner.finalizingTimers.set(owner.resultFinalizationKey(sessionId, threadId, turnId), timer());
816
+ owner.finalizingTimers.set(owner.resultFinalizationKey(sessionId, threadId, "older"), timer());
817
+ owner.finalizingTimers.set(owner.resultFinalizationKey("other", threadId, turnId), timer());
818
+ f.run = { ...f.run, state: "interrupted", providerStatus: "interrupted" };
819
+ const result = await owner.markTurnIdle(sessionId, { threadId, turnId, status: "completed" });
820
+ assert.equal(result.processed, false);
821
+ assert.equal(result.reason, "stale_terminal_turn_state");
822
+ assert.deepEqual(f.checkpoints, []);
823
+ assert.deepEqual(f.publications, []);
824
+ assert.equal(owner.activeTimers.has(`project:${sessionId}`), false);
825
+ assert.equal(owner.finalizingTimers.has(owner.resultFinalizationKey(sessionId, threadId, turnId)), false);
826
+ owner.clearSessionRecoveryTimers(sessionId);
827
+ assert.deepEqual([...owner.activeTimers.keys()], ["project:other"]);
828
+ assert.deepEqual([...owner.finalizingTimers.keys()], [owner.resultFinalizationKey("other", threadId, turnId)]);
829
+ });
830
+
831
+ async function receiptFixture(options = {}) {
832
+ const f = await settlementFixture();
833
+ f.metadata = new Map();
834
+ f.listeners = new Set();
835
+ f.observationFailures = [];
836
+ f.generation = 1;
837
+ f.subscriptions = 0;
838
+ f.unsubscriptions = 0;
839
+ f.store.readMetadataValue = async (sessionId, key) => {
840
+ assert.equal(sessionId, f.sessionId);
841
+ return f.metadata.get(key) ?? null;
842
+ };
843
+ const writeUser = f.store.writeConversationUserMessage;
844
+ f.store.writeConversationUserMessage = async (...args) => {
845
+ const written = await writeUser(...args);
846
+ f.operations.push("authored-message");
847
+ return written;
848
+ };
849
+ Object.assign(f.provider, {
850
+ currentConnectionGeneration: () => f.generation,
851
+ failObservation(error) { f.observationFailures.push(error); },
852
+ subscribe(listener) {
853
+ f.subscriptions += 1;
854
+ f.listeners.add(listener);
855
+ return () => { f.unsubscriptions += 1; f.listeners.delete(listener); };
856
+ },
857
+ async readThreadStatus() {
858
+ return { raw: { status: f.run?.providerStatus || "idle" } };
859
+ }
860
+ });
861
+ f.runtime = { store: f.store, getSession: async sessionId => {
862
+ assert.equal(sessionId, f.sessionId);
863
+ return { sessionId, agentRuns: f.run ? [structuredClone(f.run)] : [] };
864
+ } };
865
+ f.owner = createCodexAppServerRunOwner({
866
+ namespace: sessionId => `project:${sessionId}`,
867
+ createRuntime: async () => f.runtime,
868
+ createStore: async () => f.store,
869
+ acquireProvider: async () => f.provider,
870
+ deliveryStateMetadataKey: "authorized-delivery",
871
+ async publish(sessionId, event) {
872
+ assert.equal(sessionId, f.sessionId);
873
+ await f.onPublish?.(sessionId, event);
874
+ f.operations.push(event.reason);
875
+ f.publications.push(structuredClone(event));
876
+ },
877
+ ...options
878
+ });
879
+ f.emit = notification => { for (const listener of f.listeners) listener(notification); };
880
+ f.observe = () => f.owner.subscribeEvents(f.sessionId, f.provider, f.threadId, { providerKey: "account-scope" });
881
+ f.drain = () => f.owner.notificationQueue.drain(f.sessionId);
882
+ f.receipt = (messageId, text = "Expanded private context") => ({
883
+ method: "item/completed", params: {
884
+ threadId: f.threadId, turnId: f.turnId,
885
+ item: { type: "userMessage", id: `receipt-${messageId}`, clientId: messageId,
886
+ content: [{ type: "text", text }] }
887
+ }
888
+ });
889
+ return f;
890
+ }
891
+
892
+ test("original receipt owner persists authored metadata before releasing the receipt and queued answer", async () => {
893
+ const decorating = Promise.withResolvers();
894
+ const release = Promise.withResolvers();
895
+ const actorContext = { id: "actor-1" };
896
+ const metadataCalls = [];
897
+ const f = await receiptFixture({ messageMetadata: {
898
+ async actor(context) {
899
+ assert.equal(context, actorContext);
900
+ metadataCalls.push("actor");
901
+ return { actorId: context.id, actorDisplayName: "Actor" };
902
+ },
903
+ async delivered(store, sessionId, value) {
904
+ assert.equal(store, f.store);
905
+ assert.equal(sessionId, f.sessionId);
906
+ metadataCalls.push("delivered");
907
+ decorating.resolve();
908
+ await release.promise;
909
+ return { ...value, engineId: "codex" };
910
+ }
911
+ } });
912
+ const messageId = "authored-request";
913
+ f.run = { ...f.run, state: "starting", providerTurnId: "", pendingUserMessageClientIds: [messageId] };
914
+ const receipt = Promise.withResolvers();
915
+ let received = false;
916
+ receipt.promise.then(() => { received = true; f.operations.push("receipt"); });
917
+ f.owner.pendingUserMessages.set(`project:${f.sessionId}\0${messageId}`, {
918
+ attachments: [{ id: "attachment-1", name: "Diagram" }], actorContext,
919
+ turnMetadata: { actorId: "saved-actor", label: "authored" }, receipt,
920
+ text: "Review the diagram."
921
+ });
922
+ f.observe();
923
+ f.emit(f.receipt(messageId));
924
+ f.emit({ method: "item/completed", params: { threadId: f.threadId, turnId: f.turnId,
925
+ item: outputItem("early-commentary", "I am reviewing the diagram.", "commentary") } });
926
+ await decorating.promise;
927
+ assert.equal(received, false);
928
+ assert.equal(await f.store.conversationMessageIdExists(f.sessionId, messageId), false);
929
+ assert.equal(f.publications.some(event => event.payload?.conversationLogPatch?.turn?.commentary), false);
930
+ release.resolve();
931
+ await f.drain();
932
+ assert.deepEqual(await receipt.promise, { id: f.turnId });
933
+ const turns = await f.store.readConversationLog(f.sessionId);
934
+ const authored = turns.find(row => row.user?.messageId === messageId);
935
+ assert.equal(authored.user.text, "Review the diagram.");
936
+ assert.equal(authored.metadata.actorId, "saved-actor");
937
+ assert.equal(authored.metadata.actorDisplayName, "Actor");
938
+ assert.equal(authored.metadata.engineId, "codex");
939
+ assert.equal(authored.metadata.label, "authored");
940
+ assert.deepEqual(authored.user.attachments, [{ id: "attachment-1", name: "Diagram" }]);
941
+ assert.equal(JSON.stringify(turns).includes("Expanded private context"), false);
942
+ assert.deepEqual(turns.flatMap(row => row.commentary || []).map(value => value.text), ["I am reviewing the diagram."]);
943
+ assert.deepEqual(metadataCalls, ["actor", "delivered"]);
944
+ assert.deepEqual(f.run.pendingUserMessageClientIds, []);
945
+ assert.ok(f.operations.indexOf("authored-message") < f.operations.indexOf("event:codex-app-server-user-message-consumed"));
946
+ assert.ok(f.operations.indexOf("event:codex-app-server-user-message-consumed") < f.operations.indexOf("receipt"));
947
+ assert.equal(f.observationFailures.length, 0);
948
+ });
949
+
950
+ test("original receipt owner recovers the exact saved authored request once after restart", async () => {
951
+ const f = await receiptFixture();
952
+ const messageId = "saved-request";
953
+ f.run = { ...f.run, state: "starting", providerTurnId: "", pendingUserMessageClientIds: [] };
954
+ f.metadata.set("authorized-delivery", JSON.stringify({ engines: { codex: { pending: {
955
+ messageId, threadId: f.threadId,
956
+ message: "Private prepared prompt", displayMessage: "Continue the saved request.",
957
+ displayAttachments: [{ id: "saved-file", name: "Notes" }],
958
+ turnMetadata: { actorId: "original-author", label: "frozen" }
959
+ } } } }));
960
+ f.observe();
961
+ f.emit(f.receipt(messageId));
962
+ await f.drain();
963
+ f.emit(f.receipt(messageId));
964
+ await f.drain();
965
+ const rows = (await f.store.readConversationLog(f.sessionId)).filter(row => row.user?.messageId === messageId);
966
+ assert.equal(rows.length, 1);
967
+ assert.equal(rows[0].user.text, "Continue the saved request.");
968
+ assert.deepEqual(rows[0].user.attachments, [{ id: "saved-file", name: "Notes" }]);
969
+ assert.equal(rows[0].metadata.actorId, "original-author");
970
+ assert.equal(rows[0].metadata.label, "frozen");
971
+ assert.equal(JSON.stringify(rows).includes("Private prepared prompt"), false);
972
+ assert.equal(f.operations.filter(value => value === "authored-message").length, 1);
973
+ assert.equal(f.run.providerTurnId, f.turnId);
974
+ assert.equal(f.run.inputSource, "chat");
975
+ assert.deepEqual(f.run.pendingUserMessageClientIds, []);
976
+ assert.equal(f.observationFailures.length, 0);
977
+ });
978
+
979
+ test("original receipt owner keeps successor ownership through goal completion and ignores the completed predecessor", async () => {
980
+ const f = await receiptFixture();
981
+ const predecessor = f.turnId;
982
+ f.run = { ...f.run, state: "finalizing", providerStatus: "completed",
983
+ providerGoalStatus: "active", providerGoalThreadId: f.threadId };
984
+ f.observe();
985
+ f.emit({ method: "turn/started", params: { threadId: f.threadId, turn: { id: "successor", status: "inProgress" } } });
986
+ await f.drain();
987
+ f.emit({ method: "turn/started", params: { threadId: f.threadId, turn: { id: predecessor, status: "inProgress" } } });
988
+ await f.drain();
989
+ assert.equal(f.run.providerTurnId, "successor");
990
+ assert.equal(f.run.outerTurnId, "request-1");
991
+ assert.equal(f.run.inputSource, "chat");
992
+ assert.equal(f.run.events.filter(event => event.kind === "codex-app-server-turn-continued").length, 1);
993
+ f.run.providerStatus = "completed";
994
+ f.history = [{ id: "successor", status: "completed", items: [outputItem("goal-reply", "The goal reply.")] }];
995
+ f.emit({ method: "turn/completed", params: { threadId: f.threadId, turn: { id: "successor", status: "completed" } } });
996
+ await f.drain();
997
+ assert.equal(f.run.state, "finalizing");
998
+ assert.equal(f.owner.finalizingTimers.size, 0);
999
+ f.emit({ method: "thread/goal/cleared", params: { threadId: "another-thread" } });
1000
+ await f.drain();
1001
+ assert.equal(f.run.providerGoalStatus, "active");
1002
+ f.emit({ method: "thread/goal/cleared", params: { threadId: f.threadId } });
1003
+ await f.drain();
1004
+ assert.equal(f.run.state, "completed");
1005
+ assert.equal(f.run.providerTurnId, "successor");
1006
+ assert.equal(f.run.outerTurnId, "request-1");
1007
+ assert.equal(f.run.events.filter(event => event.kind === "codex-app-server-result-processed").length, 1);
1008
+ assert.deepEqual((await f.replies()).map(row => row.assistant.text), ["The goal reply."]);
1009
+ assert.equal(f.observationFailures.length, 0);
1010
+ });
1011
+
1012
+ test("original receipt owner renews only stale subscriptions and attaches an idle binding without inventing a run", async () => {
1013
+ const f = await receiptFixture();
1014
+ f.run = null;
1015
+ assert.deepEqual(f.observe(), { ok: true, status: "subscribed" });
1016
+ await f.drain();
1017
+ assert.equal(f.run, null);
1018
+ assert.deepEqual(f.operations, []);
1019
+ assert.deepEqual(f.observe(), { ok: true, status: "alreadySubscribed" });
1020
+ assert.equal(f.subscriptions, 1);
1021
+ f.generation = 2;
1022
+ assert.deepEqual(f.observe(), { ok: true, status: "resubscribed" });
1023
+ await f.drain();
1024
+ assert.equal(f.subscriptions, 2);
1025
+ assert.equal(f.unsubscriptions, 1);
1026
+ assert.equal(f.listeners.size, 1);
1027
+ assert.equal(f.run, null);
1028
+ const failure = new Error("Subscription unavailable");
1029
+ f.provider.subscribe = () => { throw failure; };
1030
+ assert.throws(() => f.owner.subscribeEvents(f.sessionId, f.provider, f.threadId, { providerKey: "other-scope" }),
1031
+ error => error === failure);
1032
+ await f.drain();
1033
+ assert.deepEqual(f.observationFailures, [failure]);
1034
+ assert.equal(f.owner.eventSubscriptions.has(`other-scope:${f.threadId}`), false);
1035
+ });
1036
+
1037
+ test("original receipt owner cannot undo Stop with a provider observation started before its durable revision", async () => {
1038
+ const f = await receiptFixture();
1039
+ const reading = Promise.withResolvers();
1040
+ const observed = Promise.withResolvers();
1041
+ f.provider.readThreadStatus = async () => {
1042
+ reading.resolve();
1043
+ return observed.promise;
1044
+ };
1045
+ const pending = f.owner.reconcileThreadStatus(f.sessionId, f.provider, f.threadId);
1046
+ await reading.promise;
1047
+ f.run = { ...f.run, state: "interrupted", providerStatus: "interrupted", events: [
1048
+ ...f.run.events, { kind: "user-stop", providerThreadId: f.threadId, providerTurnId: f.turnId }
1049
+ ] };
1050
+ observed.resolve({ raw: { status: "active", turnId: f.turnId } });
1051
+ await pending;
1052
+ assert.equal(f.run.state, "interrupted");
1053
+ assert.equal(f.run.providerStatus, "interrupted");
1054
+ assert.equal(f.run.providerTurnId, f.turnId);
1055
+ assert.equal(f.run.events.at(-1).kind, "user-stop");
1056
+ assert.equal(f.publications.some(event => event.payload?.agentRun?.state === "active"), false);
1057
+ });
1058
+
1059
+ async function goalCommandContext(f, threadId = f.threadId) {
1060
+ return { runtime: f.runtime, session: await f.runtime.getSession(f.sessionId),
1061
+ provider: f.provider, threadId, providerKey: "account-scope" };
1062
+ }
1063
+
1064
+ test("original goal command prepares the first conversation and subscribes before native activation", async () => {
1065
+ const f = await receiptFixture();
1066
+ const calls = [];
1067
+ f.run = null;
1068
+ const writeRun = f.store.writeAgentRunEvent;
1069
+ f.store.writeAgentRunEvent = (...args) => {
1070
+ f.run ||= { id: "codex_app_server", events: [] };
1071
+ return writeRun(...args);
1072
+ };
1073
+ f.provider.readGoal = async threadId => {
1074
+ assert.equal(threadId, f.threadId);
1075
+ calls.push("read-goal");
1076
+ return { goal: null };
1077
+ };
1078
+ f.provider.setGoal = async (threadId, input) => {
1079
+ assert.equal(f.listeners.size, 1, "A goal needs an observer before activation");
1080
+ calls.push("set-goal");
1081
+ return { goal: { ...input, threadId, status: "active", createdAt: 10 } };
1082
+ };
1083
+ const result = await f.owner.updateGoal(f.sessionId,
1084
+ { action: "set", threadId: "", objective: "Finish the fixture", tokenBudget: 5000 }, {
1085
+ context: await goalCommandContext(f, ""),
1086
+ async prepareConversation() {
1087
+ calls.push("prepare");
1088
+ return goalCommandContext(f);
1089
+ }
1090
+ });
1091
+ await f.drain();
1092
+ assert.equal(result.ok, true, JSON.stringify(result));
1093
+ assert.equal(result.goal.objective, "Finish the fixture");
1094
+ assert.equal(result.goal.tokenBudget, 5000);
1095
+ assert.equal(result.threadId, f.threadId);
1096
+ assert.deepEqual(calls, ["prepare", "read-goal", "set-goal"]);
1097
+ assert.equal(f.run.providerGoalStatus, "active");
1098
+ assert.equal(f.run.state, "completed", "The native scheduler has not started a provider turn");
1099
+ assert.equal(Boolean(f.run.providerTurnId), false);
1100
+ assert.equal(f.subscriptions, 1);
1101
+ assert.equal(f.observationFailures.length, 0);
1102
+ });
1103
+
1104
+ test("original goal command rejects stale identity and pauses or resumes without interrupting its turn", async () => {
1105
+ const f = await receiptFixture();
1106
+ const goal = { threadId: f.threadId, status: "active", objective: "Finish fixture", createdAt: 10, tokensUsed: 99 };
1107
+ const calls = [];
1108
+ f.provider.readGoal = async () => ({ goal: { ...goal } });
1109
+ f.provider.setGoalStatus = async (threadId, status) => {
1110
+ assert.equal(threadId, goal.threadId);
1111
+ assert.equal(f.listeners.size, 1);
1112
+ calls.push(status);
1113
+ goal.status = status;
1114
+ return { goal: { ...goal } };
1115
+ };
1116
+ f.provider.interruptTurn = async () => assert.fail("Pausing a goal must not interrupt its turn");
1117
+ f.provider.resumeThread = async () => assert.fail("Ordinary goal Resume must keep its current conversation");
1118
+ const input = { action: "pause", threadId: goal.threadId, objective: goal.objective, createdAt: goal.createdAt };
1119
+ const options = { context: await goalCommandContext(f) };
1120
+ for (const stale of [{ threadId: "another-thread" }, { objective: "older goal" }, { createdAt: 9 }]) {
1121
+ assert.equal((await f.owner.updateGoal(f.sessionId, { ...input, ...stale }, options)).ok, false);
1122
+ }
1123
+ assert.deepEqual(calls, []);
1124
+ const paused = await f.owner.updateGoal(f.sessionId, input, options);
1125
+ assert.equal(paused.ok, true, JSON.stringify(paused));
1126
+ assert.deepEqual(calls, ["paused"]);
1127
+ assert.equal(f.run.state, "active");
1128
+ const resumed = await f.owner.updateGoal(f.sessionId, { ...input, action: "resume" }, options);
1129
+ assert.equal(resumed.ok, true, JSON.stringify(resumed));
1130
+ assert.equal(resumed.goal.tokensUsed, 99);
1131
+ assert.deepEqual(calls, ["paused", "active"]);
1132
+ assert.equal(f.subscriptions, 1, "Goal controls added another listener for the same provider");
1133
+ for (const status of ["complete", "budgetLimited"]) {
1134
+ goal.status = status;
1135
+ assert.equal((await f.owner.updateGoal(f.sessionId, { ...input, action: "resume" }, options)).ok, false);
1136
+ assert.equal((await f.owner.updateGoal(f.sessionId, input, options)).ok, false);
1137
+ }
1138
+ await f.drain();
1139
+ assert.deepEqual(calls, ["paused", "active"]);
1140
+ assert.equal(f.observationFailures.length, 0);
1141
+ });
1142
+
1143
+ test("original goal command recovers saved output and clears the stopped barrier only for exact explicit Resume", async () => {
1144
+ const f = await receiptFixture();
1145
+ const goal = { threadId: f.threadId, status: "paused", objective: "Finish fixture", createdAt: 10, tokensUsed: 99 };
1146
+ f.run = { ...f.run, active: false, state: "interrupted", providerStatus: "observation_lost" };
1147
+ f.history = [{ id: f.turnId, status: "completed", items: [outputItem("before-loss", "Saved before observation was lost.")] }];
1148
+ f.provider.readGoal = async () => ({ goal: { ...goal } });
1149
+ const resumeOptions = { model: "authorized-model", cwd: "/authorized/project" };
1150
+ const context = await goalCommandContext(f);
1151
+ Object.defineProperty(context, "resumeOptions", { get() {
1152
+ f.operations.push("resume-options");
1153
+ assert.equal(f.run.events.at(-1).kind, "codex-observation-explicit-resume");
1154
+ return resumeOptions;
1155
+ } });
1156
+ f.provider.resumeThread = async (threadId, options) => {
1157
+ assert.equal(threadId, f.threadId);
1158
+ assert.equal(options, resumeOptions);
1159
+ assert.equal(f.listeners.size, 1, "Resume requires its observer first");
1160
+ assert.notEqual(f.run.providerStatus, "observation_lost");
1161
+ f.operations.push("resume-thread");
1162
+ };
1163
+ f.provider.setGoalStatus = async (_threadId, status) => {
1164
+ f.operations.push("set-goal-status");
1165
+ assert.equal(f.run.providerStatus, "interrupted");
1166
+ goal.status = status;
1167
+ return { goal: { ...goal } };
1168
+ };
1169
+ const result = await f.owner.updateGoal(f.sessionId,
1170
+ { action: "resume", threadId: f.threadId, objective: goal.objective, createdAt: goal.createdAt }, { context });
1171
+ await f.drain();
1172
+ assert.equal(result.ok, true, JSON.stringify(result));
1173
+ assert.equal(result.goal.status, "active");
1174
+ assert.equal(result.goal.tokensUsed, 99);
1175
+ assert.deepEqual((await f.replies()).map(row => row.assistant.text), ["Saved before observation was lost."]);
1176
+ const ordered = ["assistant-response-bundle", "event:codex-observation-explicit-resume",
1177
+ "resume-options", "resume-thread", "set-goal-status"].map(value => f.operations.indexOf(value));
1178
+ assert.ok(ordered.every(index => index >= 0));
1179
+ assert.deepEqual(ordered, [...ordered].sort((a, b) => a - b));
1180
+ assert.equal(f.run.events.filter(event => event.kind === "codex-observation-explicit-resume").length, 1);
1181
+ assert.equal(f.run.providerThreadId, f.threadId);
1182
+ assert.equal(f.observationFailures.length, 0);
1183
+ });
1184
+
1185
+ test("original goal command preserves a failed cancellation for retry without resuming or interrupting", async () => {
1186
+ const f = await receiptFixture();
1187
+ const goal = { threadId: f.threadId, status: "blocked", objective: "Finish fixture", createdAt: 10 };
1188
+ f.provider.readGoal = async () => ({ goal });
1189
+ f.provider.clearGoal = async () => { throw new Error("Codex unavailable"); };
1190
+ f.provider.resumeThread = async () => assert.fail("Cancel must not resume the conversation");
1191
+ f.provider.interruptTurn = async () => assert.fail("Cancel must preserve the current ordinary turn");
1192
+ const before = structuredClone(f.run);
1193
+ await assert.rejects(f.owner.updateGoal(f.sessionId, { ...goal, action: "cancel" }, {
1194
+ context: await goalCommandContext(f)
1195
+ }), /Codex unavailable/);
1196
+ await f.drain();
1197
+ assert.deepEqual((await f.provider.readGoal()).goal, goal);
1198
+ assert.deepEqual(f.run, before);
1199
+ assert.equal(f.publications.length, 0);
1200
+ });
1201
+
1202
+ // Original start/steer assertions retain the public admission and Git fixtures.
1203
+ // These bounded cases cover only the moved dispatch/receipt boundary and the
1204
+ // ownership adaptations: supplied settings, Error identity and result projection.
1205
+ function commandThreadFixture(f, providerAlreadyAvailable) {
1206
+ Object.assign(f.provider, {
1207
+ async ensureRuntime() { return {}; },
1208
+ async resumeThread(threadId) { return { id: threadId }; }
1209
+ });
1210
+ return {
1211
+ provider: f.provider,
1212
+ providerAlreadyAvailable,
1213
+ observerOptions: { providerKey: "account-scope" },
1214
+ preparation: {
1215
+ settings: async () => ({ threadSettings: {} }),
1216
+ identity: { read: () => f.threadId, async write() {} }
1217
+ }
1218
+ };
1219
+ }
1220
+
1221
+ test("original main command keeps preparation order and returns only after its authored receipt", async t => {
1222
+ const actorContext = { id: "author-1" };
1223
+ const f = await receiptFixture({ messageMetadata: {
1224
+ async actor(actor) {
1225
+ assert.equal(actor, actorContext);
1226
+ f.operations.push("actor");
1227
+ return { actorId: actor.id };
1228
+ }
1229
+ } });
1230
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1231
+ f.run = { ...f.run, state: "completed", providerStatus: "completed", providerThreadId: "", providerTurnId: "" };
1232
+ const messageId = "main-command";
1233
+ const input = { messageId, message: "Transport input", displayMessage: "Authored input",
1234
+ turnMetadata: { label: "authored" },
1235
+ async onPromptSending() { f.operations.push("sending"); } };
1236
+ const prepare = {
1237
+ async readContext() {
1238
+ f.operations.push("context");
1239
+ const session = await f.runtime.getSession(f.sessionId);
1240
+ return { runtime: f.runtime, session, selection: { runtime: f.runtime, session, threadId: () => "" } };
1241
+ },
1242
+ threadPreparation(value, prepared) {
1243
+ assert.equal(value.messageId, messageId);
1244
+ assert.equal(f.run.state, "starting");
1245
+ assert.equal(f.run.outerTurnId, messageId);
1246
+ assert.equal(f.owner.promptDeliveries.has(`project:${f.sessionId}`), true);
1247
+ f.operations.push("thread-policy");
1248
+ prepared.activeThreadId = f.threadId;
1249
+ return { provider: async () => commandThreadFixture(f, true) };
1250
+ },
1251
+ async prepareMessage(value, prepared, { starting }) {
1252
+ assert.equal(starting, true);
1253
+ assert.equal(prepared.actorContext, actorContext);
1254
+ assert.equal(f.run.providerThreadId, f.threadId);
1255
+ assert.equal(f.run.providerStatus, "starting");
1256
+ f.operations.push("prompt-policy");
1257
+ return { renderedPrompt: "Expanded private context", get turnSettings() {
1258
+ f.operations.push("turn-settings");
1259
+ return { model: "authorized-model" };
1260
+ } };
1261
+ },
1262
+ async finishMessage(value, prepared, outcome) {
1263
+ assert.equal(Object.hasOwn(outcome, "error"), false);
1264
+ assert.equal(outcome.delivery.turn.id, f.turnId);
1265
+ f.operations.push("delivery-policy");
1266
+ }
1267
+ };
1268
+ f.provider.sendTurn = async (threadId, content, options) => {
1269
+ assert.equal(threadId, f.threadId);
1270
+ assert.deepEqual(content, ["Expanded private context"]);
1271
+ assert.equal(options.clientUserMessageId, messageId);
1272
+ assert.equal(options.model, "authorized-model");
1273
+ f.operations.push("native-send");
1274
+ return { id: f.turnId, status: "inProgress" };
1275
+ };
1276
+ const result = await f.owner.withMessageDelivery(f.sessionId, messageId, { text: input.displayMessage }, () =>
1277
+ f.owner.dispatchMessage(f.sessionId, input, { actorContext }, prepare));
1278
+ assert.equal(result.value.delivered, true);
1279
+ assert.equal(result.value.deliveryMode, "new_turn");
1280
+ assert.equal(result.value.connectionReused, true);
1281
+ assert.equal(result.value.turnId, f.turnId);
1282
+ assert.equal(result.value.conversationTurn.user.messageId, messageId);
1283
+ assert.equal(result.value.conversationTurn.user.text, "Authored input");
1284
+ assert.equal(result.value.conversationTurn.metadata.actorId, actorContext.id);
1285
+ assert.deepEqual(result.value.conversationTurns, [result.value.conversationTurn]);
1286
+ assert.equal(await f.store.conversationMessageIdExists(f.sessionId, messageId), true);
1287
+ assert.equal(f.owner.promptDeliveries.size, 0);
1288
+ assert.equal(f.owner.messageDeliveries.size, 0);
1289
+ assert.equal(f.owner.pendingUserMessages.size, 0);
1290
+ const expected = ["context", "actor", "context", "actor", "event:codex-app-server-turn-claimed",
1291
+ "codex-app-server-turn-claimed", "thread-policy", "prompt-policy",
1292
+ "event:codex-app-server-user-message-owned", "sending", "turn-settings", "native-send",
1293
+ "delivery-policy", "authored-message", "codex-app-server-message-delivered"];
1294
+ assert.deepEqual(f.operations.filter(value => expected.includes(value)), expected);
1295
+
1296
+ f.operations.length = 0;
1297
+ const duplicate = await f.owner.dispatchMessage(f.sessionId, input, { actorContext }, prepare);
1298
+ assert.equal(duplicate.value.operationOutcome, "message_already_delivered");
1299
+ assert.equal(duplicate.value.duplicate, true);
1300
+ assert.deepEqual(f.operations, ["context"]);
1301
+ });
1302
+
1303
+ test("original main command retains exact failure identity and prompt cleanup positions", async t => {
1304
+ for (const failureAt of ["thread-policy", "native-send", "unavailable-return"]) await t.test(failureAt, async () => {
1305
+ const f = await receiptFixture();
1306
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1307
+ f.run = { ...f.run, state: "completed", providerStatus: "completed", providerThreadId: "", providerTurnId: "" };
1308
+ const failure = Object.assign(new Error("Original failure"), { code: -32600 });
1309
+ const finishing = Promise.withResolvers();
1310
+ const release = Promise.withResolvers();
1311
+ const promptReleased = Promise.withResolvers();
1312
+ const deletePrompt = f.owner.promptDeliveries.delete.bind(f.owner.promptDeliveries);
1313
+ f.owner.promptDeliveries.delete = key => {
1314
+ const deleted = deletePrompt(key);
1315
+ promptReleased.resolve();
1316
+ return deleted;
1317
+ };
1318
+ const messageId = `main-${failureAt}`;
1319
+ const key = `project:${f.sessionId}`;
1320
+ const prepare = {
1321
+ async readContext() {
1322
+ const session = await f.runtime.getSession(f.sessionId);
1323
+ return { runtime: f.runtime, session, selection: { runtime: f.runtime, session, threadId: () => "" } };
1324
+ },
1325
+ threadPreparation(input, prepared) {
1326
+ prepared.activeThreadId = f.threadId;
1327
+ if (failureAt !== "native-send") throw failure;
1328
+ return { provider: async () => commandThreadFixture(f) };
1329
+ },
1330
+ async prepareMessage() { return { renderedPrompt: "Prompt" }; },
1331
+ async finishMessage(input, prepared, { error }) {
1332
+ assert.equal(error, failure);
1333
+ assert.equal(prepared.activeThreadId, f.threadId);
1334
+ finishing.resolve();
1335
+ if (failureAt === "unavailable-return") return { value: release.promise };
1336
+ await release.promise;
1337
+ throw error;
1338
+ }
1339
+ };
1340
+ f.provider.sendTurn = async () => { throw failure; };
1341
+ const delivery = f.owner.withMessageDelivery(f.sessionId, messageId, { text: "Authored" }, () =>
1342
+ f.owner.dispatchMessage(f.sessionId, { messageId, message: "Authored" }, {}, prepare));
1343
+ void delivery.catch(() => null);
1344
+ await finishing.promise;
1345
+ assert.equal(f.run.providerThreadId, f.threadId);
1346
+ assert.equal(f.run.state, "failed");
1347
+ assert.equal(f.run.events.filter(event => event.kind === "codex-app-server-turn-idle").length, 1);
1348
+ if (failureAt === "unavailable-return") {
1349
+ // The original catch returns this policy Promise without awaiting it.
1350
+ await promptReleased.promise;
1351
+ assert.equal(f.owner.promptDeliveries.has(key), false);
1352
+ const unavailable = { ok: false, error: "Original worktree policy" };
1353
+ release.resolve(unavailable);
1354
+ assert.deepEqual(await delivery, { value: unavailable });
1355
+ } else {
1356
+ assert.equal(f.owner.promptDeliveries.has(key), true);
1357
+ release.resolve();
1358
+ await assert.rejects(delivery, error => error === failure);
1359
+ }
1360
+ assert.equal(f.owner.promptDeliveries.has(key), false);
1361
+ assert.equal(f.owner.messageDeliveries.size, 0);
1362
+ assert.equal(f.owner.pendingUserMessages.size, 0);
1363
+ });
1364
+ });
1365
+
1366
+ test("original dispatch prompt keeps authored input and evaluates authorized settings after validation", async () => {
1367
+ const providerCalls = [];
1368
+ const provider = {
1369
+ async sendTurn(threadId, input, params) {
1370
+ providerCalls.push({ input, params, threadId });
1371
+ return { id: "turn-1" };
1372
+ }
1373
+ };
1374
+ const authorized = {
1375
+ get turnSettings() {
1376
+ return { sandboxPolicy: { networkAccess: "enabled", type: "externalSandbox" } };
1377
+ }
1378
+ };
1379
+ const authoredText = " Do the work.\n";
1380
+ const result = await sendCodexAppServerPrompt({ prompt: authoredText, provider, threadId: "thread-1" }, authorized);
1381
+ assert.equal(result.turn.id, "turn-1");
1382
+ assert.deepEqual(result.input, [authoredText]);
1383
+ assert.deepEqual(providerCalls[0].input, [authoredText]);
1384
+ assert.equal(providerCalls[0].threadId, "thread-1");
1385
+ assert.deepEqual(providerCalls[0].params.sandboxPolicy, {
1386
+ networkAccess: "enabled",
1387
+ type: "externalSandbox"
1388
+ });
1389
+ assert.equal(providerCalls[0].params.outputSchema, undefined);
1390
+
1391
+ const order = [];
1392
+ const outputSchema = { properties: { kind: { enum: ["continue", "complete"], type: "string" } }, required: ["kind"], type: "object" };
1393
+ const attachments = [{ contentType: "image/png", get path() { order.push("image"); return "/authorized/diagram.png"; } }];
1394
+ const settings = { get turnSettings() { order.push("settings"); return { model: "authorized-model", sandboxPolicy: { type: "externalSandbox" } }; } };
1395
+ await assert.rejects(sendCodexAppServerPrompt({ prompt: " \n", attachments, provider }, settings),
1396
+ { message: "Codex app-server prompt is empty." });
1397
+ assert.deepEqual(order, []);
1398
+ await sendCodexAppServerPrompt({ prompt: authoredText, attachments, clientUserMessageId: " request-1 ",
1399
+ outputSchema, readOnly: true, provider, threadId: "thread-1" }, settings);
1400
+ assert.deepEqual(order, ["image", "settings"]);
1401
+ assert.deepEqual(providerCalls[1].input, [authoredText, { type: "localImage", path: "/authorized/diagram.png" }]);
1402
+ assert.equal(providerCalls[1].params.outputSchema, outputSchema);
1403
+ assert.equal(providerCalls[1].params.clientUserMessageId, "request-1");
1404
+ assert.equal(providerCalls[1].params.model, "authorized-model");
1405
+ assert.deepEqual(providerCalls[1].params.sandboxPolicy, { networkAccess: false, type: "readOnly" });
1406
+ await sendCodexAppServerPrompt({ prompt: authoredText, provider, outputSchema: [] }, authorized);
1407
+ assert.equal(providerCalls[2].params.outputSchema, undefined);
1408
+ });
1409
+
1410
+ function dispatchContext(f, messageId = "dispatch-request") {
1411
+ return {
1412
+ runtime: f.runtime, provider: f.provider, threadId: f.threadId, turnId: f.turnId,
1413
+ promptDeliveryKey: `project:${f.sessionId}`, messageId, clientUserMessageId: messageId,
1414
+ renderedPrompt: "Expanded private context", message: "Expanded private context",
1415
+ displayMessage: "Keep the change focused.", turnMetadata: { actorId: "original-author" },
1416
+ turnSettings: { model: "authorized-model" }
1417
+ };
1418
+ }
1419
+
1420
+ test("original dispatch start waits for a persisted receipt and ignores its late RPC reply after successor adoption", async t => {
1421
+ const persisting = Promise.withResolvers();
1422
+ const persist = Promise.withResolvers();
1423
+ const acknowledgement = Promise.withResolvers();
1424
+ const f = await receiptFixture({ messageMetadata: { async delivered(_store, _sessionId, metadata) {
1425
+ persisting.resolve();
1426
+ await persist.promise;
1427
+ return metadata;
1428
+ } } });
1429
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1430
+ const context = dispatchContext(f);
1431
+ const messageId = context.messageId;
1432
+ const receipt = Promise.withResolvers();
1433
+ f.run = { ...f.run, state: "starting", providerTurnId: "", outerTurnId: messageId };
1434
+ f.owner.pendingUserMessages.set(`${context.promptDeliveryKey}\0${messageId}`, {
1435
+ text: context.displayMessage, turnMetadata: context.turnMetadata, receipt
1436
+ });
1437
+ let rpcReplies = 0;
1438
+ let returned = false;
1439
+ f.provider.sendTurn = async (threadId, input, params) => {
1440
+ assert.equal(threadId, f.threadId);
1441
+ assert.deepEqual(input, [context.renderedPrompt]);
1442
+ assert.equal(params.clientUserMessageId, messageId);
1443
+ assert.equal(params.model, "authorized-model");
1444
+ f.emit(f.receipt(messageId));
1445
+ await acknowledgement.promise;
1446
+ rpcReplies += 1;
1447
+ return { id: f.turnId, status: "inProgress" };
1448
+ };
1449
+ f.observe();
1450
+ const sending = f.owner.startTurn(f.sessionId, {
1451
+ async onPromptSending({ threadId }) {
1452
+ assert.equal(threadId, f.threadId);
1453
+ assert.deepEqual(f.run.pendingUserMessageClientIds, [messageId]);
1454
+ }
1455
+ }, context).then(value => { returned = true; return value; });
1456
+ await persisting.promise;
1457
+ assert.equal(returned, false);
1458
+ assert.equal(await f.store.conversationMessageIdExists(f.sessionId, messageId), false);
1459
+ persist.resolve();
1460
+ const result = await sending;
1461
+ await f.drain();
1462
+ assert.equal(result.deliveredTurnId, f.turnId);
1463
+ assert.equal(result.providerFailure, "");
1464
+ assert.equal(rpcReplies, 0);
1465
+ assert.equal((await f.store.readConversationLog(f.sessionId)).filter(row => row.user?.messageId === messageId).length, 1);
1466
+ assert.equal(JSON.stringify(await f.store.readConversationLog(f.sessionId)).includes("Expanded private context"), false);
1467
+ f.run = { ...f.run, state: "finalizing", providerStatus: "completed",
1468
+ providerGoalStatus: "active", providerGoalThreadId: f.threadId };
1469
+ f.emit({ method: "turn/started", params: { threadId: f.threadId, turn: { id: "successor", status: "inProgress" } } });
1470
+ await f.drain();
1471
+ acknowledgement.resolve();
1472
+ await acknowledgement.promise;
1473
+ await f.drain();
1474
+ assert.equal(f.run.providerTurnId, "successor");
1475
+ assert.equal(f.run.state, "active");
1476
+ assert.equal(f.observationFailures.length, 0);
1477
+ });
1478
+
1479
+ test("original dispatch start preserves the handled Error identity and leaves outer failures to its caller", async () => {
1480
+ const f = await receiptFixture();
1481
+ const context = dispatchContext(f);
1482
+ const failure = Object.assign(new Error("Native request rejected"), { code: -32600, method: "turn/start" });
1483
+ f.run = { ...f.run, state: "starting", providerTurnId: "" };
1484
+ f.provider.sendTurn = async () => { throw failure; };
1485
+ const result = await f.owner.startTurn(f.sessionId, {
1486
+ async onPromptRejected() {
1487
+ assert.deepEqual(f.run.pendingUserMessageClientIds, [context.messageId]);
1488
+ f.operations.push("rejected");
1489
+ }
1490
+ }, context);
1491
+ assert.equal(result.error, failure);
1492
+ assert.equal(result.turnFailureHandled, true);
1493
+ assert.deepEqual(f.run.pendingUserMessageClientIds, []);
1494
+ assert.equal(f.run.state, "failed");
1495
+ assert.equal(f.run.error, failure.message);
1496
+ const ordered = ["event:codex-app-server-user-message-owned", "rejected",
1497
+ "event:codex-app-server-user-message-released", "event:codex-app-server-turn-idle"]
1498
+ .map(value => f.operations.indexOf(value));
1499
+ assert.ok(ordered.every(index => index >= 0));
1500
+ assert.deepEqual([...ordered].sort((a, b) => a - b), ordered);
1501
+ assert.equal(f.run.events.filter(event => event.kind === "codex-app-server-turn-idle").length, 1);
1502
+
1503
+ const unacknowledged = await receiptFixture();
1504
+ unacknowledged.run = { ...unacknowledged.run, state: "starting", providerTurnId: "" };
1505
+ unacknowledged.provider.sendTurn = async () => ({});
1506
+ await assert.rejects(unacknowledged.owner.startTurn(unacknowledged.sessionId, {}, dispatchContext(unacknowledged)),
1507
+ { message: "Codex app-server accepted the prompt without returning a turn id." });
1508
+ assert.equal(unacknowledged.run.state, "starting");
1509
+ assert.equal(unacknowledged.run.events.some(event => event.kind === "codex-app-server-turn-idle"), false);
1510
+ });
1511
+
1512
+ test("original dispatch steering persists the authored receipt while its exact native RPC remains pending", async t => {
1513
+ const f = await receiptFixture();
1514
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1515
+ const context = dispatchContext(f);
1516
+ const receipt = Promise.withResolvers();
1517
+ const acknowledgement = Promise.withResolvers();
1518
+ f.owner.pendingUserMessages.set(`${context.promptDeliveryKey}\0${context.messageId}`, {
1519
+ text: context.displayMessage, turnMetadata: context.turnMetadata,
1520
+ attachments: [{ id: "diagram", name: "Diagram" }], receipt
1521
+ });
1522
+ let rpcReplies = 0;
1523
+ f.provider.steerTurn = async (threadId, turnId, input, params) => {
1524
+ assert.equal(threadId, f.threadId);
1525
+ assert.equal(turnId, f.turnId);
1526
+ assert.deepEqual(input, [context.message, { type: "localImage", path: "/authorized/diagram.png" }]);
1527
+ assert.equal(params.clientUserMessageId, context.messageId);
1528
+ f.emit(f.receipt(context.messageId));
1529
+ await acknowledgement.promise;
1530
+ rpcReplies += 1;
1531
+ return { ok: true };
1532
+ };
1533
+ f.observe();
1534
+ const response = await f.owner.steerTurn(f.sessionId, {
1535
+ attachments: [{ contentType: "image/png", path: "/authorized/diagram.png" }],
1536
+ displayAttachments: [{ id: "diagram", name: "Diagram" }],
1537
+ async onPromptSending({ threadId, displayAttachments, turnMetadata }) {
1538
+ assert.equal(threadId, f.threadId);
1539
+ assert.equal(turnMetadata, context.turnMetadata);
1540
+ assert.deepEqual(displayAttachments, [{ id: "diagram", name: "Diagram" }]);
1541
+ assert.deepEqual(f.run.pendingUserMessageClientIds, [context.messageId]);
1542
+ }
1543
+ }, context);
1544
+ await f.drain();
1545
+ assert.equal(response.value.ok, true, JSON.stringify(response.value));
1546
+ assert.equal(response.value.operationOutcome, "delivered_to_active_turn");
1547
+ assert.equal(response.value.conversationTurn.user.messageId, context.messageId);
1548
+ assert.equal(response.session.sessionId, f.sessionId);
1549
+ assert.equal(rpcReplies, 0);
1550
+ assert.deepEqual(f.run.pendingUserMessageClientIds, []);
1551
+ const rows = (await f.store.readConversationLog(f.sessionId)).filter(row => row.user?.messageId === context.messageId);
1552
+ assert.equal(rows.length, 1);
1553
+ assert.equal(rows[0].user.text, context.displayMessage);
1554
+ assert.deepEqual(rows[0].user.attachments, [{ id: "diagram", name: "Diagram" }]);
1555
+ assert.equal(rows[0].metadata.actorId, "original-author");
1556
+ acknowledgement.resolve();
1557
+ await acknowledgement.promise;
1558
+ assert.equal(f.observationFailures.length, 0);
1559
+ });
1560
+
1561
+ test("original dispatch steering preserves deferred versus new-turn recovery and raw failure projection", async () => {
1562
+ for (const mode of ["invalid", "completed", "returned", "unrelated"]) {
1563
+ const f = await receiptFixture({ steerFailedCode: "authorized_steer_failure" });
1564
+ const context = dispatchContext(f);
1565
+ const failure = mode === "returned" ? { ok: false, error: "Native unavailable", retryable: false }
1566
+ : Object.assign(new Error("Native steer failed"), { code: mode === "unrelated" ? 5 : -32600, method: "turn/steer" });
1567
+ let rejections = 0;
1568
+ f.provider.readThreadStatus = async () => { throw new Error("Status temporarily unavailable"); };
1569
+ f.provider.steerTurn = async () => {
1570
+ if (mode === "completed") f.run = { ...f.run, state: "completed", providerStatus: "completed" };
1571
+ if (mode === "returned") return failure;
1572
+ throw failure;
1573
+ };
1574
+ const sending = f.owner.steerTurn(f.sessionId, { onPromptRejected() { rejections += 1; } }, context);
1575
+ if (mode === "unrelated") {
1576
+ await assert.rejects(sending, error => error === failure);
1577
+ assert.equal(rejections, 0);
1578
+ } else {
1579
+ const response = await sending;
1580
+ if (mode === "invalid") {
1581
+ assert.equal(response.value.operationOutcome, "active_turn_not_steerable");
1582
+ assert.equal(response.value.code, "authorized_steer_failure");
1583
+ assert.equal(response.value.retryable, true);
1584
+ assert.equal(response.session.sessionId, f.sessionId);
1585
+ assert.equal(rejections, 1);
1586
+ } else if (mode === "completed") {
1587
+ assert.equal(response.value.operationOutcome, "new_turn_required");
1588
+ assert.equal(response.value.reason, "active_turn_completed_before_delivery");
1589
+ assert.equal(response.value.threadId, f.threadId);
1590
+ assert.equal(response.value.turnId, f.turnId);
1591
+ assert.equal(response.session.sessionId, f.sessionId);
1592
+ assert.equal(rejections, 1);
1593
+ } else {
1594
+ assert.equal(response.value.operationOutcome, "steer_failed");
1595
+ assert.equal(response.value.result, failure);
1596
+ assert.equal(response.value.code, "authorized_steer_failure");
1597
+ assert.equal(Object.hasOwn(response, "session"), false);
1598
+ assert.equal(rejections, 0);
1599
+ }
1600
+ }
1601
+ assert.deepEqual(f.run.pendingUserMessageClientIds, []);
1602
+ assert.equal(f.operations.filter(value => value === "event:codex-app-server-user-message-released").length, 1);
1603
+ assert.equal((await f.store.readConversationLog(f.sessionId)).some(row => row.user?.messageId === context.messageId), false);
1604
+ }
1605
+ });
1606
+
1607
+ function controlContext(f, overrides = {}) {
1608
+ return {
1609
+ runtime: f.runtime,
1610
+ admissionError: () => null,
1611
+ threadId: () => f.threadId,
1612
+ acquireProvider: async () => f.provider,
1613
+ observationOwner: () => null,
1614
+ ...overrides
1615
+ };
1616
+ }
1617
+
1618
+ test("original control rechecks admission after preflight and before interrupt without adding a work lock", async () => {
1619
+ const f = await receiptFixture();
1620
+ const calls = [];
1621
+ let checks = 0;
1622
+ f.provider.readThreadStatus = async () => { calls.push("preflight"); return { status: "active", turnId: f.turnId }; };
1623
+ f.provider.interruptTurn = async () => assert.fail("Frozen admission must stop the native request");
1624
+ const response = await f.owner.interruptTurn(f.sessionId, { controlRequestId: "stop-1" }, controlContext(f, {
1625
+ admissionError() { checks += 1; calls.push(`admission-${checks}`); return checks === 3 ? new Error("Admission frozen") : null; },
1626
+ async acquireProvider() { calls.push("acquire"); return f.provider; }
1627
+ }));
1628
+ assert.deepEqual(calls, ["admission-1", "acquire", "preflight", "admission-2", "admission-3"]);
1629
+ assert.deepEqual(response, { value: { interrupted: false, ok: true, operationOutcome: "already_idle",
1630
+ status: "interrupted", threadId: f.threadId, turnId: f.turnId } });
1631
+ assert.equal(f.run.state, "active");
1632
+ assert.deepEqual(f.operations, []);
1633
+ });
1634
+
1635
+ test("original control keeps an unverified stop blocked and retries only its retained observation owner", async () => {
1636
+ const f = await receiptFixture();
1637
+ f.run = { ...f.run, providerStatus: "observation_lost" };
1638
+ const context = controlContext(f, { acquireProvider: async () => assert.fail("Observation loss must not acquire a replacement") });
1639
+ const missing = await f.owner.interruptTurn(f.sessionId, {}, context);
1640
+ assert.equal(missing.value.ok, false);
1641
+ assert.equal(missing.value.error, "Codex's stop could not be verified. Its runtime owner is unavailable.");
1642
+ assert.equal(missing.session.agentRuns[0].providerStatus, "observation_lost");
1643
+ assert.equal(f.run.state, "active");
1644
+ const failure = new Error("Retained stop persistence failed");
1645
+ const observationFailure = new Error("Connection was lost");
1646
+ let available = false;
1647
+ const retained = {
1648
+ observationFailure,
1649
+ async failObservation(error) {
1650
+ assert.equal(error, observationFailure);
1651
+ if (!available) throw failure;
1652
+ f.run = { ...f.run, state: "interrupted" };
1653
+ }
1654
+ };
1655
+ context.observationOwner = threadId => { assert.equal(threadId, f.threadId); return retained; };
1656
+ await assert.rejects(f.owner.interruptTurn(f.sessionId, {}, context), error => error === failure);
1657
+ assert.equal(f.run.state, "active");
1658
+ available = true;
1659
+ const stopped = await f.owner.interruptTurn(f.sessionId, {}, context);
1660
+ assert.deepEqual(stopped.value, { ok: true, interrupted: true });
1661
+ assert.equal(stopped.session.agentRuns[0].state, "interrupted");
1662
+ assert.equal(f.run.providerStatus, "observation_lost");
1663
+ });
1664
+
1665
+ test("original control preserves command-stop-unconfirmed even when preflight already reports idle", async () => {
1666
+ for (const providerPrefix of [undefined, "provider_"]) {
1667
+ const f = await receiptFixture({ errorPrefix: "host_" });
1668
+ if (providerPrefix !== undefined) f.provider.errorPrefix = providerPrefix;
1669
+ const failure = Object.assign(new Error("Owned command exit is unconfirmed"), {
1670
+ code: `${providerPrefix ?? "host_"}codex_command_stop_unconfirmed`
1671
+ });
1672
+ f.provider.readThreadStatus = async () => ({ status: "idle", turnId: f.turnId });
1673
+ f.provider.interruptTurn = async () => { throw failure; };
1674
+ await assert.rejects(f.owner.interruptTurn(f.sessionId, {}, controlContext(f)), error => error === failure);
1675
+ assert.equal(f.run.state, "active");
1676
+ assert.equal(f.run.events.some(event => event.kind === "codex-app-server-turn-idle"), false);
1677
+ }
1678
+ });
1679
+
1680
+ test("original control follows native successors but bounds interrupt races to three attempts", async t => {
1681
+ const f = await receiptFixture();
1682
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1683
+ f.run = { ...f.run, providerGoalStatus: "active", providerGoalThreadId: f.threadId };
1684
+ let nativeTurnId = f.turnId;
1685
+ const interrupted = [];
1686
+ f.provider.readThreadStatus = async () => ({ status: "active", turnId: nativeTurnId });
1687
+ f.provider.interruptTurn = async (threadId, turnId) => {
1688
+ assert.equal(threadId, f.threadId);
1689
+ assert.equal(turnId, nativeTurnId);
1690
+ interrupted.push(turnId);
1691
+ nativeTurnId = `successor-${interrupted.length}`;
1692
+ throw Object.assign(new Error("Native turn changed"), { code: -32600, method: "turn/interrupt" });
1693
+ };
1694
+ const response = await f.owner.interruptTurn(f.sessionId, {}, controlContext(f));
1695
+ assert.deepEqual(interrupted, [f.turnId, "successor-1", "successor-2"]);
1696
+ assert.equal(f.run.providerTurnId, "successor-3");
1697
+ assert.equal(f.run.outerTurnId, "request-1");
1698
+ assert.equal(f.run.providerGoalStatus, "active");
1699
+ assert.equal(response.value.operationOutcome, "already_idle");
1700
+ assert.equal(response.value.turnId, "successor-2");
1701
+ assert.equal(response.session.agentRuns[0].providerTurnId, "successor-3");
1702
+ });
1703
+
1704
+ test("original control settles a processed finalizing result before reporting an already idle turn", async () => {
1705
+ const f = await receiptFixture();
1706
+ f.run = { ...f.run, state: "finalizing", providerStatus: "completed", events: [
1707
+ { kind: "codex-app-server-result-processed", providerThreadId: f.threadId, providerTurnId: f.turnId,
1708
+ resultReason: "assistant_response" }
1709
+ ] };
1710
+ f.provider.readThreadStatus = async () => ({ status: "idle", turnId: f.turnId });
1711
+ f.provider.interruptTurn = async () => assert.fail("The saved processed result must settle without a new interrupt");
1712
+ const response = await f.owner.interruptTurn(f.sessionId, {}, controlContext(f));
1713
+ assert.equal(response.value.interrupted, false);
1714
+ assert.equal(response.value.operationOutcome, "already_idle");
1715
+ assert.equal(response.value.threadId, f.threadId);
1716
+ assert.equal(response.value.turnId, f.turnId);
1717
+ assert.equal(f.run.state, "completed");
1718
+ assert.equal(f.run.events.filter(event => event.kind === "codex-app-server-result-processed").length, 1);
1719
+ assert.equal(f.publications.some(event => event.reason === "assistant-response-bundle"), false);
1720
+ });
1721
+
1722
+ test("original control stopped-proof recovery preserves the whole-run fence and never resumes work", async () => {
1723
+ for (const raced of [false, true]) {
1724
+ const f = await receiptFixture();
1725
+ f.run = { ...f.run, providerStatus: "observation_lost", error: "Native stop unavailable" };
1726
+ const observing = Promise.withResolvers();
1727
+ const release = Promise.withResolvers();
1728
+ f.provider.readGoal = async () => ({ goal: { status: "paused", createdAt: 10, tokensUsed: 20 } });
1729
+ f.provider.readThreadStatus = async () => { observing.resolve(); await release.promise; return { status: "idle" }; };
1730
+ f.provider.resumeThread = async () => assert.fail("Stopped proof must never resume a goal");
1731
+ f.provider.sendTurn = async () => assert.fail("Stopped proof must never send a new turn");
1732
+ const recovering = f.owner.recoverObservationLoss(f.runtime, await f.runtime.getSession(f.sessionId), controlContext(f));
1733
+ await observing.promise;
1734
+ if (raced) f.run = { ...f.run, error: "A newer stop proof owns this run" };
1735
+ release.resolve();
1736
+ const recovered = await recovering;
1737
+ if (raced) {
1738
+ assert.equal(recovered.agentRuns[0].error, "A newer stop proof owns this run");
1739
+ assert.equal(f.run.state, "active");
1740
+ assert.deepEqual(f.publications, []);
1741
+ assert.equal(f.run.events.some(event => event.kind === "codex-observation-stop-recovered"), false);
1742
+ } else {
1743
+ assert.equal(f.run.state, "interrupted");
1744
+ assert.equal(f.run.providerStatus, "observation_lost");
1745
+ assert.equal(f.run.providerThreadId, f.threadId);
1746
+ assert.equal(f.run.providerGoalThreadId, f.threadId);
1747
+ assert.equal(f.run.providerGoalStatus, "paused");
1748
+ assert.equal(f.run.error, "");
1749
+ assert.equal(f.run.events.at(-1).kind, "codex-observation-stop-recovered");
1750
+ assert.equal(f.publications.at(-1).reason, "codex-observation-stop-recovered");
1751
+ assert.deepEqual(f.publications.at(-1).payload.conversationStream.messages, []);
1752
+ }
1753
+ }
1754
+ });
1755
+
1756
+ async function messageSelectionContext(f, overrides = {}) {
1757
+ return { runtime: f.runtime, session: await f.runtime.getSession(f.sessionId),
1758
+ threadId: () => f.threadId, acquireProvider: async () => ({ provider: f.provider, reused: true }), ...overrides };
1759
+ }
1760
+
1761
+ test("original message selection preserves idle, goal-gap and finalizing decisions after native reconciliation", async () => {
1762
+ for (const state of ["idle", "goal-gap", "finalizing", "no-turn-id", "active"]) {
1763
+ const f = await receiptFixture();
1764
+ if (state === "idle" || state === "goal-gap") f.run = { ...f.run, state: "completed", providerStatus: "completed", providerTurnId: "" };
1765
+ if (state === "goal-gap") f.run = { ...f.run, providerGoalStatus: "active", providerGoalThreadId: f.threadId };
1766
+ if (state === "finalizing") f.run = { ...f.run, state: "finalizing", providerStatus: "completed" };
1767
+ if (state === "no-turn-id") f.run = { ...f.run, providerTurnId: "" };
1768
+ f.provider.readThreadStatus = async () => ({ status: "unknown" });
1769
+ const selected = await f.owner.selectMessageTurn(f.sessionId, { messageId: "request" }, await messageSelectionContext(f));
1770
+ if (state === "idle") {
1771
+ assert.equal(selected.value.newTurnRequired, true);
1772
+ assert.equal(selected.value.reason, "provider_idle");
1773
+ } else if (state === "active") {
1774
+ assert.equal(Object.hasOwn(selected, "value"), false);
1775
+ assert.equal(selected.provider, f.provider);
1776
+ assert.equal(selected.threadId, f.threadId);
1777
+ assert.equal(selected.turnId, f.turnId);
1778
+ } else {
1779
+ assert.equal(selected.value.operationOutcome, "active_turn_not_ready");
1780
+ assert.equal(selected.value.retryable, true);
1781
+ assert.equal(selected.value.ok, false);
1782
+ }
1783
+ assert.equal(selected.session.sessionId, f.sessionId);
1784
+ }
1785
+ });
1786
+
1787
+ test("original message selection matches only the exact missing idle thread and preserves unrelated errors", async () => {
1788
+ for (const mode of ["missing", "other-thread", "unrelated", "active"]) {
1789
+ const f = await receiptFixture();
1790
+ if (mode !== "active") f.run = { ...f.run, state: "completed", providerStatus: "completed" };
1791
+ const failure = Object.assign(new Error(mode === "unrelated" ? "invalid thread/read configuration"
1792
+ : `thread not loaded: ${mode === "other-thread" ? "different-thread" : f.threadId}`), { code: -32600, method: "thread/read" });
1793
+ f.provider.readThreadStatus = async () => { throw failure; };
1794
+ const selecting = f.owner.selectMessageTurn(f.sessionId, {}, await messageSelectionContext(f));
1795
+ if (mode === "missing") {
1796
+ const selected = await selecting;
1797
+ assert.equal(selected.value.newTurnRequired, true);
1798
+ assert.equal(selected.value.reason, "provider_thread_missing");
1799
+ assert.equal(selected.value.threadId, f.threadId);
1800
+ } else await assert.rejects(selecting, error => error === failure);
1801
+ assert.equal(f.run.providerThreadId, f.threadId);
1802
+ assert.deepEqual(f.operations, []);
1803
+ }
1804
+ });
1805
+
1806
+ test("original message selection rereads abandoned ownership before retained identity and blocks unconfirmed observation loss", async () => {
1807
+ let f;
1808
+ f = await receiptFixture({ async recoverAdmission(runtime, session, options) {
1809
+ assert.equal(runtime, f.runtime);
1810
+ assert.equal(session.agentRuns[0].state, "starting");
1811
+ assert.deepEqual(options, { promptDeliveryActive: false });
1812
+ f.run = { ...f.run, state: "failed", providerStatus: "delivery_failed" };
1813
+ return { recovered: true, session: { ...await runtime.getSession(f.sessionId), identityRevision: "fresh" } };
1814
+ } });
1815
+ f.run = { ...f.run, state: "starting", providerTurnId: "", providerThreadId: "" };
1816
+ let acquired = 0;
1817
+ const selected = await f.owner.selectMessageTurn(f.sessionId, {}, await messageSelectionContext(f, {
1818
+ threadId(session) { assert.equal(session.identityRevision, "fresh"); return ""; },
1819
+ async acquireProvider() { acquired += 1; assert.fail("Missing identity must not acquire a provider"); }
1820
+ }));
1821
+ assert.equal(selected.value.reason, "thread_missing");
1822
+ assert.equal(acquired, 0);
1823
+ const lost = await receiptFixture({ errorPrefix: "authorized_" });
1824
+ lost.run = { ...lost.run, providerStatus: "observation_lost" };
1825
+ await assert.rejects(lost.owner.selectMessageTurn(lost.sessionId, {}, await messageSelectionContext(lost, {
1826
+ threadId: () => assert.fail("Unconfirmed loss must stop before fallback identity"),
1827
+ acquireProvider: async () => assert.fail("Unconfirmed loss must not reconnect")
1828
+ })), { code: "authorized_codex_observation_lost" });
1829
+ assert.equal(lost.run.state, "active");
1830
+ });
1831
+
1832
+ test("original command dispatch passes trusted attachment-only input without changing its empty-input default", async t => {
1833
+ for (const mode of ["start", "steer"]) await t.test(mode, async () => {
1834
+ const f = await receiptFixture();
1835
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1836
+ if (mode === "start") {
1837
+ f.run = { ...f.run, state: "completed", providerStatus: "completed", providerThreadId: "", providerTurnId: "" };
1838
+ }
1839
+ const messageId = `attachment-only-${mode}`;
1840
+ const attachments = [{ attachmentId: "picture", fileName: "picture.png", size: 10 }];
1841
+ const preparedInput = [{ type: "image", url: "data:image/png;base64,YXV0aG9yaXplZA==" }];
1842
+ const input = { messageId, message: "", displayMessage: "", displayAttachments: attachments };
1843
+ const empty = await f.owner.dispatchMessage(f.sessionId, input, {}, {
1844
+ readContext: () => assert.fail("The original empty-input default must reject before preparation")
1845
+ });
1846
+ assert.equal(empty.value.ok, false);
1847
+ assert.equal(empty.value.operationOutcome, "message_empty");
1848
+ const identities = [];
1849
+ const writeUser = f.store.writeConversationUserMessage;
1850
+ f.store.writeConversationUserMessage = (sessionId, value) => {
1851
+ identities.push(value.nativeIdentity);
1852
+ return writeUser(sessionId, value);
1853
+ };
1854
+ const prepare = {
1855
+ async readContext() {
1856
+ const session = await f.runtime.getSession(f.sessionId);
1857
+ return { runtime: f.runtime, session, selection: {
1858
+ runtime: f.runtime, session, threadId: () => mode === "start" ? "" : f.threadId,
1859
+ acquireProvider: async () => ({ provider: f.provider, reused: true })
1860
+ } };
1861
+ },
1862
+ threadPreparation(value, prepared) {
1863
+ prepared.activeThreadId = f.threadId;
1864
+ return { provider: async () => commandThreadFixture(f) };
1865
+ },
1866
+ async prepareMessage(value, prepared, { starting }) {
1867
+ return starting ? { renderedPrompt: "", turnSettings: {} } : null;
1868
+ },
1869
+ async finishMessage(value, prepared, outcome) {
1870
+ if (Object.hasOwn(outcome, "error")) throw outcome.error;
1871
+ }
1872
+ };
1873
+ const nativeInput = (threadId, content, options) => {
1874
+ assert.equal(threadId, f.threadId);
1875
+ assert.equal(content, preparedInput);
1876
+ assert.equal(options.clientUserMessageId, messageId);
1877
+ };
1878
+ f.provider.sendTurn = async (threadId, content, options) => {
1879
+ nativeInput(threadId, content, options);
1880
+ return { id: f.turnId, status: "inProgress" };
1881
+ };
1882
+ f.provider.steerTurn = async (threadId, turnId, content, options) => {
1883
+ assert.equal(turnId, f.turnId);
1884
+ nativeInput(threadId, content, options);
1885
+ return { ok: true };
1886
+ };
1887
+ const result = await f.owner.withMessageDelivery(f.sessionId, messageId, { text: "", attachments }, () =>
1888
+ f.owner.dispatchMessage(f.sessionId, input, { preparedInput }, prepare));
1889
+ assert.equal(result.value.delivered, true);
1890
+ assert.equal(result.value.turnId, f.turnId);
1891
+ if (mode === "start") assert.deepEqual(result.nativeIdentity, { threadId: f.threadId, turnId: f.turnId });
1892
+ const rows = (await f.store.readConversationLog(f.sessionId)).filter(row => row.user?.messageId === messageId);
1893
+ assert.equal(rows.length, 1);
1894
+ assert.equal(rows[0].user.text, "");
1895
+ assert.deepEqual(rows[0].user.attachments, attachments);
1896
+ assert.deepEqual(identities, [{ threadId: f.threadId, turnId: f.turnId }]);
1897
+ assert.equal(JSON.stringify(rows).includes("data:image"), false);
1898
+ assert.equal(f.owner.pendingUserMessages.size, 0);
1899
+ assert.equal(f.owner.promptDeliveries.size, 0);
1900
+ });
1901
+ });
1902
+
1903
+ test("original authorized input supports an attachment-only receipt through the same native send and writer", async t => {
1904
+ const f = await receiptFixture();
1905
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1906
+ const context = dispatchContext(f, "image-only");
1907
+ context.renderedPrompt = "";
1908
+ context.preparedInput = [{ type: "image", url: "data:image/png;base64,YXV0aG9yaXplZA==" }];
1909
+ const attachments = [{ attachmentId: "picture", fileName: "picture.png", size: 10 }];
1910
+ const identities = [];
1911
+ const writeUser = f.store.writeConversationUserMessage;
1912
+ f.store.writeConversationUserMessage = (sessionId, input) => {
1913
+ identities.push(input.nativeIdentity);
1914
+ return writeUser(sessionId, input);
1915
+ };
1916
+ const receipt = Promise.withResolvers();
1917
+ const acknowledgement = Promise.withResolvers();
1918
+ f.run = { ...f.run, state: "starting", providerTurnId: "" };
1919
+ f.owner.pendingUserMessages.set(`${context.promptDeliveryKey}\0${context.messageId}`, {
1920
+ text: "", attachments, turnMetadata: context.turnMetadata, receipt
1921
+ });
1922
+ f.provider.sendTurn = async (threadId, input, params) => {
1923
+ assert.equal(threadId, f.threadId);
1924
+ assert.equal(input, context.preparedInput);
1925
+ assert.equal(params.clientUserMessageId, context.messageId);
1926
+ f.emit({ method: "item/completed", params: { threadId, turnId: f.turnId,
1927
+ item: { type: "userMessage", id: "native-image-receipt", clientId: context.messageId,
1928
+ content: [{ type: "image", url: "not-an-authority" }] } } });
1929
+ return acknowledgement.promise;
1930
+ };
1931
+ f.observe();
1932
+ const started = await f.owner.startTurn(f.sessionId, { displayAttachments: attachments }, context);
1933
+ await f.drain();
1934
+ assert.equal(started.deliveredTurnId, f.turnId);
1935
+ const rows = (await f.store.readConversationLog(f.sessionId)).filter(row => row.user?.messageId === context.messageId);
1936
+ assert.equal(rows.length, 1);
1937
+ assert.equal(rows[0].user.text, "");
1938
+ assert.deepEqual(rows[0].user.attachments, attachments);
1939
+ assert.equal(rows[0].metadata.actorId, "original-author");
1940
+ assert.deepEqual(identities, [{ threadId: f.threadId, turnId: f.turnId }]);
1941
+ assert.deepEqual(f.run.pendingUserMessageClientIds, []);
1942
+ assert.equal(JSON.stringify(rows).includes("not-an-authority"), false);
1943
+ assert.equal(JSON.stringify(rows).includes("data:image"), false);
1944
+ acknowledgement.resolve({ id: f.turnId });
1945
+ assert.equal(f.observationFailures.length, 0);
1946
+ });
1947
+
1948
+ test("original authorized input ignores empty unowned native receipts and uses prepared steering input unchanged", async () => {
1949
+ const f = await receiptFixture();
1950
+ const before = structuredClone(f.run);
1951
+ const empty = { method: "item/completed", params: { threadId: f.threadId, turnId: f.turnId,
1952
+ item: { type: "userMessage", id: "empty", clientId: "unknown", content: [{ type: "image", url: "untrusted" }] } } };
1953
+ await f.owner.mirrorTerminalUserMessage(f.sessionId, f.threadId, empty);
1954
+ assert.deepEqual(f.run, before);
1955
+ assert.equal((await f.store.readConversationLog(f.sessionId)).some(row => row.user?.messageId === "unknown"), false);
1956
+ assert.equal(await f.store.writeConversationUserMessage(f.sessionId, { text: "", messageId: "empty", attachments: [] }), null);
1957
+ assert.equal(await f.store.writeConversationAssistantMessage(f.sessionId, { text: "", attachments: [{ attachmentId: "ignored" }] }), null);
1958
+ const context = dispatchContext(f, "prepared-steer");
1959
+ context.preparedInput = [{ type: "image", url: "data:image/png;base64,YQ==" }];
1960
+ context.message = "";
1961
+ context.displayMessage = "";
1962
+ f.provider.steerTurn = async (threadId, turnId, input) => {
1963
+ assert.equal(threadId, f.threadId);
1964
+ assert.equal(turnId, f.turnId);
1965
+ assert.equal(input, context.preparedInput);
1966
+ return { ok: true };
1967
+ };
1968
+ const response = await f.owner.steerTurn(f.sessionId, {
1969
+ displayAttachments: [{ attachmentId: "picture", fileName: "picture.png", size: 1 }]
1970
+ }, context);
1971
+ assert.equal(response.value.ok, true);
1972
+ assert.equal(response.value.conversationTurn.user.messageId, context.messageId);
1973
+ assert.equal(response.value.conversationTurn.user.text, "");
1974
+ assert.equal(response.value.conversationTurn.user.attachments.length, 1);
1975
+ });
1976
+
1977
+ test("original native publication carries exact goal metadata without changing notification order", async t => {
1978
+ const f = await receiptFixture();
1979
+ t.after(() => f.owner.clearSessionRecoveryTimers(f.sessionId));
1980
+ const observed = [];
1981
+ f.onPublish = async (_sessionId, event) => {
1982
+ if (event.reason === "codex-goal") observed.push({ value: event.nativeGoal, priorStatus: f.run.providerGoalStatus });
1983
+ };
1984
+ const goal = { threadId: f.threadId, status: "active", objective: "Keep the original goal", createdAt: 10, tokensUsed: 20 };
1985
+ f.observe();
1986
+ f.emit({ method: "thread/goal/updated", params: { threadId: f.threadId, goal } });
1987
+ await f.drain();
1988
+ f.emit({ method: "thread/goal/cleared", params: { threadId: f.threadId } });
1989
+ await f.drain();
1990
+ assert.equal(observed[0].value.goal, goal);
1991
+ assert.deepEqual(observed[0].value, { threadId: f.threadId, goal });
1992
+ assert.equal(observed[0].priorStatus, undefined, "Updated notification publishes before reconciling");
1993
+ assert.deepEqual(observed[1].value, { threadId: f.threadId, goal: null });
1994
+ assert.equal(observed[1].priorStatus, "", "Cleared notification reconciles before publishing");
1995
+ assert.equal(f.publications.filter(event => event.reason === "codex-goal").some(event => Object.hasOwn(event.payload || {}, "goal")), false);
1996
+ assert.equal(f.observationFailures.length, 0);
1997
+ });
1998
+
1999
+ test("original native publication carries the output record identity through correction after a newer run", async () => {
2000
+ const f = await receiptFixture();
2001
+ const identities = [];
2002
+ const write = f.store.writeConversationAssistantMessage;
2003
+ const upsert = f.store.upsertConversationAssistantMessage;
2004
+ f.store.writeConversationAssistantMessage = async (sessionId, input) => {
2005
+ identities.push(["append", input.nativeIdentity]);
2006
+ f.run = { ...f.run, providerTurnId: "newer-native-turn" };
2007
+ return write(sessionId, input);
2008
+ };
2009
+ f.store.upsertConversationAssistantMessage = async (sessionId, input) => {
2010
+ identities.push(["correct", input.nativeIdentity]);
2011
+ return upsert(sessionId, input);
2012
+ };
2013
+ const record = { threadId: f.threadId, turnId: f.turnId, itemId: "record-identity", text: "The original reply." };
2014
+ await f.owner.persistAssistantReply(f.runtime, f.sessionId, record);
2015
+ await f.owner.persistAssistantReply(f.runtime, f.sessionId, { ...record, conversationTurn: null, text: "The corrected reply." });
2016
+ assert.deepEqual(identities, [
2017
+ ["append", { threadId: f.threadId, turnId: f.turnId }],
2018
+ ["append", { threadId: f.threadId, turnId: f.turnId }],
2019
+ ["correct", { threadId: f.threadId, turnId: f.turnId }]
2020
+ ]);
2021
+ assert.equal(f.run.providerTurnId, "newer-native-turn");
2022
+ assert.deepEqual((await f.replies()).map(row => row.assistant.text), ["The corrected reply."]);
2023
+ assert.equal(JSON.stringify(await f.replies()).includes("nativeIdentity"), false);
2024
+ });
2025
+
2026
+ test("original live observation prepares only owned threads and saves every active main or goal barrier", async () => {
2027
+ for (const mode of ["active", "goal", "idle", "helper"]) {
2028
+ const projectContext = { project: mode };
2029
+ const contexts = [];
2030
+ const f = await receiptFixture({ runInContext(context, operation) {
2031
+ contexts.push(context);
2032
+ return operation();
2033
+ } });
2034
+ if (mode !== "active") f.run = { ...f.run, state: "completed", providerStatus: "completed" };
2035
+ if (mode === "goal") Object.assign(f.run, { providerGoalStatus: "active", providerGoalThreadId: f.threadId });
2036
+ const error = new Error("Observation failed");
2037
+ const entries = [
2038
+ { provider: f.provider, conversationId: "helper-running", runId: "helper-turn", status: "inProgress", error: "" },
2039
+ { provider: f.provider, conversationId: "helper-goal", runId: "goal-turn", status: "idle", goal: { status: "active" }, error: "" },
2040
+ { provider: f.provider, conversationId: "helper-idle", runId: "idle-turn", status: "idle", error: "" },
2041
+ { provider: {}, conversationId: "other-owner", runId: "other-turn", status: "inProgress", error: "" }
2042
+ ];
2043
+ let reads = 0;
2044
+ const getSession = f.runtime.getSession;
2045
+ f.runtime.getSession = (...args) => { reads += 1; return getSession(...args); };
2046
+ f.owner.conversations.set(`project:${f.sessionId}`, new Map(entries.map(entry => [entry.conversationId, entry])));
2047
+ const observation = f.owner.createObservation(f.sessionId, {
2048
+ projectContext,
2049
+ target(value) {
2050
+ assert.equal(value, error);
2051
+ return { threadId: mode === "helper" ? "" : f.threadId, pendingMessage: "Stop pending", message: "Stopped" };
2052
+ },
2053
+ retireProvider() { throw new Error("Preparation must not release a provider"); }
2054
+ });
2055
+ const threads = await observation.prepare({ provider: f.provider, error });
2056
+ assert.deepEqual([...threads], [
2057
+ ["helper-running", "helper-turn"], ["helper-goal", "goal-turn"], ["helper-idle", "idle-turn"],
2058
+ ...(mode === "helper" ? [] : [[f.threadId, f.turnId]])
2059
+ ]);
2060
+ assert.deepEqual(entries.map(entry => entry.error), ["Stop pending", "Stop pending", "", ""]);
2061
+ await observation.barrier({ provider: f.provider, error });
2062
+ const active = mode === "active" || mode === "goal";
2063
+ assert.deepEqual(f.run.events.map(event => event.kind), active ? ["observation-lost", "observation-lost"] : []);
2064
+ assert.equal(f.publications.length, active ? 1 : 0);
2065
+ if (active) {
2066
+ assert.equal(f.publications[0].reason, "codex-observation-lost");
2067
+ assert.equal(f.run.providerStatus, "observation_lost");
2068
+ assert.equal(f.run.error, "Stop pending");
2069
+ }
2070
+ assert.equal(reads, mode === "helper" ? 0 : 2);
2071
+ assert.deepEqual(contexts, [projectContext, projectContext]);
2072
+ }
2073
+ });
2074
+
2075
+ test("original live observation retains its provider until stopped persistence and clears streams before retirement and publication", async () => {
2076
+ for (const sharedStop of [false, true]) {
2077
+ const f = await receiptFixture();
2078
+ const error = new Error("Observation failed");
2079
+ const saveError = new Error("Stop state could not be saved");
2080
+ f.provider.observationFailure = error;
2081
+ f.store.updateConversationStream(f.sessionId, { turnId: f.turnId, messageId: "live-answer", role: "assistant", text: "Draft" });
2082
+ const failures = [];
2083
+ const entries = [{ provider: f.provider, conversationId: "helper", runId: "helper-turn", status: "inProgress",
2084
+ watcher: { failNow(value) { failures.push(value); f.operations.push("helper-failed"); } } }];
2085
+ let retired = 0;
2086
+ f.owner.conversations.set(`project:${f.sessionId}`, new Map(entries.map(entry => [entry.conversationId, entry])));
2087
+ const observation = f.owner.createObservation(f.sessionId, {
2088
+ target: () => ({ threadId: f.threadId, pendingMessage: "Stop pending", message: "Stopped" }),
2089
+ async retireProvider(provider) {
2090
+ assert.equal(provider, f.provider);
2091
+ assert.equal(f.run.state, CODEX_APP_SERVER_RUN_STATE.INTERRUPTED);
2092
+ assert.equal(f.store.readConversationStream(f.sessionId).messages.length, 0);
2093
+ retired += 1;
2094
+ f.operations.push("retired");
2095
+ }
2096
+ });
2097
+ await observation.prepare({ provider: f.provider, error });
2098
+ const write = f.store.writeAgentRunEvent;
2099
+ f.store.writeAgentRunEvent = (...args) => {
2100
+ if (args[2].event.kind === "codex-observation-stopped") throw saveError;
2101
+ return write(...args);
2102
+ };
2103
+ await assert.rejects(observation.complete({ provider: f.provider, error, sharedStop }), value => value === saveError);
2104
+ assert.equal(f.run.state, CODEX_APP_SERVER_RUN_STATE.ACTIVE);
2105
+ assert.equal(f.provider.observationFailure, error);
2106
+ assert.equal(retired, 0);
2107
+ assert.equal(entries[0].status, "inProgress");
2108
+ assert.equal(f.store.readConversationStream(f.sessionId).messages.length, 1);
2109
+ assert.deepEqual(failures, []);
2110
+ assert.equal(f.publications.some(event => event.reason === "codex-observation-stopped"), false);
2111
+ f.store.writeAgentRunEvent = write;
2112
+ f.operations = [];
2113
+ f.onPublish = (_id, event) => {
2114
+ if (event.reason !== "codex-observation-stopped") return;
2115
+ assert.equal(retired, sharedStop ? 1 : 0);
2116
+ if (!sharedStop) assert.equal(f.provider.observationFailure, null);
2117
+ assert.equal(entries[0].status, "interrupted");
2118
+ assert.deepEqual(event.payload.conversationStream.messages, []);
2119
+ };
2120
+ await observation.complete({ provider: f.provider, error, sharedStop });
2121
+ assert.equal(f.run.state, CODEX_APP_SERVER_RUN_STATE.INTERRUPTED);
2122
+ assert.equal(f.run.providerStatus, "observation_lost");
2123
+ assert.equal(f.run.error, sharedStop ? "Stopped Its shared service was stopped, affecting other sessions using it." : "Stopped");
2124
+ assert.deepEqual(failures, [error]);
2125
+ assert.deepEqual(f.operations, ["event:codex-observation-stopped", ...(sharedStop ? ["retired"] : []),
2126
+ "helper-failed", "codex-observation-stopped"]);
2127
+ }
2128
+ });
2129
+
2130
+ test("original live observation completion preserves a changed main run and reads the current helper registry", async () => {
2131
+ for (const changed of ["thread", "status"]) {
2132
+ const f = await receiptFixture();
2133
+ const error = new Error("Observation failed");
2134
+ const previous = { provider: f.provider, conversationId: "old-helper", runId: "old-turn", status: "inProgress" };
2135
+ let entries = [previous];
2136
+ f.owner.conversations.set(`project:${f.sessionId}`, new Map(entries.map(entry => [entry.conversationId, entry])));
2137
+ const observation = f.owner.createObservation(f.sessionId, {
2138
+ target: () => ({ threadId: f.threadId, pendingMessage: "Stop pending", message: "Stopped" }),
2139
+ retireProvider() { throw new Error("Independent stop must retain its shared provider"); }
2140
+ });
2141
+ await observation.prepare({ provider: f.provider, error });
2142
+ if (changed === "thread") f.run.providerThreadId = "new-thread";
2143
+ else f.run.providerStatus = "completed";
2144
+ const saved = structuredClone(f.run);
2145
+ const failures = [];
2146
+ const current = { provider: f.provider, conversationId: "current-helper", runId: "current-turn", status: "starting",
2147
+ watcher: { failNow(value) { failures.push(value); } } };
2148
+ entries = [current];
2149
+ f.owner.conversations.set(`project:${f.sessionId}`, new Map(entries.map(entry => [entry.conversationId, entry])));
2150
+ await observation.complete({ provider: f.provider, error, sharedStop: false });
2151
+ assert.deepEqual(f.run, saved);
2152
+ assert.equal(previous.status, "inProgress");
2153
+ assert.equal(current.status, "interrupted");
2154
+ assert.deepEqual(failures, [error]);
2155
+ assert.equal(f.provider.observationFailure, null);
2156
+ assert.equal(f.publications.some(event => event.reason === "codex-observation-stopped"), false);
2157
+ }
2158
+ });