fix(hook): Read hook_bead from database column, not description (gt-7m33w)
The hook discovery code was reading hook_bead from the agent bead's description field (parsed via ParseAgentFieldsFromDescription), but the slot update code writes to the hook_bead database column via 'bd slot set'. This mismatch caused polecats to see stale hook values from the description instead of the current value from the database. Fixed in: - molecule_status.go: Use agentBead.HookBead instead of parsing description - status.go: Use issue.HookBead directly - lifecycle.go: Update all GUPP and orphan detection to read from database columns instead of parsing description 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
65a5c7888f
commit
278abf15d6
@@ -322,11 +322,13 @@ func runMoleculeStatus(cmd *cobra.Command, args []string) error {
|
||||
if err == nil && agentBead != nil && agentBead.Type == "agent" {
|
||||
status.AgentBeadID = agentBeadID
|
||||
|
||||
// Parse hook_bead from the agent bead's description
|
||||
agentFields := beads.ParseAgentFieldsFromDescription(agentBead.Description)
|
||||
if agentFields != nil && agentFields.HookBead != "" {
|
||||
// Read hook_bead from the agent bead's database field (not description!)
|
||||
// The hook_bead column is updated by `bd slot set` in UpdateAgentState.
|
||||
// IMPORTANT: Don't use ParseAgentFieldsFromDescription - the description
|
||||
// field may contain stale data, causing the wrong issue to be hooked.
|
||||
if agentBead.HookBead != "" {
|
||||
// Fetch the bead on the hook
|
||||
hookBead, err = b.Show(agentFields.HookBead)
|
||||
hookBead, err = b.Show(agentBead.HookBead)
|
||||
if err != nil {
|
||||
// Hook bead referenced but not found - report error but continue
|
||||
hookBead = nil
|
||||
|
||||
@@ -174,11 +174,11 @@ func runStatus(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Pre-fetch all hook beads (referenced in agent beads) in a single query
|
||||
// Use the HookBead field from the database column, not parsed from description.
|
||||
var allHookIDs []string
|
||||
for _, issue := range allAgentBeads {
|
||||
fields := beads.ParseAgentFields(issue.Description)
|
||||
if fields != nil && fields.HookBead != "" {
|
||||
allHookIDs = append(allHookIDs, fields.HookBead)
|
||||
if issue.HookBead != "" {
|
||||
allHookIDs = append(allHookIDs, issue.HookBead)
|
||||
}
|
||||
}
|
||||
allHookBeads, _ := agentBeads.ShowMultiple(allHookIDs)
|
||||
|
||||
@@ -654,6 +654,8 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) {
|
||||
Type string `json:"issue_type"`
|
||||
Description string `json:"description"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
HookBead string `json:"hook_bead"` // Read from database column
|
||||
AgentState string `json:"agent_state"` // Read from database column
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(output, &issues); err != nil {
|
||||
@@ -669,7 +671,7 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) {
|
||||
return nil, fmt.Errorf("bead %s is not an agent bead (type=%s)", agentBeadID, issue.Type)
|
||||
}
|
||||
|
||||
// Use shared parsing from beads package
|
||||
// Parse agent fields from description for role/state info
|
||||
fields := beads.ParseAgentFieldsFromDescription(issue.Description)
|
||||
|
||||
info := &AgentBeadInfo{
|
||||
@@ -680,12 +682,15 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) {
|
||||
|
||||
if fields != nil {
|
||||
info.State = fields.AgentState
|
||||
info.HookBead = fields.HookBead
|
||||
info.RoleBead = fields.RoleBead
|
||||
info.RoleType = fields.RoleType
|
||||
info.Rig = fields.Rig
|
||||
}
|
||||
|
||||
// Use HookBead from database column directly (not from description)
|
||||
// The description may contain stale data - the slot is the source of truth.
|
||||
info.HookBead = issue.HookBead
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
@@ -872,6 +877,8 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) {
|
||||
Type string `json:"issue_type"`
|
||||
Description string `json:"description"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
HookBead string `json:"hook_bead"` // Read from database column, not description
|
||||
AgentState string `json:"agent_state"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(output, &agents); err != nil {
|
||||
@@ -885,19 +892,14 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse agent fields
|
||||
fields := beads.ParseAgentFieldsFromDescription(agent.Description)
|
||||
if fields == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if agent has work on hook
|
||||
if fields.HookBead == "" {
|
||||
// Use HookBead from database column directly (not parsed from description)
|
||||
if agent.HookBead == "" {
|
||||
continue // No hooked work - no GUPP violation possible
|
||||
}
|
||||
|
||||
// Check if agent is actively working
|
||||
if fields.AgentState == "working" || fields.AgentState == "running" {
|
||||
if agent.AgentState == "working" || agent.AgentState == "running" {
|
||||
// Check when the agent bead was last updated
|
||||
updatedAt, err := time.Parse(time.RFC3339, agent.UpdatedAt)
|
||||
if err != nil {
|
||||
@@ -907,10 +909,10 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) {
|
||||
age := time.Since(updatedAt)
|
||||
if age > GUPPViolationTimeout {
|
||||
d.logger.Printf("GUPP violation: agent %s has hook_bead=%s but hasn't updated in %v (timeout: %v)",
|
||||
agent.ID, fields.HookBead, age.Round(time.Minute), GUPPViolationTimeout)
|
||||
agent.ID, agent.HookBead, age.Round(time.Minute), GUPPViolationTimeout)
|
||||
|
||||
// Notify the witness for this rig
|
||||
d.notifyWitnessOfGUPP(rigName, agent.ID, fields.HookBead, age)
|
||||
d.notifyWitnessOfGUPP(rigName, agent.ID, agent.HookBead, age)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -948,28 +950,31 @@ func (d *Daemon) checkOrphanedWork() {
|
||||
}
|
||||
|
||||
// For each dead agent, check if they have hooked work
|
||||
// Use HookBead from database column directly (not parsed from description)
|
||||
for _, agent := range deadAgents {
|
||||
fields := beads.ParseAgentFieldsFromDescription(agent.Description)
|
||||
if fields == nil || fields.HookBead == "" {
|
||||
if agent.HookBead == "" {
|
||||
continue // No hooked work to orphan
|
||||
}
|
||||
|
||||
d.logger.Printf("Orphaned work detected: agent %s is dead but has hook_bead=%s",
|
||||
agent.ID, fields.HookBead)
|
||||
agent.ID, agent.HookBead)
|
||||
|
||||
// Determine the rig from the agent ID (gt-polecat-<rig>-<name>)
|
||||
rigName := d.extractRigFromAgentID(agent.ID)
|
||||
if rigName != "" {
|
||||
d.notifyWitnessOfOrphanedWork(rigName, agent.ID, fields.HookBead)
|
||||
d.notifyWitnessOfOrphanedWork(rigName, agent.ID, agent.HookBead)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deadAgentInfo holds info about a dead agent for orphaned work detection.
|
||||
type deadAgentInfo struct {
|
||||
ID string
|
||||
HookBead string // Read from database column, not description
|
||||
}
|
||||
|
||||
// getDeadAgents returns all agent beads with state=dead.
|
||||
func (d *Daemon) getDeadAgents() []struct {
|
||||
ID string
|
||||
Description string
|
||||
} {
|
||||
func (d *Daemon) getDeadAgents() []deadAgentInfo {
|
||||
cmd := exec.Command("bd", "list", "--type=agent", "--json")
|
||||
cmd.Dir = d.config.TownRoot
|
||||
|
||||
@@ -979,27 +984,23 @@ func (d *Daemon) getDeadAgents() []struct {
|
||||
}
|
||||
|
||||
var agents []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"issue_type"`
|
||||
Description string `json:"description"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"issue_type"`
|
||||
HookBead string `json:"hook_bead"` // Read from database column
|
||||
AgentState string `json:"agent_state"` // Read from database column
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(output, &agents); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dead []struct {
|
||||
ID string
|
||||
Description string
|
||||
}
|
||||
|
||||
var dead []deadAgentInfo
|
||||
for _, agent := range agents {
|
||||
fields := beads.ParseAgentFieldsFromDescription(agent.Description)
|
||||
if fields != nil && fields.AgentState == "dead" {
|
||||
dead = append(dead, struct {
|
||||
ID string
|
||||
Description string
|
||||
}{agent.ID, agent.Description})
|
||||
if agent.AgentState == "dead" {
|
||||
dead = append(dead, deadAgentInfo{
|
||||
ID: agent.ID,
|
||||
HookBead: agent.HookBead,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user