feat: add agent-agnostic heartbeat orchestration API#358
Conversation
Add POST /api/agents/{agentName}/heartbeat endpoint that:
- Authenticates via existing agent token flow
- Runs deterministic sync/reconciliation using shared service code
- Aggregates warnings/errors (best-effort sync failures are warnings)
- Records an AgentRun for the heartbeat pass
- Returns compact machine-readable result with status ok|warning|error
Also adds src/lib/heartbeat.ts with runSyncBestEffort and
runReconcileBestEffort helpers that reuse existing issue-sync service
functions rather than shelling out to local scripts.
Includes 16 tests covering auth, success, warning aggregation, and
failure response behavior.
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — routed smart (risk match: public_route_changes)
PR PR 358: feat: add agent-agnostic heartbeat orchestration API
Recommendation: Approve
This PR adds a well-designed heartbeat endpoint that follows existing repository patterns. The implementation is internally consistent, properly authenticated, and has comprehensive test coverage.
Change-by-Change Findings
src/app/api/agents/[agentName]/heartbeat/route.ts (new file, 123 lines)
- Implements
POST /api/agents/{agentName}/heartbeatwith a clean orchestration pattern - Calls
authorizeRequestat the top of the handler for authentication - Runs sync and reconciliation phases best-effort with error aggregation
- Records
AgentRunfor auditability (best-effort, does not block on failure) - Returns structured
AgentHeartbeatResponsewith status, timestamps, warnings, errors, and touched issue URLs
src/lib/heartbeat.ts (new file, 175 lines)
- Provides reusable
runSyncBestEffort()andrunReconcileBestEffort()helpers - Reuses existing service code from
@/lib/issue-syncrather than duplicating logic - Aggregates per-repo results into warnings/errors with proper error handling (
instanceof Errorpattern) - Empty repo list returns an error (distinguishes "nothing to do" from "something went wrong")
src/app/api/agents/[agentName]/heartbeat/route.test.ts (new file, 412 lines)
- 16 tests covering all specified scenarios: auth, success, warnings, errors, crash resilience
- Verifies AgentRun recording across all status outcomes
- Tests confirm agent-agnostic design works with any
agentName - Tests verify no grooming/lane classification fields leak into response
Standards Compliance
✅ No agent-specific names in generic docs — Uses generic agent/* pattern throughout
✅ API routes return appropriate HTTP status codes — Returns 401 for auth failures, 200 for all other outcomes (errors surfaced in response body)
✅ Error handling — Uses error instanceof Error pattern consistently
✅ AuditLog integration — AgentRun records written for every heartbeat pass (best-effort, non-blocking)
✅ Auth pattern — Uses authorizeRequest from @/lib/auth, matching all other agent API routes in the codebase
✅ Secrets — No secrets committed; DISPATCH_AGENT_TOKEN is an env var reference
Required Checks
✅ verify route access controls are in place
- The route calls
authorizeRequest(request)at line 38, returning 401 if unauthorized - Tests confirm: no auth header → 401, wrong token → 401, valid Bearer → 200
- The
authorizeRequestfunction from@/lib/auth.tsenforces bearer token auth viaDISPATCH_AGENT_TOKEN - Even if
DISPATCH_AUTH_MODE=disabledis set (open operator UI), the explicitauthorizeRequestcheck still enforces agent token auth for this route - Pattern matches other agent routes:
/api/agent-runs,/api/agent-work/*,/api/agents/[agentName]/queue
✅ check for unintended public endpoints
- No
GEThandler exported; onlyPOSTis defined - The
DISPATCH_AUTH_MODE=disabledsetting does not bypass the explicitauthorizeRequestcheck in this handler agentNamepath param is not used for authorization decisions — the same bearer token works for any agent name (agent-agnostic by design)- No rate limiting observed, but this is consistent with existing agent endpoints and not flagged in the repository standards
Evidence Provider Findings
No evidence provider findings configured.
Tool Harness Findings
✅ Verified src/app/api/agents/[agentName]/heartbeat/route.ts — confirms authorizeRequest check, best-effort phases, AgentRun recording
✅ Verified src/lib/heartbeat.ts — confirms reusable helpers, proper error aggregation, reuse of existing sync code
✅ Verified src/lib/auth.ts — confirms auth modes, bearer token handling, authorizeRequest implementation
✅ git grep for DISPATCH_AGENT_TOKEN|isAuthorizedAgent|isAuthorizedBearer returned no matches in corpus (auth helpers are mocked in tests, verified via file reads)
Unknowns / Needs Verification
None — all required checks verified from the diff and supporting files.
Linked Issue Fit
No linked issue context was provided. The PR body states "Relates to PR 357" but no linked issue details were available for comparison against acceptance criteria. The PR description is self-contained and specifies non-goals (no model judgment, no Saffron-specific behavior, no workspace-local dependencies), all of which are respected in the implementation.
Summary
Adds
POST /api/agents/{agentName}/heartbeat— an agent-agnostic heartbeat orchestration endpoint that any agent can call to trigger a deterministic sync + reconcile pass.What this PR adds
New endpoint:
POST /api/agents/{agentName}/heartbeatAuthenticates via existing agent token flow, runs sync and reconciliation best-effort, records an AgentRun, and returns a compact result:
Shared service helpers:
src/lib/heartbeat.tsrunSyncBestEffort()— wraps existingsyncIssuesForReposwith error aggregationrunReconcileBestEffort()— wraps existingreconcileClosedIssueswith error aggregationBoth reuse Dispatch's existing service code (not shell scripts or local state).
Non-goals respected
Tests
16 tests covering:
Files changed
src/app/api/agents/[agentName]/heartbeat/route.tssrc/app/api/agents/[agentName]/heartbeat/route.test.tssrc/lib/heartbeat.tsFixes #357.