Skip to content

feat: show digest item ages and flag a stale project synthesis - #426

Draft
levifig wants to merge 3 commits into
claude/gracious-cori-08ce27from
claude/digest-item-ages
Draft

levifig wants to merge 3 commits into
claude/gracious-cori-08ce27from
claude/digest-item-ages

Conversation

@levifig

@levifig levifig commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

The continuity digest presented months-old entries as current truth, with nothing saying how old they were. In this repository the first thing an agent read was a project synthesis from 30 July ("Next: PR #143"), followed by a July lineage decision about a Change model that has since been retired.

  • Every journal entry in the digest now shows its age next to its date, for example [2026-07-30 17:25, 84 days ago]. --layer reads show it too.
  • A project synthesis goes stale once it is more than 30 days old and at least 50 entries have followed it. The digest labels it, says how many entries came since, asks for a fresh wrap(project) when substantial work is finished, and moves it to the end. A fresh synthesis, or a dormant project's, keeps leading without a label.
  • When the synthesis is stale, the newest scoped wrap is shown beside it, as long as that wrap is newer.
  • Only Changes with an active reason contribute lineage keys. Retired Change files no longer put their lineage into the active-lineage layer.
  • Ages and staleness are worked out at read time. Nothing is stored, and no entry changes. --json adds age_days to context entries, plus stale and entries_since to the project synthesis.

This PR is stacked on #424 (#420), so its base is that branch. Once #424 lands, it will be rebased onto main.

Native work

Implements #419.

Verification

Ran on fc0c594, which adds the review rounds' test and wording fixes to 655e4bb; make verify-local passes on that tree.

  • Fixed-clock tests for the stale label and wrap prompt. Stale only when the synthesis is both old and overtaken: old with 50 entries after it is stale, while exactly 30 days old, old but dormant (49 entries) and fresh during a busy stretch are not. The renderer shows the stale label, the wrap prompt and the move to the end. A fresh synthesis leads with no label. Tests: TestJournalContextMarksOnlyAnOldOvertakenSynthesisStale, TestJournalContextDigestShowsAgesAndDemotesAStaleSynthesis.

    • TestJournalContextDefaultPathAgesTheSynthesisByTheReadClock drives the real CLI default path. It logs a synthesis and 50 entries, moves the read clock a month on, and sees the stale label at the end. After a fresh wrap(project), the new synthesis leads unlabelled.
    • A newer scoped wrap is shown beside a stale synthesis, and an older one is not (TestJournalContextShowsANewerScopedWrapBesideAStaleSynthesis).
  • Ages across day boundaries. Calendar-day ages in UTC: one second before midnight is 1 day, midnight is today, and a reader in another timezone gets the same answer. Every layer's entries are stamped: synthesis, checkpoint, lineage, blockers, branch recency, BranchEntries and LatestWrap. An entry from a fast clock clamps to today, and journal recent JSON carries no age (TestJournalContextStampsEveryEntryWithItsAge, TestJournalContextAgeDaysCountsUTCCalendarDays). The CLI read-clock test ages in UTC, so daylight-saving changes cannot move it.

  • This repository, read-only so far. With 655e4bb, whose rendering f0dc53f does not change, the live digest:

    • leads with today's scoped wrap;
    • labels the 30 July synthesis stale, 2687 entries since and puts it last;
    • no longer lists the July lineage/change-model-hard-cut decision;
    • comes to 6,641 bytes.

    The second half of this check needs a genuine wrap(project) in the live journal and is waiting on a decision.

  • env -u LOAF_DB make verify-local passes.

Also ran:

  • Changelog coverage: loaf check issue-changelog passes for Show how old digest items are and flag a stale project synthesis #419 against this PR's base.
  • Lineage: the existing TestDiscoverActiveChangesUnionsHEADAndWorkingTree now expects the clean, completed historical Change's foo-extra lineage to be left out; the rewritten-lineage case still keeps both keys of an active Change.

Ship

Pending Ship review.

Migration / risks

  • Ages change daily, so the digest's bytes change from one UTC date to the next; within a date they are stable.
  • Stale threshold: the 30-day, 50-entry rule is a proposal for the reviewer to accept. It is two named constants in internal/state/journal_context.go.
  • Active lineage: a Change that is clean, not on the current branch and has no unresolved lineage no longer feeds active lineage, including for --layer active-lineage reads.

Out of scope

Fixes #419


Agent notes
  • State:
    • JournalEntryRecord.AgeDays *int (age_days,omitempty) is set only by stampJournalContextAges in internal/state/journal_context.go, so other readers' JSON is unchanged.
    • Synthesis staleness uses JournalContextSynthesisStaleDays (30) and JournalContextSynthesisStaleEntries (50). entries_since counts every project entry after the synthesis, up to the watermark.
    • The scoped-checkpoint query now also runs when the synthesis is stale, and is kept only when it is newer than the synthesis.
  • Lineage: discoverActiveChangesWithOutput in internal/cli/change_context.go retains lineage keys after computing active reasons, only for Changes that have one.
  • Rendering: formatJournalEntryStamp serves both renderers. A stale synthesis is moved to the end of journalContextDigestLayers with journalContextStaleSynthesisHint; --layer project-synthesis prints the same hint.
  • Clock: tests use JournalContextOptions.Now and the Runner.Now seam added in feat: resolve journal blockers by entry id and stop listing stale ones #424.

Every journal entry in a context read now carries its age in whole UTC calendar days, shown next to its date in the digest and --layer reads. A project synthesis older than 30 days with at least 50 entries after it is labelled stale with a prompt to write a fresh wrap(project) and moves to the end of the digest, and a newer scoped wrap is shown beside it; a dormant project's synthesis keeps leading. Only active Changes contribute lineage keys, so retired Change lineage no longer appears as active. Ages and staleness are derived at read time; --json adds age_days, stale and entries_since.

Fixes #419
Review of #426: the CLI read-clock test ages in UTC so daylight-saving changes cannot shift its exact age; the stamping test now covers synthesis, checkpoint, lineage, blockers, branch recency, BranchEntries and LatestWrap; negative ages from clock skew clamp to 0; the --layer blocker line is tested with its aged stamp; an existing context test pins its clock; comments and the changelog say exactly what is aged.
@levifig

levifig commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Ship: request changes on 655e4bb (small). An independent reviewer found no product defects. It accepted the proposed stale rule: a synthesis is stale once it is more than 30 days old and at least 50 entries have followed it. Two tests needed fixing.

  • One CLI test could fail locally for an hour a night around daylight-saving changes. Another claimed to cover every layer's ages but checked three. Both are fixed in the next commit, along with the reviewer's cheap nits.
  • Show how old digest items are and flag a stale project synthesis #419's box 3 is still open. Its second half needs a genuine wrap(project) in the live journal, and Levi decides that.

Agent notes
  • Reviewed: 655e4bb9 (tree bd3b751f), stacked on feat: resolve journal blockers by entry id and stop listing stale ones #424's approved c5276d80, by a fresh read-only reviewer against Show how old digest items are and flag a stale project synthesis #419.
  • Threshold: accepted. Requiring both conditions keeps dormant projects from being nagged (Preserve useful continuity without invoking Wrap #259). In an active project the 30-day leg fires (this repo logs about 32 entries a day). Both values are named constants.
  • Should-fix:
    • internal/cli/journal_context_age_test.go: the read clock used local AddDate, which a DST change shifts by an hour around midnight UTC, against an exact 32 days ago assertion. It now uses UTC.
    • TestJournalContextStampsEveryEntryWithItsAge left lineage, checkpoint, BranchEntries and LatestWrap unchecked. It now covers each, plus a clamped future entry.
  • Nits fixed:
    • comments on Now, the renderer priority note and the entry format;
    • negative ages now clamp to 0;
    • a test of the aged stamp on --layer unresolved-blockers lines;
    • the read clock of TestJournalContextV2ActiveTruthSurvivesNoisyBranchesAndPaginatesExactly pinned, since it relied on 46 < 50 entries;
    • changelog wording narrowed to dated entries.
  • Accepted as is:
    • A branch-recency cursor can fail if the read crosses UTC midnight mid-pagination and the synthesis turns stale. This is rare; lineage-key changes already behave the same way.
    • Exploration checkpoints show no date, so they get no age.
    • Packing with the stale synthesis moved to the end has no golden test of its own.

Review nits on #426: the pagination loop in the v2 active-truth test passes the same pinned clock as its first read; the stamping test asserts the scoped wrap and lineage decision come from their own layers; the changelog names the layers whose entries carry age_days.
@levifig

levifig commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Ship: code approved on fc0c594; merge blocked on box 3. Every review finding is fixed, and the full local gate passes on this tree. Two things must happen before this lands:


Agent notes
  • Candidate: fc0c5943 (tree f1a39f3c) = 655e4bb9 + f0dc53f4 (round-1 fixes) + fc0c5943 (round-2 nits). Each delta was re-reviewed by a fresh read-only reviewer, and each returned Approve.
  • Evidence on this tree: env -u LOAF_DB make verify-local exit 0; loaf check issue-changelog passes for Show how old digest items are and flag a stale project synthesis #419 against the stacked base.
  • Boxes:
    • Ticked with recorded results: 1 (stale label, fixed clock), 2 (ages across day boundaries), 4 (verify-local).
    • Box 3 is open. The read-only half passes on the live journal: July synthesis labelled stale, July lineage gone. The fresh-wrap half is pending Levi.

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