Skip to content

feat(input): route ROLI touch and button input from blocksd - #323

Merged
hyperb1iss merged 10 commits into
mainfrom
nova/device-input
Oct 9, 2026
Merged

hyperb1iss merged 10 commits into
mainfrom
nova/device-input

Conversation

@hyperb1iss

@hyperb1iss hyperb1iss commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

What this changes

Touching a ROLI Lightpad or pressing a LUMI key now reaches Hypercolor's interaction pipeline, attributed to the device that was touched. Touches and device buttons arrive in FrameInputs.interaction with live positions and as routed InputEvents on the bus, beside host keyboard and mouse input. Effects cannot read them yet; that is the next phase.

Layer Change
hypercolor-types DeviceInputEdge (touch began, moved, ended, named button) for drivers, plus routed InputEvent::Touch and InputEvent::DeviceButton carrying the DeviceId. Touch positions are Q16.16 so InputEvent keeps deriving Eq.
hypercolor-driver-api DriverHost::device_input() returns a DeviceInputSink. attach() hands a driver a per-device publisher lease; hosts without device input return None.
hypercolor-core::input::device DeviceInputHandle, a registry modeled on browser preview input: one routable child per attached device, outside InputManager and outside the host capture consent gate.
hypercolor-core::input::routing A Device source class, selected under the Host and Merge policies and never under Browser, with touches and device buttons as held controls keyed by DeviceId.
hypercolor-daemon The driver host shares the registry that interaction routing owns, and the input pump mirrors interaction demand into it.
Blocks driver A second, subscribe-only blocksd connection turns touch and button events into edges for each adopted, connected device.

Touch movement is held state, like pointer motion: only begin, end, and cancel cross the event ring and the bus, while InteractionData.device.touches carries the live position every frame. Previews default to the Browser policy, so they stay isolated from device input just as they are from host keyboard input.

Why

The Blocks driver only lit devices. It never subscribed to blocksd's event stream, and no driver had any way to publish input, so hardware that Hypercolor lights could not also be played. The seam is driver neutral, so Push 2 pads or any other controller Hypercolor drives can publish the same way later.

The design is spec 82. The blocksd side is blocksd #14, which adds button identity and corrects touch velocity; this PR also works against older blocksd releases, where every button on a block shares one name.

Verification

  • Added or updated tests
  • Added or updated docs (README, AGENTS.md, relevant spec, or guide)
  • just verify passes locally (Rust fmt + lint + test)
  • just deny passes (required for dependency or license changes)
  • just ui-test and just ui-build pass (required for crates/hypercolor-ui/)
  • just sdk-lint, just sdk-check, and just sdk-build pass (required for sdk/)
  • just python-verify passes (required for python/)
  • just compat-check passes (required for data/drivers/vendors/*.toml)
  • just docs-build passes (required for docs or README changes)
  • cd docs && zola check passes (required for docs link/content changes)
  • Packaging scripts were syntax-checked (required for scripts/ or packaging/)
  • just e2e-build passes with the normal Servo stack (required for daemon/UI/effect integration changes)
  • just e2e-build-cpu passes when validating the CPU smoke fallback
  • just e2e passes against the Servo stack (required for end-to-end behavior changes; starts daemon/browser)
  • Tested on real hardware, simulator, or e2e harness (describe below)

just verify is unchecked because its workspace test step was run in parts. On this workstation the wgpu tests stall when they share a parallel run with the rest of the daemon suite, and each passes alone; the same stall reproduces on the base commit. Every other verify component that this change touches passed on the final head 40b02fc96, after rebasing onto current main:

  • just oss-boundary-check-strict, just api-doc-route-check, and just macos-gpu-only-check
  • cargo fmt --check, and cargo clippy --workspace --all-targets --features hypercolor-daemon/persistence-test-hooks -- -D warnings
  • hypercolor-core: device_input_tests (20, including end-to-end routing), routing_tests (16), blocks_input_tests (5, stable over 12 repeated runs), blocks_backend_tests (6), input_tests (114), the library tests (353), and the allocation contract, which now holds a device touch and button through every measured frame
  • hypercolor-types, hypercolor-driver-api, and hypercolor-driver-builtin tests
  • hypercolor-daemon library: all 1,091 tests, as 855 in parallel and the 236 wgpu tests serially

No dependencies, UI, SDK, packaging, or published docs changed; spec 82 lives under docs/specs, outside the Zola site. The e2e paths are left to CI.

An independent verifier reviewed the branch and passed it; its findings shaped the last round of fixes, each with a regression test. It compiled every intermediate commit of the pre-rebase branch with zero warnings, and ran adversarial routing scenarios with no stuck, doubled, or permanently quarantined controls: ring overflow mid-touch, supersede mid-touch, Host to Browser to Host policy flips, and a second consumer attaching mid-touch. A stress run of three injecting threads against demand toggling every 300 µs and supersedes every 3 ms routed about 665k edges with no malformed edge streams or deadlocks. It also confirmed the excluded hypercolor-ui crate still builds for wasm, and that no SDK, TUI, or platform crate matches exhaustively on InputEvent. The blocksd stream is covered by a fake blocksd harness.

Not yet tested on hardware: nothing here has run against a physical Lightpad or LUMI. The next step is touching both with an interactive effect active and watching input_events on the WebSocket.

Notes for reviewers

The router never sees a hold or release it did not observe being pressed. The router clears a quarantined control only on a release edge, so a producer that silently drops holds leaves quarantine that swallows the next tap. Each device child therefore tracks what is physically held but only announces a press while something demands interaction. Losing demand cancels every announced hold with explicit Cancelled and Released edges, and a press that began while undemanded stays invisible until it lifts.

A stale lease can never retire its successor. Attaching a device again supersedes the earlier lease with an incarnation-fenced close; the old lease's publish returns false and its drop does nothing.

Device input never stands in for host input. An attached Lightpad does not make a keyboard effect report input as available when host capture is off.

Demand, not consent, gates device input. It is not keystroke capture, so [input].enabled does not gate it. The input pump mirrors aggregate interaction demand (an interactive effect, a preview, or an input_events WebSocket subscriber) at the top of every iteration, independently of the host capture reconcile, and clears it when the pump stops. While nothing demands interaction, no device traffic reaches the bus.

blocksd events use their own connection. The frame connection pairs each request with the next line or byte it reads, so an interleaved event would be misread as a frame acknowledgement. Anything that could hide a lift cancels held input: device_removed for that device, and an undecodable line or a lost connection for every device. device_added payloads are ignored, so schema drift there cancels nothing.

Start with crates/hypercolor-core/src/input/device.rs (EdgeFold and sync_demand), then the device arms in routing.rs (event_control, reconcile_snapshots, build_interaction_into). The lock order is status control, registry writer, then child state.

Known limits: a touch whose end blocksd never sends stays held until its contact index is reused or the device detaches, and how LUMI touch x maps onto the 24 key LEDs still needs a hardware capture. Phase 2 exposes touches to effects with canvas coordinates and ships a touch-ripple effect.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for touch and button input from compatible devices, including ROLI Blocks.
    • Device input can now be included in Host and Merge interaction routing.
    • Held touches and buttons are cancelled when input capture ends or a device disconnects.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ee4aa6de-8541-4431-8790-947a87c277d5

📥 Commits

Reviewing files that changed from the base of the PR and between 274af46 and 4c4b15e.


📒 Files selected for processing (3)
  • crates/hypercolor-daemon/src/render_thread.rs
  • crates/hypercolor-daemon/src/render_thread/pipeline_runtime.rs
  • crates/hypercolor-daemon/src/startup/services.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 80413961-1b99-439c-9789-7db1fb51f8c6

📥 Commits

Reviewing files that changed from the base of the PR and between 478f4a6 and 274af46.


📒 Files selected for processing (36)
  • crates/hypercolor-core/src/device/blocks/backend.rs
  • crates/hypercolor-core/src/device/blocks/input.rs
  • crates/hypercolor-core/src/device/blocks/input/tests.rs
  • crates/hypercolor-core/src/device/blocks/mod.rs
  • crates/hypercolor-core/src/device/blocks/types.rs
  • crates/hypercolor-core/src/effect/lightscript/payload.rs
  • crates/hypercolor-core/src/effect/servo/renderer/frame_queue.rs
  • crates/hypercolor-core/src/input/device.rs
  • crates/hypercolor-core/src/input/mod.rs
  • crates/hypercolor-core/src/input/routing.rs
  • crates/hypercolor-core/src/input/traits.rs
  • crates/hypercolor-core/tests/alloc_contract_tests.rs
  • crates/hypercolor-core/tests/blocks_input_tests.rs
  • crates/hypercolor-core/tests/device_input_tests.rs
  • crates/hypercolor-core/tests/routing_tests.rs
  • crates/hypercolor-daemon/src/app_state.rs
  • crates/hypercolor-daemon/src/domain/input_status.rs
  • crates/hypercolor-daemon/src/interaction_routing.rs
  • crates/hypercolor-daemon/src/interaction_routing/tests.rs
  • crates/hypercolor-daemon/src/interactive_preview.rs
  • crates/hypercolor-daemon/src/network/host.rs
  • crates/hypercolor-daemon/src/render_thread.rs
  • crates/hypercolor-daemon/src/render_thread/input_publication.rs
  • crates/hypercolor-daemon/src/render_thread/input_publication/tests.rs
  • crates/hypercolor-daemon/src/render_thread/pipeline_runtime.rs
  • crates/hypercolor-daemon/src/startup/services.rs
  • crates/hypercolor-driver-api/src/host.rs
  • crates/hypercolor-driver-api/src/input.rs
  • crates/hypercolor-driver-api/src/lib.rs
  • crates/hypercolor-driver-api/tests/types_tests.rs
  • crates/hypercolor-driver-builtin/src/transport.rs
  • crates/hypercolor-types/src/device_input.rs
  • crates/hypercolor-types/src/event.rs
  • crates/hypercolor-types/src/lib.rs
  • crates/hypercolor-types/tests/event_tests.rs
  • docs/specs/82-device-input.md

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.



📝 Walkthrough

Walkthrough

This change adds driver-fed device touch and button input. It defines input events and per-device leases, tracks and routes device state, connects daemon demand to device input, and adds a ROLI Blocks event-stream producer.

Changes

Device Input Pipeline

Layer / File(s) Summary
Device input contracts
crates/hypercolor-types/src/device_input.rs, crates/hypercolor-types/src/event.rs, crates/hypercolor-driver-api/src/input.rs, crates/hypercolor-core/src/input/traits.rs, crates/hypercolor-core/src/input/mod.rs, crates/hypercolor-driver-api/src/host.rs, crates/hypercolor-driver-api/tests/types_tests.rs, crates/hypercolor-types/tests/event_tests.rs, crates/hypercolor-core/src/effect/..., docs/specs/82-device-input.md
Adds touch and button edge types, device-attributed input events, driver sink and publisher traits, and held device interaction data. Event serialization tests cover device attribution and touch phases.
Device input registry
crates/hypercolor-core/src/input/device.rs, crates/hypercolor-core/tests/device_input_tests.rs
Adds per-device leases, touch and button state tracking, demand-gated announcements, input validation and limits, and cancellation of announced holds when demand ends. Tests cover attachment, edge handling, demand changes, status, and state merging.
Device interaction routing
crates/hypercolor-core/src/input/routing.rs, crates/hypercolor-core/tests/{device_input_tests.rs,routing_tests.rs,alloc_contract_tests.rs}, crates/hypercolor-daemon/src/interaction_routing..., crates/hypercolor-daemon/src/render_thread/pipeline_runtime.rs, crates/hypercolor-daemon/src/interactive_preview.rs, crates/hypercolor-daemon/src/domain/input_status.rs, docs/specs/82-device-input.md
Adds device sources to route catalogs and selects them under Host and Merge policies. Routed state includes observed device holds and handles quarantine and synthesized releases. Daemon availability excludes device sources from host-only status.
Daemon demand and host wiring
crates/hypercolor-daemon/src/app_state.rs, crates/hypercolor-daemon/src/startup/services.rs, crates/hypercolor-daemon/src/network/host.rs, crates/hypercolor-daemon/src/render_thread.rs, crates/hypercolor-daemon/src/render_thread/input_publication.rs, crates/hypercolor-daemon/src/render_thread/input_publication/tests.rs, docs/specs/82-device-input.md
Passes the device input handle to driver hosts and the input publication pump. The pump mirrors interaction demand to device input and clears demand when it exits or is aborted.
ROLI Blocks event producer
crates/hypercolor-core/src/device/blocks/*, crates/hypercolor-core/tests/blocks_input_tests.rs, crates/hypercolor-driver-builtin/src/transport.rs, docs/specs/82-device-input.md
Adds Blocks event types and a dedicated event stream. Connected devices receive input leases; touch and button events publish through those leases. Stream loss, malformed or oversized lines, and device removal trigger lease handling and reconnection behavior.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant BlocksBackend
  participant BlocksInput
  participant blocksdEventSocket
  participant DeviceInputPublisher
  BlocksBackend->>BlocksInput: notify device connection
  BlocksInput->>DeviceInputPublisher: attach device lease
  BlocksInput->>blocksdEventSocket: subscribe to device, touch, and button events
  blocksdEventSocket->>BlocksInput: deliver event lines
  BlocksInput->>DeviceInputPublisher: publish converted input edges
Loading

Merge Risk

Merge Risk: ⚪ Minimal · up to 274af

This change routes ROLI touch and button input through demand-gated per-device leases. The supplied changes and tests show no established defect, so the change appears ready to merge. Behavior on physical Lightpad and LUMI hardware has not yet been tested.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 274af

Device input has demand, isolation, and cleanup controls, and no exploitable attack path was established. However, input-producer trust and production socket permissions are not fully demonstrated.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A party controlling the connected blocksd event peer could author input for all currently adopted UIDs handled by that producer, not merely one touched device. Resulting events can reach Host/Merge interaction consumers and the authoritative event bus. Control of that peer by an untrusted actor, remote exposure, and privilege escalation through downstream consumers were not established.

Trust Boundaries and Controls

  • observed — The subscription client connects by filesystem path without an application-level peer-identity check. This endpoint trust predates the PR for discovery and output traffic; the added event subscription expands its authority into interaction publication. UID filtering limits attribution but does not authenticate the socket peer.
  • inferred — The inspected daemon host is an adapter for built-in drivers and already exposes credential and runtime services. The shared sink should therefore be treated as a trusted-producer capability, not a demonstrated sandbox boundary. No inspected path establishes access by an untrusted plugin or remote caller.

Resilience and Maintainability Implications

  • observed — The event reader bounds each buffered line to 64 KiB, and oversized or undecodable input triggers lease replacement. Edge folding limits simultaneous contacts and held buttons, ignores repeated button states, and preserves releases even when the final touch position is invalid. These controls contain malformed-input state and memory effects without proving producer trust.

Hardening Proposals

  • proposed — Establish and document socket ownership and permission guarantees for supported deployments, especially the /tmp fallback. If those guarantees permit untrusted peer substitution, add an appropriate endpoint or peer-identity check before accepting interaction events.
  • proposed — Document that the shared sink requires trusted producers. Before exposing it across an untrusted driver boundary, bind attachment authority to host-verified device ownership rather than relying on caller-supplied DeviceIds.



🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 54.40% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 250 functions across 35 files. (1 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely identifies the main change: routing ROLI touch and button input from blocksd.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

Full details: Docstring Coverage

Explanation

Docstring coverage is 54.40% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 250 functions across 35 files. (1 skipped: 1 unsupported.)



  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR


  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

hyperb1iss and others added 9 commits October 8, 2026 19:17
Hardware that Hypercolor lights can also be played, but nothing in the
input pipeline could say which device a touch or button came from. The
shared vocabulary now has a driver-facing DeviceInputEdge (touch began,
moved, ended, and named buttons, in normalized device coordinates) and
two routed InputEvent variants, Touch and DeviceButton, that carry the
DeviceId. Touch positions use Q16.16 like PointerScroll so InputEvent
keeps deriving Eq, and only begin, end, and cancel are events; movement
will be held state, as pointer motion already is.

Drivers reach the pipeline through a new DriverHost::device_input()
capability returning a DeviceInputSink. attach() hands back a per-device
publisher lease. Hosts without device input return None, so existing
drivers and test hosts are unaffected.

Until device sources exist, effect payloads drop the new events the way
they drop MIDI, and synthetic releases cancel touches and release
device buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Driver-owned devices need a place to publish input that is outside the
InputManager (which allows one host interaction source) and outside the
host capture consent gate. DeviceInputHandle follows the browser preview
registry: one routable child publication per attached DeviceId, so a
device's lifetime is one source incarnation and a busy surface cannot
evict another device's edges from a shared ring.

Each child always tracks what is physically held but only announces a
press while some consumer demands interaction. Announced touches publish
begin and end edges, with movement folded into held state on the new
InteractionData.device field. Losing demand cancels every announced hold
with explicit Cancelled and Released edges, and a press that began while
undemanded stays invisible until it lifts. The router therefore never
sees a hold or release it did not observe being pressed, which would
otherwise leave stale quarantine entries that swallow a later tap.

Attaching a device again supersedes the earlier lease with an
incarnation-fenced close, so a stale lease can never retire its
successor, and DeviceInputPublisher::publish now reports supersession.
Positions are clamped, a non-finite begin is dropped and counted, and an
end is never dropped: a bad final position falls back to the last good
one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Device registry children now join the interaction route catalog as a
Device source class in their own incarnation namespace, listed after
host and browser sources. Device sources are selected under the Host
and Merge policies and never under Browser, so physical device input
behaves like physical host input: the daemon consumer sees it by
default and previews stay isolated unless a user chooses Merge.

Touches and device buttons become held controls keyed by DeviceId, so
contacts on different surfaces never alias. event_control maps touch
begin to pressed and end or cancel to released with explicit arms in
place of the old fall-through, and snapshot holds, quarantine, and held
output extend to both. Held positions always come from the latest
snapshot. Rebuilding device held output stays allocation-free: touch
lookups use a Copy key, button lookups scan, and output vectors are
refilled in place. The allocation contract test now holds a device
touch and button through every measured frame.

Input availability counts only host sources, so an attached Lightpad
cannot make a keyboard effect report input as available when host
capture is off. The status API likewise no longer counts the device
backend as host capture. The daemon's routing control owns the device
registry and passes its snapshot to every catalog refresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DaemonDriverHost now answers DriverHost::device_input() with the device
registry owned by the interaction routing control, so drivers built at
startup, reconcile, or rebind publish into the same registry the render
thread routes.

The input publication pump mirrors the interaction bit of its aggregate
capture demand into that registry at the top of every iteration, ahead
of the screen transition and capture reconcile steps that can retry on
Busy or Stale, and clears it when the pump stops. The demand is the
union of the authoritative render, preview, and input_events WebSocket
consumers. Device input therefore flows exactly while something wants
interaction, whether or not host keyboard capture is enabled, and
nothing reaches the bus otherwise.

InputPublicationPump::start takes the optional device handle directly;
tests pass None.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Blocks backend now subscribes to blocksd's device, touch, and button
events on a second connection that only writes its subscribe request.
Events never share the frame connection, which pairs each request with
the next line or byte it reads and would misread an interleaved event
as a frame acknowledgement.

The builtin factory hands the host's device input sink to the backend.
connect() attaches a publisher lease under the host's canonical
DeviceId for the adopted uid and disconnect() drops it, so blocks the
host never adopted are ignored and no uid is re-minted into an id. That
replaces the unread uid_map. blocksd touch start, move, and end become
device touch edges with the touch index as the contact, and buttons use
blocksd's SDK function name, then the protocol index.

Anything that could hide a lift supersedes leases so held input
cancels: device_removed for that device, and an undecodable line or a
lost connection for every device. device_added payloads are ignored,
so schema drift there cancels nothing. After a lost connection the
stream resubscribes with backoff from 250 ms to 5 s without cancelling
again while blocksd stays unreachable. The stream runs only while a
device is connected, the last disconnect closes it, and dropping the
backend aborts it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the device input design as built: the driver-api lease seam, the
demand-gated registry that only announces presses it can end, Device
route selection under Host and Merge, allocation-free held output, and
the blocksd event stream. It also names the known limits and the open
LUMI keybed mapping question, and lays out phase 2 (effects) and phase 3
(more producers).

A fresh-context design review shaped the lease fencing, the announced
hold model, and resolving Blocks devices from the adopted set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pump cleared device input demand only after its worker loop exited
normally. When shutdown passed its one second deadline and aborted the
supervisor, or the worker panicked, that reset never ran and device
input stayed demanded with no pump left to follow it, contrary to spec
82 section 6.

A DeviceInputDemand guard now owns the handle for the worker's
lifetime and withdraws demand on drop, so a clean exit, an abort, and
an unwinding panic all clear it. The clean exit still drops it before
releasing host capture, preserving the previous ordering. A new test
spawns the worker directly, aborts it while interaction is demanded,
and checks demand is withdrawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aborting the Blocks event stream only takes effect at the task's next
await. A stream that had already decoded an event, or was about to
supersede leases after losing its connection, could therefore run on
after a fast disconnect and reconnect, find the reconnected uid in the
shared state, and publish into (or reattach) the new stream's lease.
The host's lease supersession cannot catch this, because the stale
task reaches the new lease through the map rather than holding an old
one.

Each stream now carries a generation that every start and abort bumps
under the input state lock, and every state access the stream makes
goes through a check of that generation under the same lock. A
superseded stream drops what it decoded and exits. Dropping the
backend bumps the generation as well, so a stream that outlives its
backend cannot supersede a lease a replacement backend attached for
the same device.

Unit tests hold an aborted stream across a disconnect and reconnect,
and across a backend drop, and confirm it publishes and reattaches
nothing while a replacement stream still delivers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Blocks event reader framed lines with read_line, so a peer that
sent bytes without a newline grew the line buffer without limit.

Lines are now framed by hand over the buffered reader and capped at
64 KiB. The largest event on the subscribed categories is device_added
at a few hundred bytes, so the cap leaves two orders of magnitude for
payload growth. A longer line is discarded through its newline without
being buffered, warned about once per connection, and treated like an
undecodable line: every lease is superseded, since the skipped line
could have been a touch end, and reading continues on the next line.
Lines are now decoded from bytes, so invalid UTF-8 also counts as an
undecodable line instead of failing the read and reconnecting.

A new fake-blocksd test sends a valid touch padded to twice the cap
followed by a normal touch, and checks that the padded one is dropped,
leases are superseded, and the next line still decodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hyperb1iss

Copy link
Copy Markdown
Owner Author

Rebased onto main (no conflicts, the six original commits replay unchanged) and pushed three fixes from an independent Codex review. Each new test fails with its fix reverted.

  • Device demand clears on every pump exit (14907e6). The reset to "not demanded" used to run only after a clean worker exit, so a pump aborted at the shutdown timeout, or one that panicked, left the flag set, against spec 82 §6. A DeviceInputDemand drop guard now owns it inside run_pump. New test: aborted_pump_worker_withdraws_device_input_demand.
  • An aborted Blocks stream can't publish into its replacement (f38b445). abort() only lands at the next .await, so after a fast disconnect and reconnect, a stream that had already decoded an event could publish into the new lease for the same uid. Each stream now carries a generation, checked under the state lock before it publishes. A second verifier found the same race on the backend-drop path, and that's covered too. New tests: an_aborted_stream_cannot_publish_into_a_reconnected_device and a_dropped_backend_retires_its_stream.
  • blocksd event lines are capped at 64 KiB (274af46). The largest event we subscribe to is a few hundred bytes. An oversized line is discarded through its newline, logged once per connection, and the stream stays framed. One side effect: invalid UTF-8 is now skipped instead of forcing a reconnect. New test: an_oversized_event_line_is_skipped_and_the_stream_stays_framed.

Spec 82 §6, §7, and Testing match the new behavior. Local gates on 274af46 all pass: fmt, cargo check and clippy -D warnings across the workspace, types (640), driver-api (25), core (1792), the daemon serialized (2063), and the three alloc-contract runs.

~ via nova ⚡

Main now starts the input publication pump inside a startup progress
step, so the device input routing argument this branch adds moves
into that step's closure. No other behavior changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hyperb1iss
hyperb1iss merged commit ced501c into main Oct 9, 2026
8 of 9 checks passed
@hyperb1iss
hyperb1iss deleted the nova/device-input branch October 12, 2026 03:22
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.

1 participant