OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer commits / be3ad15

0.3.104: the OTA handler silently did nothing — `this` was undefined
0.3.100-0.3.103 shipped an OTA respawn that never respawned. The handler ran, logged "release-ready — respawning", and then nothing happened: the callback called `this.close()` from inside bootSwarmDevice, which is a plain exported FUNCTION, so in an ES module `this` is undefined and it threw a TypeError — swallowed whole by the try/catch around onRelease in serve-device-uds. Every surface said it worked. The handler was registered (probing the socket confirmed it), the sidecar pushed release-ready, the log line appeared with the right version — and the process kept running the old code while `pgh app status` reported the NEW version, because status reports the INSTALLED tree. Textbook INSTALLED != RUNNING, produced by the very fix meant to end it. Caught by publishing and watching PIDs: favela-web and atproto-bridge both respawned, hyper-proto-peer kept pid 2576529. The process lifecycle belongs to index.js — it owns `daemon` and its close — so it now registers the handler via setOtaHandler and the UDS layer only signals. No `this`, and the exit path is where the close already lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
be3ad15243adb013eb7d4e30d6f3e1e7963eddd0
by plankatron <fleeky@gmail.com> · 2026-08-01T02:29:21.000Z

Changed files (3) +25 -4

index.js +12 -0
……@@ -124,6 +124,18 @@ if (cfg.swarm) {
124124 process.on('SIGINT', shutdownM)
125125 process.on('SIGTERM', shutdownM)
126126
127 // OTA: close cleanly, then exit 75 so the supervisor respawns onto the tree the sidecar installed.
128 // Registered HERE because this scope owns `daemon` — the UDS layer only signals.
129 try {
130 const { setOtaHandler } = await import('./lib/device-node.js')
131 setOtaHandler(() => {
132 console.log(JSON.stringify({ msg: 'ota: closing for respawn (exit 75)' }))
133 Promise.resolve(daemon.close()).catch(() => {}).then(() => process.exit(75)).catch(() => process.exit(75))
134 })
135 } catch (e) {
136 console.error(JSON.stringify({ msg: 'ota handler registration failed (non-fatal)', error: e.message }))
137 }
138
127139 // Sidecar registration — THIS is the path the fleet runs (multi-DID). Without it the sidecar has no
128140 // udsPath for hpp and cannot push `release-ready`, so autoupdate installs a tree the daemon never
129141 // adopts. The primary DID's control.sock is the one to register: `pgh app status` and the OTA push
lib/device-node.js +12 -3
……@@ -7,6 +7,12 @@ import crypto from 'hypercore-crypto'
77import b4a from 'b4a'
88import fs from 'bare-fs'
99import process from 'bare-process' // for the OTA respawn exit — Bare has no global process
10
11// Set by the daemon entrypoint, which owns close() and therefore the exit. Kept module-level rather than
12// threaded through bootMultiDevice -> bootSwarmDevice because the UDS surface is created deep inside the
13// per-device boot and the only thing it needs is "tell whoever owns the process".
14let otaHandler = null
15export function setOtaHandler (fn) { otaHandler = typeof fn === 'function' ? fn : null }
1016import Hyperswarm from 'hyperswarm'
1117import ProtomuxWakeup from 'protomux-wakeup'
1218import Protomux from 'protomux'
……@@ -495,12 +501,15 @@ export async function bootSwarmDevice ({ storage, deviceMnemonic, did, signingKe
495501 // stall on an un-converged base's base.update), so a co-located client (favela-web) can ALWAYS reach the
496502 // device over local IPC even while convergence lags. Owner-gated by the filesystem → no writer-allowlist.
497503 try {
498 // OTA respawn: a clean close, then exit 75 for the supervisor. Exiting the PROCESS is the daemon's
499 // call, not the UDS module's, so it is injected here.
504 // OTA respawn. This used to call `this.close()` — but this is inside bootSwarmDevice, a plain
505 // FUNCTION, so in an ES module `this` is undefined and it threw a TypeError that serve-device-uds's
506 // try/catch swallowed: the handler logged "respawning" and then silently did nothing, which is
507 // exactly how it shipped in 0.3.100-0.3.103. The process lifecycle belongs to index.js (it owns
508 // `daemon` and its close), so it registers the handler and this only signals.
500509 udsServing = await serveDeviceUds(device, {
501510 uds: join(ctlDir, 'control.sock'),
502511 blobs,
503 onRelease: () => { Promise.resolve(this.close()).catch(() => {}).then(() => process.exit(75)).catch(() => process.exit(75)) }
512 onRelease: () => { if (otaHandler) otaHandler() }
504513 })
505514 console.log(JSON.stringify({ msg: 'device UDS control surface', uds: join(ctlDir, 'control.sock') }))
506515 } catch (e) { console.error(JSON.stringify({ msg: 'UDS control surface failed (swarm surface still up)', error: e.message })) }
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-proto-peer",
3 "version": "0.3.103",
3 "version": "0.3.104",
44 "description": "Self-custody, P2P-native atproto peer — holds the user's key + content as Hypercores and authors+signs its own repo. The source of truth in the peer/bridge split (Model B); atproto-bridge projects it to bsky.",
55 "type": "module",
66 "main": "lib/peer.js",