@@ -1823,7 +1823,7 @@ func linkSubagentSessionsForSessionsQuery(ph string) string {
18231823 )`
18241824}
18251825
1826- // clearDanglingSubagentParentQuery repairs a captured child whose LAST spawn
1826+ // clearDanglingSubagentParentQuery repairs a captured former child whose LAST spawn
18271827// edge was removed together with its spawner: both UNION branches of the
18281828// linking statement select from remaining tool_calls, so an edge-less child
18291829// can never be re-resolved there, and its parent now points at a session
@@ -1853,8 +1853,11 @@ func clearDanglingSubagentParentQuery(ph string) string {
18531853// LinkSubagentSessionsForSessions is LinkSubagentSessions scoped to the
18541854// sessions written by one sync batch: only children reachable from a batch
18551855// member's spawn edges (or batch members that are themselves children) are
1856- // re-resolved. Children in ids whose edges are all gone and whose parent no
1857- // longer exists are un-parented (see clearDanglingSubagentParentQuery).
1856+ // re-resolved. Generic changed-session IDs are deliberately ineligible for
1857+ // dangling-parent cleanup: a parser-derived parent may simply not have been
1858+ // ingested yet. Destructive cleanup is reserved for former children captured
1859+ // before a write that can remove their spawn edges and persisted through
1860+ // QueueSubagentParentCleanupRepairs.
18581861// Per-event paths — the session watcher re-syncs a single file
18591862// on every change — must use this form so their linking cost tracks the
18601863// changed batch; bulk paths (full sync, reconciliation, resync) keep the
@@ -1880,25 +1883,27 @@ func (db *DB) LinkSubagentSessionsForSessions(ids []string) error {
18801883 len (chunk ), err ,
18811884 )
18821885 }
1883- _ , err = db .getWriter ().Exec (
1884- clearDanglingSubagentParentQuery (ph ), args ... ,
1885- )
1886- if err != nil {
1887- return fmt .Errorf (
1888- "clearing dangling subagent parents for %d changed " +
1889- "sessions: %w" ,
1890- len (chunk ), err ,
1891- )
1892- }
18931886 return nil
18941887 })
18951888}
18961889
1897- // QueueSubagentParentRepairs durably records sessions whose hierarchy may be
1898- // changed by an upcoming write. Callers must queue the IDs before deleting or
1899- // replacing messages because those writes can cascade away the only spawn
1900- // edge that identifies an affected child .
1890+ // QueueSubagentParentRepairs durably records sessions whose hierarchy must be
1891+ // re-evaluated from surviving spawn edges. These generic seeds are never used
1892+ // for destructive dangling-parent cleanup; callers that captured a former
1893+ // child before removing edges use QueueSubagentParentCleanupRepairs instead .
19011894func (db * DB ) QueueSubagentParentRepairs (ids []string ) error {
1895+ return db .queueSubagentParentRepairs (ids , false )
1896+ }
1897+
1898+ // QueueSubagentParentCleanupRepairs durably records former children captured
1899+ // before an exclusion or message replacement can remove their spawn edges.
1900+ // Cleanup intent is separate from ordinary relink work so a newly parsed child
1901+ // whose parent has not arrived yet never loses valid parser-derived parentage.
1902+ func (db * DB ) QueueSubagentParentCleanupRepairs (ids []string ) error {
1903+ return db .queueSubagentParentRepairs (ids , true )
1904+ }
1905+
1906+ func (db * DB ) queueSubagentParentRepairs (ids []string , cleanup bool ) error {
19021907 if len (ids ) == 0 {
19031908 return nil
19041909 }
@@ -1911,20 +1916,39 @@ func (db *DB) QueueSubagentParentRepairs(ids []string) error {
19111916 }
19121917 defer func () { _ = tx .Rollback () }()
19131918
1914- stmt , err := tx .Prepare (`
1919+ repairStmt , err := tx .Prepare (`
19151920 INSERT INTO subagent_parent_repair_queue (session_id) VALUES (?)
19161921 ON CONFLICT(session_id) DO NOTHING` )
19171922 if err != nil {
19181923 return fmt .Errorf ("preparing subagent parent repair queue insert: %w" , err )
19191924 }
1920- defer stmt .Close ()
1925+ defer repairStmt .Close ()
1926+ var cleanupStmt * sql.Stmt
1927+ if cleanup {
1928+ cleanupStmt , err = tx .Prepare (`
1929+ INSERT INTO subagent_parent_cleanup_queue (session_id) VALUES (?)
1930+ ON CONFLICT(session_id) DO NOTHING` )
1931+ if err != nil {
1932+ return fmt .Errorf (
1933+ "preparing subagent parent cleanup queue insert: %w" , err ,
1934+ )
1935+ }
1936+ defer cleanupStmt .Close ()
1937+ }
19211938 for _ , id := range ids {
19221939 if id == "" {
19231940 continue
19241941 }
1925- if _ , err := stmt .Exec (id ); err != nil {
1942+ if _ , err := repairStmt .Exec (id ); err != nil {
19261943 return fmt .Errorf ("queueing subagent parent repair for %s: %w" , id , err )
19271944 }
1945+ if cleanupStmt != nil {
1946+ if _ , err := cleanupStmt .Exec (id ); err != nil {
1947+ return fmt .Errorf (
1948+ "queueing subagent parent cleanup for %s: %w" , id , err ,
1949+ )
1950+ }
1951+ }
19281952 }
19291953 if err := tx .Commit (); err != nil {
19301954 return fmt .Errorf ("committing subagent parent repair queue: %w" , err )
@@ -1943,6 +1967,7 @@ func (db *DB) RepairQueuedSubagentParents() error {
19431967 var pending int
19441968 err := db .getWriter ().QueryRow (`
19451969 SELECT EXISTS(SELECT 1 FROM subagent_parent_repair_queue)
1970+ OR EXISTS(SELECT 1 FROM subagent_parent_cleanup_queue)
19461971 OR EXISTS(SELECT 1 FROM pg_sync_state WHERE key = ?)` ,
19471972 subagentParentRepairQueueStateKey ,
19481973 ).Scan (& pending )
@@ -1963,6 +1988,8 @@ func (db *DB) RepairQueuedSubagentParents() error {
19631988 for {
19641989 rows , err := tx .Query (`
19651990 SELECT session_id FROM subagent_parent_repair_queue
1991+ UNION
1992+ SELECT session_id FROM subagent_parent_cleanup_queue
19661993 ORDER BY session_id LIMIT ?` , maxSQLVars / 2 )
19671994 if err != nil {
19681995 return fmt .Errorf ("listing queued subagent parent repairs: %w" , err )
@@ -1996,15 +2023,26 @@ func (db *DB) RepairQueuedSubagentParents() error {
19962023 len (chunk ), err ,
19972024 )
19982025 }
2026+ cleanupSeeds := `(SELECT session_id
2027+ FROM subagent_parent_cleanup_queue WHERE session_id IN ` + ph + `)`
19992028 if _ , err := tx .Exec (
2000- clearDanglingSubagentParentQuery (ph ), args ... ,
2029+ clearDanglingSubagentParentQuery (cleanupSeeds ), args ... ,
20012030 ); err != nil {
20022031 return fmt .Errorf (
20032032 "clearing queued dangling subagent parents for %d " +
20042033 "sessions: %w" ,
20052034 len (chunk ), err ,
20062035 )
20072036 }
2037+ if _ , err := tx .Exec (
2038+ "DELETE FROM subagent_parent_cleanup_queue WHERE session_id IN " + ph ,
2039+ args ... ,
2040+ ); err != nil {
2041+ return fmt .Errorf (
2042+ "clearing %d queued subagent parent cleanups: %w" ,
2043+ len (chunk ), err ,
2044+ )
2045+ }
20082046 if _ , err := tx .Exec (
20092047 "DELETE FROM subagent_parent_repair_queue WHERE session_id IN " + ph ,
20102048 args ... ,
@@ -2037,20 +2075,35 @@ func migrateLegacySubagentParentRepairQueueTx(tx *sql.Tx) error {
20372075 if err := json .Unmarshal ([]byte (encoded ), & ids ); err != nil {
20382076 return fmt .Errorf ("decoding legacy subagent parent repair queue: %w" , err )
20392077 }
2040- stmt , err := tx .Prepare (`
2078+ repairStmt , err := tx .Prepare (`
20412079 INSERT INTO subagent_parent_repair_queue (session_id) VALUES (?)
20422080 ON CONFLICT(session_id) DO NOTHING` )
20432081 if err != nil {
20442082 return fmt .Errorf ("preparing legacy subagent parent repair migration: %w" , err )
20452083 }
2046- defer stmt .Close ()
2084+ defer repairStmt .Close ()
2085+ cleanupStmt , err := tx .Prepare (`
2086+ INSERT INTO subagent_parent_cleanup_queue (session_id) VALUES (?)
2087+ ON CONFLICT(session_id) DO NOTHING` )
2088+ if err != nil {
2089+ return fmt .Errorf ("preparing legacy subagent parent cleanup migration: %w" , err )
2090+ }
2091+ defer cleanupStmt .Close ()
20472092 for _ , id := range ids {
20482093 if id == "" {
20492094 continue
20502095 }
2051- if _ , err := stmt .Exec (id ); err != nil {
2096+ if _ , err := repairStmt .Exec (id ); err != nil {
20522097 return fmt .Errorf ("migrating legacy subagent parent repair for %s: %w" , id , err )
20532098 }
2099+ // The JSON queue predates generic post-write and attempted-session
2100+ // seeds; every legacy ID was captured before a destructive write and
2101+ // therefore carries cleanup intent.
2102+ if _ , err := cleanupStmt .Exec (id ); err != nil {
2103+ return fmt .Errorf (
2104+ "migrating legacy subagent parent cleanup for %s: %w" , id , err ,
2105+ )
2106+ }
20542107 }
20552108 if _ , err := tx .Exec (
20562109 "DELETE FROM pg_sync_state WHERE key = ?" ,
0 commit comments