authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-02 21:41:33-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-02 21:41:33-07:00
logd668ffcd64d74daba16520cbbfb0fdb431983eac
tree7e0fb246a931f311db73f19abca9a905a081b8c8
parent3a7058da50c91906158c89fc3f845e6c04379414
signature Signed by PGP key B5690EEEBB952194

fix(server): subagent threads stop publishing tombstones to the relay (#15016)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

2 files changed, 34 insertions(+), 0 deletions(-)

apps/server/src/relay/AgentAwarenessRelay.test.ts+24
......@@ -659,6 +659,30 @@ describe("AgentAwarenessRelay", () => {
659659 }),
660660 );
661661
662 it.effect.each([
663 { label: "live", archived: false },
664 { label: "archived", archived: true },
665 ])("never publishes tombstones for $label subagent threads", ({ archived }) =>
666 Effect.gen(function* () {
667 const { relay, currentShell, publications } = yield* makeTestRelay();
668 yield* Ref.set(
669 currentShell,
670 shell({
671 lineage: {
672 rootThreadId: THREAD_ID,
673 parentThreadId: THREAD_ID,
674 relationshipToParent: "subagent",
675 },
676 ...(archived ? { archivedAt: yield* DateTime.now } : {}),
677 }),
678 );
679 yield* relay.publishThread(THREAD_ID);
680 yield* TestClock.adjust("5 seconds");
681 yield* relay.drain;
682 assert.equal(publications.length, 0);
683 }),
684 );
685
662686 it.effect("confirms a first completed state and respects disabling during confirmation", () =>
663687 Effect.gen(function* () {
664688 const { relay, secrets, currentShell, publications } = yield* makeTestRelay();
apps/server/src/relay/AgentAwarenessRelay.ts+10
......@@ -522,6 +522,16 @@ export const make = Effect.gen(function* () {
522522 // domain event, so materializing the full shell here would make the cost
523523 // of one thread's activity proportional to how many threads exist.
524524 const threadShell = yield* threads.getThreadShell(threadId);
525 if (
526 threadShell?.lineage.relationshipToParent === "subagent" &&
527 !(yield* Ref.get(publishedStateByThreadRef)).has(threadId)
528 ) {
529 // Subagents never project activity, so the relay holds no row to clear.
530 // Their events would otherwise publish a tombstone each, and every
531 // publish re-delivers the user's aggregate. Checked before the archive
532 // filter so archiving one stays quiet too.
533 return;
534 }
525535 const thread =
526536 threadShell === null || threadShell.archivedAt !== null
527537 ? Option.none<OrchestrationV2ThreadShell>()