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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,7 +655,7 @@ func updateAgentStateOnDone(cwd, townRoot, exitType, _ string) { // issueID unus
|
||||
if _, err := bd.Run("agent", "state", agentBeadID, "awaiting-gate"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: couldn't set agent %s to awaiting-gate: %v\n", agentBeadID, err)
|
||||
}
|
||||
// ExitCompleted and ExitDeferred don't set state - observable from tmux
|
||||
// ExitCompleted and ExitDeferred don't set state - observable from tmux
|
||||
}
|
||||
|
||||
// ZFC #10: Self-report cleanup status
|
||||
|
||||
@@ -53,13 +53,13 @@ func init() {
|
||||
|
||||
// StepDoneResult is the result of a step done operation.
|
||||
type StepDoneResult struct {
|
||||
StepID string `json:"step_id"`
|
||||
MoleculeID string `json:"molecule_id"`
|
||||
StepClosed bool `json:"step_closed"`
|
||||
NextStepID string `json:"next_step_id,omitempty"`
|
||||
StepID string `json:"step_id"`
|
||||
MoleculeID string `json:"molecule_id"`
|
||||
StepClosed bool `json:"step_closed"`
|
||||
NextStepID string `json:"next_step_id,omitempty"`
|
||||
NextStepTitle string `json:"next_step_title,omitempty"`
|
||||
Complete bool `json:"complete"`
|
||||
Action string `json:"action"` // "continue", "done", "no_more_ready"
|
||||
Complete bool `json:"complete"`
|
||||
Action string `json:"action"` // "continue", "done", "no_more_ready"
|
||||
}
|
||||
|
||||
func runMoleculeStepDone(cmd *cobra.Command, args []string) error {
|
||||
@@ -162,9 +162,10 @@ 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
|
||||
//
|
||||
// gt-abc.1 -> gt-abc
|
||||
// gt-xyz.3 -> gt-xyz
|
||||
// bd-mol-abc.2 -> bd-mol-abc
|
||||
func extractMoleculeIDFromStep(stepID string) string {
|
||||
// Find the last dot
|
||||
lastDot := strings.LastIndex(stepID, ".")
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user