@kodelyth/codex 2026.5.42 → 2026.6.1

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 (138) hide show
  1. package/package.json +16 -1
  2. package/doctor-contract-api.test.ts +0 -44
  3. package/doctor-contract-api.ts +0 -68
  4. package/harness.ts +0 -72
  5. package/index.test.ts +0 -230
  6. package/index.ts +0 -66
  7. package/media-understanding-provider.test.ts +0 -486
  8. package/media-understanding-provider.ts +0 -521
  9. package/prompt-overlay-runtime-contract.test.ts +0 -48
  10. package/prompt-overlay.ts +0 -21
  11. package/provider-catalog.ts +0 -83
  12. package/provider-discovery.ts +0 -45
  13. package/provider.test.ts +0 -384
  14. package/provider.ts +0 -243
  15. package/src/app-server/app-inventory-cache.test.ts +0 -176
  16. package/src/app-server/app-inventory-cache.ts +0 -324
  17. package/src/app-server/approval-bridge.test.ts +0 -1471
  18. package/src/app-server/approval-bridge.ts +0 -1211
  19. package/src/app-server/auth-bridge.test.ts +0 -1449
  20. package/src/app-server/auth-bridge.ts +0 -614
  21. package/src/app-server/auth-profile-runtime-contract.test.ts +0 -239
  22. package/src/app-server/capabilities.ts +0 -27
  23. package/src/app-server/client-factory.ts +0 -24
  24. package/src/app-server/client.test.ts +0 -563
  25. package/src/app-server/client.ts +0 -715
  26. package/src/app-server/compact.test.ts +0 -710
  27. package/src/app-server/compact.ts +0 -500
  28. package/src/app-server/computer-use.test.ts +0 -788
  29. package/src/app-server/computer-use.ts +0 -683
  30. package/src/app-server/config.test.ts +0 -879
  31. package/src/app-server/config.ts +0 -1038
  32. package/src/app-server/context-engine-projection.test.ts +0 -252
  33. package/src/app-server/context-engine-projection.ts +0 -403
  34. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +0 -80
  35. package/src/app-server/dynamic-tool-diagnostics.ts +0 -73
  36. package/src/app-server/dynamic-tool-profile.ts +0 -69
  37. package/src/app-server/dynamic-tools.test.ts +0 -1302
  38. package/src/app-server/dynamic-tools.ts +0 -623
  39. package/src/app-server/elicitation-bridge.test.ts +0 -1056
  40. package/src/app-server/elicitation-bridge.ts +0 -783
  41. package/src/app-server/event-projector.test.ts +0 -2668
  42. package/src/app-server/event-projector.ts +0 -2057
  43. package/src/app-server/image-payload-sanitizer.test.ts +0 -49
  44. package/src/app-server/image-payload-sanitizer.ts +0 -167
  45. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +0 -456
  46. package/src/app-server/local-runtime-attribution.ts +0 -39
  47. package/src/app-server/managed-binary.test.ts +0 -139
  48. package/src/app-server/managed-binary.ts +0 -193
  49. package/src/app-server/models.test.ts +0 -246
  50. package/src/app-server/models.ts +0 -172
  51. package/src/app-server/native-hook-relay.test.ts +0 -271
  52. package/src/app-server/native-hook-relay.ts +0 -150
  53. package/src/app-server/native-subagent-task-mirror.test.ts +0 -573
  54. package/src/app-server/native-subagent-task-mirror.ts +0 -497
  55. package/src/app-server/outcome-fallback-runtime-contract.test.ts +0 -404
  56. package/src/app-server/plugin-activation.test.ts +0 -336
  57. package/src/app-server/plugin-activation.ts +0 -283
  58. package/src/app-server/plugin-app-cache-key.ts +0 -74
  59. package/src/app-server/plugin-approval-roundtrip.ts +0 -122
  60. package/src/app-server/plugin-inventory.test.ts +0 -355
  61. package/src/app-server/plugin-inventory.ts +0 -357
  62. package/src/app-server/plugin-thread-config.test.ts +0 -865
  63. package/src/app-server/plugin-thread-config.ts +0 -455
  64. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +0 -33
  65. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +0 -199
  66. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +0 -102
  67. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +0 -227
  68. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +0 -2630
  69. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +0 -2630
  70. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +0 -1659
  71. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +0 -1655
  72. package/src/app-server/protocol-validators.test.ts +0 -75
  73. package/src/app-server/protocol-validators.ts +0 -203
  74. package/src/app-server/protocol.ts +0 -520
  75. package/src/app-server/rate-limit-cache.ts +0 -48
  76. package/src/app-server/rate-limits.test.ts +0 -202
  77. package/src/app-server/rate-limits.ts +0 -583
  78. package/src/app-server/request.ts +0 -73
  79. package/src/app-server/run-attempt.context-engine.test.ts +0 -1004
  80. package/src/app-server/run-attempt.test.ts +0 -9477
  81. package/src/app-server/run-attempt.ts +0 -4683
  82. package/src/app-server/run-attempt.vision-tools.test.ts +0 -35
  83. package/src/app-server/schema-normalization-runtime-contract.test.ts +0 -206
  84. package/src/app-server/session-binding.test.ts +0 -303
  85. package/src/app-server/session-binding.ts +0 -398
  86. package/src/app-server/session-history.ts +0 -44
  87. package/src/app-server/shared-client.test.ts +0 -589
  88. package/src/app-server/shared-client.ts +0 -289
  89. package/src/app-server/side-question.test.ts +0 -1175
  90. package/src/app-server/side-question.ts +0 -1007
  91. package/src/app-server/test-support.ts +0 -48
  92. package/src/app-server/thread-lifecycle.test.ts +0 -447
  93. package/src/app-server/thread-lifecycle.ts +0 -939
  94. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +0 -442
  95. package/src/app-server/timeout.ts +0 -9
  96. package/src/app-server/tool-progress-normalization.ts +0 -77
  97. package/src/app-server/trajectory.test.ts +0 -205
  98. package/src/app-server/trajectory.ts +0 -365
  99. package/src/app-server/transcript-mirror.test.ts +0 -524
  100. package/src/app-server/transcript-mirror.ts +0 -208
  101. package/src/app-server/transcript-repair-runtime-contract.test.ts +0 -44
  102. package/src/app-server/transport-stdio.test.ts +0 -171
  103. package/src/app-server/transport-stdio.ts +0 -107
  104. package/src/app-server/transport-websocket.test.ts +0 -69
  105. package/src/app-server/transport-websocket.ts +0 -90
  106. package/src/app-server/transport.ts +0 -117
  107. package/src/app-server/user-input-bridge.test.ts +0 -249
  108. package/src/app-server/user-input-bridge.ts +0 -316
  109. package/src/app-server/version.ts +0 -4
  110. package/src/app-server/vision-tools.ts +0 -12
  111. package/src/command-account.ts +0 -544
  112. package/src/command-formatters.ts +0 -425
  113. package/src/command-handlers.ts +0 -2004
  114. package/src/command-rpc.test.ts +0 -16
  115. package/src/command-rpc.ts +0 -142
  116. package/src/commands.test.ts +0 -3312
  117. package/src/commands.ts +0 -65
  118. package/src/conversation-binding-data.ts +0 -124
  119. package/src/conversation-binding.test.ts +0 -599
  120. package/src/conversation-binding.ts +0 -561
  121. package/src/conversation-control.test.ts +0 -126
  122. package/src/conversation-control.ts +0 -303
  123. package/src/conversation-turn-collector.test.ts +0 -191
  124. package/src/conversation-turn-collector.ts +0 -186
  125. package/src/conversation-turn-input.test.ts +0 -141
  126. package/src/conversation-turn-input.ts +0 -106
  127. package/src/manifest.test.ts +0 -20
  128. package/src/migration/apply.ts +0 -501
  129. package/src/migration/helpers.ts +0 -55
  130. package/src/migration/plan.ts +0 -461
  131. package/src/migration/provider.test.ts +0 -1741
  132. package/src/migration/provider.ts +0 -41
  133. package/src/migration/source.ts +0 -643
  134. package/src/migration/targets.ts +0 -25
  135. package/src/node-cli-sessions.test.ts +0 -180
  136. package/src/node-cli-sessions.ts +0 -711
  137. package/test-api.ts +0 -82
  138. package/tsconfig.json +0 -16
@@ -1,303 +0,0 @@
1
- import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js";
2
- import {
3
- isCodexFastServiceTier,
4
- resolveCodexAppServerRuntimeOptions,
5
- type CodexAppServerApprovalPolicy,
6
- type CodexAppServerSandboxMode,
7
- } from "./app-server/config.js";
8
- import type { CodexServiceTier, CodexThreadResumeResponse } from "./app-server/protocol.js";
9
- import {
10
- readCodexAppServerBinding,
11
- writeCodexAppServerBinding,
12
- } from "./app-server/session-binding.js";
13
- import { getSharedCodexAppServerClient } from "./app-server/shared-client.js";
14
- import { formatCodexDisplayText } from "./command-formatters.js";
15
-
16
- type ActiveTurn = {
17
- sessionFile: string;
18
- threadId: string;
19
- turnId: string;
20
- };
21
-
22
- type CodexAppServerBindingLookup = NonNullable<Parameters<typeof readCodexAppServerBinding>[1]>;
23
-
24
- type PermissionsMode = "default" | "yolo";
25
-
26
- const CODEX_CONVERSATION_CONTROL_STATE = Symbol.for("klaw.codex.conversationControl");
27
-
28
- function getActiveTurns(): Map<string, ActiveTurn> {
29
- const globalState = globalThis as typeof globalThis & {
30
- [CODEX_CONVERSATION_CONTROL_STATE]?: Map<string, ActiveTurn>;
31
- };
32
- globalState[CODEX_CONVERSATION_CONTROL_STATE] ??= new Map();
33
- return globalState[CODEX_CONVERSATION_CONTROL_STATE];
34
- }
35
-
36
- export function trackCodexConversationActiveTurn(active: ActiveTurn): () => void {
37
- const activeTurns = getActiveTurns();
38
- activeTurns.set(active.sessionFile, active);
39
- return () => {
40
- const current = activeTurns.get(active.sessionFile);
41
- if (current?.turnId === active.turnId) {
42
- activeTurns.delete(active.sessionFile);
43
- }
44
- };
45
- }
46
-
47
- export function readCodexConversationActiveTurn(sessionFile: string): ActiveTurn | undefined {
48
- return getActiveTurns().get(sessionFile);
49
- }
50
-
51
- export async function stopCodexConversationTurn(params: {
52
- sessionFile: string;
53
- pluginConfig?: unknown;
54
- agentDir?: string;
55
- config?: CodexAppServerBindingLookup["config"];
56
- }): Promise<{ stopped: boolean; message: string }> {
57
- const active = readCodexConversationActiveTurn(params.sessionFile);
58
- if (!active) {
59
- return { stopped: false, message: "No active Codex run to stop." };
60
- }
61
- const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
62
- const lookup = buildBindingLookup(params);
63
- const binding = await readCodexAppServerBinding(params.sessionFile, lookup);
64
- const client = await getSharedCodexAppServerClient({
65
- startOptions: runtime.start,
66
- timeoutMs: runtime.requestTimeoutMs,
67
- authProfileId: binding?.authProfileId,
68
- ...lookup,
69
- });
70
- await client.request(
71
- "turn/interrupt",
72
- {
73
- threadId: active.threadId,
74
- turnId: active.turnId,
75
- },
76
- { timeoutMs: runtime.requestTimeoutMs },
77
- );
78
- return { stopped: true, message: "Codex stop requested." };
79
- }
80
-
81
- export async function steerCodexConversationTurn(params: {
82
- sessionFile: string;
83
- message: string;
84
- pluginConfig?: unknown;
85
- agentDir?: string;
86
- config?: CodexAppServerBindingLookup["config"];
87
- }): Promise<{ steered: boolean; message: string }> {
88
- const active = readCodexConversationActiveTurn(params.sessionFile);
89
- const text = params.message.trim();
90
- if (!text) {
91
- return { steered: false, message: "Usage: /codex steer <message>" };
92
- }
93
- if (!active) {
94
- return { steered: false, message: "No active Codex run to steer." };
95
- }
96
- const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
97
- const lookup = buildBindingLookup(params);
98
- const binding = await readCodexAppServerBinding(params.sessionFile, lookup);
99
- const client = await getSharedCodexAppServerClient({
100
- startOptions: runtime.start,
101
- timeoutMs: runtime.requestTimeoutMs,
102
- authProfileId: binding?.authProfileId,
103
- ...lookup,
104
- });
105
- await client.request(
106
- "turn/steer",
107
- {
108
- threadId: active.threadId,
109
- expectedTurnId: active.turnId,
110
- input: [{ type: "text", text, text_elements: [] }],
111
- },
112
- { timeoutMs: runtime.requestTimeoutMs },
113
- );
114
- return { steered: true, message: "Sent steer message to Codex." };
115
- }
116
-
117
- export async function setCodexConversationModel(params: {
118
- sessionFile: string;
119
- model: string;
120
- pluginConfig?: unknown;
121
- agentDir?: string;
122
- config?: CodexAppServerBindingLookup["config"];
123
- }): Promise<string> {
124
- const model = params.model.trim();
125
- if (!model) {
126
- return "Usage: /codex model <model>";
127
- }
128
- const lookup = buildBindingLookup(params);
129
- const binding = await requireThreadBinding(params.sessionFile, lookup);
130
- const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
131
- const response = await resumeThreadWithOverrides({
132
- pluginConfig: params.pluginConfig,
133
- threadId: binding.threadId,
134
- authProfileId: binding.authProfileId,
135
- ...lookup,
136
- model,
137
- });
138
- await writeCodexAppServerBinding(
139
- params.sessionFile,
140
- {
141
- ...binding,
142
- cwd: response.thread.cwd ?? binding.cwd,
143
- model: response.model ?? model,
144
- modelProvider: response.modelProvider ?? binding.modelProvider,
145
- approvalPolicy: binding.approvalPolicy,
146
- sandbox: binding.sandbox,
147
- serviceTier: binding.serviceTier ?? runtime.serviceTier,
148
- },
149
- lookup,
150
- );
151
- return `Codex model set to ${formatCodexDisplayText(response.model ?? model)}.`;
152
- }
153
-
154
- export async function setCodexConversationFastMode(params: {
155
- sessionFile: string;
156
- enabled?: boolean;
157
- pluginConfig?: unknown;
158
- agentDir?: string;
159
- config?: CodexAppServerBindingLookup["config"];
160
- }): Promise<string> {
161
- const lookup = buildBindingLookup(params);
162
- const binding = await requireThreadBinding(params.sessionFile, lookup);
163
- if (params.enabled == null) {
164
- return `Codex fast mode: ${isCodexFastServiceTier(binding.serviceTier) ? "on" : "off"}.`;
165
- }
166
- const serviceTier: CodexServiceTier = params.enabled ? "priority" : "flex";
167
- // Fast mode is sent on each later turn; do not require Codex to accept an
168
- // immediate thread/resume control request just to persist the preference.
169
- await writeCodexAppServerBinding(
170
- params.sessionFile,
171
- {
172
- ...binding,
173
- serviceTier,
174
- },
175
- lookup,
176
- );
177
- return `Codex fast mode ${params.enabled ? "enabled" : "disabled"}.`;
178
- }
179
-
180
- export async function setCodexConversationPermissions(params: {
181
- sessionFile: string;
182
- mode?: PermissionsMode;
183
- pluginConfig?: unknown;
184
- agentDir?: string;
185
- config?: CodexAppServerBindingLookup["config"];
186
- }): Promise<string> {
187
- const lookup = buildBindingLookup(params);
188
- const binding = await requireThreadBinding(params.sessionFile, lookup);
189
- if (!params.mode) {
190
- return `Codex permissions: ${formatPermissionsMode(binding)}.`;
191
- }
192
- const policy = permissionsForMode(params.mode);
193
- // Native bound turns pass these settings at turn/start time, so this command
194
- // can update the local binding even when app-server resume overrides fail.
195
- await writeCodexAppServerBinding(
196
- params.sessionFile,
197
- {
198
- ...binding,
199
- approvalPolicy: policy.approvalPolicy,
200
- sandbox: policy.sandbox,
201
- },
202
- lookup,
203
- );
204
- return `Codex permissions set to ${params.mode === "yolo" ? "full access" : "default"}.`;
205
- }
206
-
207
- export function parseCodexFastModeArg(arg: string | undefined): boolean | undefined {
208
- const normalized = arg?.trim().toLowerCase();
209
- if (!normalized || normalized === "status") {
210
- return undefined;
211
- }
212
- if (normalized === "on" || normalized === "true" || normalized === "fast") {
213
- return true;
214
- }
215
- if (normalized === "off" || normalized === "false" || normalized === "flex") {
216
- return false;
217
- }
218
- return undefined;
219
- }
220
-
221
- export function parseCodexPermissionsModeArg(arg: string | undefined): PermissionsMode | undefined {
222
- const normalized = arg?.trim().toLowerCase();
223
- if (!normalized || normalized === "status") {
224
- return undefined;
225
- }
226
- if (normalized === "yolo" || normalized === "full" || normalized === "full-access") {
227
- return "yolo";
228
- }
229
- if (normalized === "default" || normalized === "guardian") {
230
- return "default";
231
- }
232
- return undefined;
233
- }
234
-
235
- export function formatPermissionsMode(binding: {
236
- approvalPolicy?: CodexAppServerApprovalPolicy;
237
- sandbox?: CodexAppServerSandboxMode;
238
- }): string {
239
- return binding.approvalPolicy === "never" && binding.sandbox === "danger-full-access"
240
- ? "full access"
241
- : "default";
242
- }
243
-
244
- async function requireThreadBinding(sessionFile: string, lookup: CodexAppServerBindingLookup = {}) {
245
- const binding = await readCodexAppServerBinding(sessionFile, lookup);
246
- if (!binding?.threadId) {
247
- throw new Error("No Codex thread is attached to this Klaw session yet.");
248
- }
249
- return binding;
250
- }
251
-
252
- async function resumeThreadWithOverrides(params: {
253
- pluginConfig?: unknown;
254
- threadId: string;
255
- authProfileId?: string;
256
- agentDir?: string;
257
- config?: CodexAppServerBindingLookup["config"];
258
- model?: string;
259
- approvalPolicy?: CodexAppServerApprovalPolicy;
260
- sandbox?: CodexAppServerSandboxMode;
261
- serviceTier?: CodexServiceTier;
262
- }): Promise<CodexThreadResumeResponse> {
263
- const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: params.pluginConfig });
264
- const client = await getSharedCodexAppServerClient({
265
- startOptions: runtime.start,
266
- timeoutMs: runtime.requestTimeoutMs,
267
- authProfileId: params.authProfileId,
268
- ...buildBindingLookup(params),
269
- });
270
- return await client.request(
271
- CODEX_CONTROL_METHODS.resumeThread,
272
- {
273
- threadId: params.threadId,
274
- ...(params.model ? { model: params.model } : {}),
275
- approvalPolicy: params.approvalPolicy ?? runtime.approvalPolicy,
276
- sandbox: params.sandbox ?? runtime.sandbox,
277
- approvalsReviewer: runtime.approvalsReviewer,
278
- ...(params.serviceTier ? { serviceTier: params.serviceTier } : {}),
279
- persistExtendedHistory: true,
280
- },
281
- { timeoutMs: runtime.requestTimeoutMs },
282
- );
283
- }
284
-
285
- function buildBindingLookup(params: {
286
- agentDir?: string;
287
- config?: CodexAppServerBindingLookup["config"];
288
- }): CodexAppServerBindingLookup {
289
- const agentDir = params.agentDir?.trim();
290
- return {
291
- ...(agentDir ? { agentDir } : {}),
292
- ...(params.config ? { config: params.config } : {}),
293
- };
294
- }
295
-
296
- function permissionsForMode(mode: PermissionsMode): {
297
- approvalPolicy: CodexAppServerApprovalPolicy;
298
- sandbox: CodexAppServerSandboxMode;
299
- } {
300
- return mode === "yolo"
301
- ? { approvalPolicy: "never", sandbox: "danger-full-access" }
302
- : { approvalPolicy: "on-request", sandbox: "workspace-write" };
303
- }
@@ -1,191 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { createCodexConversationTurnCollector } from "./conversation-turn-collector.js";
3
-
4
- describe("codex conversation turn collector", () => {
5
- it("collects streamed assistant deltas for the active turn", async () => {
6
- const collector = createCodexConversationTurnCollector("thread-1");
7
- collector.setTurnId("turn-1");
8
- const completion = collector.wait({ timeoutMs: 1_000 });
9
-
10
- collector.handleNotification({
11
- method: "item/agentMessage/delta",
12
- params: { threadId: "thread-1", turnId: "turn-1", itemId: "item-1", delta: "hello " },
13
- });
14
- collector.handleNotification({
15
- method: "item/agentMessage/delta",
16
- params: { threadId: "thread-1", turnId: "turn-1", itemId: "item-1", delta: "world" },
17
- });
18
- collector.handleNotification({
19
- method: "turn/completed",
20
- params: { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] } },
21
- });
22
-
23
- await expect(completion).resolves.toEqual({ replyText: "hello world" });
24
- });
25
-
26
- it("buffers pre-start notifications and replays only the selected turn", async () => {
27
- const collector = createCodexConversationTurnCollector("thread-1");
28
-
29
- collector.handleNotification({
30
- method: "turn/completed",
31
- params: {
32
- threadId: "thread-1",
33
- turn: {
34
- id: "turn-stale",
35
- status: "completed",
36
- items: [{ type: "agentMessage", id: "wrong", text: "stale answer" }],
37
- },
38
- },
39
- });
40
- collector.handleNotification({
41
- method: "item/agentMessage/delta",
42
- params: { threadId: "thread-1", turnId: "turn-1", itemId: "right", delta: "fresh " },
43
- });
44
- collector.handleNotification({
45
- method: "turn/completed",
46
- params: {
47
- threadId: "thread-1",
48
- turn: {
49
- id: "turn-1",
50
- status: "completed",
51
- items: [{ type: "agentMessage", id: "right", text: "fresh answer" }],
52
- },
53
- },
54
- });
55
-
56
- collector.setTurnId("turn-1");
57
-
58
- await expect(collector.wait({ timeoutMs: 1_000 })).resolves.toEqual({
59
- replyText: "fresh answer",
60
- });
61
- });
62
-
63
- it("uses completed agent message items when deltas are absent", async () => {
64
- const collector = createCodexConversationTurnCollector("thread-1");
65
- collector.setTurnId("turn-1");
66
- const completion = collector.wait({ timeoutMs: 1_000 });
67
-
68
- collector.handleNotification({
69
- method: "item/completed",
70
- params: {
71
- threadId: "thread-1",
72
- turnId: "turn-1",
73
- item: { type: "agentMessage", id: "item-1", text: "final answer" },
74
- },
75
- });
76
- collector.handleNotification({
77
- method: "turn/completed",
78
- params: { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] } },
79
- });
80
-
81
- await expect(completion).resolves.toEqual({ replyText: "final answer" });
82
- });
83
-
84
- it("ignores notifications for other threads or turns", async () => {
85
- const collector = createCodexConversationTurnCollector("thread-1");
86
- collector.setTurnId("turn-1");
87
- const completion = collector.wait({ timeoutMs: 1_000 });
88
-
89
- collector.handleNotification({
90
- method: "item/agentMessage/delta",
91
- params: { threadId: "thread-2", turnId: "turn-1", itemId: "wrong", delta: "wrong" },
92
- });
93
- collector.handleNotification({
94
- method: "item/agentMessage/delta",
95
- params: { threadId: "thread-1", turnId: "turn-2", itemId: "wrong", delta: "wrong" },
96
- });
97
- collector.handleNotification({
98
- method: "turn/completed",
99
- params: {
100
- threadId: "thread-1",
101
- turn: {
102
- id: "turn-1",
103
- status: "completed",
104
- items: [{ type: "agentMessage", id: "item-1", text: "right" }],
105
- },
106
- },
107
- });
108
-
109
- await expect(completion).resolves.toEqual({ replyText: "right" });
110
- });
111
-
112
- it("ignores unscoped deltas once the active turn is known", async () => {
113
- const collector = createCodexConversationTurnCollector("thread-1");
114
- collector.setTurnId("turn-1");
115
- const completion = collector.wait({ timeoutMs: 1_000 });
116
-
117
- collector.handleNotification({
118
- method: "item/agentMessage/delta",
119
- params: { threadId: "thread-1", itemId: "wrong", delta: "wrong" },
120
- });
121
- collector.handleNotification({
122
- method: "item/agentMessage/delta",
123
- params: { threadId: "thread-1", turnId: "turn-1", itemId: "right", delta: "right" },
124
- });
125
- collector.handleNotification({
126
- method: "turn/completed",
127
- params: { threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] } },
128
- });
129
-
130
- await expect(completion).resolves.toEqual({ replyText: "right" });
131
- });
132
-
133
- it("does not complete from unscoped turn completion once the active turn is known", async () => {
134
- const collector = createCodexConversationTurnCollector("thread-1");
135
- collector.setTurnId("turn-1");
136
- const completion = collector.wait({ timeoutMs: 1_000 });
137
-
138
- collector.handleNotification({
139
- method: "turn/completed",
140
- params: {
141
- threadId: "thread-1",
142
- turn: {
143
- status: "completed",
144
- items: [{ type: "agentMessage", id: "wrong", text: "wrong" }],
145
- },
146
- },
147
- });
148
- collector.handleNotification({
149
- method: "turn/completed",
150
- params: {
151
- threadId: "thread-1",
152
- turn: {
153
- id: "turn-1",
154
- status: "completed",
155
- items: [{ type: "agentMessage", id: "right", text: "right" }],
156
- },
157
- },
158
- });
159
-
160
- await expect(completion).resolves.toEqual({ replyText: "right" });
161
- });
162
-
163
- it("rejects failed turns with the app-server error message", async () => {
164
- const collector = createCodexConversationTurnCollector("thread-1");
165
- collector.setTurnId("turn-1");
166
- const completion = collector.wait({ timeoutMs: 1_000 });
167
-
168
- collector.handleNotification({
169
- method: "turn/completed",
170
- params: {
171
- threadId: "thread-1",
172
- turn: { id: "turn-1", status: "failed", error: { message: "model exploded" }, items: [] },
173
- },
174
- });
175
-
176
- await expect(completion).rejects.toThrow("model exploded");
177
- });
178
-
179
- it("times out when the app-server never completes the turn", async () => {
180
- vi.useFakeTimers();
181
- try {
182
- const collector = createCodexConversationTurnCollector("thread-1");
183
- const completion = collector.wait({ timeoutMs: 100 });
184
- const assertion = expect(completion).rejects.toThrow("codex app-server bound turn timed out");
185
- await vi.advanceTimersByTimeAsync(100);
186
- await assertion;
187
- } finally {
188
- vi.useRealTimers();
189
- }
190
- });
191
- });
@@ -1,186 +0,0 @@
1
- import {
2
- isJsonObject,
3
- type CodexServerNotification,
4
- type JsonObject,
5
- } from "./app-server/protocol.js";
6
-
7
- const MAX_PENDING_NOTIFICATIONS_PER_TURN = 100;
8
-
9
- export function createCodexConversationTurnCollector(threadId: string) {
10
- let turnId: string | undefined;
11
- let completed = false;
12
- let failedError: string | undefined;
13
- let timeout: ReturnType<typeof setTimeout> | undefined;
14
- const assistantTextByItem = new Map<string, string>();
15
- const assistantOrder: string[] = [];
16
- const pendingNotificationsByTurnId = new Map<string, CodexServerNotification[]>();
17
- let resolveCompletion: ((value: { replyText: string }) => void) | undefined;
18
- let rejectCompletion: ((error: Error) => void) | undefined;
19
-
20
- const rememberItem = (itemId: string) => {
21
- if (!assistantOrder.includes(itemId)) {
22
- assistantOrder.push(itemId);
23
- }
24
- };
25
- const collectReplyText = (): string => {
26
- const texts = assistantOrder
27
- .map((itemId) => assistantTextByItem.get(itemId)?.trim())
28
- .filter((text): text is string => Boolean(text));
29
- return texts.at(-1) ?? "";
30
- };
31
- const clearWaitState = () => {
32
- if (timeout) {
33
- clearTimeout(timeout);
34
- timeout = undefined;
35
- }
36
- resolveCompletion = undefined;
37
- rejectCompletion = undefined;
38
- };
39
- const finish = () => {
40
- if (completed) {
41
- return;
42
- }
43
- completed = true;
44
- if (failedError) {
45
- rejectCompletion?.(new Error(failedError));
46
- } else {
47
- resolveCompletion?.({ replyText: collectReplyText() });
48
- }
49
- clearWaitState();
50
- };
51
-
52
- const handleNotification = (notification: CodexServerNotification) => {
53
- const params = isJsonObject(notification.params) ? notification.params : undefined;
54
- if (!params || readString(params, "threadId") !== threadId) {
55
- return;
56
- }
57
- if (!turnId) {
58
- const pendingTurnId = readNotificationTurnId(params);
59
- if (pendingTurnId) {
60
- const pending = pendingNotificationsByTurnId.get(pendingTurnId) ?? [];
61
- if (pending.length < MAX_PENDING_NOTIFICATIONS_PER_TURN) {
62
- pending.push(notification);
63
- pendingNotificationsByTurnId.set(pendingTurnId, pending);
64
- }
65
- }
66
- return;
67
- }
68
- if (!isNotificationForTurn(params, threadId, turnId)) {
69
- return;
70
- }
71
- if (notification.method === "item/agentMessage/delta") {
72
- const itemId = readString(params, "itemId") ?? readString(params, "id") ?? "assistant";
73
- const delta = readTextString(params, "delta");
74
- if (!delta) {
75
- return;
76
- }
77
- rememberItem(itemId);
78
- assistantTextByItem.set(itemId, `${assistantTextByItem.get(itemId) ?? ""}${delta}`);
79
- return;
80
- }
81
- if (notification.method === "item/completed") {
82
- const item = isJsonObject(params.item) ? params.item : undefined;
83
- if (item?.type === "agentMessage") {
84
- const itemId = readString(item, "id") ?? readString(params, "itemId") ?? "assistant";
85
- const text = readTextString(item, "text");
86
- if (text) {
87
- rememberItem(itemId);
88
- assistantTextByItem.set(itemId, text);
89
- }
90
- }
91
- return;
92
- }
93
- if (notification.method === "turn/completed") {
94
- const turn = isJsonObject(params.turn) ? params.turn : undefined;
95
- const status = readString(turn, "status");
96
- if (status === "failed") {
97
- failedError =
98
- readString(readRecord(turn?.error), "message") ?? "codex app-server turn failed";
99
- }
100
- const items = Array.isArray(turn?.items) ? turn.items : [];
101
- for (const item of items) {
102
- if (!isJsonObject(item) || item.type !== "agentMessage") {
103
- continue;
104
- }
105
- const itemId = readString(item, "id") ?? `assistant-${assistantOrder.length + 1}`;
106
- const text = readTextString(item, "text");
107
- if (text) {
108
- rememberItem(itemId);
109
- assistantTextByItem.set(itemId, text);
110
- }
111
- }
112
- finish();
113
- }
114
- };
115
-
116
- return {
117
- setTurnId(nextTurnId: string) {
118
- turnId = nextTurnId;
119
- const pending = pendingNotificationsByTurnId.get(nextTurnId) ?? [];
120
- pendingNotificationsByTurnId.clear();
121
- for (const notification of pending) {
122
- handleNotification(notification);
123
- }
124
- },
125
- handleNotification,
126
- wait(params: { timeoutMs: number }): Promise<{ replyText: string }> {
127
- if (completed) {
128
- return failedError
129
- ? Promise.reject(new Error(failedError))
130
- : Promise.resolve({ replyText: collectReplyText() });
131
- }
132
- return new Promise<{ replyText: string }>((resolve, reject) => {
133
- resolveCompletion = resolve;
134
- rejectCompletion = reject;
135
- timeout = setTimeout(
136
- () => {
137
- completed = true;
138
- reject(new Error("codex app-server bound turn timed out"));
139
- clearWaitState();
140
- },
141
- Math.max(100, params.timeoutMs),
142
- );
143
- timeout.unref?.();
144
- });
145
- },
146
- };
147
- }
148
-
149
- function isNotificationForTurn(
150
- params: JsonObject,
151
- threadId: string,
152
- turnId: string | undefined,
153
- ): boolean {
154
- if (readString(params, "threadId") !== threadId) {
155
- return false;
156
- }
157
- if (!turnId) {
158
- return true;
159
- }
160
- const directTurnId = readString(params, "turnId");
161
- if (directTurnId) {
162
- return directTurnId === turnId;
163
- }
164
- const turn = isJsonObject(params.turn) ? params.turn : undefined;
165
- return readString(turn, "id") === turnId;
166
- }
167
-
168
- function readNotificationTurnId(params: JsonObject): string | undefined {
169
- return readString(params, "turnId") ?? readString(readRecord(params.turn), "id");
170
- }
171
-
172
- function readRecord(value: unknown): Record<string, unknown> | undefined {
173
- return value && typeof value === "object" && !Array.isArray(value)
174
- ? (value as Record<string, unknown>)
175
- : undefined;
176
- }
177
-
178
- function readString(record: Record<string, unknown> | JsonObject | undefined, key: string) {
179
- const value = record?.[key];
180
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
181
- }
182
-
183
- function readTextString(record: Record<string, unknown> | JsonObject | undefined, key: string) {
184
- const value = record?.[key];
185
- return typeof value === "string" && value.length > 0 ? value : undefined;
186
- }