Skip to content

Commit 93976e2

Browse files
committed
Serialize deferred tombstone confirmations through the publish worker
1 parent 617a88c commit 93976e2

1 file changed

Lines changed: 44 additions & 18 deletions

File tree

apps/server/src/relay/AgentAwarenessRelay.ts

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ export const make = Effect.gen(function* () {
272272
const cloudLinkKeyPair = yield* getOrCreateEnvironmentKeyPairFromSecretStore(secrets);
273273
const activeSnapshotPublishedRef = yield* Ref.make(false);
274274
const publishedStateByThreadRef = yield* Ref.make(new Map<ThreadId, string>());
275+
const pendingTombstoneConfirmsRef = yield* Ref.make(new Set<ThreadId>());
275276

276277
const readSecretString = (name: string) =>
277278
secrets
@@ -306,7 +307,7 @@ export const make = Effect.gen(function* () {
306307
transformClient: relayEnvironmentClient(relayConfig.environmentCredential),
307308
}).pipe(Effect.provide(FetchHttpClient.layer));
308309

309-
// Assigned after publishThreadUnsafe below; indirection keeps the deferred
310+
// Assigned after the publish worker below; indirection keeps the deferred
310311
// tombstone confirmation from making the definition self-referential.
311312
let confirmTombstoneLater: (threadId: ThreadId) => Effect.Effect<void> = () => Effect.void;
312313

@@ -408,6 +409,20 @@ export const make = Effect.gen(function* () {
408409
// immediately deletes the thread from the lock-screen card mid-
409410
// conversation. Defer, re-resolve, and only tombstone if it holds;
410411
// genuinely deleted threads still propagate a few seconds later.
412+
const alreadyPending = yield* Ref.modify(pendingTombstoneConfirmsRef, (pending) => {
413+
if (pending.has(threadId)) {
414+
return [true, pending] as const;
415+
}
416+
return [false, new Set(pending).add(threadId)] as const;
417+
});
418+
if (alreadyPending) {
419+
yield* Effect.logDebug("agent activity tombstone confirmation already pending", {
420+
environmentId,
421+
threadId,
422+
reason: snapshot.reason,
423+
});
424+
return;
425+
}
411426
yield* Effect.logInfo("agent activity tombstone deferred pending confirmation", {
412427
environmentId,
413428
threadId,
@@ -442,30 +457,25 @@ export const make = Effect.gen(function* () {
442457
});
443458
});
444459

445-
confirmTombstoneLater = (threadId) =>
446-
publishThreadUnsafe(threadId, { confirmTombstone: true }).pipe(
447-
Effect.delay("5 seconds"),
448-
Effect.catchCause((cause) =>
449-
Effect.logWarning("deferred agent activity tombstone failed", {
450-
threadId,
451-
cause: Cause.pretty(cause),
452-
}),
453-
),
454-
Effect.asVoid,
455-
);
456-
457-
const publishThread: AgentAwarenessRelay["Service"]["publishThread"] = (threadId) =>
458-
publishThreadUnsafe(threadId).pipe(
460+
const runPublishTask = (task: {
461+
readonly threadId: ThreadId;
462+
readonly confirmTombstone?: boolean;
463+
}) =>
464+
publishThreadUnsafe(task.threadId, { confirmTombstone: task.confirmTombstone === true }).pipe(
459465
Effect.catchCause((cause) => {
460466
return Effect.logWarning("agent activity publish failed", {
461-
threadId,
467+
threadId: task.threadId,
468+
confirmTombstone: task.confirmTombstone === true,
462469
cause: Cause.pretty(cause),
463470
});
464471
}),
465472
Effect.withSpan("AgentAwarenessRelay.publishThread"),
466473
withRelayClientTracing,
467474
);
468475

476+
const publishThread: AgentAwarenessRelay["Service"]["publishThread"] = (threadId) =>
477+
runPublishTask({ threadId });
478+
469479
const publishActiveThreadsUnsafe = Effect.gen(function* () {
470480
const publishAgentActivity = yield* readPublishAgentActivityEnabled.pipe(
471481
Effect.orElseSucceed(() => false),
@@ -515,7 +525,23 @@ export const make = Effect.gen(function* () {
515525
}
516526
});
517527

518-
const worker = yield* makeDrainableWorker(publishThread);
528+
const worker = yield* makeDrainableWorker(runPublishTask);
529+
530+
// Deferred confirmations re-enter the same publish worker so they serialize
531+
// against event-driven publishes instead of racing them from a detached
532+
// fiber; only the delay itself runs outside the queue.
533+
confirmTombstoneLater = (threadId) =>
534+
Effect.sleep("5 seconds").pipe(
535+
Effect.andThen(
536+
Ref.update(pendingTombstoneConfirmsRef, (pending) => {
537+
const next = new Set(pending);
538+
next.delete(threadId);
539+
return next;
540+
}),
541+
),
542+
Effect.andThen(worker.enqueue({ threadId, confirmTombstone: true })),
543+
Effect.asVoid,
544+
);
519545

520546
const start: AgentAwarenessRelay["Service"]["start"] = Effect.fn("AgentAwarenessRelay.start")(
521547
function* () {
@@ -567,7 +593,7 @@ export const make = Effect.gen(function* () {
567593
return Effect.logDebug("agent activity publishing queued thread publish", {
568594
eventType: event.type,
569595
threadId,
570-
}).pipe(Effect.andThen(worker.enqueue(threadId)));
596+
}).pipe(Effect.andThen(worker.enqueue({ threadId })));
571597
}),
572598
);
573599
},

0 commit comments

Comments
 (0)