fix(molecules): cascade-close child wisps on molecule completion (gt-zbnr)

When deacon patrol molecules completed, their child step wisps were not being
closed automatically. This caused orphan wisp accumulation - 143+ orphaned
wisps were found in one cleanup session.

The fix ensures that when a molecule completes (via gt done or gt mol step done),
all descendant step issues are recursively closed before the molecule itself.

Changes:
- done.go: Added closeDescendants() call in updateAgentStateOnDone before
  closing the attached molecule
- molecule_step.go: Added closeDescendants() call in handleMoleculeComplete
  for all roles (not just polecats)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
furiosa
2026-01-22 22:53:31 -08:00
committed by John Ogle
parent cce261c97b
commit fa9087c5d7
2 changed files with 36 additions and 13 deletions

View File

@@ -608,11 +608,21 @@ func updateAgentStateOnDone(cwd, townRoot, exitType, _ string) { // issueID unus
// has attached_molecule pointing to the wisp. Without this fix, gt done
// only closed the hooked bead, leaving the wisp orphaned.
// Order matters: wisp closes -> unblocks base bead -> base bead closes.
//
// BUG FIX (gt-zbnr): Close child wisps BEFORE closing the molecule itself.
// Deacon patrol molecules have child step wisps that were being orphaned
// when the patrol completed. Now we cascade-close all descendants first.
attachment := beads.ParseAttachmentFields(hookedBead)
if attachment != nil && attachment.AttachedMolecule != "" {
if err := bd.Close(attachment.AttachedMolecule); err != nil {
moleculeID := attachment.AttachedMolecule
// Cascade-close all child wisps before closing the molecule
childrenClosed := closeDescendants(bd, moleculeID)
if childrenClosed > 0 {
fmt.Printf(" Closed %d child step issues\n", childrenClosed)
}
if err := bd.Close(moleculeID); err != nil {
// Non-fatal: warn but continue
fmt.Fprintf(os.Stderr, "Warning: couldn't close attached molecule %s: %v\n", attachment.AttachedMolecule, err)
fmt.Fprintf(os.Stderr, "Warning: couldn't close attached molecule %s: %v\n", moleculeID, err)
}
}

View File

@@ -162,6 +162,7 @@ func runMoleculeStepDone(cmd *cobra.Command, args []string) error {
// extractMoleculeIDFromStep extracts the molecule ID from a step ID.
// Step IDs have format: mol-id.N where N is the step number.
// Examples:
//
// gt-abc.1 -> gt-abc
// gt-xyz.3 -> gt-xyz
// bd-mol-abc.2 -> bd-mol-abc
@@ -388,14 +389,26 @@ func handleMoleculeComplete(cwd, townRoot, moleculeID string, dryRun bool) error
}
if dryRun {
fmt.Printf("[dry-run] Would close child steps of %s\n", moleculeID)
fmt.Printf("[dry-run] Would unpin work for %s\n", agentID)
fmt.Printf("[dry-run] Would send POLECAT_DONE to witness\n")
return nil
}
// Unpin the molecule bead (set status to open, will be closed by gt done or manually)
// BUG FIX (gt-zbnr): Close child steps before unpinning/completing.
// Deacon patrol molecules have child step wisps that were being orphaned
// when the patrol completed. Now we cascade-close all descendants first.
workDir, err := findLocalBeadsDir()
if err == nil {
b := beads.New(workDir)
childrenClosed := closeDescendants(b, moleculeID)
if childrenClosed > 0 {
fmt.Printf("%s Closed %d child step issues\n", style.Bold.Render("✓"), childrenClosed)
}
}
// Unpin the molecule bead (set status to open, will be closed by gt done or manually)
if workDir, err := findLocalBeadsDir(); err == nil {
b := beads.New(workDir)
pinnedBeads, err := b.List(beads.ListOptions{
Status: beads.StatusPinned,