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" {
|
if err == nil && agentBead != nil && agentBead.Type == "agent" {
|
||||||
status.AgentBeadID = agentBeadID
|
status.AgentBeadID = agentBeadID
|
||||||
|
|
||||||
// Parse hook_bead from the agent bead's description
|
// Read hook_bead from the agent bead's database field (not description!)
|
||||||
agentFields := beads.ParseAgentFieldsFromDescription(agentBead.Description)
|
// The hook_bead column is updated by `bd slot set` in UpdateAgentState.
|
||||||
if agentFields != nil && agentFields.HookBead != "" {
|
// 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
|
// Fetch the bead on the hook
|
||||||
hookBead, err = b.Show(agentFields.HookBead)
|
hookBead, err = b.Show(agentBead.HookBead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Hook bead referenced but not found - report error but continue
|
// Hook bead referenced but not found - report error but continue
|
||||||
hookBead = nil
|
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
|
// 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
|
var allHookIDs []string
|
||||||
for _, issue := range allAgentBeads {
|
for _, issue := range allAgentBeads {
|
||||||
fields := beads.ParseAgentFields(issue.Description)
|
if issue.HookBead != "" {
|
||||||
if fields != nil && fields.HookBead != "" {
|
allHookIDs = append(allHookIDs, issue.HookBead)
|
||||||
allHookIDs = append(allHookIDs, fields.HookBead)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allHookBeads, _ := agentBeads.ShowMultiple(allHookIDs)
|
allHookBeads, _ := agentBeads.ShowMultiple(allHookIDs)
|
||||||
|
|||||||
@@ -654,6 +654,8 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) {
|
|||||||
Type string `json:"issue_type"`
|
Type string `json:"issue_type"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
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 {
|
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)
|
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)
|
fields := beads.ParseAgentFieldsFromDescription(issue.Description)
|
||||||
|
|
||||||
info := &AgentBeadInfo{
|
info := &AgentBeadInfo{
|
||||||
@@ -680,12 +682,15 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) {
|
|||||||
|
|
||||||
if fields != nil {
|
if fields != nil {
|
||||||
info.State = fields.AgentState
|
info.State = fields.AgentState
|
||||||
info.HookBead = fields.HookBead
|
|
||||||
info.RoleBead = fields.RoleBead
|
info.RoleBead = fields.RoleBead
|
||||||
info.RoleType = fields.RoleType
|
info.RoleType = fields.RoleType
|
||||||
info.Rig = fields.Rig
|
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
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,6 +877,8 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) {
|
|||||||
Type string `json:"issue_type"`
|
Type string `json:"issue_type"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
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 {
|
if err := json.Unmarshal(output, &agents); err != nil {
|
||||||
@@ -885,19 +892,14 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse agent fields
|
|
||||||
fields := beads.ParseAgentFieldsFromDescription(agent.Description)
|
|
||||||
if fields == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if agent has work on hook
|
// 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
|
continue // No hooked work - no GUPP violation possible
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if agent is actively working
|
// 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
|
// Check when the agent bead was last updated
|
||||||
updatedAt, err := time.Parse(time.RFC3339, agent.UpdatedAt)
|
updatedAt, err := time.Parse(time.RFC3339, agent.UpdatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -907,10 +909,10 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) {
|
|||||||
age := time.Since(updatedAt)
|
age := time.Since(updatedAt)
|
||||||
if age > GUPPViolationTimeout {
|
if age > GUPPViolationTimeout {
|
||||||
d.logger.Printf("GUPP violation: agent %s has hook_bead=%s but hasn't updated in %v (timeout: %v)",
|
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
|
// 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
|
// For each dead agent, check if they have hooked work
|
||||||
|
// Use HookBead from database column directly (not parsed from description)
|
||||||
for _, agent := range deadAgents {
|
for _, agent := range deadAgents {
|
||||||
fields := beads.ParseAgentFieldsFromDescription(agent.Description)
|
if agent.HookBead == "" {
|
||||||
if fields == nil || fields.HookBead == "" {
|
|
||||||
continue // No hooked work to orphan
|
continue // No hooked work to orphan
|
||||||
}
|
}
|
||||||
|
|
||||||
d.logger.Printf("Orphaned work detected: agent %s is dead but has hook_bead=%s",
|
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>)
|
// Determine the rig from the agent ID (gt-polecat-<rig>-<name>)
|
||||||
rigName := d.extractRigFromAgentID(agent.ID)
|
rigName := d.extractRigFromAgentID(agent.ID)
|
||||||
if rigName != "" {
|
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.
|
// getDeadAgents returns all agent beads with state=dead.
|
||||||
func (d *Daemon) getDeadAgents() []struct {
|
func (d *Daemon) getDeadAgents() []deadAgentInfo {
|
||||||
ID string
|
|
||||||
Description string
|
|
||||||
} {
|
|
||||||
cmd := exec.Command("bd", "list", "--type=agent", "--json")
|
cmd := exec.Command("bd", "list", "--type=agent", "--json")
|
||||||
cmd.Dir = d.config.TownRoot
|
cmd.Dir = d.config.TownRoot
|
||||||
|
|
||||||
@@ -979,27 +984,23 @@ func (d *Daemon) getDeadAgents() []struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var agents []struct {
|
var agents []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"issue_type"`
|
Type string `json:"issue_type"`
|
||||||
Description string `json:"description"`
|
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 {
|
if err := json.Unmarshal(output, &agents); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var dead []struct {
|
var dead []deadAgentInfo
|
||||||
ID string
|
|
||||||
Description string
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, agent := range agents {
|
for _, agent := range agents {
|
||||||
fields := beads.ParseAgentFieldsFromDescription(agent.Description)
|
if agent.AgentState == "dead" {
|
||||||
if fields != nil && fields.AgentState == "dead" {
|
dead = append(dead, deadAgentInfo{
|
||||||
dead = append(dead, struct {
|
ID: agent.ID,
|
||||||
ID string
|
HookBead: agent.HookBead,
|
||||||
Description string
|
})
|
||||||
}{agent.ID, agent.Description})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user