Repository navigation
Description
Summary
PlazaGuest.update() in the Worker isolate host (pkgs/id/worker-poc/src/guest.ts) converts the caller's participant: number with a top-end clamp only:
const clamped = Math.min(participant, 2147483647);
this.model = this.ex.plaza_update(this.model, clamped | 0, ptr, len);The native host it must evolve state identically to takes participant: u64 (pkgs/id/src/sandbox.rs:444) and clamps only the top end (i32::try_from(participant).unwrap_or(i32::MAX), sandbox.rs:452). Negative, fractional, and non-finite participants cannot exist on the native side — but on the isolate side they silently become values the native host would never deliver:
| participant | guest receives | effect (roc-chat) |
|---|---|---|
NaN |
0 (NaN | 0) |
routed as a host event — input not appended |
-Infinity |
0 |
routed as a host event — input not appended |
-1, -2147483648 |
the negative value itself | appended, as a participant class native can never produce |
1.5 |
1 (truncation by | 0) |
silently delivered as participant 1 |
Collapsing into participant 0 is exactly the failure the i32::MAX clamp fix (#2271, Codex P2 on PR #3726) was written to prevent for 4294967296 — smoke/participant-clamp.mjs pins "clamped participant must not collapse into participant 0" — but the same collapse is still reachable via NaN and -Infinity, and the negative/fractional classes diverge from native state evolution in the other direction.
The Worker's /step route validates its input (worker.ts: non-negative integer required), but PlazaGuest is the host API itself — loadHostedGuest / loadHostedModule expose it, the compare harness (compare/isolate-runner.mjs) and smoke scripts drive it directly, and any future caller inherits the silent coercion.
Evidence (verified this run)
Laptop probe ~/git/fleet/probe-2026-11-01-laneA-guestclamp/ (probe.mjs, driving the real guest.ts/manifest.ts — md5 410a375a/19b5e7b0 matching main 070f0d99 — with the checked-in examples/roc-chat/roc-chat.wasm, md5 829ff79c), one update(p, "x") per fresh guest:
participant=0 -> view="chat seen=0 last= input=" (host-event routing)
participant=1 -> view="chat seen=0 last= input=x"
participant=-1 -> view="chat seen=0 last= input=x" (native-impossible value delivered)
participant=-2147483648-> view="chat seen=0 last= input=x" (native-impossible value delivered)
participant=NaN -> view="chat seen=0 last= input=" (collapses to participant 0)
participant=-Infinity -> view="chat seen=0 last= input=" (collapses to participant 0)
participant=Infinity -> view="chat seen=0 last= input=x" (clamps to i32::MAX, correct)
participant=1.5 -> view="chat seen=0 last= input=x" (silently truncated to 1)
participant=4294967296 -> view="chat seen=0 last= input=x" (clamps to i32::MAX, correct)
Why it matters
The worker-poc's whole contract is journal/state equality with the native host (#2271). A participant value that is a host event on one host and an appended message on the other — or a participant class that exists on only one host — makes the two hosts evolve different state from the same call sequence, with no error anywhere. The values are not exotic: NaN is what JSON-adjacent JS produces from a missing/garbled numeric field, and -Infinity/NaN both arise from ordinary arithmetic on unvalidated inputs.
Acceptance criteria
PlazaGuest.updaterejects (throws, naming the value class) any participant that is not a non-negative integer: negative, fractional,NaN, and both infinities other than the top-end clamp behaviour already pinned — or, if a coercion is deliberately chosen for some class, it is documented in guest.ts and pinned by a test;NaNand-Infinitymust never reach the guest as participant 0.- Participants in
[0, i32::MAX]pass through unchanged and values abovei32::MAXkeep the existing clamp (regression:smoke/participant-clamp.mjsstill passes). - A smoke/regression script (extending
smoke/participant-clamp.mjsor a sibling) covers-1,1.5,NaN,-Infinityagainst roc-chat and asserts the chosen behaviour. - The native/isolate comparison harness (
compare/) still produces identical lines for its scripted sequences.
Dedup: open+closed search for PlazaGuest/participant/worker-poc validation found only #2271 (closed; the clamp fix itself) — this is its residual input class, not a duplicate. The Worker's /step route validation in worker.ts is a different layer and does not cover direct PlazaGuest callers.
Activity
dezren39 commentedon Nov 1, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up
PlazaGuest.updatecoerces NaN/-Infinity to participant 0 and passes negative/fractional participants, then routing the fix packet.pkgs/id/worker-poc/src/guest.ts14 remaining items
dezren39 commentedon Nov 3, 2026
🤖 Agent note · launched (opencode-claimant)
id(worker-poc): PlazaGuest.update rejects non-u64-representable parti…
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done
queue/queuedwith no live taskTriage outcome: the fix had already been built — this shift reviewed and landed it. Issue #4074 is CLOSED.
Fresh state found this shift:
queue/6284-4074-plaza-guest-validation— then task 6284 was marked done with the PR still OPEN and this issue's labels left atqueue/queued(the desync this task was filed for).clean, head checks pass/neutral, diff is exactly the validation change plus its smoke regression; Codex review failed to run (usage limits, zero findings) — disposition recorded on PR id(worker-poc): PlazaGuest.update rejects non-u64-representable participants (fix #4074) #4796.2c81856381398170d59bda275caeb3e63e4f5f8e); issue id(worker-poc): PlazaGuest.update collapses NaN and -Infinity to participant 0 (host-event routing) and passes negative/fractional participants the native u64 host can never produce #4074 auto-closed by the PR'sFixes #4074. Claim labels stripped; thequeue/queueddesync is gone.