Repository navigation
The event log keeps a window of time, bounded by size #346
Copy link
Copy link
Closed
Labels
task:readySpec checked. An agent may implement it once its Depends on allow.Spec checked. An agent may implement it once its Depends on allow.
Description
Part of #329.
Scope
After this PR, the event history is kept for a length of time, seven days by default, with a size cap as the backstop, and simlock events --since 7d reaches back across every kept generation. Durations accept a d unit on every surface. ADR 0016 §4 fixes the rules.
Technical spec
Modules touched
src/core/config.ts— new keyseventLog.retention(duration, default 7 days) andeventLog.maxBytes(default 256 MiB), validated as positive integers in milliseconds and bytes;eventLog.maxBytesbelow twiceeventLog.rotateBytesis rejected at load, naming the key;eventLog.rotateBytesunchanged.src/contract/schemas.tsfor theconfig.getshape.src/ports/logger.tsNodeFileLogSink— rotation keeps numbered generations<path>.1(newest) upward; after a rotation it deletes generations whose newest line is older thanretention, then the oldest until the total fitsmaxBytes; the current file is never deleted; the same sweep runs once when the sink opens at daemon start. The sink takesretentionMsandtotalMaxBytesas options; its existingmaxByteskeeps meaning one generation.daemon.logkeeps today's one-generation rule: the two options are absent there. Retention is judged per generation: a generation goes when its newest line is older thanretention, so a generation mixing old and new lines stays until its newest line ages out.src/bus/event-file.tsreadEventFile— reads the current file first, then.1,.2and so on until a generation is missing, sorts by time then seq and deduplicates by id, as today with two files. Newest first means a rotation that lands mid-read can only make a generation show up twice, never go missing; a generation missing when opened is empty. It exposes the oldest timestamp it holds for ADR 0016 §5 (oldestTson the result, or a sibling function).- A shared duration parser, in one place both import (
src/contract/orsrc/core/), acceptingms,s,m,h,d;src/cli/index.tsparseDurationandsrc/http/app.tsparseDurationbecome callers of it. docs/CONFIGURATION.md,docs/CLI.md(--sincereach and thedunit),docs/HTTP-API.md(?since=unit).
Contract and event changes
config.getoutput gainseventLog.retentionandeventLog.maxBytes. Additive.- No event changes.
Rules in play
architecture.mdrule 10: one duration parser.safety.md: deletion touches only files the sink itself wrote (<path>.Nbeside its own path), never anything else in the data directory.testing.mdrule 3: the deletion paths (by age, by size, the current file never) each have a test that fails when the path is removed.
Tests
- a rotation renames the current file to
.1and shifts existing generations up by one - a generation whose newest line is older than the retention is deleted on the next rotation
- when the generations exceed
maxBytesthe oldest are deleted until the total fits, and the current file is never deleted - a sink without retention options keeps one generation, as
daemon.logdoes today - the sweep runs when the sink opens, so a generation past retention is deleted at daemon start without a rotation
readEventFilemerges three generations and the current file in time order with no duplicate idsreadEventFileloses no event when a rotation shifts every generation between its reads of the current file and.1readEventFilereports the oldest timestamp it holds, andundefinedfor an empty history- the shared duration parser accepts
7das seven days and rejects7w config.getreports the two new keys at their defaultssimlock config set eventLog.retentionrejects a non-positive value, naming the key- a config with
eventLog.maxBytesbelow twiceeventLog.rotateBytesis rejected at load, naming the key - (e2e)
simlock events --since 7dwith the daemon stopped reads events from a generation older than.1
Done when
- With
eventLog.rotateBytesset small enough that a scripted run against the fake driver rotates at least three times,simlock events --since 7dreturns every event of the run in time order, with the daemon running and with it stopped. - With
eventLog.retentionset to a few seconds, a run, a pause longer than that, and a second run that rotates at least twice, every generation holding only first-run events is gone, andsimlock events --since 7dshows no first-run event from those generations. - With
eventLog.maxBytesset to twicerotateBytes, no more than two files exist beside the current one after many rotations. simlock events --since 2dandGET /v1/events?since=2dboth accept the unit.docs/CONFIGURATION.mddescribes the threeeventLog.*keys, that retention is judged per generation by its newest line, and how retention and the size cap interact.
Out of scope
- Reading figures from the history.
- Any change to
daemon.logrotation.
Depends on
Approval
- Approved for delivery
Written by an agent.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
task:readySpec checked. An agent may implement it once its Depends on allow.Spec checked. An agent may implement it once its Depends on allow.
Activity
github-actions commentedon Oct 5, 2026
Now
task:ready: approved, technical spec present.test: the event log keeps a window of time, bounded by size (#346)
feat(events): keep the event log for a window of time, bounded by size (
test: kill surviving mutants in the event log sweep and reader (#346)
test: make the total size cap tests able to fail when the cap breaks (#…
V3RON commentedon Oct 5, 2026
Handoff
Done: PR #353 (draft, branch
task/346) implements the spec:eventLog.retentionandeventLog.maxBytes, numbered generations with the sweep after rotation and at open, the reader merging generations newest-first, the shared duration parser withd, docs. 13 of 13 spec tests green; round 1's two findings fixed in 17fe052. Mutate: 174 mutants, 8 alive, all explained as equivalent in the PR body.Not done: two blocking findings confirmed in round 2, and rule 14 allows no round 3.
Findings:
src/ports/logger.test.ts:471the mtime-fallback test cannot fail: withreturn 0in place ofstatSync(file).mtimeMsatlogger.ts:300all 29 tests stay green. Needs a fresh generation whose untimestamped last line is kept by its mtime.src/ports/logger.ts:241#shiftGenerationsrenames one at a time and the reader stops at the first missing generation, so a read landing between the.1→.2rename and current→.1silently returns no generations. Breaks the spec's "never go missing" line; the comment atsrc/bus/event-file.ts:13states the false guarantee. Fix in the sink (rename oldest-first so no gap opens) or the reader (continue past a missing generation).Blocked on: nothing. One more fix run and a review round, started by a person.
Written by an agent.
fix: the event reader survives a rotation caught between renames, and…
test: kill surviving mutants in the sweep, the duration parser and th…
fix: the event file sink uses the daemon's clock and an e2e proves re…