Skip to content

feat: long-poll on the storage nodes; one-time background prompt; no startup "Device ready" flash - #1108

Merged
cryptskii merged 6 commits into
mainfrom
cryptskii/eager-darwin-1668e4
Oct 3, 2026
Merged

cryptskii merged 6 commits into
mainfrom
cryptskii/eager-darwin-1668e4

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

Summary

Transfers settle as soon as the other side's step lands, instead of at the next poll.

Storage node: POST /api/v2/b0x/wait (B0xWaitRequest)

  • Answers 200 with the spools that hold an entry at or after the device's mark for them. It answers at once if one already does; otherwise it holds the request until one lands.
  • Every submit wakes the waits held on that spool. The wake-up is in-process: a node's spool is written only by its own submit route.
  • A wait that runs out answers 204. The bound is AppLimits.wait_bound (25 s deployed), enforced by a TimeoutLayer on the wait route. The node reads no clock, keeping to storage spec §1 rule 4 (tests/no_clock_reads.rs).
  • The route sits outside the node's request timeout and concurrency limit. It has its own cap of 4096 held waits, beyond which it answers 503. A held wait holds no database connection.
  • Nothing is read, marked or changed by a wait.

App: dsm_sdk::sdk::inbox_waiter

  • Runs beside the inbox poller, while the poller runs: in the foreground, or in the background while a transfer settles.
  • Holds one wait per member over every inbox route the device reads, and wakes a sync the moment any member answers.
  • Each spool's mark is where the last sync read that member's spool to its end, kept in memory.
  • When a sync moves the routes, the waits are restarted on the new ones. A member that fails, or doesn't serve waits (404 before the redeploy), is rested for 30 s. Repeated instant answers back off from 1 s up to 30 s.
  • While the waits cover the fleet (N − K + 1 members, so every delivery lands on one of them), the poller drops to a safety net: 5 s while settling (was 2 s) and 30 s on screen (was 5 s). Otherwise it keeps its current cadence.

Android: one-time background prompt

  • BatteryExemption opens the system's "let this app run in the background" dialog once per install, the first time the app starts with a wallet while still battery-optimised.
  • This keeps the wallet's own node connection alive through Doze. No push service is involved.

Frontend: no "Device ready" flash at startup

  • The chip-trace "Device ready" scene played on every move from the publishing screen to the wallet. An ordinary launch can pass the publishing screen for a moment.
  • It now plays only when the securing screen was shown in that run.

Related Issues

Follows #1107 (2 s / 5 s polling stopgap).

Testing

  • cargo fmt --all -- --check, and clippy -D warnings on dsm_sdk and dsm_storage_node
  • scripts/real_code_guard.py clean
  • Storage node: full cargo test green, including the new tests/b0x_wait.rs and the wait case in tests/resource_limits.rs, run against Postgres
  • SDK: inbox_waiter / inbox_poller tests, including a wait held at in-process nodes over TLS (test_support::nodes) and answered when a delivery lands
  • Frontend: FxProvider tests, including a new case for a launch through the publishing screen, which fails on the old cue; tsc and eslint clean
  • make android: not built locally (no Android SDK in this environment); CI builds it

Checklist

  • I kept unrelated changes out of this PR
  • No TODO|FIXME|HACK|XXX added

Notes

  • The waits need the nodes redeployed. Until then the app gets 404, rests each member, and polls exactly as it does today.
  • REQUEST_IGNORE_BATTERY_OPTIMIZATIONS is fine for side-loaded beta builds. A Play Store release needs Google's approval for it, or a switch to opening the battery settings list instead.
  • Code-map pins are repinned in a follow-up commit on this branch once the map and evidence run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u


Generated by Claude Code

claude added 6 commits October 2, 2026 23:23
…yncs the moment it does

Storage node: POST /api/v2/b0x/wait (B0xWaitRequest) answers as soon as
any listed spool holds an entry at or after the device's mark, or with
none after at most 25 s. Woken in-process by every submit; served outside
the node's request timeout and concurrency limit, bounded by its own cap
on waits held, and holding no database connection while it waits.

App (dsm_sdk): an inbox waiter beside the poller holds a wait on every
member over this device's inbox routes and wakes a sync the moment one
answers. Waits mark each spool where the last sync read it to its end.
While the waits cover the fleet (N - K + 1 members), the poller slows to
a safety net: 5 s while settling, 30 s on screen.

Work in progress: the waiter's real-node test is written but not yet run;
code-map pins not yet moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
…onnection survives Doze

The first time the app starts with a wallet and is still battery-optimised,
it opens the system's 'let this app run in the background' dialog
(REQUEST_IGNORE_BATTERY_OPTIMIZATIONS), or the optimisation list where the
dialog does not exist. Asked once per install; a refusal is kept. No push
service is involved: the wallet keeps its own connection to the nodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
The chip-trace 'Device ready' scene played on every transition from the
publishing screen to the wallet. An ordinary launch of a device set up long
ago can report the publishing phase for a moment before the core confirms
the identity is published, so the scene flashed at startup with nothing new
to celebrate. It now plays only when the securing screen was shown in this
run: the device was set up here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
… of its own

Storage nodes read no clock and run no timer (storage spec §1 rule 4,
tests/no_clock_reads.rs). The wait handler's own deadline is gone: the
wait route carries a TimeoutLayer that answers 204 once AppLimits.wait_bound
passes (25 s deployed), as the request timeout already bounds every other
route. B0xWaitRequest loses wait_ms; the device reads 204 as a quiet round.
The wait tests move to tests/b0x_wait.rs, beside the other timing tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
ci/no_clock_and_no_json.sh refuses wall-clock reads outside the BLE
transport. A member's rest is now a timer that puts it back after 30 s,
a round answered before a 1 s timer fires counts as quick, and the wait
for a sync is bounded by a sleep. No Instant::now() remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
Code-only moves (the shared proto gained the wait messages, which regenerates
the types every crate reads). Map built at 5995fe2; the evidence each pin
names ran at that tree: 254 dsm, 47 dsm_sdk, 22 dsm_storage_node tests, all
passing. make requirement-map-intent INTENT_BUILT=android,node: 635 PINNED,
0 failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4wDTToHmJfkYYJvMKXt3u
@cryptskii
cryptskii merged commit 1f67a74 into main Oct 3, 2026
27 checks passed
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.

2 participants