authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-22 15:42:22-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-22 15:42:22-07:00
log4e00471d1ab340b46742565bb6ce6462160b4d0c
treec42369f95552acfdad02ee54c32628b8bf6e446c
parent6e9c57f7ba9eab9e987da7a660664a1df683e9cf
signature Signed by PGP key B5690EEEBB952194

fix(server): stop completed Codex threads from staying stuck on working (#7937)


2 files changed, 64 insertions(+), 8 deletions(-)

apps/server/src/provider/Layers/CodexAdapter.test.ts+61
...@@ -513,6 +513,67 @@ function startLifecycleRuntime() {...@@ -513,6 +513,67 @@ function startLifecycleRuntime() {
513}513}
514514
515lifecycleLayer("CodexAdapterLive lifecycle", (it) => {515lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
516 it.effect("does not reactivate an idle child after a parent interaction", () =>
517 Effect.gen(function* () {
518 const { adapter, runtime } = yield* startLifecycleRuntime();
519 const eventsFiber = yield* Stream.runCollect(Stream.take(adapter.streamEvents, 3)).pipe(
520 Effect.forkChild,
521 );
522
523 const childEvent = (id: string, method: string, payload: Record<string, unknown>) => ({
524 id: asEventId(id),
525 kind: "notification" as const,
526 provider: ProviderDriverKind.make("codex"),
527 createdAt: "2026-01-01T00:00:00.000Z",
528 method,
529 threadId: asThreadId("thread-1"),
530 turnId: asTurnId("turn-1"),
531 payload,
532 });
533
534 yield* runtime.emit(
535 childEvent("evt-child-running", "collabAgent/turnStarted", {
536 agentThreadId: "child-1",
537 agentPath: "/root/audit",
538 }),
539 );
540 yield* runtime.emit(
541 childEvent("evt-child-idle", "collabAgent/turnCompleted", {
542 agentThreadId: "child-1",
543 agentPath: "/root/audit",
544 turn: { status: "completed" },
545 }),
546 );
547 yield* runtime.emit(
548 childEvent("evt-child-interacted", "collabAgent/activity", {
549 agentThreadId: "child-1",
550 agentPath: "/root/audit",
551 activityKind: "interacted",
552 }),
553 );
554 yield* runtime.emit(
555 childEvent("evt-other-child-running", "collabAgent/turnStarted", {
556 agentThreadId: "child-2",
557 agentPath: "/root/other",
558 }),
559 );
560
561 const events = Array.from(yield* Fiber.join(eventsFiber));
562 NodeAssert.deepStrictEqual(
563 events.map((event) =>
564 event.type === "task.updated"
565 ? { taskId: event.payload.taskId, status: event.payload.status }
566 : { type: event.type },
567 ),
568 [
569 { taskId: "child-1", status: "running" },
570 { taskId: "child-1", status: "idle" },
571 { taskId: "child-2", status: "running" },
572 ],
573 );
574 }),
575 );
576
516 it.effect("maps completed agent message items to canonical item.completed events", () =>577 it.effect("maps completed agent message items to canonical item.completed events", () =>
517 Effect.gen(function* () {578 Effect.gen(function* () {
518 const { adapter, runtime } = yield* startLifecycleRuntime();579 const { adapter, runtime } = yield* startLifecycleRuntime();
apps/server/src/provider/Layers/CodexAdapter.ts+3-8
...@@ -590,14 +590,9 @@ function mapCollabAgentEvent(...@@ -590,14 +590,9 @@ function mapCollabAgentEvent(
590 },590 },
591 ];591 ];
592 }592 }
593 // interacted → the child is (again) actively driven.593 // Reading a child's result also emits "interacted" after its turn is idle.
594 return [594 // Only the child's turn or thread lifecycle can prove it resumed work.
595 {595 return [];
596 ...base,
597 type: "task.updated",
598 payload: { taskId, status: "running", ...statusLinkage },
599 },
600 ];
601 }596 }
602 case "collabAgent/turnStarted":597 case "collabAgent/turnStarted":
603 return [598 return [