diff --git a/.trinity/seals/queen_QueenDispatchExit.json b/.trinity/seals/queen_QueenDispatchExit.json index ccefd9787b..8181ce0e8a 100644 --- a/.trinity/seals/queen_QueenDispatchExit.json +++ b/.trinity/seals/queen_QueenDispatchExit.json @@ -1,24 +1,25 @@ { - "built_by": "t27c-bootstrap@0.5.2+0d573722c", + "built_by": "t27c-bootstrap@0.5.2+e1b4c32a0", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:9d5fb4742395ee3443463b27b5cbf9b4d4e24a4561fc4433210e3c225477b5eb", - "gen_hash_rust": "sha256:1171a040c6c748255961613d0a238c6debd70cee6b47d305d0eab69bbce30be9", - "gen_hash_verilog": "sha256:62fe95606912c285411e434f9b1d427c2f932738ba70059a96bef9057f12741c", - "gen_hash_zig": "sha256:a57022e1dd161143ef6cd379e303852fbda7f23ed292ad1680421a0a0ce65c6b", + "gen_hash_c": "sha256:2c3626742c7e3b35d1017fe866c795f2ad11ef98da00ef5acd0fcd56381adf74", + "gen_hash_rust": "sha256:283d83b27e34e082e497c000eb9b0df7a92beeb208dbf31e9e91514176f97095", + "gen_hash_verilog": "sha256:ad80632bfdf3d74db36766d51f961e34c388f0d4783e08c75e564782727c379b", + "gen_hash_zig": "sha256:d702797b3c2045ca3d0c178608bc6387d377bccbea53bf852746c00b5ed4e34a", "module": "QueenDispatchExit", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-09T21:26:10Z", + "sealed_at": "2026-10-17T16:11:16Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:ac1f481c62b16c3783ffeb83c682422a0c0fd0498169d8c101db160768913a3b", + "spec_hash": "sha256:2a1b5a1bb24298ee05319cd9458d0a31c4e596a4d87fe9e360fdc93dc9a56037", "spec_path": "specs/queen/dispatch_exit.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 10, - "total": 10 + "passed": 11, + "total": 11, + "vacuous": 0 }, "toolchain": { "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", diff --git a/docs/now/2026-10-17-dispatch-exit-provider.md b/docs/now/2026-10-17-dispatch-exit-provider.md new file mode 100644 index 0000000000..45214609ae --- /dev/null +++ b/docs/now/2026-10-17-dispatch-exit-provider.md @@ -0,0 +1,17 @@ +# NOW -- dispatch_exit.t27: a provider refusal never charges the issue (2026-10-17) + +## specs/queen/dispatch_exit.t27 (Closes #10008, part of #9976) + +- The bee dispatcher as keyed actors (gHashTag/trios `queen-bee-actors.ts`, `TRIOS_QUEEN_DISPATCH=actors`) asks this card whether an end counts against the issue. + - Until now `counts_against_issue(DX_PROVIDER_ERROR)` was true, so three quota refusals in a row (`RUN_LIMIT`) gave the issue up. + - A quota or an overload belongs to the key that served the turn. The next dispatch takes another key, and `key_failover.t27` `charges_issue` (#9993) charges only a bad request. The two cards now agree. +- Unchanged: a bad request (`HC_4XX`) still gives up at once, and a refusal that keeps repeating still ends at `STREAK_LIMIT` (6) with the backoff between tries. +- New test `a_provider_refusal_never_charges_the_issue`: 8 asserts, including that a timeout and an unknown end are still counted runs. +- Railway lab, master de2cff882: + - `t27c test-report`: 11 tests, 0 FAIL, 0 vacuous, 4 invariants. + - `tri mutate spec`: 81 of 84 mutants killed. + - The 3 survivors are equivalent mutants in `restart_wait_seconds`, which this change does not touch: lines 192, 198 and 202 flip `>=` and `>` at a clamp, and at the boundary both branches return the same value (`LEASE_TTL_SECONDS - age` is 0; `wait` equals `BACKOFF_CAP`). + - `dispatch_exit.wasm` (t27c gen-c + zig 0.16.0 cc wasm32-wasi -Oz -DNDEBUG reactor, every fn exported): + - The old card rebuilds to `31c0e524...`, the hash pinned in gHashTag/trios `specs/PIN`, so the recipe is the one in production. + - The new card builds to `b990a601e4c1806095688683e3c25f0b5176124d368dfd258fdcdd7934c56327`, the same hash on two builds. +- The TypeScript hosts read this card from the wasm, so trios takes the change by re-vendoring the `.t27`, the `.wasm` and the `specs/PIN` lines. There is no hand-written code. diff --git a/specs/queen/dispatch_exit.t27 b/specs/queen/dispatch_exit.t27 index 36cbd440f7..4f9fa86b1e 100644 --- a/specs/queen/dispatch_exit.t27 +++ b/specs/queen/dispatch_exit.t27 @@ -25,6 +25,9 @@ ; once its lease was taken from it. What is left -- a silent runtime that still holds its lease past ; one TTL without a heartbeat -- contradicts the lease rule itself (a lease expires one TTL after the ; last renewal), so UNKNOWN names an observation that is inconsistent, not a cause nobody looked for. +; #10008 (2026-10-17): a provider error no longer counts against the issue. A quota or an overload +; belongs to the key that served the turn, the next try takes another key, and key_failover.t27 +; charges_issue charges only a bad request; this card now agrees with it. ; Not here: the wiring. The host is TypeScript in gHashTag/BrowserOS (queen-dispatch.ts); the test ; `conformance_rows` is one assert per row, inputs to code, so the host can run the same rows. ASCII ; only (L3). @@ -145,11 +148,16 @@ pub const BACKOFF_CAP : u32 = 300; /// Whether this end is a counted run of the issue. An agent runtime is a transient child (actors.t27 /// queen_restart_type): normal and shutdown end it for good and count nothing, a cancel is an /// interruption (control.t27 cancel_counts_against_issue). A dropped connection is an interruption -/// too: the runtime was alive and the work did nothing wrong. Every other end is a failed run. +/// too: the runtime was alive and the work did nothing wrong. A provider error is the provider's or +/// the key's, not the issue's (#10008): the next try takes another key, and key_failover.t27 +/// charges_issue charges only a bad request. A bad request still gives up at once, by its own rule in +/// restart_decision, and a refusal that repeats still ends at STREAK_LIMIT. Every other end is a +/// failed run. pub fn counts_against_issue(reason: u8) -> bool { if (reason == DX_NORMAL) { return false; } if (reason == DX_SHUTDOWN) { return false; } if (reason == DX_NOCONNECTION) { return false; } + if (reason == DX_PROVIDER_ERROR) { return false; } return true; } @@ -267,7 +275,7 @@ test the_third_counted_run_gives_up_and_an_interruption_is_not_counted { assert counts_against_issue(DX_NOCONNECTION) == false; assert counts_against_issue(DX_KILLED) == true; assert counts_against_issue(DX_TIMEOUT) == true; - assert counts_against_issue(DX_PROVIDER_ERROR) == true; + assert counts_against_issue(DX_PROVIDER_ERROR) == false; assert counts_against_issue(DX_UNKNOWN) == true; assert restart_decision(DX_TIMEOUT, HC_NONE, 1, 1) == RD_LATER; assert restart_decision(DX_TIMEOUT, HC_NONE, 2, 2) == RD_GIVE_UP; @@ -279,6 +287,20 @@ test the_third_counted_run_gives_up_and_an_interruption_is_not_counted { assert restart_decision(DX_SHUTDOWN, HC_NONE, 9, 9) == RD_NONE; } +/// #10008: a quota, an overload or a 5xx is the key's, not the issue's. Past RUN_LIMIT counted runs +/// it still waits and tries again; a bad request still gives up at once; a refusal that repeats ends +/// at STREAK_LIMIT like any interruption. +test a_provider_refusal_never_charges_the_issue { + assert restart_decision(DX_PROVIDER_ERROR, HC_NONE, RUN_LIMIT - 1, RUN_LIMIT - 1) == RD_LATER; + assert restart_decision(DX_PROVIDER_ERROR, HC_429, RUN_LIMIT, RUN_LIMIT) == RD_LATER; + assert restart_decision(DX_PROVIDER_ERROR, HC_5XX, 9, STREAK_LIMIT - 1) == RD_LATER; + assert restart_decision(DX_PROVIDER_ERROR, HC_NONE, 0, STREAK_LIMIT) == RD_GIVE_UP; + assert restart_decision(DX_PROVIDER_ERROR, HC_4XX, 0, 0) == RD_GIVE_UP; + assert restart_decision(DX_PROVIDER_ERROR, HC_4XX, RUN_LIMIT, 1) == RD_GIVE_UP; + assert restart_decision(DX_TIMEOUT, HC_NONE, RUN_LIMIT - 1, RUN_LIMIT - 1) == RD_GIVE_UP; + assert restart_decision(DX_UNKNOWN, HC_429, RUN_LIMIT - 1, 0) == RD_GIVE_UP; +} + test an_interruption_that_repeats_also_ends { assert restart_decision(DX_NOCONNECTION, HC_NONE, 0, STREAK_LIMIT - 1) == RD_LATER; assert restart_decision(DX_NOCONNECTION, HC_NONE, 0, STREAK_LIMIT) == RD_GIVE_UP;