fix: snake_case SlotStatus + scheduler debug logs

Two issues found while end-to-end testing against a running
harborforge-backend:

  - SlotStatus enum values: backend stores snake_case
    ("not_started" / "ongoing" / …), not the camelCase the
    OpenClaw plugin's TypeScript types.ts misled the initial
    drop into using. Heartbeat responses came back with
    Slot.Status="not_started" which the scheduler never matched
    against SlotStatus("NotStarted"), so dispatchSlot never
    fired. Aligned with backend's actual enum string values
    (verified via heartbeat response shape).

  - Added info-level logs at slot selection + dispatchSlot
    entry + WakeAgent fire/result so operators can see the
    plugin's decision chain in production without enabling
    debug. Cheap (~one tick per agent per heartbeat interval).

E2E in sim: backend returns slots=1 → selection chosen=true →
dispatch enter → WakeAgent enqueued ok → backend slot ongoing
→ next heartbeat returns slots=0.
This commit is contained in:
h z
2026-06-03 11:42:18 +01:00
parent 78b1ec5181
commit bc1ab7b6ea
2 changed files with 26 additions and 7 deletions

View File

@@ -158,6 +158,9 @@ func (s *Scheduler) tickForAgent(ctx context.Context, agent ReportableAgent, now
chosen = slot
}
}
s.host.Log("info", "calendar slot selection", map[string]any{
"agent": agent.ID, "available": len(resp.Slots), "chosen": chosen != nil,
})
if chosen != nil {
s.dispatchSlot(ctx, agent.ID, *chosen)
}
@@ -173,14 +176,19 @@ func (s *Scheduler) tickForAgent(ctx context.Context, agent ReportableAgent, now
// transition immediately.
func (s *Scheduler) dispatchSlot(ctx context.Context, agentID string, slot Slot) {
ident := slot.SlotIdent()
s.host.Log("info", "calendar dispatchSlot enter", map[string]any{
"agent": agentID, "slot_ident": ident,
})
s.mu.Lock()
if _, dup := s.activeBySlotIdent[ident]; dup {
s.mu.Unlock()
s.host.Log("info", "calendar dispatchSlot skipped (already active)", map[string]any{"slot": ident})
return
}
if _, agentBusy := s.activeByAgentID[agentID]; agentBusy {
// Don't pick up another slot until the current one resolves.
s.mu.Unlock()
s.host.Log("info", "calendar dispatchSlot skipped (agent has active slot)", map[string]any{"agent": agentID})
return
}
now := time.Now().UTC()
@@ -191,12 +199,21 @@ func (s *Scheduler) dispatchSlot(ctx context.Context, agentID string, slot Slot)
message := buildWakeMessage(slot)
source := "calendar:" + ident
s.host.Log("info", "calendar firing WakeAgent", map[string]any{
"agent": agentID, "slot": ident, "source": source, "msg_len": len(message),
})
if err := s.host.WakeAgent(ctx, sdkplugin.WakeAgentRequest{
AgentID: agentID, Message: message, Source: source,
}); err != nil {
s.host.Log("warn", "calendar WakeAgent failed", map[string]any{
"agent": agentID, "err": err.Error(),
})
s.resolveLocally(ident, agentID, SlotAborted, "", "wake failed: "+err.Error())
return
}
s.host.Log("info", "calendar WakeAgent enqueued ok", map[string]any{
"agent": agentID, "slot": ident,
})
// Mark Ongoing on the backend.
update := SlotAgentUpdate{
Status: SlotOngoing, StartedAt: now.Format("15:04:05"),