Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,79 @@
# DECISIONS

2026-09-17 the served kernel derives verdicts, and a slow seat read is not a
stale one.

**The evaluator lock is BUILT, not pinned by hand (default, unruled).** No
ruling says where the served `--evaluator-lock` comes from, and there were two
ways to serve one: transcribe a generated `EVALUATOR.sha256` into this
repository as data (the way tally carries its own `ORACLE.sha256` locks), or
generate it in a derivation from the two inputs this repo already pins. Taken:
the derivation. `modules/tally-b.nix` runs the KERNEL's own
`${inputs.tally-b}/tools/make-evaluator-lock.sh` with `--root
${inputs.tally-lake}` at build time, so not one digest is typed here and the
lock cannot disagree with either pin without the derivation changing. The cost
is that reading the lock means building it (guard G3 now builds one small
derivation, offline); the gain is that a transcribed lock can rot silently
against a bumped input and a generated one cannot. The consequence to know: a
`tally-lake` bump CHANGES the lock, which changes the `evaluator.lock` cell of
every verdict derived after it — that is the point, a verdict names the bytes
that judged it, and it is why the lake is not in `rollingInputOverrides`.

**The locked argv is ONE store word.** `pkgs/tally-evaluator` wraps
`tools/e2e-evaluator.sh` so the argv the kernel hashes is
`<store path>/bin/tally-evaluator` rather than the two words the audit's probe C
ran (`/bin/sh <checkout>/tools/e2e-evaluator.sh`). A checkout path in a locked
argv is a lock over a file `git pull` can move; a store path is not. Everything
per-evaluation still travels on stdin — T7-3, and the only reason an argv can
be pinned at all. `node` is deliberately absent from the wrapper's
`runtimeInputs`: the stdin item names the interpreter by absolute path, because
the node the lake's `scripts/node-env.sh` records is the one its packages were
resolved against.

**Serving the lock changes nothing for the campaign.** The kit's `eval(...)`
entries stay `/bin/sh -c true` no-ops. `DEFERRED.md` `DF-U-D13-4` carries why:
what a mechanical verdict for CUBS IS is Tom's line (integration G7), and the
per-item stdin an `eval(...)` entry would need is the uplink render tool's
second step, not this module's constant `stdin` cell.

**The Claude seat reader is bounded at 16 s, and a failed read re-publishes the
READER'S cache (default, unruled).** D-B54 fixes the arithmetic (30 + 1 + 20 =
51 < 60) and leaves the reader's own bound free inside the 20-second
`TimeoutStartSec`; the reads are concurrent, so three of them fit in one window.
12 seconds was cutting off reads that were going to land — MEASURED over the
uplink's `events.jsonl` for 2026-09-16T04Z → 09-17T04Z, `cc` read
`STALE-MEASURED` on 135 of 286 wakes. And when a read genuinely cannot land, the
retained reading is now taken from the reader's own `.window-cache-<seat>.json`
in preference to this feeder's own last row on a tie: the cache is the
measurement, the row is a projection of it that drops cells (the five-hour
`resets_at`, the null `model_split`), so re-publishing the row degrades the
retained reading a little further on every failed read while re-publishing the
cache does not — and `reading_age_seconds` becomes the age of the measurement,
which is what `reading_source` already claimed by naming that file.
`tests/tally-b/probe-seat-feeder-timeout.sh` holds both halves down and was
MEASURED red against the pre-change feeder in exactly those places. Neither
change can manufacture a reading from an expired token: re-logging in on `cc`
and `cc3` stays Tom's (`DEFERRED.md` DF-U-D12-3).

**The one red is INHERITED, and it is the `nas-topology` assertion — not an
offline-input problem.** `nix flake check --offline --no-build` is rc 1 in this
repository, which is why clause A of `tests/tally-b/test-tally-b-input.sh` is
red and this item's composite oracle exits 1. The tail that reproduces now —
MEASURED 2026-09-17 on this branch and byte-identical on a detached worktree at
`main` (202d9c31) — is `checks.x86_64-linux.nas-topology` → `error: assertion
'(! ((builtins).elem 8731
(coordinator).networking.firewall.interfaces.wlp192s0.allowedTCPPorts))' failed`
at `flake.nix:1694`: a real eval assertion about a coordinator firewall port,
belonging to whoever opened 8731, and NOT the "an input is missing offline"
`[ENV]` reading an earlier pass of this item gave it. That earlier pass reached
`checks.x86_64-linux.nas-personal-tailnet` → `error: path
'pl6rmijq3dkwqw9cf16wzpycsc7gb9m2-86byf0qaz7f0vgj7x4km4zc9q446skd0-source' is
not valid` first instead; which of the two surfaces first depends on evaluation
order, so a rerun may show either. Either way nothing here introduced it — this
branch edits no `flake.nix` and no firewall or NAS configuration — and nothing
here fences it out of a probe to make an rc green. `DEFERRED.md` `DF-FLAKE-1`
carries both tails and who discharges them.

2026-09-13 flake checkouts no longer ride into host closures, chrome-stream
is installed, and a switch refuses a stale raw-dotfiles checkout.

Expand Down
15 changes: 14 additions & 1 deletion DEFERRED.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ about.
| DF-U-D12-3 | Refreshing the expired `cc3` OAuth token | D-B5 records this as the TOM LINE "run `claude` once on cc3". This feeder may read the credential through `stamp-receipt.py window`; it may not perform an interactive login | Tom runs `claude` once with the cc3 seat configuration. Until then the enabled feeder writes a fresh UNKNOWN row with the reader's reason |
| DF-U-D12-4 | Releasing work onto the `codex` row | D-B6 says the login is third-party and the row is never proposed onto. Reading `rate_limits` from its rollout is not a spend and does not change ownership | A later explicit ownership ruling (TL-6); this unit preserves `owner: third-party` in every write |
| DF-UTIL-TOK-1 | Counting a Halogen token window (a request with >= 128 completion tokens in the journal) as an evidence event for a box-hour | `/home/tom/research-methods/cards/UTIL-01.md:81-82`: "A metrics delta is NOT an evidence event … only Tom widens it". Since 2026-09-13 (#312) the worker's sampler reads Halogen's per-request `serve_api:` journal line and util-row sums `tokens_in` / `tokens_out` from it, but a token window carries no fingerprint and no receipt, so `receipt_is_evidence()` is deliberately unchanged and box-hours are still graded on receipts and drain-ledger rows alone | Tom, by editing the card's evidence definition. Until then the windows feed `tokens_in` / `tokens_out` / `tokens_grade` only, never a disposition |
| DF-U-D13-2 | Passing `--evaluator-lock` to the served kernel, so a verdict is derived at all | `services.tally-kernel.evaluatorLock` defaults to null, and null is the honest state: the lock pins `apps/evaluator`'s bytes, which do not exist until U-A17 delivers them in mecattaf/tally-ts-sdk (tally docs/socket.md §4: "Without --evaluator-lock, no verdict is ever derived at all"). Locking a path that does not exist would be a stub standing in for a deliverable | U-A17's delivery, then a follow-up pin of the lock file's path in this module — an edit reviewed like any other, which the topology check will see |
| DF-U-D13-4 | `[SCOPE]` Giving the kit's `eval(...)` entries a real mechanical verdict — the CUBS entries and `eval(build:LOCAL-SMOKE)` stay `/bin/sh -c true` no-ops | Serving the lock changes what the KERNEL can derive, not what the CAMPAIGN asks for, and the two must not move in one act: the 58 hand-driven runs of `~/mecattaf/cubs-campaign/receipts/REVIEW-2026-09-17.md` were graded under the no-op entries, so flipping them in the same PR would change the campaign's semantics mid-flight with no ruling behind it. WHAT a mechanical verdict for CUBS is (integration G7: the validation exit plus revert-and-rerun as AC#4's verdict) is Tom's line and is asked, not assumed. The other half is mechanical and belongs elsewhere: an `eval(...)` entry must hand `tools/e2e-evaluator.sh` a whole per-item JSON object on stdin (card, deliverable repo, commit file, usage source, receipt out, node, repo), and rendering that per item is the SECOND step of the uplink render tool that D-A owns — this module's `stdin` cell is a constant string. (Typed `[SCOPE]`, not `[OTHER-REPO]`: only the G7 ruling is outside this repository — the actionable half is a reviewed edit to `home/tally-uplink.nix` right here, in D-A's lane rather than this one) | Tom rules G7; then the kit entry is a reviewed edit to `home/tally-uplink.nix` (D-A's render tool supplying the per-item stdin), and `tally-uplink-topology` sees it. Discharged when a `verdict` on the chain names a CUBS item and the lock this module serves |
| DF-U-D14-3 | Setting `services.tally-uplink.plan` — the acceptor's plan body | Null is the honest state, not an oversight. The plan body is the ACCEPTOR's, re-POSTed to arm and re-arm; authoring one in this module would be the lake proposing from the wrong side of the seam, and arming is Tom's act. (The kit half of this row is discharged: `home/tally-uplink.nix` sets `kit` to the reviewed store kit file, and `tally-uplink-topology` asserts it reaches the rendered argv.) | Tom arms the acceptor; then a follow-up edit of this option, reviewed like any other change, which the `tally-uplink-topology` check sees because it asserts `cfg.plan == null` and no `--plan` in the argv today |
| DF-U-D18-2 | Setting an anti-starvation number for the filler lane, and implementing D-B10's age-based promotion (a filler item passed over by 10 non-filler releases proposed at level 1 for one release) | Spec §4.4.3 names TL-10 as the anti-starvation number and says that "until it exists the lane runs whenever the row is free". The promotion is a RELEASE-STATION decision over a backlog — the acceptor's `levels` document and the lake's proposals — and a clock has no backlog to age. Putting a promotion rule in a timer would be this unit deciding admission, which is the one thing its own card forbids ("the timer only wakes the uplink's filler pass, the kernel admits") | Tom for TL-10's number; the release station (§2.2c) and the lake's selector for the promotion itself. Nothing in this repository shortens the wait, and the five-minute cadence stands meanwhile: it is the drain's own, so neither filler can crowd the other out |
| DF-U-D18-3 | Moving the academic drain onto a LEASE over the kernel's socket, so the two fillers alternate UNDER the kernel's admission rather than beside it | Spec §2.4 lists the drain as an *unleased* tenant of the GPU row "until TL-15 moves the drain onto a lease over the socket (U-D11 wires the call)". Until that happens, D-B10's round-robin is carried here by equal cadence plus the lane's own `/running`-empty gate — two mechanisms outside the kernel — and this unit cannot lease on another unit's behalf. MEASURED 2026-09-07: `tally-drain.service` runs `tally --socket /run/user/1000/tally/tally.sock daemon drain`, the LIVE daemon's socket, not the rewrite kernel's `~/.local/state/tally-rewrite/kernel.sock` | U-D11 wires the call; TL-15 is the line. Discharged when the drain's GPU turn is a lease on the chain and the `tally-filler-topology` cadence equality can be replaced by an admission-side assertion |
Expand All @@ -28,6 +28,19 @@ about.
| DF-136-2 | Turning on the nightly bulk-admission timer (`myNas.paperless.bulk.enable = true`) and writing the measured canary throughput, idle/import RSS and projected completion into DECISIONS.md | The 2026-09-13 flip shipped the gate off by design. A timer that runs unattended OCR on the house router needs a measurement first, and no measurement can exist before the deploy (DECISIONS.md 2026-09-13) | The orchestrator or Tom, after the post-deploy canary run in hosts/nas/paperless.nix steps 3 to 5; one manual `systemctl start paperless-bridge-bulk` run may precede it |
| DF-CLIENT-11 | Setting sshd `ClientAliveInterval` / `ClientAliveCountMax`, so the coordinator reaps a dead projector's `remote-client-bridge` in seconds instead of at TCP keepalive | #385 is a client-seat change and deliberately changes no sshd behaviour: a `ClientAlive*` line in modules/common.nix applies to every host's sshd and every live ssh session (the orchestration running inside herdr included). The laptop side is already bounded — herdr's managed ssh config sends ServerAlive 15 s × 4, and `home/dot_local/bin/herdr-projector` exits its own master and removes its sockets — so what lingers is one idle bridge process on the coordinator. MEASURED 2026-09-13: `grep -rn ClientAlive modules hosts` in this tree is empty | Tom, if lingering bridges ever pile up (`pgrep -af remote-client-bridge` on the coordinator); then a reviewed edit to modules/common.nix |
| DF-SCREEN-OCR-1 | Putting `screen-ocr` (Mod+Ctrl+S) under a prioritized GPU lease instead of calling Halogen directly | Tom, 2026-09-15: a busy Halogen must not break the flow, and hierarchical prioritization of GPU work belongs to `tally`, which is still being built. Until then the script has no idle gate: it waits behind in-flight requests (curl `--max-time 600`) and fails without touching the clipboard | Tom, once `tally` offers a lease a short interactive request can take; then a reviewed edit to `home/dot_local/bin/screen-ocr` |
| DF-FLAKE-1 | `[ENV]` Making `nix flake check --offline --no-build` rc 0 in this repository. It is rc 1 today, which is what makes clause A of `tests/tally-b/test-tally-b-input.sh` red and this item's composite oracle rc 1 | Inherited, never introduced here: this branch edits no `flake.nix` and no firewall or NAS configuration (`git diff --name-only main...HEAD` names 11 files, none of them `flake.nix`), and BOTH tails are `main`'s. (a) The one reproducing now — MEASURED 2026-09-17 on this branch AND, byte-identical, on a detached worktree at `main` (202d9c31): `checks.x86_64-linux.nas-topology` → `error: assertion '(! ((builtins).elem 8731 (coordinator).networking.firewall.interfaces.wlp192s0.allowedTCPPorts))' failed` at `flake.nix:1694`. That is a real eval assertion about a coordinator firewall port, NOT an offline-input problem — the `[ENV]` tag on this row means inherited from the tree this lane was handed, not that the store is short of a path. (b) The one an earlier pass of this item hit first: `checks.x86_64-linux.nas-personal-tailnet` → `error: path 'pl6rmijq3dkwqw9cf16wzpycsc7gb9m2-86byf0qaz7f0vgj7x4km4zc9q446skd0-source' is not valid`. Which of the two surfaces first depends on evaluation order, so a rerun may show either tail. Neither is fenced out of any probe or oracle here: the red stays visible and clause A is reported red rather than laundered | Whoever owns the coordinator `wlp192s0` line that opened 8731 — or the `nas-topology` assertion itself, if the port is intended — and, for (b), whoever can supply that input. Another lane is already on it (branch `fix/fleet-connectivity-artifact-count`). Discharged when `nix flake check --offline --no-build` is rc 0 in this repository and clause A of `test-tally-b-input.sh` passes on its own |

**DF-U-D13-2 is gone (2026-09-17).** It deferred PASSING `--evaluator-lock` to
the served kernel, on one condition: `apps/evaluator` existing in the lake to be
locked. It exists, in the pinned `tally-lake` input's own store path, so the
condition occurred and the row left. `modules/tally-b.nix` now BUILDS the lock —
the kernel's own `tools/make-evaluator-lock.sh` over `pkgs/tally-evaluator`'s
fixed argv and `apps/evaluator`'s bytes — and the coordinator's ExecStart carries
it. Nothing about it is transcribed by hand, and guard G3 of
`tests/tally-b/probe-u-d13-guards.sh` recomputes every row with the kernel's own
`scripts/verify-evaluator-lock.sh` (and takes it red under a foreign argv). What
the lock does NOT do is change any kit entry: `DF-U-D13-4` above carries that
half.

**The U-D19 switch rows are gone (2026-09-13, #380).** DF-U-D12-1, DF-U-D13-1,
DF-U-D14-1, DF-U-D14-2, DF-U-D14-4, DF-U-D15-1, DF-U-D16-1, DF-U-D16-2,
Expand Down
32 changes: 24 additions & 8 deletions docs/local-ai/seat-feeder.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,21 @@ staleness bound. A service still active at 20 seconds is terminated and its
unit fails; it cannot silently publish outside that envelope. The worker
configuration evaluates with no feeder unit.

The Claude service runs its three readers concurrently. Each reader retains
its 12-second timeout, leaving eight seconds inside the service cap for row
shaping and publication. As each read returns, that seat is shaped and written
immediately; a slow seat cannot hold completed seats in a batch.
The Claude service runs its three readers concurrently. Each reader is bounded
at 16 seconds, leaving four seconds inside the service cap for row shaping and
publication. Because the reads are concurrent, three of them fit inside ONE
16-second window and not inside three. As each read returns, that seat is shaped
and written immediately; a slow seat cannot hold completed seats in a batch.

The bound was 12 seconds until 2026-09-17. MEASURED over the uplink's
`events.jsonl` for 2026-09-16T04Z → 09-17T04Z, `cc` read `STALE-MEASURED` on
**135 of 286** wakes: a read that lands in thirteen seconds was a MEASURED
reading this box threw away for a retained one, and a read that cannot land in
sixteen is a reader that is not answering this tick. The four seconds left over
are ample — shaping is arithmetic and publication is one `os.replace` per row;
the cap exists so a HUNG reader cannot push the service past
`TimeoutStartSec`, not to budget the shaping. `tests/tally-b/probe-seat-feeder-timeout.sh`
is the fixture that holds both halves down.

## Source boundaries

Expand Down Expand Up @@ -116,10 +127,15 @@ endpoint answers in whole percents. The feeder follows that through:
`STALE-MEASURED` with `reading_age_seconds`, `reading_observed_at`,
`reading_source` and the failure's own `stale_reason`.

Two retained sources, and the newer wins: the row this feeder last published
(already in the contract's shape) and the reader's own
`.window-cache-<seat>.json`, whose directory the reader hard-codes and which
`TALLY_WINDOW_CACHE_DIR` names so a fixture can redirect it. Ages chain off
Two retained sources, and the newer wins — **and on a tie the reader's own cache
wins**: `.window-cache-<seat>.json`, whose directory the reader hard-codes and
which `TALLY_WINDOW_CACHE_DIR` names so a fixture can redirect it, is preferred
over the row this feeder last published. The cache is the reading; the published
row is a projection of it, and going back through the projection loses cells the
row never promised to carry (the five-hour `resets_at` the sentinel-window branch
drops, a `model_split` the endpoint answered null for). So the cache is what
`reading_source` names and `reading_age_seconds` is the cache's true age, not the
age of whatever the last row happened to say. Ages chain off
`reading_observed_at`, the instant the numbers were MEASURED, never off
publication, so re-publishing a re-publication cannot make a reading look
younger than it is. Only when nothing at all was retained does the row fall
Expand Down
Loading