Repository navigation
feat: distinguish a quiet arXiv inbox from a broken one #58
Description
Overview
The knowledge board renders an empty arXiv inbox as nothing waiting — the nightly arXiv digest fills this. — the same line for two opposite states:
arXiv genuinely announced no strong-lensing papers, or the digest ran and the
filing failed so the papers were lost. On 2026-08-25 it was the second, and
nothing on the page said so.
arxiv_papers.yml already killed this exact ambiguity on the Slack side — empty
days still post a heartbeat, so silence in #papers always means a broken run.
The board has no equivalent. This task gives it one.
Split out of complete/2026/08/arxiv-inbox-pat-scope.md, which fixed the filing
bug (PR PyAutoMind#317 makes a denied push redden the run) but deliberately left
this open: that half only helps someone reading CI, and this half is for the
person reading the board.
Plan
- Give
arxiv-inbox.mda singlelast digest: <YYYY-MM-DD>line, owned by
scripts/inbox_actions.pylike every other part of the inbox format. No new
state file. - One line covers both options the prompt listed: a stamp dated today over an
empty inbox is the quiet-day marker (absence becomes positive evidence);
a stamp four days old is thelast filled:signal. Because it is replaced
rather than accumulated, the sweep never has to age it out. - Make the digest stamp on quiet days too. Today the inbox step is skipped
whenever the fetch returns zero papers, so a stamp would go stale on exactly
the days it exists to prove were quiet. This is the PyAutoMind leg. - Render the date on the board, and compute the staleness warning in the
reader's browser, not at render time (see the trap below). - Warn after 2 weekdays with no digest.
Detailed implementation plan
Affected Repositories
- PyAutoMemory (primary) — the stamp and the board
- PyAutoMind — the workflow leg that writes the stamp on quiet days
The prompt scopes this to PyAutoMemory alone. The PyAutoMind leg is not
scope creep: a stamp that only updates on days papers arrive cannot
distinguish a quiet day from a broken one, which is the entire point.
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./PyAutoMemory | main | clean |
| ./PyAutoMind | claude/inbox-board-staleness-signal-onzg34 | clean |
Suggested branch: claude/inbox-board-staleness-signal-onzg34 (both repos —
the session's designated branch; no worktree, this is a web-github run)
The load-bearing trap: a render-time warning can never fire
knowledge_board.yml re-renders on pushes to arxiv-inbox.md, plus a weekly
Monday cron. When filing breaks there is no push — so the published page freezes
at its last good render, and a staleness warning computed server-side would be
frozen with it, showing "0 days since last digest" forever. It would be absent
in precisely the failure it exists for.
So the split is:
- the absolute date is rendered into the page — honest even when frozen;
- the warning is computed from it client-side, at view time.
The weekly cron re-renders the page anyway; the JS makes it honest in between.
Implementation Steps
PyAutoMemory/scripts/inbox_actions.pyINBOX_STALE_WEEKDAYS = 2, besideINBOX_WINDOW_DAYS, with the reasoning:
the digest is nominally 02:00 UTC Mon–Fri and GitHub cron only ever jitters
later (0–3 h), so a single missed weekday is reachable without anything
being wrong; two is a pattern.STAMP_RE = ^last digest:\s*(\d{4}-\d{2}-\d{2})\s*$.last_digest(text) -> str | None.set_last_digest(text, date) -> str— replaces the stamp in place, else
inserts directly after the---separator.weekdays_since(stamp, today) -> int.stampsubcommand, printingstamped:<date>like the other one-token
subcommands.append's insertion point is unchanged:STAMP_REcannot match
INBOX_LINE_RE, and the stamp sits above the papers under every ordering
(appendinserts after the last paper line, or at EOF when there are none).
PyAutoMemory/scripts/board.pycollect()gainsinbox_last_digest; carried through--json.- Markdown: a freshness clause in both branches — populated
(2 waiting · last digest 2026-08-25 · lapse after 7 days) and empty
(nothing waiting — last digest ran 2026-08-25 and found nothing). - HTML: the same, wrapped in
<time datetime=… data-stale-weekdays=2>. _EXTRA_JS: compute the weekday age at view time and swap in
⚠ no digest for N weekdays — filing may be broken.- A missing stamp reads neutrally (
no digest run recorded yet), not as
a warning:spawn.pymapsarxiv-inbox.mdtoEMPTY, so a freshly
spawned template legitimately has no stamp and has never run a digest.
PyAutoMemory/arxiv-inbox.md— document the stamp in the format block.PyAutoMind/.github/workflows/arxiv_papers.yml- The
count == '0'heartbeat step also writesarxiv_survivors.jsonas
{"papers": []}— exactly what the Claude step already writes on its
empty day, so every path produces the file. - Drop
if: count != '0'from the inbox step and add thestampcall, so
every weekday run stamps. The existing warn-and-exit-0 guard on a
missing survivors file stays as the real-failure signal. - Unchanged: the step still runs after the Slack POST, so "a cross-repo
failure must never cost the morning's post" holds. - Deliberate side effect: one commit per weekday to PyAutoMemory, which
re-renders the board daily. That is the heartbeat.
- The
- Tests —
tests/test_inbox_actions.py(stamp round-trip, insertion point,
replacement not accumulation, weekday arithmetic across a weekend, stamp is
not parsed as a paper) andtests/test_board.py(both render branches,
missing stamp, the date reaching the HTML).
Not provable in CI
arxiv_papers.yml cannot be fired from a branch that edits it —
claude-code-action refuses to run when the workflow file differs from the
default branch's copy, and then exits success (recorded trap, cost a wasted
dispatch in arxiv-inbox-pat-scope). That leg gets bash -n, a YAML parse and
reasoned review only; the first genuine exercise is the scheduled run on main.
Pre-existing failure to clear first
PyAutoMemory@main is currently red on both make validate and make test:
commit 498e1a8 added a tracked .claude/ directory without adding it to
ALLOWED_TOP_DIRS in scripts/validate_structure.py. Unrelated to this task,
but any PR is red on arrival until it is fixed. One line, in its own commit.
Key Files
PyAutoMemory/scripts/inbox_actions.py— single owner of the inbox line
format, the window and every transition; the stamp joins themPyAutoMemory/scripts/board.py— renders the inbox above the reading queuePyAutoMemory/arxiv-inbox.md— the inbox itself, and its format blockPyAutoMind/.github/workflows/arxiv_papers.yml— the nightly digestPyAutoMemory/scripts/validate_structure.py— the pre-existing red
Original Prompt
Click to expand starting prompt
The knowledge board's empty inbox should distinguish quiet from broken
Type: feature
Target: PyAutoMemory
Repos:
- PyAutoMemory
Difficulty: small
Autonomy: supervised
Priority: normal
Status: formalised
Filed: 2026-08-25
Split out of complete/2026/08/arxiv-inbox-pat-scope.md, which fixed the filing
bug but deliberately left this open.
The gap
The board renders an empty arXiv inbox as:
arXiv inbox (suggested overnight — un-acted papers lapse after 7 days)
nothing waiting — the nightly arXiv digest fills this.
That line is shown for two completely different states:
- arXiv genuinely announced no strong-lensing papers; or
- the digest ran and the filing failed, so papers were lost.
On 2026-08-25 it was the second, and nothing on the page said so. The reader has
no way to tell without opening GitHub Actions.
Why this is a known-solved problem here
arxiv_papers.yml already fixed the identical ambiguity on the Slack side. From
its header comment (user, 2026-07-13):
Empty days STILL post a short "no new strong-lensing papers today" heartbeat —
so silence in #papers always means a broken run, never a genuinely empty one.
The board has no equivalent. The digest's own invariant stops at Slack and does
not reach the surface the paper-management feature actually asks the human to
use.
Task
Give the inbox a freshness signal so an empty board is readable. Options, in
rough order of cost:
last filled:line — the board renders the date of the most recent
successfulappend/sweep. An empty inbox dated today is quiet; one dated
four days ago is suspect. Cheapest, no new state if it can be derived from
arxiv-inbox.md's git history or a marker the digest writes.- Explicit quiet-day marker — the digest writes a dated "no papers" line on
genuinely empty days, exactly mirroring the Slack heartbeat. Makes absence
positive evidence, at the cost of a line the sweep must then age out. - Freshness warning — the board flags an inbox with no write in N weekdays.
Strongest signal, needs a threshold nobody has tuned yet.
Prefer whichever needs no new state file. Note that scripts/inbox_actions.py
is the single owner of the line format, the window and every transition — a
marker line must go through it, not through board.py or a workflow's shell.
Note on the sibling failure mode
PR PyAutoMind#317 made the workflow fail loudly when the push is denied, so a
future filing failure reddens the run. This task covers the other half: the
board should be honest even when nobody is reading CI.
Activity
Jammy2211 commentedon Aug 25, 2026
Shipped
main963388amaina82b96fMerged library-first:
arxiv_papers.ymlcallsinbox_actions.py stamp, so the Memory side had to land before the Mind side or the nightly run would have failed on a subcommand that did not exist yet.What landed
inbox_actions.pyowns alast digest: <date>line inarxiv-inbox.md, rewritten on every digest run whether or not papers arrive. One line does both options this issue scoped: dated today over an empty inbox it is the quiet-day marker, making absence positive evidence; days old it is the "last filled" signal. Replaced rather than accumulated, so the sweep has nothing to age out.The board reads it back as:
Two decisions worth keeping
The warning is computed in the reader's browser, not at render time.
knowledge_board.ymlrepublishes on pushes toarxiv-inbox.md— exactly what stops happening when filing breaks. The page then freezes at its last good render, so a server-side warning would sit at "0 weekdays" forever and never fire in the one failure it exists for. The date is a fact and survives the freeze; the verdict is recomputed against the reader's clock. A page already stale when rendered still warns without JS.Threshold 2 weekdays, not 1 day. Cron only ever jitters later, so a board rendered early can see yesterday's stamp with nothing wrong. Weekdays because the digest is Mon–Fri: a Monday board reading Friday's stamp is three calendar days but zero missed runs.
A missing stamp reads neutrally, never as a warning —
spawn.pyemptiesarxiv-inbox.mdfor the template repos, so a freshly spawned Memory has never run a digest and has nothing to be late for.Scope note
The issue scoped this to PyAutoMemory. The PyAutoMind leg was added deliberately: a stamp that only updates on days papers arrive cannot distinguish a quiet day from a broken one, which is the whole point. The digest's inbox step lost its
count != 0guard and the empty-day heartbeat now writesarxiv_survivors.jsonas{"papers": []}, so every path still produces the file and a missing one stays a real failure. Side effects, both intended: a quiet day is now a one-line commit that re-renders the board daily, and the sweep runs every weekday instead of waiting for the next paper day.Validation
92 PyAutoMemory tests (23 new, was 69) ·
make validategreen · 197 PyAutoMind tests ·lifecycle.py checkOK · YAML parses · everyrun:block passesbash -n· both PRs green on their final head shas.Beyond CI: the shipped JS (extracted from
board.py, not retyped) was cross-checked against the Python weekday arithmetic over 30 consecutive dates including weekends, and all four digest paths were rehearsed against a real checkout with the shell blocks extracted verbatim from the workflow — quiet day/same date (no commit), quiet day/new date (heartbeat commit), paper day (paper lands below the stamp), quiet day sweeping lapsed papers.Not proven live:
claude-code-actionrefuses to run whenarxiv_papers.ymldiffers from the default branch's copy and then exits success, so no branch dispatch can reach those steps. The scheduled 02:00 UTC run onmainis the first genuine end-to-end exercise.Incidental
mainwas red onmake validateandmake testfrom498e1a8(tracked.claude/missing fromALLOWED_TOP_DIRS). #59 landed the same one-line fix while this branch was in flight; the conflict was resolved in main's favour and the redundant commit contributes nothing.Generated by Claude Code