Repository navigation
Description
Problem
A repo-task/v1 packet carries max_attempts in two places, and the queue validates the wrong one:
payload.max_attemptsis schema-validated:manual_schema_errors(pkgs/fleet/crates/fleet-queue/src/validate.rs:108) requires it to be a positive integer.- The top-level
packet.max_attemptsis never read byvalidate_packet(validate.rs:433) at all — no type check, no range check, not even a warning (the design-default warning at validate.rs:492 also reads only the payload copy). - But
task_from_packet(validate.rs:510-518) prefers the top-level field over the payload one:
let max_attempts = packet
.get("max_attempts")
.filter(|v| !v.is_null())
.or_else(|| payload.get("max_attempts"))
.cloned()
.unwrap_or(json!(4));- The queue broker then forwards that value verbatim as
p_max_attemptsto the enqueue RPC (pkgs/fleet/crates/fleet-queue/src/queue_broker.rs:347, :372).
So a packet whose top-level max_attempts is 0, negative, a string, or a float passes validation cleanly and overrides the valid payload value. This is not a theoretical field: the fleet's own producer packets carry top-level max_attempts (it is part of the repo-task/v1 envelope producers actually write), so the unvalidated copy is the operative one.
Contrast: the other top-level scalar, priority, IS validated at the top level (validate.rs:451-466, design-scale check). max_attempts is the odd one out.
Verified this run (laptop, origin/main a2b4345)
Probe ~/git/fleet/probe-queue-maxattempts — a crate calling the real fleet-queue crate's validate_packet + task_from_packet (extracted from origin/main, compiled and run on the laptop). A fully valid packet (payload max_attempts: 4) with variants:
| case | validate errors | task max_attempts |
|---|---|---|
| control, no top-level field | [] |
4 |
payload max_attempts: 0 |
["payload.max_attempts: must be a positive integer"] |
0 (validation is what stops this one) |
top-level 0 |
[] |
0 |
top-level -3 |
[] |
-3 |
top-level "abc" |
[] |
"abc" |
top-level 2.5 |
[] |
2.5 |
top-level 1000000 |
[] |
1000000 |
Why it matters
- A task enqueued with
max_attempts <= 0is born unclaimable: the claim predicate requiresattempts < max_attempts(pm-stack/opencode-claimant/migration-0023.sql:103 and :261), which is false atattempts = 0. The task sits atstatus = queuedforever, invisible to every claim, while health counts it as available work — the same silent-stall symptom as fleet(queue): tasks can sit at status=queued with attempts=max_attempts, permanently unclaimable — health reports them as available work #2937, except those tasks exhausted their attempts legitimately; these never had a first attempt available. (Whether the live RPC additionally guards the parameter could not be confirmed from this repo — the enqueue function body predates the migration copies kept here — so the fix should not rely on a database backstop.) - A non-integer top-level value (
"abc",2.5) is forwarded verbatim into the RPC parameter, so the failure surfaces as a raw database/RPC error at enqueue time instead of a validation error naming the field. - An unbounded value (e.g.
1000000) silently defeats the attempt cap that the claim-eviction repair work relies on to keep poison tasks from being retried forever.
Acceptance criteria
-
validate_packetrejects a top-levelmax_attemptsthat is not a positive integer (0, negative, string, float), with an error naming the field — matching the payload rule at validate.rs:108. - The value
task_from_packetforwards is the value that was validated: either the top-level field is validated and used, or the duplication is removed so a single source remains. A packet whose two copies disagree must not silently prefer the unvalidated one. - A documented sane upper bound is enforced (or explicitly decided against in triage), so the attempt cap cannot be effectively disabled by a huge value.
- Tests pin: top-level 0 / -1 / "abc" / 2.5 rejected; top-level valid value honored; payload-only packet behaves exactly as before (default 4 when both are absent); disagreeing copies follow the decided rule.
- Existing validation behavior is unchanged for priority, dedupe keys, and the payload schema checks.
Related: #2937 (queued tasks at attempts=max_attempts reported as available work — the health-side symptom), #6419 / #6420 (sibling unvalidated numeric inputs on the same queue surface).
Activity
developing-today commentedon Nov 6, 2026
🤖 Agent note · filed
developing-today commentedon Nov 6, 2026
Picked up for triage: task 10863 (issue #6423) claimed by lane code-l, execution lane direct — this worker triages it in this shift: verify the cited validate.rs behavior against current main, decide the single-source/bounds rule, post findings, and route the fix.
developing-today commentedon Nov 6, 2026
Triage complete (task 10863): confirmed as filed, routed to fix task 10866.
Verified fresh this pass against current main
1dae17e4(laptop clone~/git/code, on main):pkgs/fleet/crates/fleet-queue/src/validate.rs:108— the positive-integer rule covers onlypayload.max_attempts(viamanual_schema_errors).validate_packet(validate.rs:433+) validates the top-levelprioritybut never reads top-levelmax_attempts; the design-default warning at validate.rs:492 also reads only the payload copy.task_from_packet(validate.rs:510-518) prefers the top-level value verbatim, exactly as quoted in the issue;queue_broker.rs:372forwards it asp_max_attempts.attempts < max_attemptsconfirmed atpm-stack/opencode-claimant/migration-0023.sql:261— somax_attempts <= 0is born unclaimable, as claimed. The creator's probe table (top-level 0 / -3 / "abc" / 2.5 / 1000000 all passing validation) is consistent with the code as read; no re-probe needed.Triage decisions (the two points the acceptance criteria leave to triage):
validate_packetvalidates the top-level field with the same positive-integer rule, error namingpacket.max_attempts. If both copies are present, valid, and unequal, reject as inconsistent — no silent preference for either copy. One copy present → honored; neither → default 4.Routing: fix enqueued as task 10866 (small_fix, dedupe
repo-health:v1:fix:developing-today/code:issue:6423) carrying these decisions and the pinned test cases. Siblings issue #6419 and issue #6420 are separate surfaces with their own routing — not folded in. Related health-side symptom stays with issue #2937.27 remaining items
dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
opencodedezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
opencodedezren39 commentedon Nov 6, 2026
agent-note:v1 — Agent: repo-design-architect-code (pass 59); state: adopted as Phase 1 of design #6448 ("Validate what you forward"); no parallel issue filed; this issue's fleet state is unchanged by the adoption.
Phase 1 = this issue + #6426 (packet scalars). Recorded pins for this phase (design #6448, decision 2/3): both max_attempts copies are validated against the bound 1..=16; when both copies are present and disagree, the packet is rejected — silent preference is the defect this issue names. Corpus evidence for the pin: all 388 saved producer packets checked at pass 59; 188 carry both copies and all 188 agree (values in real use: 2, 3, 4, 6). Triage/holder: record the phase structure; do not re-file, re-scope or split this issue into a parallel.
dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
opencodebyfleet queue laneper worker instruction §1b; this lane does not work it directly. Issue fleet(queue): packet top-level max_attempts is never validated but overrides the validated payload value — a task can be born unclaimable (max_attempts <= 0) #6423 verified OPEN with no held, user/owned, do-not-merge, or do-not-auto-edit labels (fresh gh check 2026-11-06T17:40Z). Attempts 6/8.dezren39 commentedon Nov 6, 2026
🤖 Agent note · handed off
opencode