Router: optimization candidates from the ledger, CI and parked cards #199
Open
time
wants to merge 2 commits from
claude/card-171-router-optimization-7t2dp6 into main
pull from: claude/card-171-router-optimization-7t2dp6
merge into: teahubxyz:main
teahubxyz:main
teahubxyz:gh-pages
teahubxyz:well-known-mail
teahubxyz:havre/catalog-18-plus-20261024
teahubxyz:havre/byok-unfiltered-20261024
teahubxyz:time/card-260
teahubxyz:modern-havre
teahubxyz:uncensor-havre
teahubxyz:havre-split-hide
teahubxyz:havre-split
teahubxyz:havre-model-sort
teahubxyz:time/card-244
teahubxyz:time/card-223
teahubxyz:havre-art-and-snap
teahubxyz:havre-remix-edit
teahubxyz:havre-voice-in-tools
teahubxyz:havre-asmr-mode
teahubxyz:havre-update-pill
teahubxyz:havre-whisper
teahubxyz:claude/card-202-truncated-review-2wh43h
teahubxyz:time/card-211
teahubxyz:claude/card-198-superseded-cards-p3jqzg
teahubxyz:havre-live
teahubxyz:havre-open
teahubxyz:claude/taskmaster-haggle-checkout-f57aux
teahubxyz:havre-memory
teahubxyz:havre-photoshoot
teahubxyz:havre-workflows
teahubxyz:claude/card-173-rank-spine-cards
teahubxyz:ceo/sync-back-union
teahubxyz:claude/router-pick-outcomes-99zcnh
teahubxyz:time/card-48
teahubxyz:time/card-98
teahubxyz:time/card-170
teahubxyz:time/card-89
teahubxyz:time/card-160
teahubxyz:time/card-114-gh-pages
teahubxyz:time/card-113-gh-pages
teahubxyz:time/card-96-gh-pages
teahubxyz:time/card-85
teahubxyz:havre-next2
teahubxyz:havre-cast
teahubxyz:time/card-82
teahubxyz:time/card-80
teahubxyz:time/card-79
teahubxyz:ceo/worker-base-and-slack
teahubxyz:time/card-87
teahubxyz:time/card-94
teahubxyz:chatgpt/parallel-api
teahubxyz:time/card-95
teahubxyz:time/card-71
teahubxyz:mention-ledger-token
teahubxyz:astra-board
teahubxyz:havre-next
teahubxyz:venture/slyng
teahubxyz:havre-brutalist
teahubxyz:ship-from-repo
teahubxyz:time/card-63
teahubxyz:time/card-60
teahubxyz:time/card-61
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
31f382d0e9 |
Router: answer haiku's review of the optimization candidates
Two of the four points hold and are fixed; two don't, and nothing changed for them. The review was read from the diff alone, and the two it gets wrong are both about code the diff doesn't carry. **The cloud lane (holds as a test, not as a bug).** `pick_cloud` already skips a candidate with no card: `router.py:333` reads `if r["n"] is not None and r["actionable"] and ...`, and so does `order()` at `router.py:357`. Both guards came with the cloud lane in #197, for `venture:new`, which has `n: None` for the same reason an `opt:` candidate does; this card added a second kind of cardless candidate and inherited them. So `tm.label(None, "cloud")` cannot happen and the table cannot say "to the cloud today" for an `opt:` row. No code changed. The test the review asks for is worth having and is now there: an `opt:` candidate judged value 5 ranks first (3.75 against 0.765) under `per_day: 2, sizes: ["S", "M"]`, and the cloud lane takes only the real card behind it. Dropping the `n is not None` from `pick_cloud` fails it. **The parked window (holds as wording only).** The arithmetic is already what the review proposes. `_recent(at, today, 1)` is `at >= today - 1 day`, and a parked card is a candidate when that is false, i.e. when `at < today - 1 day` — the review's own suggested fix. In whole days that is "parked the day before yesterday or earlier", which is what "older than a day" means at day granularity, so the threshold matches the card. What was wrong was the docstring's "nor is one parked only today": it also holds back one parked yesterday. `parked_candidates` now says so, and the boundary is pinned by test (0 and 1 days yield nothing, 2 yields the candidate). No code changed. **A closed card no longer lets its source come back (fixed).** This was real. `named` was given only the open cards, so the day an optimization card closed the same finding was proposed again — the ledger's window is seven days and outlives the card written about it, and `repeat_of` cannot catch it because the title carries the dollar amount, which moves. `router_tick` now keys the cards closed lately too (`named(open_issues + closed)`; `closed_lately()` moved above the candidate gathering, no extra call). Suppression lasts as long as `closed_lately`'s fortnight, by which time the seven-day window no longer holds the week the card was about, so a finding that is still true comes back as a fresh question rather than never. **`parked_cards` ignored a human retry (fixed).** The fourth point arrived truncated at "astra.py, `parked_c", so I audited the function instead of guessing at it, and it had a gap of its own: it checked #78's automatic retries but not `/time retry`. The heartbeat clears a parked card's `last` for either one (`time.py:1613` for the typed retry, `time.py:1622` for the automatic), so a card someone trusted had already asked to retry was still proposed as "retry or split #N" — a card for work the next heartbeat was about to do. It now skips those, reusing `tm.retry_requested` so the rule is the heartbeat's, and the tests cover the three cases that distinguish it: a trusted retry after the beat holds the card back, a stranger's does not, and neither does one older than the beat it would answer. Audited and left alone: `open_pr` stays on `linked_prs`, not `adoptable`. It looks like the broader check, but a card whose last beat is `error` is never adopted (`time.py:1637` adopts only from `None`/`needs-done`/`would-dispatch`), so a `PR: #N` line on a parked card does not put it in the review path, and using `adoptable` would have hidden a genuinely stuck card. `linked_prs` is also exactly what the heartbeat uses for this same decision at `time.py:1622`. `python3 .github/time/test_time.py`, `test_seed.py` and `test_housekeeping.py` are green. Each fix fails its new check when reverted. Closes #171 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqK2e5z4sMp2BKghDt1D8w |
||
|
|
cf38e08677 |
Router: optimization candidates from the ledger, CI and parked cards
The router only ranked work someone had written a card for, so optimization showed up only when a human noticed it. It now reads three of the factory's own records over the last seven days and proposes the rest itself, deterministically. router.py, one synthetic candidate per finding (`opt:<source>:<id>`, kind optimization, no card yet, a drafted title and body that carries its own key and a `DONE:` line): - `ledger_candidates`: the week's spend grouped by model and by entry point (a line's `entry` where it has one, else `pool`, the route it booked to). A group that took over half the bill is a candidate, where there is a second group in that dimension to compare it with; so is a model whose spend per call is double its own median, which is a few heavy calls behind a cheap average. Pennies (under $0.25 a week) are left alone and one group yields at most one candidate. - `run_candidates`: Actions runs as `/actions/runs` gives them (the shape `tea_actions` reads: the list, or `workflow_runs`). A workflow with two or more failed or cancelled runs in the window is a candidate, with the failure count and the last run's id. - `parked_candidates`: a card parked by an `error` or `stuck` beat older than a day, with no open PR and #78's automatic retries spent ("retry or split #N"). One Time will still retry on its own is left to it. - `named` / `opt_candidates`: a source an open card already names (`Router: opt:<source>:<id>` in the body) is not proposed again. astra.py gathers the three sources (`ledger_week`, `action_runs`, `parked_cards`; each is context, so unreadable means no candidates from it, never a failed run) and the seat judges them like any other candidate. Live, a picked one is written as a card, unqueued and a candidate from tomorrow, and is never worked on in place; dry, the ranked table shows it as "would be written as a card". The log line and the beat's `opt` say which source became which card. Tests (test_time.py section 171): a ledger with one dominant route yields one candidate, and so does the entry-point dimension, the median rule, pennies, a single group and a stale window; two failed runs of one workflow yield one candidate with its count and last run id; a parked card yields one unless it has an open PR, retries left or was parked today; a source an open card names yields none; Astra live writes the card unqueued while dry writes nothing. Closes #171 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EzRXxLDJF4YDoZgzSsbW24 |