feat(runtime): events-daemon split — audit lane leaves the domain store (ADR-170) - #2152
Closed
ohdearquant wants to merge 9 commits into
Closed
feat(runtime): events-daemon split — audit lane leaves the domain store (ADR-170)#2152ohdearquant wants to merge 9 commits into
ohdearquant wants to merge 9 commits into
Conversation
…ave the domain store Splits event persistence into a separate daemon owning events.db: SQLite's single-writer constraint is per file, and a measured 24h window shows ~96% of main-store write traffic by dispatch count is observational (event-plane rows ~30:1 against domain mutations). Fire-and-forget socket hand-off with a bounded queue and an explicit drop policy makes the loss-tolerant durability class concrete; reads open events.db read-only (WAL 1-writer-N-readers). Domain availability never depends on events-daemon liveness. 🤖 Generated with Claude Code
…ter claim, state legacy-row tenure 🤖 Generated with Claude Code
…ppends stay Consumer analysis during implementation found the legacy events table is not a pure observational sink: the schedule drain's creator-provenance fence, the kg projection worker's guarded event INSERT, and the graph query compiler's cross-substrate UNION all depend on event rows co-resident with domain state. Moving the whole plane breaks them by construction (the first was caught as a hard test failure; the others would have failed silently). Amended decision: only the ADR-133 idempotent audit-batch lane (~82% of measured event rows, the one-per-dispatch class that scales with load) moves to events.db; plain appends stay on the domain store; trait-level reads merge both stores. Also records the host-class contract (resolver emits embedded mode; resident daemon hosts upgrade to forwarding) and the per-database socket derivation. 🤖 Generated with Claude Code
…re (ADR-170) Implements the ADR-170 events daemon and append-class routing: - events_split module: events daemon loop (flock guard, peer-uid admission, length-prefixed JSON frames, versioned protocol with typed refusals), per-process split client (bounded fire-and-forget queue with drop+count+log policy, synchronous framed round-trips for the idempotent lane and reads), supervisor (probe socket, respawn with backoff), and path derivation (events.db beside the main db; socket beside the events db, never global). - SplitEventStore: the handle KhiveRuntime::events returns under the split. Routes append_events_idempotent (the audit batch, the measured bulk of event write volume) to the events lane; plain appends stay on the legacy store, whose raw-SQL consumers (schedule provenance fence, kg projection guarded inserts, graph-query substrate union) require co-resident rows; reads merge both stores in the shared (created_at, id) DESC order. - Host classes: the shared config resolver emits embedded (socket-less) mode for every file-backed resolution; the resident daemon entrypoints upgrade to socket forwarding and supervise the events daemon from the same resolved config, so client and supervisor cannot anchor at diverging paths. Multi-backend boots re-anchor the split beside the declared main backend. - kkernel events-daemon subcommand; kkernel exec inherits embedded mode from the resolver. KHIVE_EVENTS_SPLIT=0 is the deployment kill-switch. Tests: split-store routing (plain to legacy, idempotent to lane, merged and windowed reads), socket round-trip idempotency dispositions, fire-and-forget delivery, queue-overflow drop accounting, dead-socket typed errors with local preflight, protocol-skew refusal, daemon guard exclusivity, direct mode, and the resolver/daemon-upgrade contract. Full workspace check plus the runtime, mcp, kkernel, and touched pack suites pass; clippy and rustdoc clean under -D warnings. 🤖 Generated with Claude Code
ohdearquant
marked this pull request as ready for review
August 24, 2026 13:26
…ority The synchronous audit lane's outage behavior was unstated: document that the flusher retries a failed generation up to a capped attempt count with short backoff then drops it terminally with a surfaced reason, that its pending buffer is hard-capped with explicit admission refusal at the cap, and that supervisor respawn bounds outage duration — bounded at both ends, same loss-tolerant class as the fire-and-forget queue. Also promote the legacy audit purge tool from optional to worth-scheduling: at the measured audit rate the accumulated legacy tonnage is a material share of main-store size. 🤖 Generated with Claude Code
…re (ADR-170) Implements the ADR-170 events daemon and append-class routing: - events_split module: events daemon loop (flock guard, peer-uid admission, length-prefixed JSON frames, versioned protocol with typed refusals), per-process split client (bounded fire-and-forget queue with drop+count+log policy, synchronous framed round-trips for the idempotent lane and reads), supervisor (probe socket, respawn with backoff), and path derivation (events.db beside the main db; socket beside the events db, never global). - SplitEventStore: the handle KhiveRuntime::events returns under the split. Routes append_events_idempotent (the audit batch, the measured bulk of event write volume) to the events lane; plain appends stay on the legacy store, whose raw-SQL consumers (schedule provenance fence, kg projection guarded inserts, graph-query substrate union) require co-resident rows; reads merge both stores in the shared (created_at, id) DESC order. - Host classes: the shared config resolver emits embedded (socket-less) mode for every file-backed resolution; the resident daemon entrypoints upgrade to socket forwarding and supervise the events daemon from the same resolved config, so client and supervisor cannot anchor at diverging paths. Multi-backend boots re-anchor the split beside the declared main backend. - kkernel events-daemon subcommand; kkernel exec inherits embedded mode from the resolver. KHIVE_EVENTS_SPLIT=0 is the deployment kill-switch. Tests: split-store routing (plain to legacy, idempotent to lane, merged and windowed reads), socket round-trip idempotency dispositions, fire-and-forget delivery, queue-overflow drop accounting, dead-socket typed errors with local preflight, protocol-skew refusal, daemon guard exclusivity, direct mode, and the resolver/daemon-upgrade contract. Full workspace check plus the runtime, mcp, kkernel, and touched pack suites pass; clippy and rustdoc clean under -D warnings. 🤖 Generated with Claude Code
…ass boundary The ADR-170 amendment's load-bearing claim — plain-append classes stay visible to raw-SQL consumers of the legacy events table — was backed by enumeration, and two of the three consumers fail silently if the classification drifts. Guard it with the consumers' own access pattern: a raw SQL read against the legacy backend must find a plain-appended row (reddens if that class ever moves), must not find an audit-lane row, and the identical query against the lane backend must find the moved row, so an empty first arm can never be a dead query. 🤖 Generated with Claude Code
… sidecar names, read-only safety - verify peer uid on every client connection and bind the daemon socket 0600 inside a trusted directory, failing closed on either - derive the events sidecar db and socket names from the main db file stem so two databases sharing a directory never collide - read-only runtimes never create a missing events db and open an existing one read-only - classify pool/timeout store errors as transient in the audit batch lane, retried under the existing bounded attempt budget - replace offset pagination with keyset pagination in event-count windows, with a loud error on an unpageable single-microsecond cluster - supervisor observes daemon shutdown, reaps its child between probes, and treats a foreign-uid socket as unreachable - per-call connections replace the shared request mutex; cfg(unix) gates keep non-unix builds compiling
This was referenced Aug 24, 2026
Owner
Author
ohdearquant
added a commit
that referenced
this pull request
Aug 25, 2026
Part 1 of the events-daemon split series (ADR-170), replacing #2152 with reviewable slices. This part adds the `events_split` module — daemon loop, forwarding client, split store, and the config/naming helpers — plus the two visibility widenings it needs in `daemon.rs`. Nothing constructs the module yet: runtime routing and configuration land in part 2, transport wiring in part 3, and the full daemon test suite in part 4. The tests here cover the contracts this module itself defines (sidecar naming, wire retryability). Two deliberate corrections over the draft in #2152: - The events sidecar derives from the main database's **full file name** with a canonicalized parent (`khive.db` → `khive.db.events.db`). A stem-derived name silently shares one sidecar between `a.db` and `a.sqlite` in the same directory, and an uncanonicalized path mints a distinct sidecar per alias of one database. - Wire retryability defers to `StorageError::is_retryable()` instead of re-enumerating variants. The hand-rolled subset turned transient writer contention (`WriterTaskBusy`, `Transaction`) into a terminal error on the client side of the socket.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements ADR-170 (#2151): a dedicated events daemon owning
events.db, with event persistence routed by append class.What moves, what stays
events.db, owned by a supervised events daemon on its own Unix socket (same framing and peer-uid admission as the main daemon socket).eventstable has raw-SQL consumers whose correctness depends on co-resident rows: the schedule drain's creator-provenance fence, the kg projection worker's guarded event INSERT (transactional with main-store state), and the graph query compiler's cross-substrate UNION. Moving the whole plane breaks them by construction; the first was caught as a hard test failure during implementation, the others would have failed silently.get_event,query_events,count_events) merge both stores, so consumers on theEventStoretrait observe one event plane.Host classes
The shared config resolver emits embedded (socket-less) mode for every file-backed resolution — one-shot CLI hosts and tests have no events daemon. The resident daemon entrypoints upgrade the resolved config to socket forwarding and supervise the events daemon from that same resolved config, so forwarding clients and the supervisor cannot anchor at diverging paths. Multi-backend boots re-anchor the split beside the declared main backend. Socket paths derive beside the events database they serve, never from a process-global location.
KHIVE_EVENTS_SPLIT=0is the deployment kill-switch back to legacy behavior.Testing
cargo check --all-targets; khive-runtime (1316), khive-mcp (427), kkernel, and all touched pack suites pass; clippy and rustdoc clean under-D warnings.Stacked on #2151 (ADR); the two ADR commits at the base of this branch belong to that PR and this one reduces to the implementation once it merges.
🤖 Generated with Claude Code