diff --git a/src/bundled/codex/runtime.test.ts b/src/bundled/codex/runtime.test.ts index 55c5b82fa..3b3610d1d 100644 --- a/src/bundled/codex/runtime.test.ts +++ b/src/bundled/codex/runtime.test.ts @@ -31,6 +31,7 @@ afterEach(() => { for (const runtime of runtimes) runtime.dispose(); runtimes.length = 0; vi.unstubAllGlobals(); + vi.restoreAllMocks(); }); /** External app-server protocol fixture: turns complete only when released by @@ -962,6 +963,20 @@ it("publishes unexpected exit and empty-completion failures instead of silently content: expect.stringContaining("Codex exited"), }); }); +it("sets no deadline of its own on a turn", async () => { + // Any deadline Buzz set would already have passed. + vi.spyOn(AbortSignal, "timeout").mockImplementation(() => + AbortSignal.abort( + new DOMException("The operation timed out.", "TimeoutError"), + ), + ); + const f = fixture(); + await f.runtime.run(f.delivery("a long job")); + await vi.waitFor(() => expect(f.starts()).toHaveLength(1)); + await f.complete(); + await vi.waitFor(() => expect(f.publish).toHaveBeenCalledTimes(1)); + expect(f.publish.mock.calls[0]?.[0]).toMatchObject({ content: "DONE" }); +}); it("ends the server when its agent stops running and suppresses a late final answer", async () => { const f = fixture(); await f.runtime.run(f.delivery("work")); diff --git a/src/bundled/codex/runtime.ts b/src/bundled/codex/runtime.ts index 89d0f8828..b1519aaaa 100644 --- a/src/bundled/codex/runtime.ts +++ b/src/bundled/codex/runtime.ts @@ -602,11 +602,8 @@ export class CodexRuntime { let notReady!: (error: unknown) => void; const turnAbort = new AbortController(); const server = this.open(); - const signal = AbortSignal.any([ - entry.abort.signal, - turnAbort.signal, - AbortSignal.timeout(29 * 60_000), - ]); + // No deadline of our own: a turn runs until Codex ends it. + const signal = AbortSignal.any([entry.abort.signal, turnAbort.signal]); const active: Active = { rpc: server.rpc, ready: new Promise((resolve, reject) => { diff --git a/src/features/agents/service.test.ts b/src/features/agents/service.test.ts index a3996117e..a08f24fd1 100644 --- a/src/features/agents/service.test.ts +++ b/src/features/agents/service.test.ts @@ -697,6 +697,38 @@ it("aborts the in-flight run, and drops what is queued, when the agent is remove expect(run).toHaveBeenCalledTimes(1); }); +// STOPGAP(agent-working-reactions): delete with working-reactions.ts. +it("keeps a mention's reactions however long its handed-off work runs", async () => { + vi.useFakeTimers(); + const { service, run, emit, native } = await setup(); + await service.create({ type: "example/echo", name: "Echo" }); + let finish = () => {}; + const turn = new Promise((resolve) => { + finish = resolve; + }); + run.mockImplementationOnce(async ({ workingUntil }) => workingUntil(turn)); + emit({ + events: [ + event("m", { + tags: [ + ["h", channel], + ["p", bot], + ], + }), + ], + channelId: channel, + }); + await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(1)); + await vi.advanceTimersByTimeAsync(0); + expect(await published(native, 7)).toHaveLength(2); + // Buzz sets no deadline on an agent's work; the harness decides. + await vi.advanceTimersByTimeAsync(2 * 60 * 60_000); + expect(await published(native, 5)).toEqual([]); + finish(); + await vi.advanceTimersByTimeAsync(0); + expect(await published(native, 5)).toHaveLength(2); +}); + it("drops its events while another copy of the app holds the agent, and runs once it holds it", async () => { const { service, run, emit, native } = await setup(); await service.create({ type: "example/echo", name: "Echo" }); diff --git a/src/features/agents/service.ts b/src/features/agents/service.ts index 4fd73a126..0782fe652 100644 --- a/src/features/agents/service.ts +++ b/src/features/agents/service.ts @@ -1309,7 +1309,7 @@ export class AgentsService extends Service implements Agents { } /** Counts a run as working until it and everything it handed to - * `workingUntil` have settled, or the longest run allowed has passed. A + * `workingUntil` have settled, however long that takes. A * mention run is reacted to, and shown typing here, meanwhile: both are * STOPGAPs, and `done` is empty without them. */ private working(runner: Runner, { trigger, channelId }: Job) { @@ -1342,10 +1342,8 @@ export class AgentsService extends Service implements Agents { const stop = () => { if (holds < 0) return; holds = -1; - clearTimeout(timer); for (const end of done) end(); }; - const timer = setTimeout(stop, TIMEOUT_LIMIT_MS); const release = () => { if (holds > 0 && --holds === 0) stop(); };