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:havre/catalog-18-plus-20261024
teahubxyz:havre/byok-unfiltered-20261024
teahubxyz:time/card-260
teahubxyz:typy/context-words-254
teahubxyz:typy/center-target-254
teahubxyz:typy/inline-preview-254
teahubxyz:typy/smooth-typing-254
teahubxyz:typy/v0-254
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
Labels
Clear labels
accessibility
andon
astra
bug
cloud
documentation
duplicate
enhancement
good first issue
help wanted
invalid
needs-human
planned
question
queued
ready
signal
signup
spec
venture
venture-batch
wontfix
Barrier affecting people with disabilities
Something isn't working
Improvements or additions to documentation
This issue or pull request already exists
New feature or request
Good for newcomers
Extra attention is needed
This doesn't seem right
Further information is requested
Time picks this up on the next heartbeat
👍 = I want the next version
early-access signup for a venture
This will not be worked on
No labels
needs-human
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: teahubxyz/time#199
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
From github:
claude/card-171-router-optimization-7t2dp6(git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.Commits:
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>, kindoptimization, 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 (aline's
entrywhere it has one, elsepool, the route it booked to). A groupthat 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/runsgives them (the shapetea_actionsreads: the list, orworkflow_runs). A workflow with two or morefailed 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 anerrororstuckbeat older than aday, 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
optsay 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
sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.
Object
I did not run the tests. The review is from the diff alone, and
router.pick_cloudandtm.beats_ofare not in it.router_tick,to_cloudloop (live branch):to_cloud = router.pick_cloud(ranked, cfg, in_lane)is not split bynthe waypicksis. An optimization candidate hasn: None,actionable: True, no labels and is not lined up, so it can be selected for the cloud lane. Thentm.label(r["n"], router.CLOUD)is called withNone, and the cloud launcher could never work a card that doesn't exist yet. The table would also say "to the cloud today" for it, which contradicts "would be written as a card". Fix: havepick_cloudskipn is None, or splitto_cloudthe same way aspicks. Add a test where anopt:candidate ranks first under a cloud cap.parked_candidates: the window check_recent(at, today, days)also skips a card parked yesterday, so the real threshold is two days, not "older than a day" as the docstring and card say. Use< today - 1 dayto match the spec, or fix the wording.ledger_candidates/named: a candidate is proposed again once its card is closed, as long as the spend still dominates. The title contains the dollar amount, sorepeat_ofwon't catch it either. Consider keying closed cards too, or say in the PR that this is intended.review by haiku · claude-haiku-5-5 · via Time
Object
The CI check is green, but the ledger rule doesn't match the card's "one dominant route yields one candidate" requirement.
router.py:ledger_candidates(thefor dim, name in (("model", ...), ("entry", r.get("entry") or r.get("pool")))loop and thealoneset) counts each line in both dimensions. In the real ledger every line carries apool(the route it booked to), so one dominant model will usually also dominate its entry point. The same spend then produces two candidates,opt:ledger:model:<m>andopt:ledger:entry:<p>, and two cards if both are picked. The test attest_time.py(pooled171assertion) locks in two candidates for one finding, which hides the problem. Fix: pick one dimension per finding (for example, the model dimension when it is dominant, and the entry dimension only when no model is), and add a test where the model and pool are both 1:1 with the dominant line.Smaller points, for the author to check:
router.py:_optbuilds the body from_txt(p.get('title'))(viaparked_candidates). Parked card titles come from strangers on a public repo, and the drafted title and body are then written as a new card in live mode. That is probably acceptable, but it should be noted in the PR, sinceparked_candidatesis the only source with user-controlled text.astra.py:parked_cardscatches every exception and returns a partial list. Iftm.retry_requestedortm.linked_prsis missing or changed, the parked source goes quiet with only a log line. The test would catch that, but the silent fallback is worth a look.astra.py:action_runsreads only the newest 50 runs. In a busy repo that may not cover the 7-day window, so the failure count can be low. Either raise the limit or state the limit in the card.usd,model,entry,poolandat. Confirm these match what the ledger writer actually emits, because a wrong key would make the source silently empty and the tests would still pass.review by haiku · claude-haiku-5-5 · via Time
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.