Skip to content

feat: distinguish a quiet arXiv inbox from a broken one #58

Description

@Jammy2211

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.md a single last digest: <YYYY-MM-DD> line, owned by
    scripts/inbox_actions.py like 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 the last 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

  1. PyAutoMemory/scripts/inbox_actions.py
    • INBOX_STALE_WEEKDAYS = 2, beside INBOX_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.
    • stamp subcommand, printing stamped:<date> like the other one-token
      subcommands.
    • append's insertion point is unchanged: STAMP_RE cannot match
      INBOX_LINE_RE, and the stamp sits above the papers under every ordering
      (append inserts after the last paper line, or at EOF when there are none).
  2. PyAutoMemory/scripts/board.py
    • collect() gains inbox_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.py maps arxiv-inbox.md to EMPTY, so a freshly
      spawned template legitimately has no stamp and has never run a digest.
  3. PyAutoMemory/arxiv-inbox.md — document the stamp in the format block.
  4. PyAutoMind/.github/workflows/arxiv_papers.yml
    • The count == '0' heartbeat step also writes arxiv_survivors.json as
      {"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 the stamp call, 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.
  5. 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) and tests/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 them
  • PyAutoMemory/scripts/board.py — renders the inbox above the reading queue
  • PyAutoMemory/arxiv-inbox.md — the inbox itself, and its format block
  • PyAutoMind/.github/workflows/arxiv_papers.yml — the nightly digest
  • PyAutoMemory/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
    successful append/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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions