@kodelyth/codex 2026.5.40 → 2026.5.42

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 (178) hide show
  1. package/dist/client-ChMX13_o.js +642 -0
  2. package/dist/client-factory-D3dIsp4Y.js +9 -0
  3. package/dist/command-formatters-BRW7_Nu7.js +519 -0
  4. package/dist/command-handlers-P2IqtXaZ.js +1462 -0
  5. package/dist/compact-baos5flR.js +329 -0
  6. package/dist/computer-use-VfLvTMaa.js +367 -0
  7. package/dist/config-CezENx_E.js +510 -0
  8. package/dist/doctor-contract-api.js +53 -0
  9. package/dist/harness.js +51 -0
  10. package/dist/index.js +1133 -0
  11. package/dist/media-understanding-provider.js +335 -0
  12. package/dist/models-B9DhrIwD.js +110 -0
  13. package/dist/node-cli-sessions-De4_DuFw.js +1216 -0
  14. package/dist/plugin-activation-BlMuJeXz.js +452 -0
  15. package/dist/prompt-overlay.js +12 -0
  16. package/dist/protocol-C9UWI98H.js +9 -0
  17. package/dist/protocol-validators-BGBspNmF.js +5988 -0
  18. package/dist/provider-catalog.js +84 -0
  19. package/dist/provider-discovery.js +33 -0
  20. package/dist/provider.js +150 -0
  21. package/dist/rate-limit-cache-CHuacE27.js +24 -0
  22. package/dist/request-CTQKUxaa.js +89 -0
  23. package/dist/rolldown-runtime-DUslC3ob.js +14 -0
  24. package/dist/run-attempt-DqV2OU1R.js +5366 -0
  25. package/dist/session-binding-3PzU7ZTW.js +222 -0
  26. package/dist/shared-client-Cnyr9dyT.js +631 -0
  27. package/dist/side-question-CP5XlA0U.js +667 -0
  28. package/dist/test-api.js +45 -0
  29. package/dist/thread-lifecycle-DBJetBuV.js +1561 -0
  30. package/dist/vision-tools-Cl_5a93K.js +1379 -0
  31. package/doctor-contract-api.test.ts +44 -0
  32. package/doctor-contract-api.ts +68 -0
  33. package/harness.ts +72 -0
  34. package/index.test.ts +230 -0
  35. package/index.ts +66 -0
  36. package/klaw.plugin.json +24 -85
  37. package/media-understanding-provider.test.ts +486 -0
  38. package/media-understanding-provider.ts +521 -0
  39. package/package.json +3 -3
  40. package/prompt-overlay-runtime-contract.test.ts +48 -0
  41. package/prompt-overlay.ts +21 -0
  42. package/provider-catalog.ts +83 -0
  43. package/provider-discovery.ts +45 -0
  44. package/provider.test.ts +384 -0
  45. package/provider.ts +243 -0
  46. package/src/app-server/app-inventory-cache.test.ts +176 -0
  47. package/src/app-server/app-inventory-cache.ts +324 -0
  48. package/src/app-server/approval-bridge.test.ts +1471 -0
  49. package/src/app-server/approval-bridge.ts +1211 -0
  50. package/src/app-server/auth-bridge.test.ts +1449 -0
  51. package/src/app-server/auth-bridge.ts +614 -0
  52. package/src/app-server/auth-profile-runtime-contract.test.ts +239 -0
  53. package/src/app-server/capabilities.ts +27 -0
  54. package/src/app-server/client-factory.ts +24 -0
  55. package/src/app-server/client.test.ts +563 -0
  56. package/src/app-server/client.ts +715 -0
  57. package/src/app-server/compact.test.ts +710 -0
  58. package/src/app-server/compact.ts +500 -0
  59. package/src/app-server/computer-use.test.ts +788 -0
  60. package/src/app-server/computer-use.ts +683 -0
  61. package/src/app-server/config.test.ts +879 -0
  62. package/src/app-server/config.ts +1038 -0
  63. package/src/app-server/context-engine-projection.test.ts +252 -0
  64. package/src/app-server/context-engine-projection.ts +403 -0
  65. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +80 -0
  66. package/src/app-server/dynamic-tool-diagnostics.ts +73 -0
  67. package/src/app-server/dynamic-tool-profile.ts +69 -0
  68. package/src/app-server/dynamic-tools.test.ts +1302 -0
  69. package/src/app-server/dynamic-tools.ts +623 -0
  70. package/src/app-server/elicitation-bridge.test.ts +1056 -0
  71. package/src/app-server/elicitation-bridge.ts +783 -0
  72. package/src/app-server/event-projector.test.ts +2668 -0
  73. package/src/app-server/event-projector.ts +2057 -0
  74. package/src/app-server/image-payload-sanitizer.test.ts +49 -0
  75. package/src/app-server/image-payload-sanitizer.ts +167 -0
  76. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +456 -0
  77. package/src/app-server/local-runtime-attribution.ts +39 -0
  78. package/src/app-server/managed-binary.test.ts +139 -0
  79. package/src/app-server/managed-binary.ts +193 -0
  80. package/src/app-server/models.test.ts +246 -0
  81. package/src/app-server/models.ts +172 -0
  82. package/src/app-server/native-hook-relay.test.ts +271 -0
  83. package/src/app-server/native-hook-relay.ts +150 -0
  84. package/src/app-server/native-subagent-task-mirror.test.ts +573 -0
  85. package/src/app-server/native-subagent-task-mirror.ts +497 -0
  86. package/src/app-server/outcome-fallback-runtime-contract.test.ts +404 -0
  87. package/src/app-server/plugin-activation.test.ts +336 -0
  88. package/src/app-server/plugin-activation.ts +283 -0
  89. package/src/app-server/plugin-app-cache-key.ts +74 -0
  90. package/src/app-server/plugin-approval-roundtrip.ts +122 -0
  91. package/src/app-server/plugin-inventory.test.ts +355 -0
  92. package/src/app-server/plugin-inventory.ts +357 -0
  93. package/src/app-server/plugin-thread-config.test.ts +865 -0
  94. package/src/app-server/plugin-thread-config.ts +455 -0
  95. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
  96. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
  97. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
  98. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
  99. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
  100. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
  101. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
  102. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
  103. package/src/app-server/protocol-validators.test.ts +75 -0
  104. package/src/app-server/protocol-validators.ts +203 -0
  105. package/src/app-server/protocol.ts +520 -0
  106. package/src/app-server/rate-limit-cache.ts +48 -0
  107. package/src/app-server/rate-limits.test.ts +202 -0
  108. package/src/app-server/rate-limits.ts +583 -0
  109. package/src/app-server/request.ts +73 -0
  110. package/src/app-server/run-attempt.context-engine.test.ts +1004 -0
  111. package/src/app-server/run-attempt.test.ts +9477 -0
  112. package/src/app-server/run-attempt.ts +4683 -0
  113. package/src/app-server/run-attempt.vision-tools.test.ts +35 -0
  114. package/src/app-server/schema-normalization-runtime-contract.test.ts +206 -0
  115. package/src/app-server/session-binding.test.ts +303 -0
  116. package/src/app-server/session-binding.ts +398 -0
  117. package/src/app-server/session-history.ts +44 -0
  118. package/src/app-server/shared-client.test.ts +589 -0
  119. package/src/app-server/shared-client.ts +289 -0
  120. package/src/app-server/side-question.test.ts +1175 -0
  121. package/src/app-server/side-question.ts +1007 -0
  122. package/src/app-server/test-support.ts +48 -0
  123. package/src/app-server/thread-lifecycle.test.ts +447 -0
  124. package/src/app-server/thread-lifecycle.ts +939 -0
  125. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +442 -0
  126. package/src/app-server/timeout.ts +9 -0
  127. package/src/app-server/tool-progress-normalization.ts +77 -0
  128. package/src/app-server/trajectory.test.ts +205 -0
  129. package/src/app-server/trajectory.ts +365 -0
  130. package/src/app-server/transcript-mirror.test.ts +524 -0
  131. package/src/app-server/transcript-mirror.ts +208 -0
  132. package/src/app-server/transcript-repair-runtime-contract.test.ts +44 -0
  133. package/src/app-server/transport-stdio.test.ts +171 -0
  134. package/src/app-server/transport-stdio.ts +107 -0
  135. package/src/app-server/transport-websocket.test.ts +69 -0
  136. package/src/app-server/transport-websocket.ts +90 -0
  137. package/src/app-server/transport.ts +117 -0
  138. package/src/app-server/user-input-bridge.test.ts +249 -0
  139. package/src/app-server/user-input-bridge.ts +316 -0
  140. package/src/app-server/version.ts +4 -0
  141. package/src/app-server/vision-tools.ts +12 -0
  142. package/src/command-account.ts +544 -0
  143. package/src/command-formatters.ts +425 -0
  144. package/src/command-handlers.ts +2004 -0
  145. package/src/command-rpc.test.ts +16 -0
  146. package/src/command-rpc.ts +142 -0
  147. package/src/commands.test.ts +3312 -0
  148. package/src/commands.ts +65 -0
  149. package/src/conversation-binding-data.ts +124 -0
  150. package/src/conversation-binding.test.ts +599 -0
  151. package/src/conversation-binding.ts +561 -0
  152. package/src/conversation-control.test.ts +126 -0
  153. package/src/conversation-control.ts +303 -0
  154. package/src/conversation-turn-collector.test.ts +191 -0
  155. package/src/conversation-turn-collector.ts +186 -0
  156. package/src/conversation-turn-input.test.ts +141 -0
  157. package/src/conversation-turn-input.ts +106 -0
  158. package/src/manifest.test.ts +20 -0
  159. package/src/migration/apply.ts +501 -0
  160. package/src/migration/helpers.ts +55 -0
  161. package/src/migration/plan.ts +461 -0
  162. package/src/migration/provider.test.ts +1741 -0
  163. package/src/migration/provider.ts +41 -0
  164. package/src/migration/source.ts +643 -0
  165. package/src/migration/targets.ts +25 -0
  166. package/src/node-cli-sessions.test.ts +180 -0
  167. package/src/node-cli-sessions.ts +711 -0
  168. package/test-api.ts +82 -0
  169. package/tsconfig.json +16 -0
  170. package/doctor-contract-api.js +0 -7
  171. package/harness.js +0 -7
  172. package/index.js +0 -7
  173. package/media-understanding-provider.js +0 -7
  174. package/prompt-overlay.js +0 -7
  175. package/provider-catalog.js +0 -7
  176. package/provider-discovery.js +0 -7
  177. package/provider.js +0 -7
  178. package/test-api.js +0 -7
@@ -0,0 +1,599 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const sharedClientMocks = vi.hoisted(() => ({
7
+ getSharedCodexAppServerClient: vi.fn(),
8
+ }));
9
+
10
+ const agentRuntimeMocks = vi.hoisted(() => ({
11
+ ensureAuthProfileStore: vi.fn(),
12
+ loadAuthProfileStoreForSecretsRuntime: vi.fn(),
13
+ resolveApiKeyForProfile: vi.fn(),
14
+ resolveAuthProfileOrder: vi.fn(),
15
+ resolveDefaultAgentDir: vi.fn(() => "/agent"),
16
+ resolvePersistedAuthProfileOwnerAgentDir: vi.fn(),
17
+ resolveProviderIdForAuth: vi.fn((provider: string) => provider),
18
+ saveAuthProfileStore: vi.fn(),
19
+ }));
20
+
21
+ vi.mock("./app-server/shared-client.js", () => sharedClientMocks);
22
+ vi.mock("klaw/plugin-sdk/agent-runtime", () => agentRuntimeMocks);
23
+
24
+ import {
25
+ handleCodexConversationBindingResolved,
26
+ handleCodexConversationInboundClaim,
27
+ startCodexConversationThread,
28
+ } from "./conversation-binding.js";
29
+
30
+ let tempDir: string;
31
+
32
+ function mockCallArg(mock: ReturnType<typeof vi.fn>, callIndex = 0, argIndex = 0): unknown {
33
+ const call = mock.mock.calls[callIndex];
34
+ if (!call) {
35
+ throw new Error(`Expected mock call ${callIndex}`);
36
+ }
37
+ return call[argIndex];
38
+ }
39
+
40
+ describe("codex conversation binding", () => {
41
+ beforeEach(async () => {
42
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "klaw-codex-binding-"));
43
+ });
44
+
45
+ afterEach(async () => {
46
+ sharedClientMocks.getSharedCodexAppServerClient.mockReset();
47
+ agentRuntimeMocks.ensureAuthProfileStore.mockReset();
48
+ agentRuntimeMocks.loadAuthProfileStoreForSecretsRuntime.mockReset();
49
+ agentRuntimeMocks.resolveApiKeyForProfile.mockReset();
50
+ agentRuntimeMocks.resolveAuthProfileOrder.mockReset();
51
+ agentRuntimeMocks.resolveDefaultAgentDir.mockClear();
52
+ agentRuntimeMocks.resolvePersistedAuthProfileOwnerAgentDir.mockReset();
53
+ agentRuntimeMocks.resolveProviderIdForAuth.mockClear();
54
+ agentRuntimeMocks.saveAuthProfileStore.mockReset();
55
+ await fs.rm(tempDir, { recursive: true, force: true });
56
+ });
57
+
58
+ beforeEach(() => {
59
+ agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
60
+ version: 1,
61
+ profiles: {},
62
+ });
63
+ agentRuntimeMocks.resolveAuthProfileOrder.mockReturnValue([]);
64
+ agentRuntimeMocks.resolveDefaultAgentDir.mockReturnValue("/agent");
65
+ agentRuntimeMocks.resolveProviderIdForAuth.mockImplementation((provider: string) => provider);
66
+ });
67
+
68
+ it("uses the default Codex auth profile and omits the public OpenAI provider for new binds", async () => {
69
+ const sessionFile = path.join(tempDir, "session.jsonl");
70
+ const config = {
71
+ auth: { order: { "openai-codex": ["openai-codex:default"] } },
72
+ };
73
+ const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
74
+ agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
75
+ version: 1,
76
+ profiles: {
77
+ "openai-codex:default": {
78
+ type: "oauth",
79
+ provider: "openai-codex",
80
+ access: "access-token",
81
+ },
82
+ },
83
+ });
84
+ agentRuntimeMocks.resolveAuthProfileOrder.mockReturnValue(["openai-codex:default"]);
85
+ sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
86
+ request: vi.fn(async (method: string, requestParams: Record<string, unknown>) => {
87
+ requests.push({ method, params: requestParams });
88
+ return {
89
+ thread: { id: "thread-new", sessionId: "session-1", cwd: tempDir },
90
+ model: "gpt-5.4-mini",
91
+ };
92
+ }),
93
+ });
94
+
95
+ await startCodexConversationThread({
96
+ config: config as never,
97
+ sessionFile,
98
+ workspaceDir: tempDir,
99
+ model: "gpt-5.4-mini",
100
+ modelProvider: "openai",
101
+ });
102
+
103
+ const authOrderParams = mockCallArg(agentRuntimeMocks.resolveAuthProfileOrder) as {
104
+ cfg?: unknown;
105
+ provider?: unknown;
106
+ };
107
+ expect(authOrderParams?.cfg).toBe(config);
108
+ expect(authOrderParams?.provider).toBe("openai-codex");
109
+ const sharedClientParams = mockCallArg(sharedClientMocks.getSharedCodexAppServerClient) as {
110
+ authProfileId?: unknown;
111
+ };
112
+ expect(sharedClientParams?.authProfileId).toBe("openai-codex:default");
113
+ expect(requests).toHaveLength(1);
114
+ expect(requests[0]?.method).toBe("thread/start");
115
+ expect(requests[0]?.params.model).toBe("gpt-5.4-mini");
116
+ expect(requests[0]?.params).not.toHaveProperty("modelProvider");
117
+ await expect(fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8")).resolves.toContain(
118
+ '"authProfileId": "openai-codex:default"',
119
+ );
120
+ });
121
+
122
+ it("preserves Codex auth and omits the public OpenAI provider for native bind threads", async () => {
123
+ const sessionFile = path.join(tempDir, "session.jsonl");
124
+ agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
125
+ version: 1,
126
+ profiles: {
127
+ work: {
128
+ type: "oauth",
129
+ provider: "openai-codex",
130
+ access: "access-token",
131
+ refresh: "refresh-token",
132
+ expires: Date.now() + 60_000,
133
+ },
134
+ },
135
+ });
136
+ await fs.writeFile(
137
+ `${sessionFile}.codex-app-server.json`,
138
+ JSON.stringify({
139
+ schemaVersion: 1,
140
+ threadId: "thread-old",
141
+ cwd: tempDir,
142
+ authProfileId: "work",
143
+ modelProvider: "openai",
144
+ }),
145
+ );
146
+ const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
147
+ sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
148
+ request: vi.fn(async (method: string, requestParams: Record<string, unknown>) => {
149
+ requests.push({ method, params: requestParams });
150
+ return {
151
+ thread: { id: "thread-new", sessionId: "session-1", cwd: tempDir },
152
+ model: "gpt-5.4-mini",
153
+ modelProvider: "openai",
154
+ };
155
+ }),
156
+ });
157
+
158
+ await startCodexConversationThread({
159
+ sessionFile,
160
+ workspaceDir: tempDir,
161
+ model: "gpt-5.4-mini",
162
+ modelProvider: "openai",
163
+ });
164
+
165
+ const sharedClientParams = mockCallArg(sharedClientMocks.getSharedCodexAppServerClient) as {
166
+ authProfileId?: unknown;
167
+ };
168
+ expect(sharedClientParams?.authProfileId).toBe("work");
169
+ expect(requests).toHaveLength(1);
170
+ expect(requests[0]?.method).toBe("thread/start");
171
+ expect(requests[0]?.params.model).toBe("gpt-5.4-mini");
172
+ expect(requests[0]?.params).not.toHaveProperty("modelProvider");
173
+ await expect(fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8")).resolves.toContain(
174
+ '"authProfileId": "work"',
175
+ );
176
+ await expect(
177
+ fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8"),
178
+ ).resolves.not.toContain('"modelProvider": "openai"');
179
+ });
180
+
181
+ it("stores and uses the owning agent dir for bound app-server sessions", async () => {
182
+ const sessionFile = path.join(tempDir, "session.jsonl");
183
+ const agentDir = path.join(tempDir, "agents", "bot-a", "agent");
184
+ sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
185
+ request: vi.fn(async () => ({
186
+ thread: { id: "thread-new", sessionId: "session-1", cwd: tempDir },
187
+ model: "gpt-5.4-mini",
188
+ })),
189
+ });
190
+
191
+ const data = await startCodexConversationThread({
192
+ sessionFile,
193
+ workspaceDir: tempDir,
194
+ agentDir,
195
+ model: "gpt-5.4-mini",
196
+ });
197
+
198
+ const sharedClientParams = mockCallArg(sharedClientMocks.getSharedCodexAppServerClient) as {
199
+ agentDir?: unknown;
200
+ };
201
+ expect(sharedClientParams?.agentDir).toBe(agentDir);
202
+ expect(data.agentDir).toBe(agentDir);
203
+ });
204
+
205
+ it("clears the Codex app-server sidecar when a pending bind is denied", async () => {
206
+ const sessionFile = path.join(tempDir, "session.jsonl");
207
+ const sidecar = `${sessionFile}.codex-app-server.json`;
208
+ await fs.writeFile(sidecar, JSON.stringify({ schemaVersion: 1, threadId: "thread-1" }));
209
+
210
+ await handleCodexConversationBindingResolved({
211
+ status: "denied",
212
+ decision: "deny",
213
+ request: {
214
+ data: {
215
+ kind: "codex-app-server-session",
216
+ version: 1,
217
+ sessionFile,
218
+ workspaceDir: tempDir,
219
+ },
220
+ conversation: {
221
+ channel: "discord",
222
+ accountId: "default",
223
+ conversationId: "channel:1",
224
+ },
225
+ },
226
+ });
227
+
228
+ await expect(fs.stat(sidecar)).rejects.toHaveProperty("code", "ENOENT");
229
+ });
230
+
231
+ it("consumes inbound bound messages when command authorization is absent", async () => {
232
+ const result = await handleCodexConversationInboundClaim(
233
+ {
234
+ content: "run this",
235
+ channel: "discord",
236
+ isGroup: true,
237
+ },
238
+ {
239
+ channelId: "discord",
240
+ pluginBinding: {
241
+ bindingId: "binding-1",
242
+ pluginId: "codex",
243
+ pluginRoot: tempDir,
244
+ channel: "discord",
245
+ accountId: "default",
246
+ conversationId: "channel-1",
247
+ boundAt: Date.now(),
248
+ data: {
249
+ kind: "codex-app-server-session",
250
+ version: 1,
251
+ sessionFile: path.join(tempDir, "session.jsonl"),
252
+ workspaceDir: tempDir,
253
+ },
254
+ },
255
+ },
256
+ );
257
+
258
+ expect(result).toEqual({ handled: true });
259
+ });
260
+
261
+ it("routes bound Codex CLI node sessions through node resume", async () => {
262
+ const resumeCodexCliSessionOnNode = vi.fn(async () => ({
263
+ ok: true as const,
264
+ sessionId: "019e2007-1f7e-7eb1-a42b-8c01f4b9b5cd",
265
+ text: "done",
266
+ }));
267
+
268
+ const result = await handleCodexConversationInboundClaim(
269
+ {
270
+ content: "continue the task",
271
+ channel: "discord",
272
+ isGroup: true,
273
+ commandAuthorized: true,
274
+ },
275
+ {
276
+ channelId: "discord",
277
+ pluginBinding: {
278
+ bindingId: "binding-1",
279
+ pluginId: "codex",
280
+ pluginRoot: tempDir,
281
+ channel: "discord",
282
+ accountId: "default",
283
+ conversationId: "channel-1",
284
+ boundAt: Date.now(),
285
+ data: {
286
+ kind: "codex-cli-node-session",
287
+ version: 1,
288
+ nodeId: "mb-m5",
289
+ sessionId: "019e2007-1f7e-7eb1-a42b-8c01f4b9b5cd",
290
+ cwd: "/repo",
291
+ },
292
+ },
293
+ },
294
+ {
295
+ resumeCodexCliSessionOnNode,
296
+ timeoutMs: 1234,
297
+ },
298
+ );
299
+
300
+ expect(result).toEqual({ handled: true, reply: { text: "done" } });
301
+ expect(resumeCodexCliSessionOnNode).toHaveBeenCalledWith({
302
+ nodeId: "mb-m5",
303
+ sessionId: "019e2007-1f7e-7eb1-a42b-8c01f4b9b5cd",
304
+ prompt: "continue the task",
305
+ cwd: "/repo",
306
+ timeoutMs: 1234,
307
+ });
308
+ });
309
+
310
+ it("recreates a missing bound thread and preserves auth plus turn overrides", async () => {
311
+ const sessionFile = path.join(tempDir, "session.jsonl");
312
+ agentRuntimeMocks.ensureAuthProfileStore.mockReturnValue({
313
+ version: 1,
314
+ profiles: {
315
+ work: {
316
+ type: "oauth",
317
+ provider: "openai-codex",
318
+ access: "access-token",
319
+ },
320
+ },
321
+ });
322
+ await fs.writeFile(
323
+ `${sessionFile}.codex-app-server.json`,
324
+ JSON.stringify({
325
+ schemaVersion: 1,
326
+ threadId: "thread-old",
327
+ cwd: tempDir,
328
+ authProfileId: "work",
329
+ model: "gpt-5.4-mini",
330
+ modelProvider: "openai",
331
+ approvalPolicy: "on-request",
332
+ sandbox: "workspace-write",
333
+ serviceTier: "fast",
334
+ }),
335
+ );
336
+ const requests: Array<{ method: string; params: Record<string, unknown> }> = [];
337
+ const notificationHandlers: Array<(notification: Record<string, unknown>) => void> = [];
338
+ sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
339
+ request: vi.fn(async (method: string, requestParams: Record<string, unknown>) => {
340
+ requests.push({ method, params: requestParams });
341
+ if (method === "turn/start" && requestParams.threadId === "thread-old") {
342
+ throw new Error("thread not found: thread-old");
343
+ }
344
+ if (method === "thread/start") {
345
+ return {
346
+ thread: { id: "thread-new", sessionId: "session-1", cwd: tempDir },
347
+ model: "gpt-5.4-mini",
348
+ };
349
+ }
350
+ if (method === "turn/start" && requestParams.threadId === "thread-new") {
351
+ setImmediate(() => {
352
+ for (const handler of notificationHandlers) {
353
+ handler({
354
+ method: "turn/completed",
355
+ params: {
356
+ threadId: "thread-new",
357
+ turn: {
358
+ id: "turn-new",
359
+ status: "completed",
360
+ items: [
361
+ {
362
+ id: "assistant-1",
363
+ type: "agentMessage",
364
+ text: "Recovered",
365
+ },
366
+ ],
367
+ },
368
+ },
369
+ });
370
+ }
371
+ });
372
+ return { turn: { id: "turn-new" } };
373
+ }
374
+ throw new Error(`unexpected method: ${method}`);
375
+ }),
376
+ addNotificationHandler: vi.fn((handler) => {
377
+ notificationHandlers.push(handler);
378
+ return () => undefined;
379
+ }),
380
+ addRequestHandler: vi.fn(() => () => undefined),
381
+ });
382
+
383
+ const result = await handleCodexConversationInboundClaim(
384
+ {
385
+ content: "hi again",
386
+ bodyForAgent: "hi again",
387
+ channel: "telegram",
388
+ isGroup: false,
389
+ commandAuthorized: true,
390
+ },
391
+ {
392
+ channelId: "telegram",
393
+ pluginBinding: {
394
+ bindingId: "binding-1",
395
+ pluginId: "codex",
396
+ pluginRoot: tempDir,
397
+ channel: "telegram",
398
+ accountId: "default",
399
+ conversationId: "5185575566",
400
+ boundAt: Date.now(),
401
+ data: {
402
+ kind: "codex-app-server-session",
403
+ version: 1,
404
+ sessionFile,
405
+ workspaceDir: tempDir,
406
+ },
407
+ },
408
+ },
409
+ { timeoutMs: 500 },
410
+ );
411
+
412
+ expect(result).toEqual({ handled: true, reply: { text: "Recovered" } });
413
+ expect(requests.map((request) => request.method)).toEqual([
414
+ "turn/start",
415
+ "thread/start",
416
+ "turn/start",
417
+ ]);
418
+ const sharedClientParams = mockCallArg(sharedClientMocks.getSharedCodexAppServerClient) as {
419
+ authProfileId?: unknown;
420
+ };
421
+ expect(sharedClientParams?.authProfileId).toBe("work");
422
+ expect(requests[1]?.params.model).toBe("gpt-5.4-mini");
423
+ expect(requests[1]?.params.approvalPolicy).toBe("on-request");
424
+ expect(requests[1]?.params.sandbox).toBe("workspace-write");
425
+ expect(requests[1]?.params.serviceTier).toBe("priority");
426
+ expect(requests[1]?.params).not.toHaveProperty("modelProvider");
427
+ expect(requests[2]?.params.threadId).toBe("thread-new");
428
+ expect(requests[2]?.params.approvalPolicy).toBe("on-request");
429
+ expect(requests[2]?.params.serviceTier).toBe("priority");
430
+ const savedBinding = JSON.parse(
431
+ await fs.readFile(`${sessionFile}.codex-app-server.json`, "utf8"),
432
+ );
433
+ expect(savedBinding.threadId).toBe("thread-new");
434
+ expect(savedBinding.authProfileId).toBe("work");
435
+ expect(savedBinding.approvalPolicy).toBe("on-request");
436
+ expect(savedBinding.sandbox).toBe("workspace-write");
437
+ expect(savedBinding.serviceTier).toBe("priority");
438
+ expect(savedBinding).not.toHaveProperty("modelProvider");
439
+ });
440
+
441
+ it("returns a clean failure reply when app-server turn start rejects", async () => {
442
+ const sessionFile = path.join(tempDir, "session.jsonl");
443
+ const agentDir = path.join(tempDir, "agents", "bot-b", "agent");
444
+ await fs.writeFile(
445
+ `${sessionFile}.codex-app-server.json`,
446
+ JSON.stringify({
447
+ schemaVersion: 1,
448
+ threadId: "thread-1",
449
+ cwd: tempDir,
450
+ authProfileId: "openai-codex:work",
451
+ }),
452
+ );
453
+ const unhandledRejections: unknown[] = [];
454
+ const onUnhandledRejection = (reason: unknown) => {
455
+ unhandledRejections.push(reason);
456
+ };
457
+ process.on("unhandledRejection", onUnhandledRejection);
458
+ sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
459
+ request: vi.fn(async (method: string) => {
460
+ if (method === "turn/start") {
461
+ throw new Error(
462
+ "unexpected status 401 Unauthorized: Missing bearer <@U123> [trusted](https://evil) @here",
463
+ );
464
+ }
465
+ throw new Error(`unexpected method: ${method}`);
466
+ }),
467
+ addNotificationHandler: vi.fn(() => () => undefined),
468
+ addRequestHandler: vi.fn(() => () => undefined),
469
+ });
470
+
471
+ try {
472
+ const result = await handleCodexConversationInboundClaim(
473
+ {
474
+ content: "hi",
475
+ bodyForAgent: "hi",
476
+ channel: "telegram",
477
+ isGroup: false,
478
+ commandAuthorized: true,
479
+ },
480
+ {
481
+ channelId: "telegram",
482
+ pluginBinding: {
483
+ bindingId: "binding-1",
484
+ pluginId: "codex",
485
+ pluginRoot: tempDir,
486
+ channel: "telegram",
487
+ accountId: "default",
488
+ conversationId: "5185575566",
489
+ boundAt: Date.now(),
490
+ data: {
491
+ kind: "codex-app-server-session",
492
+ version: 1,
493
+ sessionFile,
494
+ workspaceDir: tempDir,
495
+ agentDir,
496
+ },
497
+ },
498
+ },
499
+ { timeoutMs: 50 },
500
+ );
501
+ await new Promise<void>((resolve) => setImmediate(resolve));
502
+
503
+ expect(result).toEqual({
504
+ handled: true,
505
+ reply: {
506
+ text: "Codex app-server turn failed: unexpected status 401 Unauthorized: Missing bearer &lt;\uff20U123&gt; \uff3btrusted\uff3d\uff08https://evil\uff09 \uff20here",
507
+ },
508
+ });
509
+ const replyText = result?.reply?.text ?? "";
510
+ expect(replyText).not.toContain("<@U123>");
511
+ expect(replyText).not.toContain("[trusted](https://evil)");
512
+ expect(replyText).not.toContain("@here");
513
+ expect(unhandledRejections).toStrictEqual([]);
514
+ } finally {
515
+ process.off("unhandledRejection", onUnhandledRejection);
516
+ }
517
+ });
518
+
519
+ it("falls back to content when the channel body for agent is blank", async () => {
520
+ const sessionFile = path.join(tempDir, "session.jsonl");
521
+ const agentDir = path.join(tempDir, "agents", "bot-b", "agent");
522
+ await fs.writeFile(
523
+ `${sessionFile}.codex-app-server.json`,
524
+ JSON.stringify({
525
+ schemaVersion: 1,
526
+ threadId: "thread-1",
527
+ cwd: tempDir,
528
+ }),
529
+ );
530
+ let notificationHandler: ((notification: unknown) => void) | undefined;
531
+ const turnStartParams: Record<string, unknown>[] = [];
532
+ sharedClientMocks.getSharedCodexAppServerClient.mockResolvedValue({
533
+ request: vi.fn(async (method: string, requestParams: Record<string, unknown>) => {
534
+ if (method === "turn/start") {
535
+ turnStartParams.push(requestParams);
536
+ setImmediate(() =>
537
+ notificationHandler?.({
538
+ method: "turn/completed",
539
+ params: {
540
+ threadId: "thread-1",
541
+ turn: {
542
+ id: "turn-1",
543
+ status: "completed",
544
+ items: [{ type: "agentMessage", id: "item-1", text: "done" }],
545
+ },
546
+ },
547
+ }),
548
+ );
549
+ return { turn: { id: "turn-1" } };
550
+ }
551
+ throw new Error(`unexpected method: ${method}`);
552
+ }),
553
+ addNotificationHandler: vi.fn((handler: (notification: unknown) => void) => {
554
+ notificationHandler = handler;
555
+ return () => undefined;
556
+ }),
557
+ addRequestHandler: vi.fn(() => () => undefined),
558
+ });
559
+
560
+ const result = await handleCodexConversationInboundClaim(
561
+ {
562
+ content: "use the fallback prompt",
563
+ bodyForAgent: "",
564
+ channel: "telegram",
565
+ isGroup: false,
566
+ commandAuthorized: true,
567
+ },
568
+ {
569
+ channelId: "telegram",
570
+ pluginBinding: {
571
+ bindingId: "binding-1",
572
+ pluginId: "codex",
573
+ pluginRoot: tempDir,
574
+ channel: "telegram",
575
+ accountId: "default",
576
+ conversationId: "5185575566",
577
+ boundAt: Date.now(),
578
+ data: {
579
+ kind: "codex-app-server-session",
580
+ version: 1,
581
+ sessionFile,
582
+ workspaceDir: tempDir,
583
+ agentDir,
584
+ },
585
+ },
586
+ },
587
+ { timeoutMs: 50 },
588
+ );
589
+
590
+ expect(result).toEqual({ handled: true, reply: { text: "done" } });
591
+ const sharedClientParams = mockCallArg(sharedClientMocks.getSharedCodexAppServerClient) as {
592
+ agentDir?: unknown;
593
+ };
594
+ expect(sharedClientParams?.agentDir).toBe(agentDir);
595
+ expect(turnStartParams[0]?.input).toEqual([
596
+ { type: "text", text: "use the fallback prompt", text_elements: [] },
597
+ ]);
598
+ });
599
+ });