@@ -37,25 +37,26 @@ type PendingSpawn struct {
3737 mailbox * mail.Mailbox `json:"-"`
3838}
3939
40- // CheckInboxForSpawns discovers pending spawns from POLECAT_STARTED messages
41- // in the Deacon's inbox. Uses mail as source of truth (ZFC principle).
42- func CheckInboxForSpawns (townRoot string ) ([]* PendingSpawn , error ) {
43- // Get Deacon's mailbox
40+ // getDeaconMailbox resolves the Deacon's mailbox from a town root.
41+ func getDeaconMailbox (townRoot string ) (* mail.Mailbox , error ) {
4442 router := mail .NewRouter (townRoot )
4543 mailbox , err := router .GetMailbox ("deacon/" )
4644 if err != nil {
4745 return nil , fmt .Errorf ("getting deacon mailbox: %w" , err )
4846 }
47+ return mailbox , nil
48+ }
4949
50- // Get all messages (both read and unread - we track by archival status)
50+ // checkMailboxForSpawns discovers pending spawns from POLECAT_STARTED messages
51+ // in the given mailbox. Extracted from CheckInboxForSpawns for testability.
52+ func checkMailboxForSpawns (mailbox * mail.Mailbox ) ([]* PendingSpawn , error ) {
5153 messages , err := mailbox .List ()
5254 if err != nil {
5355 return nil , fmt .Errorf ("listing messages: %w" , err )
5456 }
5557
5658 var pending []* PendingSpawn
5759
58- // Look for POLECAT_STARTED messages
5960 for _ , msg := range messages {
6061 if ! strings .HasPrefix (msg .Subject , "POLECAT_STARTED " ) {
6162 continue
@@ -96,6 +97,16 @@ func CheckInboxForSpawns(townRoot string) ([]*PendingSpawn, error) {
9697 return pending , nil
9798}
9899
100+ // CheckInboxForSpawns discovers pending spawns from POLECAT_STARTED messages
101+ // in the Deacon's inbox. Uses mail as source of truth (ZFC principle).
102+ func CheckInboxForSpawns (townRoot string ) ([]* PendingSpawn , error ) {
103+ mailbox , err := getDeaconMailbox (townRoot )
104+ if err != nil {
105+ return nil , err
106+ }
107+ return checkMailboxForSpawns (mailbox )
108+ }
109+
99110// TriggerResult holds the result of attempting to trigger a pending spawn.
100111type TriggerResult struct {
101112 Spawn * PendingSpawn
@@ -167,47 +178,61 @@ func TriggerPendingSpawns(townRoot string, timeout time.Duration) ([]TriggerResu
167178 return results , nil
168179}
169180
170- // ClearPendingSpawn archives the POLECAT_STARTED message for a specific session,
171- // removing it from the pending list. Used by the Deacon after observing that a
172- // session has been triggered via AI-based observation.
173- func ClearPendingSpawn (townRoot , sessionName string ) error {
174- pending , err := CheckInboxForSpawns (townRoot )
175- if err != nil {
176- return fmt .Errorf ("checking inbox: %w" , err )
177- }
178-
181+ // clearPendingSpawnFromList archives all POLECAT_STARTED messages for a specific
182+ // session from the given pending list. Extracted for testability.
183+ func clearPendingSpawnFromList (pending []* PendingSpawn , sessionName string ) error {
179184 for _ , ps := range pending {
180185 if ps .Session == sessionName {
181- if ps .mailbox != nil {
182- return ps .mailbox .Archive (ps .MailID )
186+ if ps .mailbox == nil {
187+ return fmt .Errorf ("nil mailbox for pending spawn session %s (mail_id: %s)" , sessionName , ps .MailID )
188+ }
189+ if err := ps .mailbox .Archive (ps .MailID ); err != nil {
190+ return fmt .Errorf ("archiving mail %s for session %s: %w" , ps .MailID , sessionName , err )
183191 }
184- return nil
185192 }
186193 }
187-
188- return fmt .Errorf ("no pending spawn found for session: %s" , sessionName )
194+ return nil
189195}
190196
191- // PruneStalePending archives POLECAT_STARTED messages older than the given age.
192- // Old spawns likely had their sessions die without triggering.
193- func PruneStalePending (townRoot string , maxAge time.Duration ) (int , error ) {
197+ // ClearPendingSpawn archives all POLECAT_STARTED messages for a specific session,
198+ // removing it from the pending list. Used by the Deacon after observing that a
199+ // session has been triggered via AI-based observation.
200+ // Idempotent: returns nil if no matching pending spawn is found (race-safe).
201+ func ClearPendingSpawn (townRoot , sessionName string ) error {
194202 pending , err := CheckInboxForSpawns (townRoot )
195203 if err != nil {
196- return 0 , err
204+ return fmt . Errorf ( "checking inbox: %w" , err )
197205 }
206+ return clearPendingSpawnFromList (pending , sessionName )
207+ }
198208
209+ // pruneStalePendingFromList archives POLECAT_STARTED messages older than the
210+ // given age from the provided pending list. Extracted for testability.
211+ func pruneStalePendingFromList (pending []* PendingSpawn , maxAge time.Duration ) (int , error ) {
199212 cutoff := time .Now ().Add (- maxAge )
200213 pruned := 0
201214
202215 for _ , ps := range pending {
203216 if ps .SpawnedAt .Before (cutoff ) {
204- // Archive stale spawn message
205- if ps .mailbox != nil {
206- _ = ps .mailbox .Archive (ps .MailID )
217+ if ps .mailbox == nil {
218+ return pruned , fmt .Errorf ("nil mailbox for stale pending spawn (mail_id: %s)" , ps .MailID )
219+ }
220+ if err := ps .mailbox .Archive (ps .MailID ); err != nil {
221+ return pruned , fmt .Errorf ("archiving stale mail %s: %w" , ps .MailID , err )
207222 }
208223 pruned ++
209224 }
210225 }
211226
212227 return pruned , nil
213228}
229+
230+ // PruneStalePending archives POLECAT_STARTED messages older than the given age.
231+ // Old spawns likely had their sessions die without triggering.
232+ func PruneStalePending (townRoot string , maxAge time.Duration ) (int , error ) {
233+ pending , err := CheckInboxForSpawns (townRoot )
234+ if err != nil {
235+ return 0 , err
236+ }
237+ return pruneStalePendingFromList (pending , maxAge )
238+ }
0 commit comments