Skip to content

The event log keeps a window of time, bounded by size #346

Closed

Description

@V3RON

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 keys eventLog.retention (duration, default 7 days) and eventLog.maxBytes (default 256 MiB), validated as positive integers in milliseconds and bytes; eventLog.maxBytes below twice eventLog.rotateBytes is rejected at load, naming the key; eventLog.rotateBytes unchanged. src/contract/schemas.ts for the config.get shape.
  • src/ports/logger.ts NodeFileLogSink — rotation keeps numbered generations <path>.1 (newest) upward; after a rotation it deletes generations whose newest line is older than retention, then the oldest until the total fits maxBytes; the current file is never deleted; the same sweep runs once when the sink opens at daemon start. The sink takes retentionMs and totalMaxBytes as options; its existing maxBytes keeps meaning one generation. daemon.log keeps 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 than retention, so a generation mixing old and new lines stays until its newest line ages out.
  • src/bus/event-file.ts readEventFile — reads the current file first, then .1, .2 and 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 (oldestTs on the result, or a sibling function).
  • A shared duration parser, in one place both import (src/contract/ or src/core/), accepting ms, s, m, h, d; src/cli/index.ts parseDuration and src/http/app.ts parseDuration become callers of it.
  • docs/CONFIGURATION.md, docs/CLI.md (--since reach and the d unit), docs/HTTP-API.md (?since= unit).

Contract and event changes

  • config.get output gains eventLog.retention and eventLog.maxBytes. Additive.
  • No event changes.

Rules in play

  • architecture.md rule 10: one duration parser.
  • safety.md: deletion touches only files the sink itself wrote (<path>.N beside its own path), never anything else in the data directory.
  • testing.md rule 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 .1 and 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 maxBytes the oldest are deleted until the total fits, and the current file is never deleted
  • a sink without retention options keeps one generation, as daemon.log does today
  • the sweep runs when the sink opens, so a generation past retention is deleted at daemon start without a rotation
  • readEventFile merges three generations and the current file in time order with no duplicate ids
  • readEventFile loses no event when a rotation shifts every generation between its reads of the current file and .1
  • readEventFile reports the oldest timestamp it holds, and undefined for an empty history
  • the shared duration parser accepts 7d as seven days and rejects 7w
  • config.get reports the two new keys at their defaults
  • simlock config set eventLog.retention rejects a non-positive value, naming the key
  • a config with eventLog.maxBytes below twice eventLog.rotateBytes is rejected at load, naming the key
  • (e2e) simlock events --since 7d with the daemon stopped reads events from a generation older than .1

Done when

  • With eventLog.rotateBytes set small enough that a scripted run against the fake driver rotates at least three times, simlock events --since 7d returns every event of the run in time order, with the daemon running and with it stopped.
  • With eventLog.retention set 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, and simlock events --since 7d shows no first-run event from those generations.
  • With eventLog.maxBytes set to twice rotateBytes, no more than two files exist beside the current one after many rotations.
  • simlock events --since 2d and GET /v1/events?since=2d both accept the unit.
  • docs/CONFIGURATION.md describes the three eventLog.* 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.log rotation.

Depends on

Approval

  • Approved for delivery

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 5, 2026
  2. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 5, 2026
  3. github-actions commented on Oct 5, 2026

    @github-actions

    Now task:ready: approved, technical spec present.

  4. self-assigned this
    on Oct 5, 2026
  5. V3RON commented on Oct 5, 2026

    @V3RON
    ContributorAuthor

    Handoff

    Done: PR #353 (draft, branch task/346) implements the spec: eventLog.retention and eventLog.maxBytes, numbered generations with the sweep after rotation and at open, the reader merging generations newest-first, the shared duration parser with d, 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:471 the mtime-fallback test cannot fail: with return 0 in place of statSync(file).mtimeMs at logger.ts:300 all 29 tests stay green. Needs a fresh generation whose untimestamped last line is kept by its mtime.
    • src/ports/logger.ts:241 #shiftGenerations renames one at a time and the reader stops at the first missing generation, so a read landing between the .1→.2 rename and current→.1 silently returns no generations. Breaks the spec's "never go missing" line; the comment at src/bus/event-file.ts:13 states 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.

  6. assigned and unassigned on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions