dispatch.log records only scheduled ticks, so ~half of all reviews are invisible to every cost measurement nf-7pi ← Beads

open priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/724 by tools/beads/import_github.py's reconcile pass.

Filed unclaimed and unlabelled from a strategic-uplift window, working #702. Backlog.

~/.ninefold-reviewer/dispatch.log is the only durable record of what the reviewer does, and it records only scheduled ticks. A run started by review_now.py shells out to dispatch.py directly, so its stdout goes to the caller's terminal and nothing is appended to dispatch.log. Measured on the sandbox box, 2026-08-07:

| | count | |---|---| | transcripts in logs/ on disk | 184 | | distinct transcript paths named anywhere in dispatch.log | 91 | | occurrences of on-demand in dispatch.log | 0 |

So roughly half of all reviews leave no trace in the file every cost and cadence question gets answered from.

Why it matters now rather than when #578 landed

#678 measured Tier-0 review cost — *"$69.66 for one chunk, 54 full reviews and 0 restamps in a day"* — from dispatch.log. If on-demand runs are absent from that file, that figure undercounts by roughly the on-demand share. At the split above it is closer to double. I am not claiming #678's number is wrong as computed; I am claiming its denominator is not "reviews", it is "scheduled reviews", and nothing says so.

⚠️ #702 makes this structural rather than incidental. Before it, on-demand was a convenience and the cron did most of the work. After it, the dev pulling a review is the *primary* path and the cron is a backstop — so the fraction dispatch.log cannot see goes from "a lot" to "most", and the reviewer's own record of itself degrades exactly as it becomes the thing worth measuring.

The workaround I used, and why it is not a fix

To get the split for #702 I cross-referenced transcript filenames on disk against the paths dispatch.log names, then filtered by file size (>100 KB = a real review, <1 KB = an aborted spawn). That works today and is fragile in two ways:

Shapes

- Cheapest: have dispatch.py append its per-PR outcome line to dispatch.log on --on-demand too, tagged so the two are distinguishable (#N: done (on-demand) — …). ⚠️ It must not stamp the heartbeat — review_now.py's guardrail 2 exists precisely because on-demand runs holding the heartbeat green would mask a dead cron (#352). Writing a log line and stamping a heartbeat are separate acts and only the second is dangerous. ⚠️ Interacts with #483: poll.py reads this file's *mtime* as the dispatcher-liveness signal, so adding a second class of writer means a dead cron could read as warm whenever any session pulls a review. That is the #462 detection this would break, and it is the reason this is not a one-liner. - Better: a small append-only ledger separate from dispatch.log (one JSON line per review: sha, result, trigger, on-demand or tick, duration), which sidesteps the mtime problem entirely because nothing reads *its* mtime for liveness.

Related: #483 (log growth and retention), #678 (the cost measurement this undercounts), #576/#578 (where on-demand came from), #702 (which makes it the primary path).

Dependencies

None.

Comments

No comments.

Add a comment