@@ -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