* feat: add SQLite-backed activity event logging layer
Implements a structured diagnostic event trail for the orchestrator.
When unexpected behavior occurs (stuck sessions, silent CI failures,
missed PR transitions), operators can now reconstruct timelines with
`ao events` rather than guessing from logs.
Key design decisions driven by Codex review:
- FTS5 external-content table uses INSERT/DELETE triggers so search
actually works without manual rebuild
- ts_epoch (epoch ms) used for all time comparisons to avoid text vs
SQLite datetime() ambiguity near cutoff
- PRAGMA busy_timeout=3000 handles WAL lock contention across
CLI/lifecycle/web processes
- user_version schema versioning for future migrations
- EventType renamed to ActivityEventKind to avoid collision with
existing types.ts export
- Event names match existing vocabulary (ci.failing, review.pending)
- ActivityStateCache (Map) tracks previous activity state so
lifecycle-manager can emit activity.transition diffs
- session.spawn_failed captures failed spawns via wrapper try/catch
- better-sqlite3 in optionalDependencies: AO keeps working if native
build fails; getDb() returns null and writes become no-ops
New files:
- packages/core/src/events-db.ts — lazy DB init, WAL, schema+triggers
- packages/core/src/activity-events.ts — write API, sanitizer
- packages/core/src/query-activity-events.ts — query + FTS + stats
- packages/cli/src/commands/events.ts — `ao events list/search/stats`
Wired into:
- lifecycle-manager.ts: lifecycle.transition + activity.transition
- session-manager.ts: session.spawned, session.spawn_failed, session.killed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(activity-events): address PR review comments
- redactValue: remove value-string regex check that silently erased
error messages mentioning auth terms (key-based redaction is sufficient)
- sanitizeData: reject payloads >16KB instead of slicing (sliced JSON
is malformed and corrupts JSON output)
- lifecycle-manager: prune activityStateCache alongside states in the
per-poll stale-entry cleanup loop (prevents unbounded growth)
- events CLI: warn on unrecognised --since duration format instead of
silently applying no time filter
- searchActivityEvents: accept optional projectId and push it into SQL
WHERE clause instead of post-LIMIT in-memory filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(activity-events): review pass fixes
- formatRow: pad level string before chalk-wrapping so ANSI codes
don't corrupt column alignment in ao events output
- events-db: add PRAGMA synchronous=NORMAL (WAL+NORMAL is standard
recommendation; avoids per-write fsync in the poll hot path)
- events-db: emit console.warn when _dbFailed is set so operators know
events are being dropped instead of failing silently forever
- activity-events: remove dead redactValue wrapper (was a no-op after
the previous fix; call site now directly assigns the value)
- activity-events: remove unused session.cleanup from ActivityEventKind
(no call site emitted it; dead API surface)
- query-activity-events: add optional limit param to searchActivityEvents
(default 100, max 1000, parameterized); add --limit flag to ao events search
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(activity-events): widen source/kind to allow plugin-defined values
ActivityEventInput.source and .kind accept ActivityEventSource|string
and ActivityEventKind|string respectively, so new event sources (e.g.
scm-github, notifier-slack) don't require editing core types.
The named union members are preserved for IDE autocomplete on known values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(events): level column alignment, filter kind widening, negative limit guard
- events.ts: padEnd(5) → padEnd(9) so level column aligns with the 9-char LEVEL header
- query-activity-events.ts: ActivityEventFilter.kind widened to ActivityEventKind|string
for consistency with ActivityEventInput.kind (plugin-defined kinds can now be queried)
- query-activity-events.ts: negative/NaN limit values sanitized with Number.isFinite +
Math.max(1,...) to prevent SQLite LIMIT -N returning the full table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(events): FTS alias, credential URL sanitization, periodic retention
- query-activity-events.ts: use full table name in MATCH (activity_events_fts
MATCH ?) instead of alias to avoid 'no such column: fts' in some FTS5 builds
- activity-events.ts: redact https://token@host URL credentials in string values;
add hourly retention sweep so long-lived processes don't grow DB indefinitely
- events-fts-integration.test.ts: real SQLite integration test for FTS5 search,
projectId filter, and epoch-based time filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): remove inline import() type annotations in integration test
ESLint @typescript-eslint/consistent-type-imports forbids import() in type
positions — replaced with any to keep the test working without the type imports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(events): parse data to structured JSON in --json output; add test proving value sanitizer correctness
- ao events list/search --json now parses ActivityEvent.data from JSON
string back to a structured object, making it jq-friendly without
requiring double-parsing by callers (P1 finding from PR review)
- Add test "preserves error messages that mention sensitive words in
values" to refute Greptile's false-positive P1 finding: SENSITIVE_KEY_RE
only matches key names, not string values; "token expired" and
"authorization header missing" values are preserved correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(events): BM25 relevance ordering for FTS search; add versioned JSON envelope
Search now orders by FTS5 rank (BM25) instead of ts_epoch, returning most relevant
events first. --json output now wraps events in { version, query, meta, events }
for stable CLI contracts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(events): quote FTS tokens to prevent false negatives on operator-like terms
Searching for words like "OR", "AND", or "NOT" would produce empty results
because SQLite FTS5 treated them as operators after token joining. Wrapping
each token in double quotes forces literal phrase matching.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(tests): update FTS assertion for quoted tokens; raise timeout on slow audit test
query-activity-events test expected the old unquoted join format; update to
match the quoted form added in the previous commit. The agent-report audit
trail test occasionally exceeds the 5 s default on CI runners; raise to 15 s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(qa): ISSUE-001 - type events stats entries
* test(events): cover session and lifecycle event call sites
* test(core): wait for lifecycle branch adoption
* chore: add activity events changeset
* fix activity event review feedback
* batch prune old activity events
* record activity event when spawn starts
* Fix activity event FTS rebuild and sanitization guard
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>