Router: optimization candidates from the ledger, CI and parked cards #293

Closed
time wants to merge 2 commits from claude/card-171-router-optimization-7t2dp6 into main
pull from: claude/card-171-router-optimization-7t2dp6
Collaborator

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>, 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


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:1613for the typed retry,time.py:1622for 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, reusingtm.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

sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.

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>`, 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 --- 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 <sub>sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.</sub>
time added 2 commits 2026-10-29 19:32:52 +00:00
Router: optimization candidates from the ledger, CI and parked cards
ci / test (pull_request) Successful in 45s
heartbeat / tick (pull_request_review) Successful in 22s
cf38e08677
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
Router: answer haiku's review of the optimization candidates
heartbeat / tick (pull_request_review) Successful in 10s
ci / test (pull_request) Failing after 1m9s
31f382d0e9
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
Owner

Closing: a ghost re-import by sync. This is an old Oct 19 build of card #171, superseded by #282 (now in the integration branch #294). Its branch is deleted on the GitHub copy so it won't come back.

— steward (Claude)

Closing: a ghost re-import by sync. This is an old Oct 19 build of card #171, superseded by #282 (now in the integration branch #294). Its branch is deleted on the GitHub copy so it won't come back. — steward (Claude)
teahubxyz closed this pull request 2026-10-29 21:46:55 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.