OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer commits / 681fd3e

0.3.100: OTA — register with the sidecar, and respawn on release-ready
hpp HAD a UDS control surface but never registered it with pgh-sidecar, so `pgh app status hyper-proto-peer` reported registered:false and the sidecar had no udsPath to push `release-ready` to. Autoupdate installed new trees and the daemon kept running the old code — the same INSTALLED != RUNNING trap, on the daemon that holds the signing key. Two halves: - maintainSidecarRegistration on the primary control.sock, so the sidecar can find us at all (and `pgh app status` resolves) - a `release-ready` handler that closes cleanly and exits 75 The handler is INJECTED into serveDeviceUds as `onRelease` rather than exiting there: that module serves a device and must not own the process lifecycle. device-node supplies the callback because it owns close(). ⚠ NOT pgh-runtime's fetch path — it fetches ROOT trees without dep closures, producing a broken install. The sidecar already did the work. Both are best-effort and non-fatal: this daemon's job is serving and signing for its DID, not being manageable, so a registration failure logs and boots anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
681fd3e165ff47e902ea60a6a886a685f7742adc
by plankatron <fleeky@gmail.com> · 2026-08-01T01:54:34.000Z

Changed files (5) +95 -3

index.js +13 -0
……@@ -152,6 +152,19 @@ if (cfg.swarm) {
152152 const shutdown = async () => { try { await node.close() } catch {} ; process.exit(0) }
153153 process.on('SIGINT', shutdown)
154154 process.on('SIGTERM', shutdown)
155
156 // Register the control socket with pgh-sidecar. Without this the sidecar has no udsPath for us, so it
157 // cannot push `release-ready` at all — hpp was installed by autoupdate and then just kept running the
158 // old code until someone restarted it by hand. Registration also makes `pgh app status` resolve us.
159 // Best-effort and non-fatal: the daemon's job is serving the DID, not being manageable.
160 try {
161 const { maintainSidecarRegistration } = await import('./lib/sidecar.js')
162 const ctl = join(cfg.storage, 'control.sock')
163 maintainSidecarRegistration({ udsPath: ctl, name: process.env.PGH_APP_NAME || 'hyper-proto-peer', runtime: 'bare', version: null }) // the sidecar treats version as informational; reading package.json here is not worth a failure path
164 console.log(JSON.stringify({ msg: 'sidecar registration armed', uds: ctl }))
165 } catch (e) {
166 console.error(JSON.stringify({ msg: 'sidecar registration unavailable (non-fatal)', error: e.message }))
167 }
155168 }
156169} else {
157170const vaultPath = join(cfg.storage, 'peer-identity.json')
lib/device-node.js +8 -1
……@@ -6,6 +6,7 @@
66import crypto from 'hypercore-crypto'
77import b4a from 'b4a'
88import fs from 'bare-fs'
9import process from 'bare-process' // for the OTA respawn exit — Bare has no global process
910import Hyperswarm from 'hyperswarm'
1011import ProtomuxWakeup from 'protomux-wakeup'
1112import Protomux from 'protomux'
……@@ -494,7 +495,13 @@ export async function bootSwarmDevice ({ storage, deviceMnemonic, did, signingKe
494495 // stall on an un-converged base's base.update), so a co-located client (favela-web) can ALWAYS reach the
495496 // device over local IPC even while convergence lags. Owner-gated by the filesystem → no writer-allowlist.
496497 try {
497 udsServing = await serveDeviceUds(device, { uds: join(ctlDir, 'control.sock'), blobs })
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.
500 udsServing = await serveDeviceUds(device, {
501 uds: join(ctlDir, 'control.sock'),
502 blobs,
503 onRelease: () => { Promise.resolve(this.close()).catch(() => {}).then(() => process.exit(75)).catch(() => process.exit(75)) }
504 })
498505 console.log(JSON.stringify({ msg: 'device UDS control surface', uds: join(ctlDir, 'control.sock') }))
499506 } catch (e) { console.error(JSON.stringify({ msg: 'UDS control surface failed (swarm surface still up)', error: e.message })) }
500507 const allow = Array.isArray(writers) ? writers.map((w) => String(w).toLowerCase()).filter(Boolean) : null
lib/serve-device-uds.js +17 -1
……@@ -10,10 +10,26 @@ import b4a from 'b4a'
1010import { Service } from 'hyper-svc'
1111import { conflictDiag } from './conflict-watch.js'
1212
13export async function serveDeviceUds (device, { uds, blobs = null } = {}) {
13export async function serveDeviceUds (device, { uds, blobs = null, onRelease = null } = {}) {
1414 if (!uds) throw new Error('serveDeviceUds: uds path required')
1515 const svc = new Service({ name: 'hyper-proto-peer', uds })
1616
17 // OTA — the app half of the push contract. The sidecar performs the FULL install (tree + dep closure +
18 // materialize) and then calls this; our only job is to exit cleanly so the supervisor respawns onto the
19 // new tree. Injected rather than exiting here: this module serves a device and must not own the
20 // process lifecycle.
21 //
22 // ⚠ NOT pgh-runtime's fetch path — it fetches root trees WITHOUT dep closures, which produces a broken
23 // install (the MODULE_NOT_FOUND respawn loop that took the membrane down on 2026-07-31).
24 if (onRelease) {
25 svc.handle('release-ready', async (ctx, params = {}) => {
26 const ver = (params && params.version) || '?'
27 console.log(JSON.stringify({ msg: 'ota: release-ready — sidecar installed; respawning', version: ver }))
28 setImmediate(() => { try { onRelease(ver) } catch {} })
29 return { ok: true, willRespawn: true }
30 })
31 }
32
1733 svc.handle('get-state', async () => device.state())
1834 // conflict-diag: hypercore conflicts seen SINCE BOOT. A conflict freezes a core and stops it
1935 // replicating while the served view stays correct, so a forked node looks healthy on every other
lib/sidecar.js +56 -0
……@@ -0,0 +1,56 @@
1// pgh-sidecar registration — makes the daemon a first-class pgh app. Registering our UDS
2// with the sidecar lets `app-lookup` resolve us (so `pgh app status atproto-bridge` and the
3// readiness probe work) and survives the boot race (sidecar socket may appear after us).
4// Mirrors pear-git-v3/lib/sidecar-wait.js + server.js registerWithSidecar.
5
6import fs from '#node:fs'
7import path from '#node:path'
8import os from '#node:os'
9import process from '#node:process'
10
11export function sidecarSockPath () {
12 return process.env.PGH_SIDECAR_SOCK || path.join(os.homedir(), '.pgh', 'svc.sock')
13}
14
15// Poll for the sidecar socket (it may be created after this daemon boots — the boot race).
16export async function waitForSidecarSock ({ timeoutMs = 120000, intervalMs = 1000, shouldStop = () => false } = {}) {
17 const sock = sidecarSockPath()
18 const deadline = Date.now() + timeoutMs
19 while (!fs.existsSync(sock)) {
20 if (shouldStop()) return false
21 if (Date.now() >= deadline) return false
22 await new Promise((r) => setTimeout(r, intervalMs))
23 }
24 return true
25}
26
27// app-register (last-writer-wins, idempotent) our live UDS + pid with the sidecar.
28export async function registerWithSidecar ({ udsPath, name, runtime = 'bare', version = null }) {
29 const sidecarSock = sidecarSockPath()
30 if (!fs.existsSync(sidecarSock)) return { registered: false, reason: 'sidecar not running' }
31 const installName = name || process.env.PGH_APP_NAME || 'atproto-bridge'
32 try {
33 const { Client } = await import('hyper-svc')
34 const c = await Client.connectUds(sidecarSock)
35 await c.call('app-register', { name: installName, udsPath, pid: process.pid, runtime, version })
36 await c.close()
37 return { registered: true, name: installName }
38 } catch (err) {
39 return { registered: false, reason: err.message }
40 }
41}
42
43// Register once the sidecar is up, then re-register on an interval (the sidecar keeps its
44// registry in memory, so a sidecar restart silently drops us). Returns a stop() fn.
45export function maintainSidecarRegistration ({ udsPath, name, runtime, version, intervalMs = 30000 } = {}) {
46 let timer = null
47 let stopped = false
48 ;(async () => {
49 const ready = await waitForSidecarSock({ shouldStop: () => stopped })
50 if (stopped) return
51 if (ready) await registerWithSidecar({ udsPath, name, runtime, version })
52 timer = setInterval(() => { registerWithSidecar({ udsPath, name, runtime, version }) }, intervalMs)
53 if (timer.unref) timer.unref()
54 })()
55 return () => { stopped = true; if (timer) clearInterval(timer) }
56}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-proto-peer",
3 "version": "0.3.99",
3 "version": "0.3.100",
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",