authorgravatar for saphid@gmail.comAlex Southwell <saphid@gmail.com> 2026-10-14 19:04:46+11:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 01:04:46-07:00
log173a97097659be5f77ab2c05cf5dfe84be3eb094
treef4548b10ccb0b3c8d5b5dd296f90e6be39b57cd1
parent5bba764432366a41fa14ea0df694c2a7fca35c4c
signature Signed by PGP key B5690EEEBB952194

fix(server): bound activity publishes so stalled responses release the worker (#18830)

Co-authored-by: SAPHID <saphid@users.noreply.github.com> Co-authored-by: GPT-6.1 Sol <noreply@openai.com>

2 files changed, 66 insertions(+), 10 deletions(-)

apps/server/src/relay/AgentAwarenessRelay.test.ts+54
...@@ -431,6 +431,60 @@ describe("AgentAwarenessRelay", () => {...@@ -431,6 +431,60 @@ describe("AgentAwarenessRelay", () => {
431 }),431 }),
432 );432 );
433433
434 it.effect("a stalled response body releases later threads and uses bounded retries", () =>
435 Effect.gen(function* () {
436 const bodyStarted = Promise.withResolvers<void>();
437 const secondRead = yield* Deferred.make<void>();
438 const currentShell = yield* Ref.make(shell());
439 const { relay, publications } = yield* makeTestRelay({
440 readShell: (threadId) =>
441 threadId === SECOND_THREAD_ID
442 ? Deferred.succeed(secondRead, undefined).pipe(
443 Effect.as(shell({ id: SECOND_THREAD_ID })),
444 )
445 : Ref.get(currentShell),
446 respond: (attempt) => {
447 if (attempt !== 1) return new Response("relay unavailable", { status: 503 });
448 const response = new Response(null, {
449 headers: { "content-type": "application/json" },
450 });
451 return Object.assign(response, {
452 arrayBuffer: () => {
453 bodyStarted.resolve();
454 return new Promise<ArrayBuffer>(() => {});
455 },
456 });
457 },
458 });
459 const first = yield* relay.publishThread(THREAD_ID).pipe(Effect.forkChild);
460 yield* Effect.promise(() => bodyStarted.promise);
461 const second = yield* relay.publishThread(SECOND_THREAD_ID).pipe(Effect.forkChild);
462 yield* TestClock.adjust("9999 millis");
463 assert.equal(publications.length, 1);
464 yield* Ref.set(currentShell, shell({ title: "Latest title" }));
465 yield* TestClock.adjust("1 millis");
466 // Assert progress before draining so the base fails instead of hanging.
467 assert.isTrue(yield* Deferred.isDone(secondRead));
468 yield* Fiber.join(first);
469 yield* Fiber.join(second);
470 assert.equal(publications.length, 2);
471 assert.isTrue(publications[1]?.url.includes(SECOND_THREAD_ID) ?? false);
472 for (const delay of [1_000, 2_000, 4_000, 8_000, 16_000]) {
473 const attempts = publications.length;
474 yield* TestClock.adjust(delay - 1);
475 yield* relay.drain;
476 assert.equal(publications.length, attempts);
477 yield* TestClock.adjust(1);
478 yield* relay.drain;
479 assert.equal(publications.length, attempts + 2);
480 assert.equal(publications[attempts]?.state?.threadTitle, "Latest title");
481 }
482 yield* TestClock.adjust("1 minute");
483 yield* relay.drain;
484 assert.equal(publications.length, 12);
485 }),
486 );
487
434 it.effect("retries a failed final state without another thread event", () =>488 it.effect("retries a failed final state without another thread event", () =>
435 Effect.gen(function* () {489 Effect.gen(function* () {
436 const { relay, currentShell, shellReads, publications } = yield* makeTestRelay({490 const { relay, currentShell, shellReads, publications } = yield* makeTestRelay({
apps/server/src/relay/AgentAwarenessRelay.ts+12-10
...@@ -502,16 +502,18 @@ export const make = Effect.gen(function* () {...@@ -502,16 +502,18 @@ export const make = Effect.gen(function* () {
502 reason: input.reason,502 reason: input.reason,
503 });503 });
504504
505 const response = yield* relayClient.server.publishAgentActivity({505 const response = yield* relayClient.server
506 params: {506 .publishAgentActivity({
507 environmentId,507 params: {
508 threadId,508 environmentId,
509 },509 threadId,
510 payload: {510 },
511 state: input.state,511 payload: {
512 proof,512 state: input.state,
513 },513 proof,
514 });514 },
515 })
516 .pipe(Effect.timeout("10 seconds"));
515517
516 yield* Effect.logInfo("agent activity publish completed", {518 yield* Effect.logInfo("agent activity publish completed", {
517 environmentId,519 environmentId,