diff --git a/buzz/k8s/buzz-agent-control.yaml b/buzz/k8s/buzz-agent-control.yaml new file mode 100644 index 0000000..b7c0b46 --- /dev/null +++ b/buzz/k8s/buzz-agent-control.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: buzz-agent-control +spec: + ports: + - port: 80 + targetPort: 8095 +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: buzz-agent-control +subsets: + - addresses: + - ip: 10.0.0.215 + ports: + - port: 8095 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: buzz-agent-control +spec: + rules: + - host: ac.ch4t.buzz + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: buzz-agent-control + port: + number: 80 + tls: + - hosts: + - ac.ch4t.buzz + secretName: ac-ch4t-tls +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: ac-ch4t-buzz +spec: + secretName: ac-ch4t-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - ac.ch4t.buzz diff --git a/buzz/ws-listener/listener-raw.mjs b/buzz/ws-listener/listener-raw.mjs index db122e0..4cd345b 100644 --- a/buzz/ws-listener/listener-raw.mjs +++ b/buzz/ws-listener/listener-raw.mjs @@ -14,13 +14,55 @@ import { execFileSync } from 'node:child_process' const RELAY = 'wss://ch4t.buzz' const ME = '4abe5fcdf9695be34bdfc8fe82297aaaf0cf65b5ca92b6c789f6f8783b5ee197' const INBOX = '/home/opc/.buzz-inbox' -const REACTED = '/home/opc/.buzz-reacted' const STATE = '/home/opc/.buzz-last-seen' +const PENDING = '/home/opc/.buzz-pending.json' 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) +const RECEIPT_EMOJIS = ['👀', '💬'] + +function loadPending() { + try { return JSON.parse(fs.readFileSync(PENDING, 'utf8')) } catch { return {} } +} +function savePending(o) { fs.writeFileSync(PENDING, JSON.stringify(o)) } + +// 👀 = seen, 💬 = working. Both removed when zai posts its reply. +function addReceipts(evt, ch) { + const p = loadPending() + p[ch] = p[ch] || [] + if (p[ch].includes(evt.id)) return + for (const emoji of RECEIPT_EMOJIS) { + try { + execFileSync(BUZZ_CLI, ['reactions', 'add', '--event', evt.id, '--emoji', emoji], { + env: { ...process.env, BUZZ_RELAY_URL: 'https://ch4t.buzz', BUZZ_PRIVATE_KEY: keyText.match(/SECRET:\s*(\S+)/)[1] }, + stdio: 'ignore', + timeout: 15000, + }) + } catch (e) { + console.error('reaction add failed:', emoji, String(e).slice(0, 80)) + } + } + p[ch].push(evt.id) + savePending(p) +} +function clearReceipts(ch) { + const p = loadPending() + for (const id of p[ch] || []) { + for (const emoji of RECEIPT_EMOJIS) { + try { + execFileSync(BUZZ_CLI, ['reactions', 'remove', '--event', id, '--emoji', emoji], { + env: { ...process.env, BUZZ_RELAY_URL: 'https://ch4t.buzz', BUZZ_PRIVATE_KEY: keyText.match(/SECRET:\s*(\S+)/)[1] }, + stdio: 'ignore', + timeout: 15000, + }) + } catch {} + } + } + delete p[ch] + savePending(p) +} function lastSeen() { try { return parseInt(fs.readFileSync(STATE, 'utf8').trim(), 10) } catch { return Math.floor(Date.now() / 1000) } @@ -112,23 +154,9 @@ ws.onmessage = (msg) => { fs.appendFileSync(INBOX, JSON.stringify(evt) + '\n') console.error('message received from', evt.pubkey.slice(0, 10)) const inCh = (evt.tags.find((t) => t[0] === 'h') || [])[1] - if (inCh) startTyping(inCh) - // Instant receipt: emoji reaction (NIP-25 kind 7) on the sender's message, - // so the sender always sees that zai is alive — before any LLM work starts. - let reacted = false - try { reacted = fs.readFileSync(REACTED, 'utf8').split('\n').includes(evt.id) } catch {} - if (!reacted) { - try { - execFileSync(BUZZ_CLI, ['reactions', 'add', '--event', evt.id, '--emoji', '👀'], { - env: { ...process.env, BUZZ_RELAY_URL: 'https://ch4t.buzz', BUZZ_PRIVATE_KEY: keyText.match(/SECRET:\s*(\S+)/)[1] }, - stdio: 'ignore', - timeout: 15000, - }) - fs.appendFileSync(REACTED, evt.id + '\n') - console.error('👀 receipt sent') - } catch (e) { - console.error('reaction failed:', String(e).slice(0, 100)) - } + if (inCh) { + startTyping(inCh) + addReceipts(evt, inCh) } } if (type === 'EOSE') console.error('EOSE — live')