From bc1ab7b6eab81f2952ad0ecaf01a2c76b583bcd5 Mon Sep 17 00:00:00 2001 From: hzhang Date: Wed, 3 Jun 2026 11:42:18 +0100 Subject: [PATCH] fix: snake_case SlotStatus + scheduler debug logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/calendar/scheduler.go | 17 +++++++++++++++++ internal/calendar/types.go | 16 +++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/internal/calendar/scheduler.go b/internal/calendar/scheduler.go index 2a82a79..f594130 100644 --- a/internal/calendar/scheduler.go +++ b/internal/calendar/scheduler.go @@ -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"), diff --git a/internal/calendar/types.go b/internal/calendar/types.go index aa8669c..5b1848f 100644 --- a/internal/calendar/types.go +++ b/internal/calendar/types.go @@ -8,16 +8,18 @@ package calendar import "time" // SlotStatus enumerates the lifecycle. String values match backend's -// SlotStatus enum verbatim (camelCase as stored in DB). +// SlotStatus enum verbatim (snake_case — verified via heartbeat +// response shape against running harborforge-backend). type SlotStatus string const ( - SlotNotStarted SlotStatus = "NotStarted" - SlotOngoing SlotStatus = "Ongoing" - SlotFinished SlotStatus = "Finished" - SlotAborted SlotStatus = "Aborted" - SlotDeferred SlotStatus = "Deferred" - SlotPaused SlotStatus = "Paused" + SlotNotStarted SlotStatus = "not_started" + SlotOngoing SlotStatus = "ongoing" + SlotFinished SlotStatus = "finished" + SlotAborted SlotStatus = "aborted" + SlotDeferred SlotStatus = "deferred" + SlotPaused SlotStatus = "paused" + SlotSkipped SlotStatus = "skipped" ) // SlotType: work vs on_call. Affects whether the agent flips to busy.