Skip to content

feat(queen): the round's claim is decided by the claim card, proven equal to the hand-written rule first (Closes #1767) - #1768

Merged
gHashTag merged 8 commits into
gHashTag:queenfrom
dmitrii-f-t27:feat/queen-claim-card
Oct 15, 2026
Merged

gHashTag merged 8 commits into
gHashTag:queenfrom
dmitrii-f-t27:feat/queen-claim-card

Conversation

@dmitrii-f-t27

@dmitrii-f-t27 dmitrii-f-t27 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1767. Spec first: gHashTag/t27#8765, PR gHashTag/t27#8768 (specs/queen/claim.t27).

What changes

stateOfDispatch decides every round, for every dispatch row, whether the row still claims its issue. Its body was hand-written. It now calls the claim card's claim_of as generated wasm, the same way queen-holds.ts calls holds.wasm. The signature, the six words and the callers are unchanged: the round's board tasks and the kanban's dispatchRowState.

  • specs/queen/claim.t27: the card, from t27#8768 head d4cedbc5. specs/queen/review_valve.t27: the file the card imports from, at t27 master c31c059ef. Both are vendored byte-identical. The card imports escalation_kind, next_step, the kind and step codes, RETRY_FLOOR_MINUTES and OBSOLETE_STATE, so it restates nothing of the valve, and the boundary reads the word obsolete from the card's typed constants.
  • specs/queen/claim.wasm: built with t27c gen-c (0.5.2 at c31c059ef), then zig 0.16.0 cc --target=wasm32-wasi -Oz -DNDEBUG -mexec-model=reactor. It exports the card's functions and the ones it imports. Two builds give one hash, and the module has 0 imports. specs/PIN names the sha256 of all three files.
  • queen-claim-card.gen.ts: the verbatim output of t27c gen-ts.
  • queen-claim.ts: the boundary. It maps the review_state word to a V_ code, calls claim_of, and maps the CL_ code back to its word. A word with no rule is V_OTHER, which the card holds with no clock, as before. A code the card does not define (a drifted card) holds the row as awaitingReview and is logged once per code. It does not throw, because the round maps every row with no per-row try (t27-bees point 3, 6aa063e).
  • queen-tick.ts: the body of stateOfDispatch is one call. SEND_BACK_IDLE_FLOOR_MS, WAIT_FROZEN_FLOOR_MS, FREE_ATTEMPT_CEILING and REVIEWER_MISS_CEILING now come from the card's generated constants. The review-valve imports the old body used are removed.

The clock crosses the boundary in whole minutes. Every floor is a whole number of minutes, so idleMs >= floorMs holds exactly when floor(idleMs / 60000) >= floorMinutes. NaN and negative clocks read 0, which keeps the hold, as every comparison against NaN did. The counters are integer NOT NULL DEFAULT 0 columns.

Proven equal before the switch

tests/api/queen-claim-card.test.ts keeps the old body verbatim, with its comments, as the reference. The reference reads the old literal floors and ceilings and the valve's hand mirror in queen-review-valve.ts, never the card.

  • Grid. The grid has 2,211,840 finished rows. It combines 16 review_state values (null, '', every word with a rule, case and prototype traps, a number); 27 idle clocks (a minute and a millisecond either side of the 30/60/120/360/2880-minute floors, NaN, negative, 1e12, Infinity); send-backs, ceilings, releases, criteria (unread/null/0/2), free attempts and reviewer misses either side of their ceilings. On every row, the card and the reference give the same word. The card's answers on the grid include every finished claim. A bee that has not ended is running for every row. dispatchRowState reads rows exactly as before.
  • Mutants. The grid must not be blind, so it has to catch each of 22 mutants of the reference: each floor moved by ±1 minute (empty, send-back, frozen wait), the default ceiling moved by ±1, unread criteria read as none, the valve's ceiling hour moved by a minute, and the boundary forgetting each of the 10 words.
  • Kind ceilings. The dead-letter and reviewer-miss ceilings change only the escalation's kind: the valve gives those kinds the same step as an unrecorded one. tri mutate on the spec found exactly that. They are pinned through the card's escalation_kind_of against the hand mirror on 1,200 rows, and a ±1 move of either ceiling is caught.
  • Spec asserts. Every assert in the 11 test blocks of claim.t27 is read from the vendored spec and run against the wasm.
  • Valve mirror. The hand mirror escalationKind/nextStep/wantsBackfill equals the spec's own functions compiled into the card, on their whole input grid.
  • A deliberate violation. I built a wasm from the spec with SEND_BACK_FLOOR_MINUTES = 61 and swapped it in. 4 tests failed: the PIN hash, the spec's own assert, the grid equality, and the +1-minute mutant (the card now matched it). I then restored the real wasm and checked its hash.

Checks run locally (bun 1.3.6, head 087fd88; 6aa063e changes only claimWord and its test: 50 pass)

  • bun test tests/api/queen-claim-card.test.ts: 49 pass, 0 fail, 2.3 s.
  • send-back-lease, queen-adversarial-review, send-back-valve-row, queen-review-valve, queen-holds: unchanged and passing (with the new file: 132 pass, 13 skip, 0 fail).
  • bun run test:api (apps/server): 2021 tests in 156 files, 1956 pass, 65 skip, 0 fail.
  • bun run typecheck (apps/server): clean.
  • biome ci on the changed files: no errors. The 2 warnings are the existing complexity warnings on runRound and reviewFinishedDispatches.

CI on this PR versus queen

Code Quality is red on queen itself (the last four pushes, 82bcc93 included). I compared the findings of this PR's run with those of the queen run, file by file:

  • Biome: the same 73 findings. None of them is in a file this PR touches.
  • Typecheck (agent workspace, which reads apps/server sources without allowJs): the first head of this PR added one finding, a TS7016 for an import of the untyped queen-review-valve.gen.js. Head 087fd88 removes it: the word now comes from the card's typed constants. One existing finding on queen-tick.ts (the KIND_SEND_BACK_CEILING import) is also gone. The local run on 087fd88 shows no finding in queen-claim.ts.
  • Spec side (t27#8768): t27c test-report 11/11, 0 vacuous. tri mutate spec: 40/40 killed. The seal holds 11/11 at Zig 0.16.

Recorded, not changed

When a spent ceiling is handed back, queen-dispatch.ts resets send_backs to 0 and leaves ceiling_releases = 1. The first send-back of the new attempt reaches the valve with a spent release (claim_of(true, sendBack, idle < 60, 1, 2, releases 1) gives cancelled). closeObsolete then closes it inside its hour, which contradicts this file's own "then one more set". This PR keeps today's answer exactly. The spec test pins the behaviour, so a fix will be a visible change to the valve or the card, filed separately.

Not in this PR

  • rings/T27-02/tick_core.t27 state_of_dispatch is now a stale third copy. Retiring it is the next step, together with the tick's two other pure decisions (boundaryPathsOf, parseVerdictBlock).
  • No deploy has been verified yet. Merging into queen deploys the Queen; the live check comes after that.

Dmitrii Fedorov (@dmitrii-f-t27)

…qual to the hand-written rule first (Closes gHashTag#1767)

stateOfDispatch keeps its signature and its callers (the round, the
board's dispatchRowState). Its body is now the claim card's claim_of as
generated wasm (gHashTag/t27 specs/queen/claim.t27, t27#8765, PR
t27#8768), which imports the review valve's escalation_kind and
next_step instead of restating them. queen-claim.ts only maps the
review_state word to the card's code and the answer back to its word.

- specs/queen/claim.t27 and review_valve.t27 vendored byte-identical;
  claim.wasm = t27c gen-c + zig 0.16.0 cc wasm32-wasi, two builds, one
  hash, 0 imports; all three pinned in specs/PIN.
- SEND_BACK_IDLE_FLOOR_MS, WAIT_FROZEN_FLOOR_MS, FREE_ATTEMPT_CEILING and
  REVIEWER_MISS_CEILING now come from the card (t27c gen-ts).
- tests/api/queen-claim-card.test.ts keeps the hand-written body verbatim
  as the reference: the card answers it on all 2,211,840 finished rows of
  the grid; 22 mutants of the reference are each caught; the spec's own
  asserts run against the wasm; the valve's hand mirror equals the
  spec's escalation_kind/next_step on their whole input grid.
… constants (Refs gHashTag#1767)

The agent workspace typechecks apps/server sources without allowJs, so
importing OBSOLETE_STATE from the untyped queen-review-valve.gen.js added
a TS7016 there. claim.t27 now imports OBSOLETE_STATE from review_valve.t27
(t27#8768 head d4cedbc5) and t27c gen-ts emits it typed. The wasm is
byte-identical (08fc2b32); PIN updated for the new spec blob.
@tri-claw

tri-claw Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Summary by t27-bees

Checked by the t27 compiler

t27c parse, parse-complete and typecheck on each changed .t27 file at 28db983. Nothing was generated, built or run. A pass means the file is well formed, not that it is right.

  • trios/agent-server/specs/queen/claim.t27: parses and typechecks.
  • trios/agent-server/specs/queen/review_valve.t27: parses and typechecks.

Read by the model

The pull request replaces the hand-written stateOfDispatch function with a call to a WebAssembly module generated from a t27 specification (claim.t27). The WASM module is built from the spec and the review valve spec it imports, and the change is proven correct through an exhaustive grid test (2.2M rows) and mutant testing that catches 22 injected faults.

Worth a look

  • trios/agent-server/apps/server/src/api/services/queen-claim.ts:72-82 — The reported Set grows without bound if a drifted WASM returns many distinct unknown codes. Only 6 valid claim codes exist (0–5), but a malicious or corrupted module could return arbitrary integers, causing unbounded memory growth. Consider a bounded cache or a fixed-size ring buffer.

  • trios/agent-server/apps/server/src/api/services/queen-claim.ts:95-101 — idleMinutes relies on u32 from queen-card-wasm to clamp NaN, negative, and Infinity to 0. The u32 implementation is not shown in the diff; if it does not explicitly handle these cases (e.g., Number(NaN) >>> 0 === 0 but Number(-1) >>> 0 === 4294967295), the contract "unreadable or negative reads 0" could be violated.

  • trios/agent-server/apps/server/src/api/services/queen-tick.ts:823-841 — The pre-check for TRIOS_QUEEN_RELEASE_DEAD_CLAIMS bypasses the claim card entirely for finished send-backs under the ceiling. This logic duplicates the card's own send-back floor logic (the card also frees send-backs under the ceiling after SEND_BACK_FLOOR_MINUTES). If the flag is on, a send-back with idleMs < 60 minutes is freed immediately by the pre-check, whereas the card would hold it until the hour elapses. The test confirms this is intentional ("changes exactly the send-backs under the ceiling"), but the divergence from the card's own timing is a behavioral fork worth documenting.

  • trios/agent-server/apps/server/src/api/services/queen-tick.ts:748-777 — SEND_BACK_IDLE_FLOOR_MS and WAIT_FROZEN_FLOOR_MS are now derived from the card's minute constants (SEND_BACK_FLOOR_MINUTES * 60 * 1000, WAIT_FROZEN_FLOOR_MINUTES * 60 * 1000). The old hardcoded values (60 minutes, 6 hours) match the card's current constants (60, 360), but if the card's constants change, these derived values change automatically — which is the point — but any other code reading these exported constants will see the new values without a corresponding test for those callers.

  • trios/agent-server/apps/server/tests/api/queen-claim-card.test.ts:34-40 — The reference implementation uses let for constants so mutants can mutate them. This works for the test but means the reference closure captures mutable bindings; if the test runner ever parallelizes these tests, mutants would interfere with each other. Currently Bun runs tests sequentially by default, so this is safe today.


Read: 8 files, +1829 -146, the whole diff up to 60000 characters (the rest was not read). Head 28db983. Model: openai-compatible/nvidia/nemotron-3-ultra-550b-a55b. Commands: @t27-bees review | summary | help | pause | resume. Free for every repository.

…s logged, instead of failing every round (Refs gHashTag#1767)

claimWord threw on a code outside CL_. The round maps every in-flight
row through stateOfDispatch with no per-row try, so one such row would
fail runRound before closeObsolete and dispatch on every round while it
stays in the 7-day window, and the boards with it. Every other card
consumer degrades to a conservative branch. Now the row is held as
awaitingReview (the claim the card gives a verdict it does not know: the
issue and its files stay held) and logger.error names the card and the
code, once per code per process. A card that cannot load still throws
from loadCardWasm; PIN hashes and the per-code test still catch drift.

Raised by the t27-bees review on gHashTag#1768 (point 3).
@dmitrii-f-t27

Copy link
Copy Markdown
Collaborator Author

Replying to the t27-bees review of 087fd88, point by point. Head is now 6aa063e.

  1. idleMinutes and negative clocks. No change. u32 does not do a bare >>> 0. It is Math.max(0, Math.min(0xffff_ffff, Math.floor(n))) >>> 0 (queen-card-wasm.ts:197), so a negative or NaN clock reads 0 and keeps the hold. queen-claim-card.test.ts asserts idleMinutes(-60000) === 0 and idleMinutes(NaN) === 0. The grid also runs NaN and -1 minute against the old rule, with no disagreement.
  2. The criteria seam. No change. undefined and null both read as UNREAD_CRITERIA = 1, which is what ?? 1 gave. 0 reads as none (KIND_NO_CRITERIA), and every count valveCounters can pass (an Array.length) is kept as is. Three tests pin this: the grid with criteria undefined/null/0/2, the 1,200-row escalation_kind_of comparison, and the "unread criteria read as none" mutant.
  3. claimWord throwing. Agreed, fixed in 6aa063e. The round maps every in-flight row through stateOfDispatch and has no per-row try. One row with a code outside CL_ would therefore fail runRound on every round while the row stays in the 7-day window, and the boards would fail with it. Every other card consumer degrades to a conservative branch instead. Such a row is now held as awaitingReview, which is the claim the card already gives a verdict it does not know: the issue and its files stay held. logger.error names the card and the code, once per code per process. A card that cannot load still throws from loadCardWasm, and the PIN hashes and the per-code test still catch drift in CI.
  4. Infinity → 0xffffffff. No change. Saturation keeps the order for any u32 bound the card could add, and Infinity and 1e12 are in the grid. In production the clock is finite or NaN.
  5. PIN. No change. The three hashes match the files, and the test fails if the spec, the imported valve or the wasm drift from PIN, or if the wasm stops answering the spec's own asserts.

Red checks on this PR: runner / Biome and runner / Typecheck. They are not caused by this PR. Code Quality is red on queen itself: run 38079747023 at 82bcc93, and the three pushes before it. I compared the findings of this PR's run 38081376442 with that base run, file by file and with line numbers stripped:

  • Biome reports the same 73 findings.
  • Typecheck reports 25 against 26 on queen. There are no new findings. One existing finding is gone: the KIND_SEND_BACK_CEILING import.
  • At 6aa063e, a local bun run typecheck in apps/agent reports nothing in queen-claim.ts.

Action needed outside this PR: the existing Biome and Typecheck findings on queen. Every Tests / * job is green at 087fd88, including server-api (2021 tests, 0 fail) and server-pglive.

Dmitrii Fedorov (@dmitrii-f-t27)

@dmitrii-f-t27

Copy link
Copy Markdown
Collaborator Author

The same comparison on head 6aa063e, run 38083058828, against queen run 38079747023 (82bcc93):

  • Biome: 73 findings against 73, the same set.
  • Typecheck: 25 findings against 26. No finding exists only in this PR. One finding exists only on queen: the KIND_SEND_BACK_CEILING import in queen-tick.ts.
  • This PR's files: neither check names queen-claim.ts or queen-claim-card.gen.ts.

The fix belongs to the existing queen findings, outside this PR.

Dmitrii Fedorov (@dmitrii-f-t27)

@dmitrii-f-t27

Copy link
Copy Markdown
Collaborator Author

Tests / server-pglive failed on 6aa063e, in run 38083058829, attempt 1. The cause is a crash of the runtime, not a failing test: panic: Segmentation fault at address 0xFFFFFFFFFFFFFFF8 … This indicates a bug in Bun (Bun 1.3.6, Linux x64).

  • No test reported (fail).
  • Only 3 of the 21 pglive files ran before the crash: queen-waits-rows-live, queen-contributor-keys-live and queen-actors-pg-edges-live. None of them imports queen-tick or the claim card.
  • The other 18 files did not run, so this attempt gives no result either way.
  • On 087fd88 the same suite was green.

I re-ran only that job, as attempt 2.

Dmitrii Fedorov (@dmitrii-f-t27)

…ueen-claim-card

# Conflicts:
#	trios/agent-server/specs/PIN
@dmitrii-f-t27

Copy link
Copy Markdown
Collaborator Author

Head 9a9e124 merged the current queen (dfb6d97). The two red checks still have the same cause, and none of their findings comes from this PR. Compared against queen run 38087839871:

  • Biome (38088131961): the same 69 file/rule findings as queen, and the same summary line, "Found 20 errors. Found 122 warnings.", on both.
  • Typecheck: 25 findings against 26 on queen, none of them only in this PR. The one finding on queen that is gone here is the KIND_SEND_BACK_CEILING import in queen-tick.ts.

Locally, on the merged tree, test:api passes: 2077 tests in 161 files, 0 failures.

Dmitrii Fedorov (@dmitrii-f-t27)

@dmitrii-f-t27

Copy link
Copy Markdown
Collaborator Author

The two red test jobs on 9a9e124 (run 38088131937) are not caused by this PR. I re-ran the failed jobs.

  • Tests / server-api. 2077 tests ran: 2008 passed, 1 failed. The failure is queen-turn-stop-bench.test.ts:514, "B: process turns that leave a grandchild, half of them deaf to SIGTERM, real processes". The test starts 10 real processes, kills them, and expects 0 survivors 10 s later; 1 survived. It measures how the OS kills process groups on the runner. It does not touch the claim card or stateOfDispatch. The same test passed on queen dfb6d97 (run 38087839830), on this PR's previous heads, and locally on this merged tree (test:api: 2077 tests, 0 fail).
  • Tests / server-pglive. Bun 1.3.6 crashed with Segmentation fault at address 0xFFFFFFFFFFFFFFF8 after queen-waits-rows-live, queen-contributor-keys-live and queen-actors-pg-edges-live. No test failed. queen itself crashes the same way at the same point in run 38087839830 (dfb6d97). The crash comes and goes: on this PR's 6aa063e, attempt 1 crashed and attempt 2 passed in full.

Action needed outside this PR: the intermittent Bun 1.3.6 segfault in test:pglive on queen.

Dmitrii Fedorov (@dmitrii-f-t27)

…ueen-claim-card

# Conflicts:
#	trios/agent-server/specs/PIN
…ueen-claim-card

# Conflicts:
#	trios/agent-server/specs/PIN
…ueen-claim-card

# Conflicts:
#	trios/agent-server/apps/server/src/api/services/queen-tick.ts
@dmitrii-f-t27

Copy link
Copy Markdown
Collaborator Author

@gHashTag, this PR waits on gHashTag/t27#8768 (the spec merges first) and on one decision.

Head 6d4a440 merges queen 2b1e397. It includes #1894, the dead-claims check. I kept that check exactly where the hand-written rule asked it: in front of the card, for a finished sendBack only, behind TRIOS_QUEEN_RELEASE_DEAD_CLAIMS. With the flag on, the grid still answers what the old rule answered, except the send-backs under the ceiling, which are failed at once. Three mutants of that check are caught. Your queen-claims-now.test.ts passes unchanged.

Why the same conflict keeps coming back. Every card adds its lines to the end of specs/PIN, so this PR conflicts whenever queen moves. It has been resolved three times, always by keeping both sides. Merging is the only lasting fix.

State.

  • Every Tests / * job was green at 087fd88, and again once server-api and server-pglive were re-run on 9a9e124.
  • runner / Biome and runner / Typecheck are red on queen itself. This PR adds no finding to either (compared file by file).
  • test:pglive on queen is crashed intermittently by Bun 1.3.6 (Segmentation fault, e.g. run 38087839830).

Action needed.

  1. Decide whether this may merge while Code Quality is red on queen. I do not merge over a red check whose cause is on the base.
  2. After spec(queen): claim.t27 - the round's stateOfDispatch as a card, the review valve imported (Closes #8765) t27#8768 is merged, either merge this or reply "merge" and I will.

Dmitrii Fedorov (@dmitrii-f-t27)

…ueen-claim-card

# Conflicts:
#	trios/agent-server/specs/PIN
@gHashTag
gHashTag merged commit 090bc8a into gHashTag:queen Oct 15, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants