Repository navigation
feat(observability): export standalone queue, ownership and execution signals (#1252) - #1332
Conversation
Add GET /api/observability: a bounded, authenticated, read-only JSON exporter for the signal/state catalogue of docs/observability.md, derived entirely from Dispatch's own cache with no exporter dependency. - lib/observability-signals.ts: pure derivation with allowlisted dimensions only (no repo/issue/agent identities or error text), as-of-scrape windowed gauges (900/3600 failures, 1800/3600 activity), explicit unknown vs zero, system-emitter exclusion (hosted-groomer leases, non-task AgentRun rows), age-tied PR-fix overdue unacked, and A6 age evidence that only counts assignments with persisted assign_agent audit age (unknown-age assignments are never aged). - lib/observability-snapshot.ts: read-only collector (findMany / findFirst / count only); reuses the canonical admission rule from lib/queue-admission and the real IssueSyncRun status values. - app/api/observability/route.ts: maintainer-tier GET, no-store. - tests: scenario matrix (healthy idle, busy churn, stranded assignment, no first sync, failing discovery, inactive worker, held dependency) proving distinguishable observed/unknown states; promptly-claimed churn is not aged stranded while an actual aged assignment is; fresh audit failures stay observable in-window at equal aggregate counts; read-only guards on collector and route. - docs/observability-snapshot.md + minimal status notes in docs/observability.md; env var DISPATCH_OBSERVABILITY_STRANDED_ASSIGNMENT_MS documented in .env.example and AGENTS.md.
- A6 age evidence is presence-based, not ordering-based: a no-op re-assign of already-delivered work no longer re-arms the aged clock (delivered-then-reassigned issues stay out of the aged count). - AgentWork pickup rows are filtered by worker identity like leases, so a hypothetical system-emitter AgentWork cannot suppress A4. - Ack generation parsing mirrors the canonical parseAgentHandoutToken rule (last-@ boundary, integer >= 1), matching @-bearing agent names. - Assignment evidence horizon widened to 30d and declared in the snapshot as evidenceLookbackSeconds: expired evidence reports as age-unknown with the horizon visible, never silently un-aged. - A4-style observation is explicitly unknown (null) when the sync job is disabled and a success series exists — the fresh-sync guard is not evaluable, so neither true nor false. - docs: note aggregate-only gauges vs the catalogue lane dimension and the full-table Lease/AgentWork read cost; tests for all of the above.
joryirving
left a comment
There was a problem hiding this comment.
Blocker-only review on exact head 6adc8347453d70a8800b70d16757f580f7ec1899: APPROVE. The new GET /api/observability surface is maintainer-gated through the existing default-deny route tiering, carries no identity/error-text dimensions, and the collector is read-only (database reads plus pure derivation; no forge or scheduler mutations). The implementation reuses the canonical admission rule, preserves explicit unknown-vs-zero semantics, uses age-tied evidence for stranded assignments and PR-fix hand-outs, excludes non-task/system activity from execution signals, and keeps windowed values as gauges. I found no material correctness, security, ownership, or data-integrity blocker under the agreed merge-biased standard. CI, Security Audit, Build Dispatch Image, and PR Smoke are green; AI PR Review was still running when this approval was submitted.
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M3.1-Flash (anthropic) — primary route · pr-reviewer-action v3.7.0
Findings (1 major)
| Severity | Location | Finding |
|---|---|---|
docs/observability-snapshot.md:21 |
The contract promises an allowlisted AgentWork checkpoint dimension, but the snapshot output exposes only AgentWork state, leaving the implemented signal catalogue incomplete. |
Request changes: the snapshot promises an AgentWork checkpoint dimension but does not expose one.
Change-by-change findings
- Claim 1 violated:
docs/observability-snapshot.mdsays the bounded dimensions include AgentWorkstate/checkpoint, butObservabilitySnapshot.executionexposes onlyagentWorkByState; add the allowlisted checkpoint dimension or remove the contract promise if it is intentionally deferred. - Claim 2 held: derivation remains pure, uses the documented fixed windows, and keeps absent baselines distinct from zero.
- Claim 3 held: the collector uses read queries only and does not invoke forge or scheduler mutation paths.
Sources
src/lib/observability-signals.tssrc/lib/observability-snapshot.tssrc/app/api/observability/route.tssrc/lib/queue-admission.tssrc/lib/pr-fix-queue.tssrc/lib/auth.ts
Linked Issue Fit
The read-only snapshot, distinguishability matrix, task-run allowlist, real sync statuses, age-tied assignment/PR-fix evidence, and window gauges align with PR 1252; however, the incomplete AgentWork state catalogue prevents the implementation from fully matching the referenced catalogue.
Tool Harness Findings
The harness confirmed maintainer-by-default route tiers, worker-token 403 handling, OIDC integration through authorizeRequest, canonical PR-fix ack parsing, shared queue-admission logic, and the relevant Prisma models; its tool budget was exhausted before a complete caller scan.
Unknowns or Needs Verification
The referenced PR 1237/PR 1239 source was not available for direct retrieval, so the exact catalogue fields were checked against the repository's docs/observability.md and the new snapshot contract rather than the external source.
Reviewed under the maintainer's blocker-only policy. The finding is technically valid: the snapshot contract/catalogue names an AgentWork checkpoint distribution that this slice does not expose. That is an observability completeness gap, not a material correctness, security, ownership, data-integrity, or irreversible-behavior defect. The endpoint's core purpose remains satisfied and the missing dimension is suitable for follow-up/dogfooding rather than a merge blocker.
Closes #1252
Implements the Snapshot API (approach D) of the #1237/#1239 design catalogue (
docs/observability.md§7):GET /api/observability— a bounded, authenticated, read-only JSON snapshot of Dispatch's own work-supply, execution, ownership, PR-fix, admission, grooming, and pipeline signals. No exporter dependency, no/metrics, no coupling to Courier or any dashboard installation.What lands
src/lib/observability-signals.ts— pure derivation over loaded rows; all dimension allowlists live here; explicit unknown-vs-zero; as-of-scrape windowed gauges only (failures900/3600s, activity1800/3600s).src/lib/observability-snapshot.ts— read-only collector (findMany/findFirst/countonly; no forge/GitHub calls, no scheduler mutation).src/app/api/observability/route.ts— maintainer-tier GET,Cache-Control: no-store.docs/observability-snapshot.md+ minimal status notes indocs/observability.md;DISPATCH_OBSERVABILITY_STRANDED_ASSIGNMENT_MSin.env.example/ AGENTS.md.c0e02d7(no blockers); all findings fixed in6adc834(presence-based A6 age evidence, AgentWork worker-identity pickup filter, canonical ack parsing, declared evidence horizon, unknown A4 state when the sync job is disabled).Acceptance mapping
agedPastThreshold: 0while a known-age un-executed assignment produces≥ 1; GitHub-side assignments with no persisted age evidence stayageUnknownand are never aged (fail-open). Re-assigning already-delivered work does not re-arm the clock.900 swindow even as an older one expires — neverdelta()/increase()-able by construction.create/update/updateMany/upsert/delete/deleteManyall throw; a 200 response proves no write path executes. Claim release, requeue, status mutation, credential changes, and mandatory backends are absent.Invariants this change must keep — and every other path enforcing the same rule
IssueSyncRunstatuses arerunning/completed/failed(nosuccess) — writers:src/lib/sync-lock.ts:160,src/app/api/sync/route.ts:67,src/app/api/sync/scheduled/route.ts(completed/failedpaths). Kept: the snapshot matches exactly"completed"/"failed"and buckets anything else as unknown; the §4.9 source-drift check indocs/observability.test.tsis untouched and still passes.QUEUED+dispatchedGeneration === generation+"<agent>@<generation>"acks parsed at the LAST@— writer/enforcer:src/lib/pr-fix-queue.ts(ackPrFixHandout,reclaimStalePrFixHandouts,parseAgentHandoutToken). Kept: the snapshot'sackedGenerationsmirrors the canonical rule (integer ≥ 1,@-bearing agent names tested); the reclaimer itself is unchanged; overdue is age-tied toPR_FIX_HANDOUT_TIMEOUT_MS, matching A7's reviewed age-tied predicate.evaluateQueueAdmissioninsrc/lib/queue-admission.ts, used by/queue//next-taskviasrc/lib/agent-queue-fetch.ts. Kept: the snapshot reuses that exact function +loadAdmissionRuns(no reimplementation, no divergence); queue routes untouched.PARKEDis operational exhaustion,BLOCKEDis the only human decision — enforced at write time byresolvePrFixOutcome/markPrFixIteminsrc/lib/pr-fix-queue.tsand surfaced bysrc/lib/pr-fix-surfacing.ts. Kept: settlement buckets them separately;parkedByOutcomenever feedshumanDecisionsBlocked(test-pinned).src/lib/groomer/run.tshasOtherLease) and therunTypewriters intasks/report/heartbeat. Kept: execution/pickup/reported/started gauges filter by theagent/*-emitter exclusion and the taskrunTypeallowlist (implement,followup-pr), not subtraction.GET /audit, board API) legitimately carry drill-down identities; this snapshot must not. Kept: bounded allowlists withunknownbuckets everywhere, and a test asserting repo names, issue numbers, agent names andacmenever appear in the serialized snapshot.requiredTierForRouteinsrc/lib/auth.ts. Kept: noWORKER_ALLOWLISTentry was added; route test asserts 401 unauthenticated / 403 worker-tier.docs/observability.test.tsno-rate-function rule). Kept: everything is recomputed from row timestamps per request; documented in the snapshot contract.Local validation (run against head
6adc834, cwd/workspace)npm testequivalent —NODE_ENV=development npx vitest run: 202 files passed, 4 skipped; 5085 passed, 24 skipped, 1 todo (includesdocs/observability.test.tsstructural/citation checks andenv-example.test.ts).npx tsc --noEmit: exit 0.npx eslint .: 0 errors (1 pre-existing warning insrc/app/login/page.tsx, unrelated).CI is not asserted green — image build / Trivy (advisory) / AI review are external and left to the operator. No external verification was waited on.
Handoff
Settled decisions: snapshot JSON (approach D) only — a
/metricsscrape exporter and OTel remain deferred per §10; A6 age evidence is presence-based with a declared 30-day evidence horizon (evidenceLookbackSeconds);nullmeans unknown (absent sync series,off-mode admission, unevaluable A4 guard) and never zero. Remaining external verification: CI on this head, and an operator smoke ofGET /api/observabilitywith a maintainer token against a live instance.