Replace the six inline queryAll calls in routes-agents-standup.ts with
typed domain methods: db.agents gains listDirectReportsForStandup and
listReportStatusesForStandup (LEFT JOIN aggregation), db.tasks gains
listAssigneeTasksForStandup and listTasksForAssignees (safe on empty
assignee list), db.approvals gains listAgentPendingApprovals and
listAgentResolvedApprovals. Shared listRecentTaskComments is reused.
Replace the ad-hoc getApproval helper and the filterable list query
in routes-approvals.ts with db.approvals methods. Add findApprovalById
(non-throwing lookup returning null) and listApprovalsForApi (accepts
requestedByAgentId / relatedTaskId / statuses filters and builds the
prepared statement inside the domain layer).
Replace six inline queryAll calls in routes-work-graph.ts with domain
methods. db.entityCrud gains listGoals, getGoalForUpdate, listProjects,
getProjectForUpdate. db.knowledge gains listSprints. db.company gains
getDashboardSnapshot, which returns the raw agent/task/activity rows
the dashboard handler aggregates. The route-level row shapes are
preserved in the new GoalListRow / ProjectListRow / SprintListRow
types so HTTP responses remain identical.
Replace inline queryAll calls in routes-meetings.ts with two new
domain methods on db.knowledge: listMeetings (powers
GET /api/companies/:companyId/meetings) and getMeeting (used by the
PATCH meeting handler). The MeetingListRow shape mirrors the historical
inline projection so HTTP responses stay byte-stable. Also adds
listKnowledgeEntries here for use by routes-misc.
Replace raw queryAll calls in routes-files.ts with two new domain
methods on db.documents: listDocuments (powers
GET /api/companies/:companyId/documents) and getDocumentForUpdate (used
by the PATCH document handler). The DocumentListRow / DocumentLookupRow
shapes preserve the historical inline-SQL projections so HTTP responses
remain identical.
Replace inline queryAll calls in routes-tasks.ts with two new domain
methods: tasks.listTasksByStatuses (used by GET /api/companies/:id/issues)
and documents.listImportantDocsForReview (used by the deliverable-review
gate). Routes now hold no SQL or column-mapping logic; the
TaskListByStatusesRow shape preserves the existing HTTP response fields
including requiresUserReview as a boolean.
Replace 17-table snapshot fetch + 6 JS filter passes with 5 scoped
SELECTs targeting only inbox-relevant rows:
- approvals where state = 'pending'
- runs where status in ('failed', 'timed_out') and created_at > 24h
- agents where budget breach > 80%
- tasks where status in ('blocked', 'in_review')
- agent_messages with __board__ read-receipt left join filtered to
company/incident channel or urgent priority
Agent name lookup is a single id->name map built from one scoped agents
read. Return shape unchanged.
Replace 17-table snapshot fetch + per-agent JS filters with:
- scoped agents row read (excluding terminated/pending_approval)
- tasks GROUP BY assignee_agent_id with conditional sums for
in_progress / blocked / done-in-24h counts
- runs GROUP BY agent_id over 24h window with correlated subquery
for last status, plus max(created_at) for last run timestamp
- cost_entries GROUP BY agent_id over 24h window for cost
- activity GROUP BY actor for review-cycle counts
- single SELECTs for company-level counters (completed tasks, run
aggregates, blocked tasks, pending approvals, urgent unread, active
sprint)
Return shape unchanged.
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.