| author | |
| committer | |
| log | 83eb396c407f167e70e29b29826d94f9ba1eb3aa |
| tree | 3758599bc4a36f6256efd2da62dcb449741e8710 |
| parent | 59a383e1ea4914ea12d3551dce2f715b59218618 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 100 insertions(+), 1 deletions(-)
apps/server/src/codexAppServerManager.test.ts+36| ... | ... | @@ -207,6 +207,42 @@ describe("classifyCodexStderrLine", () => { |
| 207 | 207 | }); |
| 208 | 208 | }); |
| 209 | 209 | |
| 210 | describe("process stderr events", () => { | |
| 211 | it("emits classified stderr lines as notifications", () => { | |
| 212 | const manager = new CodexAppServerManager(); | |
| 213 | const emitEvent = vi | |
| 214 | .spyOn(manager as unknown as { emitEvent: (...args: unknown[]) => void }, "emitEvent") | |
| 215 | .mockImplementation(() => {}); | |
| 216 | ||
| 217 | ( | |
| 218 | manager as unknown as { | |
| 219 | emitNotificationEvent: ( | |
| 220 | context: { session: { threadId: ThreadId } }, | |
| 221 | method: string, | |
| 222 | message: string, | |
| 223 | ) => void; | |
| 224 | } | |
| 225 | ).emitNotificationEvent( | |
| 226 | { | |
| 227 | session: { | |
| 228 | threadId: asThreadId("thread-1"), | |
| 229 | }, | |
| 230 | }, | |
| 231 | "process/stderr", | |
| 232 | "fatal: permission denied", | |
| 233 | ); | |
| 234 | ||
| 235 | expect(emitEvent).toHaveBeenCalledWith( | |
| 236 | expect.objectContaining({ | |
| 237 | kind: "notification", | |
| 238 | method: "process/stderr", | |
| 239 | threadId: "thread-1", | |
| 240 | message: "fatal: permission denied", | |
| 241 | }), | |
| 242 | ); | |
| 243 | }); | |
| 244 | }); | |
| 245 | ||
| 210 | 246 | describe("normalizeCodexModelSlug", () => { |
| 211 | 247 | it("maps 5.3 aliases to gpt-5.3-codex", () => { |
| 212 | 248 | expect(normalizeCodexModelSlug("5.3")).toBe("gpt-5.3-codex"); |
apps/server/src/codexAppServerManager.ts+17-1| ... | ... | @@ -1046,7 +1046,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 1046 | 1046 | continue; |
| 1047 | 1047 | } |
| 1048 | 1048 | |
| 1049 | this.emitErrorEvent(context, "process/stderr", classified.message); | |
| 1049 | this.emitNotificationEvent(context, "process/stderr", classified.message); | |
| 1050 | 1050 | } |
| 1051 | 1051 | }); |
| 1052 | 1052 | |
| ... | ... | @@ -1354,6 +1354,22 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 1354 | 1354 | }); |
| 1355 | 1355 | } |
| 1356 | 1356 | |
| 1357 | private emitNotificationEvent( | |
| 1358 | context: CodexSessionContext, | |
| 1359 | method: string, | |
| 1360 | message: string, | |
| 1361 | ): void { | |
| 1362 | this.emitEvent({ | |
| 1363 | id: EventId.makeUnsafe(randomUUID()), | |
| 1364 | kind: "notification", | |
| 1365 | provider: "codex", | |
| 1366 | threadId: context.session.threadId, | |
| 1367 | createdAt: new Date().toISOString(), | |
| 1368 | method, | |
| 1369 | message, | |
| 1370 | }); | |
| 1371 | } | |
| 1372 | ||
| 1357 | 1373 | private emitEvent(event: ProviderEvent): void { |
| 1358 | 1374 | this.emit("event", event); |
| 1359 | 1375 | } |
apps/server/src/provider/Layers/CodexAdapter.test.ts+34| ... | ... | @@ -474,6 +474,40 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => { |
| 474 | 474 | }), |
| 475 | 475 | ); |
| 476 | 476 | |
| 477 | it.effect("maps process stderr notifications to runtime.warning", () => | |
| 478 | Effect.gen(function* () { | |
| 479 | const adapter = yield* CodexAdapter; | |
| 480 | const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); | |
| 481 | ||
| 482 | lifecycleManager.emit("event", { | |
| 483 | id: asEventId("evt-process-stderr"), | |
| 484 | kind: "notification", | |
| 485 | provider: "codex", | |
| 486 | threadId: asThreadId("thread-1"), | |
| 487 | createdAt: new Date().toISOString(), | |
| 488 | method: "process/stderr", | |
| 489 | turnId: asTurnId("turn-1"), | |
| 490 | message: "The filename or extension is too long. (os error 206)", | |
| 491 | } satisfies ProviderEvent); | |
| 492 | ||
| 493 | const firstEvent = yield* Fiber.join(firstEventFiber); | |
| 494 | ||
| 495 | assert.equal(firstEvent._tag, "Some"); | |
| 496 | if (firstEvent._tag !== "Some") { | |
| 497 | return; | |
| 498 | } | |
| 499 | assert.equal(firstEvent.value.type, "runtime.warning"); | |
| 500 | if (firstEvent.value.type !== "runtime.warning") { | |
| 501 | return; | |
| 502 | } | |
| 503 | assert.equal(firstEvent.value.turnId, "turn-1"); | |
| 504 | assert.equal( | |
| 505 | firstEvent.value.payload.message, | |
| 506 | "The filename or extension is too long. (os error 206)", | |
| 507 | ); | |
| 508 | }), | |
| 509 | ); | |
| 510 | ||
| 477 | 511 | it.effect("preserves request type when mapping serverRequest/resolved", () => |
| 478 | 512 | Effect.gen(function* () { |
| 479 | 513 | const adapter = yield* CodexAdapter; |
apps/server/src/provider/Layers/CodexAdapter.ts+13| ... | ... | @@ -1267,6 +1267,19 @@ function mapToRuntimeEvents( |
| 1267 | 1267 | ]; |
| 1268 | 1268 | } |
| 1269 | 1269 | |
| 1270 | if (event.method === "process/stderr") { | |
| 1271 | return [ | |
| 1272 | { | |
| 1273 | type: "runtime.warning", | |
| 1274 | ...runtimeEventBase(event, canonicalThreadId), | |
| 1275 | payload: { | |
| 1276 | message: event.message ?? "Codex process stderr", | |
| 1277 | ...(event.payload !== undefined ? { detail: event.payload } : {}), | |
| 1278 | }, | |
| 1279 | }, | |
| 1280 | ]; | |
| 1281 | } | |
| 1282 | ||
| 1270 | 1283 | if (event.method === "windows/worldWritableWarning") { |
| 1271 | 1284 | return [ |
| 1272 | 1285 | { |