Skip to content

Per-spec event history: GET /v1/events?spec= serves the durable record - #196

Merged
udaychandra merged 1 commit into
mainfrom
agent/sail-spec-events
Aug 18, 2026
Merged

Per-spec event history: GET /v1/events?spec= serves the durable record#196
udaychandra merged 1 commit into
mainfrom
agent/sail-spec-events

Conversation

@udaychandra

Copy link
Copy Markdown
Contributor

Spec: sail-spec-events (mast PR: standardapplied/mast — same branch name).

Why

The audit store keeps every RECORD-class event forever (retention prunes telemetry only), but the API could only answer the single global /v1/events/recent?limit= window. Mast's rooms backfilled from the last 100 global events, so on an active control plane a spec's lifecycle rows (dispatches, stops, review beats, guardrails, snapshots) silently vanished from its room while sitting durably in the store. Storage existed; retrieval was never finished.

What

  • GET /v1/events?spec=<id>[&since=<eventId>][&limit=] — a thin route over the store. spec is required for this shape (the unscoped firehose stays /recent); since is the exclusive monotonic event-id cursor for gap-fill after SSE reconnects, omitted means the newest limit rows; results are oldest first; limit is clamped (default 100, max 1000); same auth tier as every other read.
  • EventStore.forSpec(specId, afterId, limit, excludedTypes) — the bounded variant. Both query shapes range-scan idx_events_spec (spec_id + implicit rowid); a query-plan test asserts no table scan.
  • RECORD-only — telemetry types are excluded in SQL so they never occupy the window. The telemetry taxonomy now lives once, in Event.WellKnownTypes.TELEMETRY_TYPES, shared by retentionClass and the retention sweeper. Retention itself is unchanged.
  • Semantics — a spec with no events answers an empty list, never 404; history stays readable while the spec row exists. A stored row whose data payload no longer parses is served without its data rather than failing the whole read.
  • Wiring — the production server's SailOperations never had a working /recent source (SpecStoreAuditPersister is not an AuditPersister), so the new read is wired explicitly via useEvents(eventStore) in ServerStartCommand.
  • Contract — event history is node-local: there is no event replica, and events recorded on another FDE's box never land in this box's store. ARCHITECTURE.md now states this plainly (same provenance posture as agent logs). The fleet-consistent room content remains the synced stores: messages, reviews, runs.

Behavior change: GET /v1/events without spec now answers 422 with a pointer to both shapes (previously 405).

Verification

mvn clean verify green (all modules, coverage gates included). New tests: store scoping/since exclusivity/limit/telemetry exclusion/query plan; operations validation, wiring-absent, corrupt-data tolerance; route auth, missing/invalid spec, since validation, limit clamping.

The audit store keeps every RECORD-class event forever, but the API could
only answer the global /v1/events/recent window, so a spec's lifecycle
rows vanished from its room on an active control plane. Add the bounded
scoped read: EventStore.forSpec(specId, afterId, limit, excludedTypes)
range-scans idx_events_spec (verified by a query-plan test), the route
requires spec, treats since as an exclusive monotonic event-id cursor
for SSE gap-fill, clamps limit, and serves RECORD-class events only —
the telemetry taxonomy now lives once in Event.WellKnownTypes and is
shared with the retention sweeper. A spec with no events answers an
empty list, not 404. The record is node-local by contract; ARCHITECTURE
now states it plainly.
@udaychandra
udaychandra merged commit 27360d5 into main Aug 18, 2026
3 checks passed
@udaychandra
udaychandra deleted the agent/sail-spec-events branch August 18, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant