diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-19 23:03:56 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-19 23:03:56 +0000 |
| commit | 7e8967decbc8221694953abf1435fda8aaf18824 (patch) | |
| tree | 3cee147c32da1565ec1e5ea72b0ddf131077dd66 /internal/storage/db.go | |
| parent | e2f5379e00747f17d91ee1c90828d4494c2eb4d8 (diff) | |
feat: agent status dashboard with availability timeline and Gemini quota detection
- Detect Gemini TerminalQuotaError (daily quota) as BUDGET_EXCEEDED, not generic FAILED
- Surface container stderr tail in error so quota/rate-limit classifiers can match it
- Add agent_events table to persist rate-limit start/recovery events across restarts
- Add GET /api/agents/status endpoint returning live agent state + 24h event history
- Stats dashboard: agent status cards, 24h availability timeline, per-run execution table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/storage/db.go')
| -rw-r--r-- | internal/storage/db.go | 63 |
1 files changed, 63 insertions, 0 deletions
diff --git a/internal/storage/db.go b/internal/storage/db.go index 8bc9864..0d11b4e 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -99,6 +99,16 @@ func (s *DB) migrate() error { key TEXT PRIMARY KEY, value TEXT NOT NULL )`, + `CREATE TABLE IF NOT EXISTS agent_events ( + id TEXT PRIMARY KEY, + agent TEXT NOT NULL, + event TEXT NOT NULL, + timestamp DATETIME NOT NULL, + until DATETIME, + reason TEXT + )`, + `CREATE INDEX IF NOT EXISTS idx_agent_events_agent ON agent_events(agent)`, + `CREATE INDEX IF NOT EXISTS idx_agent_events_timestamp ON agent_events(timestamp)`, } for _, m := range migrations { if _, err := s.db.Exec(m); err != nil { @@ -858,3 +868,56 @@ func (s *DB) SetSetting(key, value string) error { _, err := s.db.Exec(`INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, key, value) return err } + +// AgentEvent records a rate-limit state change for an agent. +type AgentEvent struct { + ID string + Agent string + Event string // "rate_limited" | "available" + Timestamp time.Time + Until *time.Time // non-nil for "rate_limited" events + Reason string // "transient" | "quota" +} + +// RecordAgentEvent inserts an agent rate-limit event. +func (s *DB) RecordAgentEvent(e AgentEvent) error { + _, err := s.db.Exec( + `INSERT INTO agent_events (id, agent, event, timestamp, until, reason) VALUES (?, ?, ?, ?, ?, ?)`, + e.ID, e.Agent, e.Event, e.Timestamp.UTC(), timeOrNull(e.Until), e.Reason, + ) + return err +} + +// ListAgentEvents returns agent events since the given time, newest first. +func (s *DB) ListAgentEvents(since time.Time) ([]AgentEvent, error) { + rows, err := s.db.Query( + `SELECT id, agent, event, timestamp, until, reason FROM agent_events WHERE timestamp >= ? ORDER BY timestamp DESC LIMIT 500`, + since.UTC(), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var events []AgentEvent + for rows.Next() { + var e AgentEvent + var until sql.NullTime + var reason sql.NullString + if err := rows.Scan(&e.ID, &e.Agent, &e.Event, &e.Timestamp, &until, &reason); err != nil { + return nil, err + } + if until.Valid { + e.Until = &until.Time + } + e.Reason = reason.String + events = append(events, e) + } + return events, rows.Err() +} + +func timeOrNull(t *time.Time) interface{} { + if t == nil { + return nil + } + return t.UTC() +} |
