buzz listener: since-based replay + 2min reconnect cycle (zombie-socket proof); DM coverage verified

This commit is contained in:
zai-agent 2026-09-16 23:35:35 +00:00
parent 9fd63b176b
commit 6fa28abe20

View file

@ -1,20 +1,34 @@
// Raw WebSocket listener for Buzz #general — no relay abstraction.
// Handles NIP-42 auth explicitly: on ["AUTH", challenge] we sign kind 22242.
// On a matching kind-9 event from another pubkey: spool to inbox, exit 0.
// Buzz presence + inbox listener (raw WebSocket — no relay abstraction).
// - Persistent authenticated connection (NIP-42 challenge/response).
// - Subscribes to ALL kind-9 chat messages in the workspace (any channel, DMs
// included). On a new message from another pubkey: spool the event to the
// inbox and exit 0 — the ZCode background-task notification wakes the agent.
// - Presence: `buzz users set-presence --status online` heartbeat every 60s
// (relay TTL 180s). Listener alive = zai "online"; dead = offline in ~3 min.
// - Reconnect/expires: exits 2 (closed) or 3 (4h lifetime); the supervisor
// (ZCode background task) restarts it.
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure'
import fs from 'node:fs'
import { execFileSync } from 'node:child_process'
const RELAY = 'wss://ch4t.buzz'
const CH = '733ba424-73c7-4170-ac85-ba00d3a80f05'
const ME = '4abe5fcdf9695be34bdfc8fe82297aaaf0cf65b5ca92b6c789f6f8783b5ee197'
const INBOX = '/home/opc/.buzz-inbox'
const MAX_LIFETIME = 4 * 3600 * 1000
const STATE = '/home/opc/.buzz-last-seen'
const MAX_LIFETIME = 120 * 1000 // reconnect every 2 min: replays since lastSeen (zombie-socket proof)
const keyText = fs.readFileSync('/home/opc/buzz-agent-key.txt', 'utf8')
const SK = Uint8Array.from(keyText.match(/SECRET:\s*(\S+)/)[1].match(/.{2}/g).map((h) => parseInt(h, 16)))
const MY_PUBKEY = getPublicKey(SK)
function lastSeen() {
try { return parseInt(fs.readFileSync(STATE, 'utf8').trim(), 10) } catch { return Math.floor(Date.now() / 1000) }
}
function saveLastSeen(ts) {
const cur = lastSeen()
if (ts > cur) fs.writeFileSync(STATE, String(ts))
}
const ws = new WebSocket(RELAY)
const seen = new Set()
let authEventId = null
@ -23,8 +37,8 @@ let subscribed = false
function subscribe() {
if (subscribed) return
subscribed = true
ws.send(JSON.stringify(['REQ', 'general', { kinds: [9], '#h': [CH], since: Math.floor(Date.now() / 1000) }]))
console.error('subscribed')
ws.send(JSON.stringify(['REQ', 'workspace', { kinds: [9], since: lastSeen() }]))
console.error('subscribed to all kind-9 messages since', lastSeen())
}
function sendAuth(challenge) {
@ -58,23 +72,21 @@ ws.onmessage = (msg) => {
}
if (type === 'NOTICE') { console.error('NOTICE:', rest[0]); return }
if (type === 'EVENT') {
const [, subid, evt] = data
if (subid !== 'general') return
if (evt.pubkey === ME || seen.has(evt.id)) return
const evt = rest[1]
if (!evt || evt.pubkey === ME || seen.has(evt.id)) return
seen.add(evt.id)
saveLastSeen(evt.created_at + 1)
fs.appendFileSync(INBOX, JSON.stringify(evt) + '\n')
console.error('message received from', evt.pubkey.slice(0, 10))
process.exit(0)
}
if (type === 'EOSE') console.error('EOSE — waiting for live events')
if (type === 'EOSE') console.error('EOSE — live')
}
ws.onclose = () => { console.error('ws closed'); process.exit(2) }
ws.onerror = () => {}
setTimeout(() => { console.error('lifetime elapsed'); process.exit(3) }, MAX_LIFETIME)
// Presence heartbeat: relay TTL is 180s, contract says refresh every 60s.
// Listener alive = zai "online"; backend dead = offline within 3 minutes.
const BUZZ_CLI = '/usr/local/bin/buzz'
const heartbeat = () => {
try {