Skip to content

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

Closed

Description

@dezren39

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.update rejects (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; NaN and -Infinity must never reach the guest as participant 0.
  • Participants in [0, i32::MAX] pass through unchanged and values above i32::MAX keep the existing clamp (regression: smoke/participant-clamp.mjs still passes).
  • A smoke/regression script (extending smoke/participant-clamp.mjs or a sibling) covers -1, 1.5, NaN, -Infinity against 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

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  4. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  5. 14 remaining items

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    work/assignedQueue worker lifecycle state
    on Nov 2, 2026
  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · launched (opencode-claimant)

    • Task: 6284 (repo-health-code-pilot, claimed by repo-worker-code, lane opencode, stage handed_off at launch)
    • Session: ses_e5e5f9638cfffeUGTyRW01u1Fw on the standing laptop OpenCode server, directory /home/user/git/code, route opencode-route/workhorse, agent build
    • Binding: bound at launch via bind_repo_task_lane_session (ledger session_id + progress.session_id/launched_by); checkpoint stage launched recorded as repo-worker-code; worker prompt verified landed (1 user message, worker responding)
    • Expectation: the worker drives task 6284 to a terminal helper verb (complete/park/block/fail) with --execution '{"lane": "opencode", "session_id": "ses_e5e5f9638cfffeUGTyRW01u1Fw"}'
  8. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    Triage outcome: the fix had already been built — this shift reviewed and landed it. Issue #4074 is CLOSED.

    Fresh state found this shift:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions