@@ -68,8 +68,15 @@ type Active = {
6868 recovery ?: Recovery
6969}
7070
71+ type Notice = {
72+ recovery : Recovery
73+ delivered : Deferred . Deferred < void >
74+ }
75+
7176type State = {
7277 jobs : SynchronizedRef . SynchronizedRef < Map < string , Active > >
78+ /** Backgrounded work whose completion notice its owner has not admitted yet, by notice ID. */
79+ notices : Map < SessionMessage . ID , Notice >
7380 scope : Scope . Scope
7481}
7582
@@ -126,6 +133,11 @@ export type BlockInput = {
126133
127134export type BlockResult = { type : "finished" ; info : Info } | { type : "backgrounded" ; info : Info }
128135
136+ export type Undelivered = {
137+ notificationID : SessionMessage . ID
138+ recovery : Recovery
139+ }
140+
129141export type BackgroundAllInput = {
130142 sessionID : SessionSchema . ID
131143 type ?: string
@@ -142,6 +154,13 @@ export interface Interface {
142154 readonly cancelSession : ( sessionID : SessionSchema . ID ) => Effect . Effect < Info [ ] >
143155 readonly pendingBackground : Effect . Effect < readonly Background [ ] >
144156 readonly runningBackground : Effect . Effect < readonly Recovery [ ] >
157+ /**
158+ * Backgrounded work whose completion notice has not been admitted yet, running or not:
159+ * each notice will still wake its owner Session.
160+ */
161+ readonly undelivered : Effect . Effect < readonly Undelivered [ ] >
162+ /** Resolves once the notice is admitted or its Session is removed; at once for an unknown notice. */
163+ readonly awaitDelivered : ( notificationID : SessionMessage . ID ) => Effect . Effect < void >
145164 readonly completeBackground : ( notificationID : SessionMessage . ID ) => Effect . Effect < void >
146165}
147166
@@ -205,6 +224,7 @@ export const make = Effect.gen(function* () {
205224 const db = ( yield * Database . Service ) . db
206225 const state : State = {
207226 jobs : yield * SynchronizedRef . make ( new Map ( ) ) ,
227+ notices : new Map ( ) ,
208228 scope : yield * Scope . Scope ,
209229 }
210230
@@ -403,13 +423,16 @@ export const make = Effect.gen(function* () {
403423 } )
404424
405425 const markBackground = Effect . fnUntraced ( function * ( job : Active ) {
426+ const notificationID = job . recovery ? ( job . info . notificationID ?? SessionMessage . ID . create ( ) ) : undefined
427+ if ( job . recovery && notificationID && ! state . notices . has ( notificationID ) )
428+ state . notices . set ( notificationID , { recovery : job . recovery , delivered : yield * Deferred . make < void > ( ) } )
406429 const next = {
407430 ...job ,
408431 isBackgrounded : true ,
409432 blockingSessions : new Map < SessionSchema . ID , number > ( ) ,
410433 info : {
411434 ...job . info ,
412- ...( job . recovery ? { notificationID : job . info . notificationID ?? SessionMessage . ID . create ( ) } : { } ) ,
435+ ...( notificationID ? { notificationID } : { } ) ,
413436 } ,
414437 }
415438 yield * persistBackground ( next )
@@ -498,6 +521,16 @@ export const make = Effect.gen(function* () {
498521 . where ( or ( eq ( JobTable . session_id , sessionID ) , eq ( JobTable . child_session_id , sessionID ) ) )
499522 . run ( )
500523 . pipe ( Effect . orDie )
524+ // A removed Session admits no notice, so nothing may keep waiting on one.
525+ yield * Effect . forEach (
526+ [ ...state . notices ] . filter (
527+ ( [ , notice ] ) =>
528+ ownerSessionID ( notice . recovery ) === sessionID ||
529+ ( notice . recovery . kind === "subagent" && notice . recovery . childSessionID === sessionID ) ,
530+ ) ,
531+ ( [ notificationID ] ) => deliver ( notificationID ) ,
532+ { discard : true } ,
533+ )
501534 yield * SynchronizedRef . update ( state . jobs , ( jobs ) => {
502535 const next = new Map ( jobs )
503536 for ( const [ id , job ] of jobs ) {
@@ -536,6 +569,24 @@ export const make = Effect.gen(function* () {
536569 Effect . withSpan ( "Job.runningBackground" ) ,
537570 )
538571
572+ const undelivered : Interface [ "undelivered" ] = Effect . sync ( ( ) =>
573+ [ ...state . notices ] . map ( ( [ notificationID , notice ] ) => ( { notificationID, recovery : notice . recovery } ) ) ,
574+ ) . pipe ( Effect . withSpan ( "Job.undelivered" ) )
575+
576+ const awaitDelivered : Interface [ "awaitDelivered" ] = ( notificationID ) =>
577+ Effect . suspend ( ( ) => {
578+ const notice = state . notices . get ( notificationID )
579+ return notice ? Deferred . await ( notice . delivered ) : Effect . void
580+ } )
581+
582+ const deliver = ( notificationID : SessionMessage . ID ) =>
583+ Effect . suspend ( ( ) => {
584+ const notice = state . notices . get ( notificationID )
585+ if ( ! notice ) return Effect . void
586+ state . notices . delete ( notificationID )
587+ return Deferred . succeed ( notice . delivered , undefined )
588+ } )
589+
539590 const completeBackground : Interface [ "completeBackground" ] = Effect . fn ( "Job.completeBackground" ) ( ( notificationID ) =>
540591 SynchronizedRef . updateEffect ( state . jobs , ( jobs ) =>
541592 Effect . gen ( function * ( ) {
@@ -546,7 +597,7 @@ export const make = Effect.gen(function* () {
546597 next . delete ( entry [ 0 ] )
547598 return next
548599 } ) ,
549- ) ,
600+ ) . pipe ( Effect . andThen ( deliver ( notificationID ) ) ) ,
550601 )
551602
552603 return Service . of ( {
@@ -560,6 +611,8 @@ export const make = Effect.gen(function* () {
560611 cancelSession,
561612 pendingBackground,
562613 runningBackground,
614+ undelivered,
615+ awaitDelivered,
563616 completeBackground,
564617 } )
565618} )
0 commit comments