Skip to content

gc order history: a bounded read is not store-complete — any positive --limit answers for the city store alone while printing a RIG column, and it cost a P1 on the refinery merge cadence (gc-6a6vz) - #155

Merged
zook-bot merged 1 commit into
mainfrom
polecat/gc-6a6vz
Aug 22, 2026

Conversation

@zook-bot

Copy link
Copy Markdown

Summary

Defect

gc order history <name> is store-complete only when the read is unbounded
(--limit 0). Any positive --limit — including the default 50, and including
a limit far larger than the number of rows that exist — returns runs from the
city store alone, while printing them under a RIG column. The output
therefore looks city-wide and is not.

Measured (loomington, 2026-08-20, 43 retained refinery-reconcile runs across 4 rigs)

gc order history refinery-reconcile --limit 40              -> gascity only
gc order history refinery-reconcile --limit 100             -> gascity only
gc order history refinery-reconcile --since 20m             -> gascity only
gc order history refinery-reconcile --since 20m --limit 100 -> gascity only
gc order history refinery-reconcile --rig gc-toolkit --limit 40 -> gc-toolkit (11)
gc order history refinery-reconcile --limit 0               -> all 4 rigs
gc order history refinery-reconcile --since 20m --limit 0   -> all 4 rigs

--limit 100 truncating to one rig with only 43 rows in existence rules out an
exhausted budget. The bounded and unbounded reads are different code paths, and
only the unbounded one fans out across the per-rig stores.

Because the city store is the mayor rig's store, the bounded read always answers
"gascity". To a reader that looks like the one rig that is working, not the
only rig I looked at
.

Why it matters

This is the triage surface for scope = "rig" orders, whose entire point is one
registration per importing rig. The help text steers straight into the trap —
"On a city with a long order-run history an unbounded read costs tens of
seconds, so prefer keeping a bound when triaging" — so the documented triage
path is the one that under-reports.

It has already cost a P1. gc-toolkit bead tk-fdstg was filed at severity 1
reporting that the refinery-reconcile order (the refinery merge cadence, i.e.
the merge queue's clock) "has NEVER fired on gc-toolkit" and that the rig was
running only on a retired /tmp daemon. It had in fact been firing in lockstep
with the other three rigs since the order's first tick — 19 passes, first one at
the same second as gascity's. The reporter checked gc order list, ruled out
city.toml overrides, ruled out suspension, and explicitly flagged
gc order history as "NOT a usable check here: it returned only gascity rows
even at --limit 40" — and still concluded the opposite of the truth, because no
reachable surface would answer per-rig. (bd list does not enumerate the
tracking beads at all: they are wisps.)

Patch site

gc order history — the history read path behind cmd/gc order history. The
unbounded branch already enumerates every rig store; the bounded branch does
not. Fix by making the bounded path fan out over the same store set, merging by
executed descending before applying the limit, so --limit N means "the N
most recent runs in the city" rather than "the N most recent runs in whichever
store I read first".

Worth checking whether --since and --rig are pushed into the per-store query
or applied after the fetch — with the fan-out fixed, --since is what keeps an
unbounded-shaped read cheap (measured ~3s here for --since 20m --limit 0).

Test

A city with ≥2 rigs, each having run the same rig-scoped order. Assert that
gc order history <name> --limit N (for N greater than the total row count)
returns rows from every rig, and that its rows are the N most recent by
executed across all stores.

Downstream

gc-toolkit's doctor/check-refinery-merge-cadence reads history with
--since <window> --limit 0 and its hermetic test asserts the --limit 0 is
present, so it stays correct either way; nothing there needs to change when this
lands. docs/refinery-merge-cadence.md in that pack documents the trap.

Filed from gc-toolkit tk-fdstg; determination in that repo at
specs/tk-fdstg/merge-cadence-false-alarm.md.

Implementation notes

Correction to "Patch site" — converse sitting 2026-08-22T14:2xZ (visit gc-qcvca). Defect RE-CONFIRMED live; the named fix location is wrong.

Still reproduces, today, on the running binary (loomington, 44 retained
refinery-reconcile runs, 11 per rig across 4 rigs):

gc order history refinery-reconcile --limit 40   -> 11 rows, gascity only
gc order history refinery-reconcile --limit 100  -> 11 rows, gascity only
gc order history refinery-reconcile --limit 0    -> 44 rows, all 4 rigs

--limit 100 returning 11 of 44 existing rows rules out an exhausted budget, as
the original report argued.

The mechanism is one level up from where this bead points. The bead says
"the bounded branch does not fan out ... fix by making the bounded path fan out
over the same store set". But doOrderHistoryBounded ALREADY fans out — it takes
an orderStoresResolver and is the same function the unbounded path calls.
Patching it changes nothing, because a bounded named read never reaches it.

The split is in routeOrderHistory (cmd/gc/cmd_order.go, ~1398-1430). GC_DEBUG=1
prints the decision:

--limit 40 -> cmd=order history route=api
--limit 0  -> cmd=order history route=fallback reason=unlimited

Three branches fall back to the local multi-store iterator — name == ""
(multi-order) and bounds.Limit <= 0 (unlimited) — while a NAMED read with a
POSITIVE limit is routed to the server: c.GetOrderHistory(orderScopedName(...),
bounds.Limit, ""). That server read answers from the city store alone, and the
renderer still prints a RIG column. The comment above the unlimited branch
states the reason the fallback exists ("The API has no wire representation for
an unlimited read"), which is why only that branch was ever made store-complete.

So the fix is one of two places, neither of them doOrderHistoryBounded:
(a) make the API's /orders/history handler store-complete — merge across every
rig store by executed-desc BEFORE applying the row bound; or
(b) stop routing rig-scoped order reads to the API and always use the local
fan-out iterator, taking the latency cost the help text already warns about.
(a) keeps the API fast path; (b) is smaller and needs no server change.

Fork/upstream classification (the FIRST method, blob hash + per-file log):
upstream/main carries the IDENTICAL routeOrderHistory — same three branches,
same GetOrderHistory call at the same place. This is a GENUINE UPSTREAM DEFECT,
not fork-local debt. Fix in fork first, tag upstream_pr_candidate.

Rebase risk is real but small and unchanged: cmd/gc/cmd_order.go is
fork-touched (1 commit ours: 3d65376, refuse unbound city registration of a
rig-scoped order, gc-g3pgp) AND upstream-moving (1 commit theirs: b3f125f,
route session-/graph-/convergence-class reads to their owning store, gastownhall#5209).
Three distinct blobs at merge-base / origin/main / upstream/main. Flag this to
whoever takes it. Note b3f125f is the same CLASS of fix as this bead
(routing a read to its owning store) but does not touch order history.

Implemented 2026-08-22 (polecat gc-toolkit.nux, branch polecat/gc-6a6vz)

Commit dd40aba — fix(order): make a bounded order-history read store-complete.

ROOT CAUSE (differs from the "Patch site" guess in the filing, which pointed at
the read): the read was never the problem. doOrderHistoryBounded ALREADY fans
out across every matching registration, merges newest-first, and only then
applies the bound — exactly the semantics the bead asks for. The defect is one
level up, in ROUTING.

routeOrderHistory (cmd/gc/cmd_order.go) sends a single-order bounded query to
the supervisor API, and that request carries ONE scoped_name. A rig-scoped
order is registered once per importing rig, so a bare name with no --rig names
N registrations. orderScopedName resolves it via findOrder, which returns the
FIRST match — so the API was asked about one rig and the other N-1 were dropped
silently, while the response still rendered a RIG column. Because the city store
is the mayor rig's store, that first match is always gascity, which is why the
symptom read as "only gascity ever runs".

This also explains the two facts in the filing that a read-side bug would not:
why --limit 100 truncated to one rig with only 43 rows in existence (not budget
exhaustion — a different store set), and why --rig gc-toolkit worked (it names
exactly one registration, so the single scoped_name is correct).

FIX: stay on the local iterator whenever the name resolves to more than one
registration, alongside the existing multi-order and unlimited fallbacks and
using the same logRoute(... "fallback", ...) idiom. +36 lines in cmd_order.go,
no deletions, no change to the read path. Deliberately narrow: a rig-qualified
read and a city-scoped order each resolve to exactly one registration and keep
the API route, so this does not push every bounded read onto the slower local
scan. Per-store reads stay bounded by --limit, so the fan-out is N bounded
reads, not the unbounded scan the help text warns about.

ACCEPTANCE MET — 4 new tests in cmd/gc/cmd_order_history_store_completeness_test.go,
verified by inverted-fix run (source reverted to base, tests kept):
TestRouteOrderHistoryBoundedStaysLocalWhenNameSpansRigs FAILS without fix
TestRouteOrderHistoryBoundedUsesAPIWhenRigQualified guard (passes both)
TestOrderHistoryBoundedReadIsStoreCompleteAcrossRigs bead's stated test:
limit 50 > 6 total rows -> 3 rows from EACH of 2 rigs
TestOrderHistoryBoundedReadKeepsNewestAcrossRigs limit 2 -> newest
across ALL stores (rig-a, rig-b), not the newest of the first store

The routing tests pass a nil API client and assert the route= line logRoute
emits before any request is built, rather than standing up httptest servers.
An earlier draft used two real listeners and pushed the untagged
http_test_server census to 319 calls / 67 files against a 317 / 66 baseline —
and that ratchet's invariant is "cannot grow" (TESTING.md, Small + Source debt
ratchets, ga-80po0c.2.2). The baseline is therefore left untouched, not raised;
the decision is fully observable from stderr either way.

GATES (this rig configures no test/lint/build commands; run by hand):
go build ./... PASS
go vet ./cmd/gc ./internal/orders ./internal/api PASS
go test ./cmd/gc -run Order PASS (11.5s)
go test ./cmd/gc -run Doctor PASS (67.2s)
go test ./internal/testpolicy/... PASS (census green, no ratchet)
make test-fast-parallel (full push gate) PASS — sentinel RC=0, 10/10 jobs
including unit-core. Run by hand because the shared rig repo sets
core.hooksPath=/dev/null, so no git hook fires for any agent here
(filed separately as gc-w89av, P1).

NOT CHANGED, noted for follow-up: the filing asks to check whether --since and
--rig are pushed into the per-store query. --rig IS pushed down (registrations
are filtered before any store is opened). --since is NOT — it is applied after
the fetch, in the row loop. With the fan-out corrected, pushing --since down is
what would keep an unbounded-shaped read cheap; that is a separate performance
change with its own risk, so it is deliberately out of scope here.
2026-08-22 REFINERY GATE (gascity/gc-toolkit.refinery, wisp gc-wisp-jl9).

make test-fast-parallel EXTRA_TEST_ENV='GC_TEST_INNER_P=2', TMPDIR=/var/tmp/rp-6a6vz
HEAD dd40aba, 19:25:44Z -> 19:38:54Z (13m10s), sentinel RC=0.
Header 'Running 10 fast job(s) with LOCAL_TEST_JOBS=2 inner_p=2'; 10/10 jobs ok
(unit-core + all six cmd/gc shards); runner printed "All fast jobs passed".
Run ran for real: fresh fixtures under the run's own TMPDIR (gc-home-,
gascity-subprocess, beads-circuit, TestCustomTypesCheck_
).

Ran the gate explicitly because core.hooksPath=/dev/null in the shared rig
.git/config disables the pre-push hook (a push returns RC=0 in seconds with zero
job lines — the documented silent-no-op signature), so a green push here is not
evidence of a green gate.

Rebase onto origin/main was a no-op (branch already contained main tip 9e89da7);
push was a fast-forward no-op; remote confirmed at dd40aba.

Parked as a pre-open gating anchor (merge_result=pre_open_gate, check_set=codex).
Codex signoff dispatched as gc-oejd5 against origin/main...origin/polecat/gc-6a6vz.
pre-open-resolve.sh opens the PR once check.codex=green@dd40aba.

Refinery handoff

  • Issue: gc-6a6vz (bug, P1)
  • Source branch: polecat/gc-6a6vz
  • Target: main
  • Codex signed off pre-open at dd40aba4; PR opened codex-green.

`gc order history <name>` was store-complete only with `--limit 0`. Any
positive `--limit` -- including the default 50, and including a limit far
larger than the number of retained runs -- answered from a single store
while still rendering a RIG column, so a one-rig answer was
indistinguishable from a city-wide one.

Root cause is in the routing, not in the read. routeOrderHistory sends a
single-order bounded query to the supervisor API, and that request carries
one `scoped_name`. But a rig-scoped order is registered once per importing
rig, so a bare name with no `--rig` names N registrations at once.
orderScopedName resolves it through findOrder, which returns the FIRST
match -- so the API was asked about one rig and the other N-1 were dropped
silently. Because the city store is the mayor rig's store, the answer was
always that rig, which reads as "the one rig that is working" rather than
"the only rig I looked at".

That under-report has already cost a P1: gc-toolkit bead tk-fdstg was filed
at severity 1 reporting that the refinery-reconcile order had never fired on
gc-toolkit, when it had in fact been firing in lockstep with the other three
rigs since the order's first tick. The reporter explicitly flagged
`gc order history` as unusable there -- it returned only gascity rows even
at `--limit 40` -- and still reached the opposite of the truth, because no
reachable surface would answer per-rig.

The fix stays on the local iterator whenever the name resolves to more than
one registration, alongside the existing multi-order and unlimited
fallbacks and using the same `logRoute(... "fallback", ...)` idiom. That
iterator was already correct: it walks every matching registration, merges
newest-first, and only then applies the bound, so `--limit N` means "the N
most recent runs in the city" rather than "the N most recent runs in
whichever store I read first". No change was needed in the read path itself.

Deliberately narrow. A rig-qualified read (`--rig <name>`) still resolves to
exactly one registration and keeps the API route, so this does not push
every bounded read back onto the slower local scan; a city-scoped order has
a single registration and is likewise unaffected. The per-store reads stay
bounded by `--limit`, so the fan-out costs N bounded reads, not the
unbounded scan the help text warns about.

The routing tests pass a nil API client and assert on the route= line that
logRoute emits before any request is built, rather than standing up an
httptest server. The decision is fully observable from that line, and the
untagged http_test_server census is a "cannot grow" ratchet (TESTING.md,
Small and Source debt ratchets, ga-80po0c.2.2) -- an earlier draft using two
real listeners pushed it to 319 calls / 67 files against a 317 / 66 baseline.
Spending that ceiling to observe what stderr already reports would have been
a poor trade, so the baseline is left untouched rather than raised.

Validation: four new tests in cmd_order_history_store_completeness_test.go.
TestRouteOrderHistoryBoundedStaysLocalWhenNameSpansRigs is the regression --
it fails before this change with "bounded read of a name spanning 2 rigs was
routed to the API". TestRouteOrderHistoryBoundedUsesAPIWhenRigQualified
guards the other side so the API route is not lost. The remaining two pin
the bead's stated acceptance: with a limit larger than the total row count
every rig is represented, and with a limit smaller than it the rows kept are
the newest across all stores rather than the newest of one. Existing
`-run Order` (11.5s) and `-run Doctor` (67.2s) suites in cmd/gc stay green,
as do go build ./..., go vet, and ./internal/testpolicy/... (the census).

Not changed, noted for follow-up: `--since` is applied after the fetch
rather than pushed into the per-store query (`--rig` genuinely is pushed
down, by filtering registrations before any store is opened). With the
fan-out corrected, pushing `--since` down is what would keep an
unbounded-shaped read cheap; that is a separate performance change and
carries its own risk, so it is left out of this fix.
@zook-bot

Copy link
Copy Markdown
Author

Codex signoff (pre-open, comment-only — not an approval):

VERDICT: COMMENT
Reviewed branch: polecat/gc-6a6vz
Reviewed base: main
Reviewed commit: dd40aba

Scope checked: Full diff for cmd/gc/cmd_order.go and cmd/gc/cmd_order_history_store_completeness_test.go, plus surrounding order-history routing, bounded local iterator behavior, route logging, existing route-history tests, and anchor gc-6a6vz intent.

Findings: None.

Verification: go test ./cmd/gc -run 'OrderHistory' -count=1 -> PASS; go test ./cmd/gc -run 'Order' -count=1 -> PASS; go test ./internal/testpolicy/... -count=1 -> PASS; go vet ./cmd/gc ./internal/orders ./internal/api -> PASS; git diff --check origin/main...dd40aba -> PASS.

Not checked: full make test-fast-parallel was not rerun in this review pass.

@zook-bot
zook-bot merged commit 7c05072 into main Aug 22, 2026
72 of 84 checks passed
zook-bot added a commit that referenced this pull request Aug 25, 2026
…z) (#155)

`gc order history <name>` was store-complete only with `--limit 0`. Any
positive `--limit` -- including the default 50, and including a limit far
larger than the number of retained runs -- answered from a single store
while still rendering a RIG column, so a one-rig answer was
indistinguishable from a city-wide one.

Root cause is in the routing, not in the read. routeOrderHistory sends a
single-order bounded query to the supervisor API, and that request carries
one `scoped_name`. But a rig-scoped order is registered once per importing
rig, so a bare name with no `--rig` names N registrations at once.
orderScopedName resolves it through findOrder, which returns the FIRST
match -- so the API was asked about one rig and the other N-1 were dropped
silently. Because the city store is the mayor rig's store, the answer was
always that rig, which reads as "the one rig that is working" rather than
"the only rig I looked at".

That under-report has already cost a P1: gc-toolkit bead tk-fdstg was filed
at severity 1 reporting that the refinery-reconcile order had never fired on
gc-toolkit, when it had in fact been firing in lockstep with the other three
rigs since the order's first tick. The reporter explicitly flagged
`gc order history` as unusable there -- it returned only gascity rows even
at `--limit 40` -- and still reached the opposite of the truth, because no
reachable surface would answer per-rig.

The fix stays on the local iterator whenever the name resolves to more than
one registration, alongside the existing multi-order and unlimited
fallbacks and using the same `logRoute(... "fallback", ...)` idiom. That
iterator was already correct: it walks every matching registration, merges
newest-first, and only then applies the bound, so `--limit N` means "the N
most recent runs in the city" rather than "the N most recent runs in
whichever store I read first". No change was needed in the read path itself.

Deliberately narrow. A rig-qualified read (`--rig <name>`) still resolves to
exactly one registration and keeps the API route, so this does not push
every bounded read back onto the slower local scan; a city-scoped order has
a single registration and is likewise unaffected. The per-store reads stay
bounded by `--limit`, so the fan-out costs N bounded reads, not the
unbounded scan the help text warns about.

The routing tests pass a nil API client and assert on the route= line that
logRoute emits before any request is built, rather than standing up an
httptest server. The decision is fully observable from that line, and the
untagged http_test_server census is a "cannot grow" ratchet (TESTING.md,
Small and Source debt ratchets, ga-80po0c.2.2) -- an earlier draft using two
real listeners pushed it to 319 calls / 67 files against a 317 / 66 baseline.
Spending that ceiling to observe what stderr already reports would have been
a poor trade, so the baseline is left untouched rather than raised.

Validation: four new tests in cmd_order_history_store_completeness_test.go.
TestRouteOrderHistoryBoundedStaysLocalWhenNameSpansRigs is the regression --
it fails before this change with "bounded read of a name spanning 2 rigs was
routed to the API". TestRouteOrderHistoryBoundedUsesAPIWhenRigQualified
guards the other side so the API route is not lost. The remaining two pin
the bead's stated acceptance: with a limit larger than the total row count
every rig is represented, and with a limit smaller than it the rows kept are
the newest across all stores rather than the newest of one. Existing
`-run Order` (11.5s) and `-run Doctor` (67.2s) suites in cmd/gc stay green,
as do go build ./..., go vet, and ./internal/testpolicy/... (the census).

Not changed, noted for follow-up: `--since` is applied after the fetch
rather than pushed into the per-store query (`--rig` genuinely is pushed
down, by filtering registrations before any store is opened). With the
fan-out corrected, pushing `--since` down is what would keep an
unbounded-shaped read cheap; that is a separate performance change and
carries its own risk, so it is left out of this fix.

Co-authored-by: refinery costing <refinery@local>
zook-bot added a commit that referenced this pull request Aug 25, 2026
…z) (#155)

`gc order history <name>` was store-complete only with `--limit 0`. Any
positive `--limit` -- including the default 50, and including a limit far
larger than the number of retained runs -- answered from a single store
while still rendering a RIG column, so a one-rig answer was
indistinguishable from a city-wide one.

Root cause is in the routing, not in the read. routeOrderHistory sends a
single-order bounded query to the supervisor API, and that request carries
one `scoped_name`. But a rig-scoped order is registered once per importing
rig, so a bare name with no `--rig` names N registrations at once.
orderScopedName resolves it through findOrder, which returns the FIRST
match -- so the API was asked about one rig and the other N-1 were dropped
silently. Because the city store is the mayor rig's store, the answer was
always that rig, which reads as "the one rig that is working" rather than
"the only rig I looked at".

That under-report has already cost a P1: gc-toolkit bead tk-fdstg was filed
at severity 1 reporting that the refinery-reconcile order had never fired on
gc-toolkit, when it had in fact been firing in lockstep with the other three
rigs since the order's first tick. The reporter explicitly flagged
`gc order history` as unusable there -- it returned only gascity rows even
at `--limit 40` -- and still reached the opposite of the truth, because no
reachable surface would answer per-rig.

The fix stays on the local iterator whenever the name resolves to more than
one registration, alongside the existing multi-order and unlimited
fallbacks and using the same `logRoute(... "fallback", ...)` idiom. That
iterator was already correct: it walks every matching registration, merges
newest-first, and only then applies the bound, so `--limit N` means "the N
most recent runs in the city" rather than "the N most recent runs in
whichever store I read first". No change was needed in the read path itself.

Deliberately narrow. A rig-qualified read (`--rig <name>`) still resolves to
exactly one registration and keeps the API route, so this does not push
every bounded read back onto the slower local scan; a city-scoped order has
a single registration and is likewise unaffected. The per-store reads stay
bounded by `--limit`, so the fan-out costs N bounded reads, not the
unbounded scan the help text warns about.

The routing tests pass a nil API client and assert on the route= line that
logRoute emits before any request is built, rather than standing up an
httptest server. The decision is fully observable from that line, and the
untagged http_test_server census is a "cannot grow" ratchet (TESTING.md,
Small and Source debt ratchets, ga-80po0c.2.2) -- an earlier draft using two
real listeners pushed it to 319 calls / 67 files against a 317 / 66 baseline.
Spending that ceiling to observe what stderr already reports would have been
a poor trade, so the baseline is left untouched rather than raised.

Validation: four new tests in cmd_order_history_store_completeness_test.go.
TestRouteOrderHistoryBoundedStaysLocalWhenNameSpansRigs is the regression --
it fails before this change with "bounded read of a name spanning 2 rigs was
routed to the API". TestRouteOrderHistoryBoundedUsesAPIWhenRigQualified
guards the other side so the API route is not lost. The remaining two pin
the bead's stated acceptance: with a limit larger than the total row count
every rig is represented, and with a limit smaller than it the rows kept are
the newest across all stores rather than the newest of one. Existing
`-run Order` (11.5s) and `-run Doctor` (67.2s) suites in cmd/gc stay green,
as do go build ./..., go vet, and ./internal/testpolicy/... (the census).

Not changed, noted for follow-up: `--since` is applied after the fetch
rather than pushed into the per-store query (`--rig` genuinely is pushed
down, by filtering registrations before any store is opened). With the
fan-out corrected, pushing `--since` down is what would keep an
unbounded-shaped read cheap; that is a separate performance change and
carries its own risk, so it is left out of this fix.

Co-authored-by: refinery costing <refinery@local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants