// probe-room.mjs — classify ONE room's StateBase health (run one-per-process so
// a corruption abort is isolated). Opens via the SAME router path the game uses
// (so roomSeed/encryptionKey/membership wired identically), then reports the
// signals that distinguish the failure classes:
// • opens clean vs throws (on-disk corestore corruption)
// • base.writable + writer/indexer count
// • __members__ projection, and whether SELF is in it (deadlock signature)
// • entry count
//
// bare tools/probe-room.mjs --storage <root> --room <roomId>
import { startDaemon } from '../index.js'
import b4a from 'b4a'
const args = Bare.argv.slice(2)
const gi = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null }
const storage = gi('--storage'); let roomId = gi('--room'); const title = gi('--title')
if (!storage || (!roomId && !title)) { console.log('usage: bare tools/probe-room.mjs --storage <root> (--room <roomId> | --title <name>)'); Bare.exit(1) }
setTimeout(() => { console.error('VERDICT: TIMEOUT(90s) — possible admit-loop/hang'); Bare.exit(2) }, 90000)
Bare.on('unhandledRejection', (err) => { console.error('VERDICT: CRASH(unhandledRejection):', err && err.message ? err.message : String(err)); Bare.exit(7) })
const d = await startDaemon({ storage, socketPath: '/tmp/keet-probe.sock', pidFile: '/tmp/keet-probe.pid', debug: false })
await d.core.backendAPI.ready()
if (!roomId && title) {
const rooms = await d.router._rooms()
const hit = rooms.find(r => r.title === title)
if (!hit) { console.error('no room titled', JSON.stringify(title)); Bare.exit(1) }
roomId = hit.roomId
}
let sb
try {
sb = await d.router._getStateBase(roomId)
} catch (err) {
console.error('VERDICT: OPEN_THREW:', err && err.message ? err.message : String(err))
Bare.exit(6)
}
const self = sb._selfKey ? sb._selfKey.toString('hex') : null
let writers = '?'; let indexers = '?'
try { const b = sb.base; writers = (b.activeWriters && b.activeWriters.size) ?? '?'; indexers = (b.linearizer && b.linearizer.indexers && b.linearizer.indexers.length) ?? '?' } catch {}
let members = null
try {
const all = await sb.list('')
if (all.__members__ !== undefined) members = typeof all.__members__ === 'string' ? JSON.parse(all.__members__) : all.__members__
var entryCount = Object.keys(all).filter(k => k !== '__members__').length
} catch (e) { console.error('list() threw:', e.message) }
const memberKeys = members ? Object.keys(members) : []
const selfInMembers = self && members ? (members[self] != null || members[self.toLowerCase()] != null) : false
console.log('================ PROBE: %s ================', roomId.slice(0, 16))
console.log('self key :', self ? self.slice(0, 16) : '(none)')
console.log('opened : OK')
console.log('writable :', sb.writable)
console.log('writers/idx :', writers, '/', indexers)
console.log('entries :', entryCount ?? '?')
console.log('__members__ :', memberKeys.length, 'keys:', memberKeys.map(k => k.slice(0, 8)).join(','))
console.log('SELF in mem :', selfInMembers)
// Classification
let verdict
if (!members) verdict = 'NO_MEMBERS_PROJECTION (never had a writer project __members__)'
else if (!selfInMembers) verdict = 'MEMBERSHIP_DEADLOCK_RISK (self NOT in __members__ → member set() loops, never commits)'
else if (sb.writable) verdict = 'HEALTHY (self in members + writable)'
else verdict = 'ADMISSION_NOT_LANDED (self in members but not writable → optimistic admit not acked by indexer)'
console.log('VERDICT :', verdict)
Bare.exit(0)