The Agent.Status field conflated two different concepts:
- Whether the agent worker process is running (worker lifecycle)
- Whether Claude Code is actively executing a task (execution state)
This caused the status to incorrectly show running when only the tmux panel was open (worker running, no task).
Separated concerns into two distinct status fields:
- Worker Status (computed from
last_seentimestamp): online/offline - Claude Status (reported by agent): idle/running/waiting
- Added
ClaudeStatustype with constants:idle,running,waiting - Added
WorkerStatustype with constants:online,offline - Added
ClaudeStatusfield toAgentstruct - Added
DerivedWorkerStatus(threshold)helper method to compute worker status fromlast_seen - Kept
Statusfield for backward compatibility (deprecated)
- Added
claude_statuscolumn toagentstable with check constraint - Migrated existing
statusvalues toclaude_status - Added index on
claude_statusfor filtering - Updated
claim_task()stored procedure to remove automatic agent status update - Agent heartbeat is now sole source of truth for Claude status
- Updated
UpsertAgent()to accept and storeclaude_statusfield - Removed automatic status updates from:
ClaimTask()- no longer sets agent status torunningCompleteTask()- no longer sets agent status toidleFailTask()- no longer sets agent status toidle
- Agent heartbeat becomes sole source of truth for Claude status
- Updated
UpsertAgent()to handleclaude_statuscolumn in SQL queries - Updated
ListAgents()to fetchclaude_statusfrom database
- Added
ClaudeStatusfield toagentsHeartbeatRequeststruct - Updated
handleAgentsHeartbeat()to acceptclaude_statusfield (falls back tostatusfor backward compatibility) - Created
agentResponsestruct that includes computedworker_status - Updated
handleAgentsList()to compute and returnworker_statusfor each agent (30-second threshold)
- Updated
handleDashboard()to computeworker_statusfor each agent before caching - Dashboard responses now include both
worker_statusandclaude_status
- Updated
heartbeat()function to send both:claude_status(new field) - explicit Claude execution statestatus(legacy field) - kept for backward compatibility with same value
- No changes needed to call sites - all heartbeat calls remain unchanged
- Added
deriveWorkerStatus(lastSeen)function - computes online/offline from timestamp (30s threshold) - Added
workerStatusBadge(lastSeen)function - renders worker status badge (online=green, offline=red) - Added
claudeStatusBadge(claudeStatus)function - renders Claude status badge (idle=muted, running=green, waiting=yellow) - Updated
renderAgents()to display two status columns:- Worker status (online/offline)
- Claude status (idle/running/waiting)
- Updated colspan from 6 to 7 for empty state
- Added two column headers: "Worker" and "Claude" (replaced single "Status" column)
- Updated empty state colspan from 6 to 7
- Added
.badge.errclass for offline status (red) - Added
.badge.muted-badgeclass for idle status (muted gray)
| Scenario | Worker Status | Claude Status | Current Task ID |
|---|---|---|---|
| Worker running, no task | online |
idle |
"" (empty) |
| Worker running, task executing | online |
running |
"<uuid>" |
| Worker running, waiting for user input | online |
waiting |
"<uuid>" |
| Worker crashed/stopped | offline |
(last known) | (last known) |
- Task claimed via API → Coordinator auto-sets
agent.status = "running" - Agent sends heartbeat → May override to
"idle"if no task incurrent-task.json - Tmux panel open, no task → Status incorrectly shows
"running"
- Task claimed via API → No automatic agent status update
- Agent sends heartbeat with
claude_status:- No task claimed →
"idle" - Task claimed and executing →
"running" - Interactive prompt detected →
"waiting"
- No task claimed →
- Worker status derived from
last_seen:- Recent heartbeat (< 30s) →
"online" - Stale heartbeat (> 30s) →
"offline"
- Recent heartbeat (< 30s) →
- Dashboard clearly shows: Worker=Online, Claude=Idle (correct state)
- Run database migration:
supabase/migrations/0003_dual_status.sql - Deploy updated Coordinator with dual status support
- Old agents continue working (send
status, Coordinator stores bothstatusandclaude_status)
- Deploy updated agent code (sends both
claude_statusandstatus) - Agents report accurate Claude execution state via heartbeat
- Dashboard automatically shows dual status for all agents
- Users see clear distinction between worker lifecycle and Claude execution state
- ✅ Go code compiles successfully:
cd coordinator && go build ./cmd/coordinator
See RUNBOOK.md for detailed testing procedures:
- Worker online, no task: Start worker, verify Worker=online, Claude=idle
- Worker online, task executing: Create/claim task, verify Worker=online, Claude=running
- Worker online, waiting: Trigger interactive prompt, verify Worker=online, Claude=waiting
- Worker offline: Stop worker, wait 30s, verify Worker=offline
- Task completion: Complete task, verify Claude returns to idle
# Check agent status
curl -s http://localhost:8080/v1/agents | jq '.[] | {name, worker_status, claude_status, current_task_id, last_seen}'
# Send heartbeat with claude_status
AGENT_ID="$(cat agent/data/agent-id.txt | tr -d '\n')"
curl -X POST http://localhost:8080/v1/agents/heartbeat \
-H 'Content-Type: application/json' \
-d "{\"agent_id\":\"${AGENT_ID}\",\"name\":\"test-agent\",\"claude_status\":\"idle\",\"current_task_id\":\"\"}"coordinator/internal/model/model.go- Status types and Agent structcoordinator/internal/store/memory/memory.go- Memory store logiccoordinator/internal/store/postgres/postgres.go- Postgres queriescoordinator/internal/httpapi/handlers.go- API request/response handlingcoordinator/internal/httpapi/dashboard.go- Dashboard aggregationsupabase/migrations/0003_dual_status.sql- Database schema (NEW)
agent/clw-agent.js- Heartbeat payload
coordinator/internal/httpapi/ui/app.js- UI rendering logiccoordinator/internal/httpapi/ui/index.html- HTML structurecoordinator/internal/httpapi/ui/styles.css- CSS styling
- ✅ Old agents sending only
statuscontinue to work (mapped toclaude_status) - ✅
statusfield kept in model for legacy compatibility - ✅ API accepts both
statusandclaude_statusfields - ✅ No breaking changes to existing integrations
- Agent Detection: Automatically detect interactive prompts and set
claude_status=waiting - Alerting: Notify when worker goes offline
- Metrics: Track average task execution time per Claude status transition
- UI Filters: Filter agents by worker status or Claude status in dashboard
- Issue: #1
- Requirements:
REQUIREMENTS.md - Testing Guide:
RUNBOOK.md - Project Guide:
CLAUDE.md