Replace the racy three-step heartbeat path (listAgentTasks ->
findReusableHeartbeatTask -> saveTask -> checkoutTask) with a single
transactional acquireHeartbeatTask method. The prior flow allowed two
concurrent scheduler ticks to both observe "no reusable heartbeat" and
both INSERT a fresh [Heartbeat] task before either CAS-checked out,
producing duplicates that cleanupDuplicateHeartbeatTasks had to mop up.
acquireHeartbeatTask runs inside BEGIN IMMEDIATE so concurrent ticks
serialize on the writer lock. Inside the transaction it first attempts
to claim any existing claimable [Heartbeat] task (todo/backlog/done/
in_review with checkout_run_id IS NULL), reviving done/in_review by
resetting status. If nothing is claimable but an in-flight heartbeat
exists (status NOT IN done/cancelled), it returns null instead of
creating a duplicate. Only when no heartbeat task exists at all does it
INSERT a new one, already in 'in_progress' with checkout_run_id set.
cleanupDuplicateHeartbeatTasks remains as deletion-side defense.
Replace 17-table snapshot fetch + 50+ JS filters per agent with five
scoped aggregation queries:
- tasks GROUP BY assignee_agent_id with conditional sums for done /
assigned / cancelled counts
- runs (within cutoff) GROUP BY agent_id with conditional sums for
succeeded / failed counts
- runs GROUP BY agent_id over started_at/finished_at for avg duration
+ max(created_at) as last active
- cost_entries GROUP BY agent_id for total cost
- activity GROUP BY actor restricted to status-change actions for
review cycle count
Company-level totals also collapse to single COUNT/SUM queries instead
of in-memory reductions over the full snapshot. Return shape unchanged.
Add explicit LIMIT to all hot-path tables in listSnapshot regardless of
companyId, raise cross-company caps from 500/250 to 2000/1000 so dashboard
no longer truncates silently at 500.
Add migration 28 with composite indexes covering snapshot WHERE/ORDER BY
columns: tasks(company_id, created_at|updated_at), approvals(company_id,
created_at), task_comments(company_id, created_at), documents(company_id,
updated_at), meetings(company_id, scheduled_at), sprints(company_id,
start_date), runs(agent_id, created_at), cost_entries(agent_id,
created_at), activity(company_id, actor, action), tasks(company_id,
assignee_agent_id, status). Indexes match exact ORDER BY direction so
SQLite can satisfy reads via index scan without temp sort.
Stop writing new session data back to the legacy tasks.session_json
column; updateTaskSession now upserts the active task_sessions row
(updating the session pointed to by tasks.active_session_id, creating
one when absent), and getTaskSession reads from the same row so
session data has a single source of truth. Adds migration 27 to
backfill pre-existing tasks.session_json values into task_sessions
idempotently via INSERT OR IGNORE guarded on task_id. The legacy
column is kept for read-only history and is not dropped.
Stop silently swallowing duplicate-column errors in the migration
runner; any real migration failure now throws. Replace try/catch-based
column additions in ensureOptionalColumns and migrations 19-22 with
pragma table_info checks so repeated startup is idempotent without
tolerating error signals. Reorder initializeDatabase so runMigrations
runs before ensureOptionalColumns to avoid migration 13 adding columns
that ensureOptionalColumns already created on fresh databases.