thinkpool-pair 0.7.166 → 0.7.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/account.mjs +24 -0
- package/bridge.mjs +49 -6
- package/package.json +1 -1
package/account.mjs
CHANGED
|
@@ -271,6 +271,12 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
271
271
|
// account without per-room probing. We TRACK this machine (name + served room
|
|
272
272
|
// codes + version) on a per-account channel; the dashboard subscribes read-only.
|
|
273
273
|
const machine = os.hostname().replace(/\.local$/, '')
|
|
274
|
+
|
|
275
|
+
// ── supervisor logging (cascade brg-instrument, supervisor-log lane) ─────────
|
|
276
|
+
const SUP_ID = (randomUUID?.() || '').slice(0, 8)
|
|
277
|
+
let lastClaimHeld = null // track state changes for claim_tick logging
|
|
278
|
+
console.log(`◇ supervisor boot sup=${SUP_ID} host=${machine} pid=${process.pid} npx=${VERSION || 'dev'}`)
|
|
279
|
+
|
|
274
280
|
const acct = sb.channel(`tpacct:${session.user.id}`, { config: { presence: { key: machine }, broadcast: { self: false } } })
|
|
275
281
|
let restarting = false // set by the "restart" broadcast → stop() exits non-zero so a supervisor respawns us
|
|
276
282
|
// Web "attach this session" card → bind an unbound/new room to a local dir (and
|
|
@@ -463,6 +469,13 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
463
469
|
else if (!held && standbyHost !== host) process.stderr.write(`\n ◇ another device (${host}) is serving this account — standing by.\n (this Mac takes over automatically if that device goes offline.)\n`)
|
|
464
470
|
claimHeld = held
|
|
465
471
|
standbyHost = held ? null : host
|
|
472
|
+
|
|
473
|
+
// ── supervisor claim_tick logging (cascade brg-instrument) ─────────────────
|
|
474
|
+
if (lastClaimHeld !== held) {
|
|
475
|
+
console.log(`claim_tick sup=${SUP_ID} held=${held} holder=${held ? machine : host}`)
|
|
476
|
+
lastClaimHeld = held
|
|
477
|
+
}
|
|
478
|
+
|
|
466
479
|
return held
|
|
467
480
|
} catch {
|
|
468
481
|
// Transient claim error: keep serving if we already hold; never START
|
|
@@ -660,7 +673,12 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
660
673
|
delete env.THINKPOOL_PAIR_AUTOUPDATE
|
|
661
674
|
// Hand the child the owner JWT for authed web writes (code-mockup, L16).
|
|
662
675
|
if (currentAccessToken) env.TP_ACCESS_TOKEN = currentAccessToken
|
|
676
|
+
|
|
677
|
+
// ── supervisor child_spawn logging (cascade brg-instrument) ─────────────────
|
|
678
|
+
const alreadyHad = children.has(room)
|
|
663
679
|
const child = spawn(process.execPath, [BRIDGE, room, '--headless', '--auto=claude'], { cwd: dir, stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env })
|
|
680
|
+
console.log(`child_spawn sup=${SUP_ID} room=${room} pid=${child.pid} dir=${dir} had_prior=${alreadyHad}`)
|
|
681
|
+
|
|
664
682
|
children.set(room, child)
|
|
665
683
|
childIdle.set(room, false) // unknown until it reports → never restart a fresh child
|
|
666
684
|
childBetween.set(room, false)
|
|
@@ -738,6 +756,9 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
738
756
|
}
|
|
739
757
|
})
|
|
740
758
|
const dropRoom = (code) => {
|
|
759
|
+
// ── supervisor child_exit logging (cascade brg-instrument) ─────────────────
|
|
760
|
+
console.log(`child_exit sup=${SUP_ID} room=${room} pid=${child?.pid || '?'} code=${code || '?'}`)
|
|
761
|
+
|
|
741
762
|
children.delete(room); childIdle.delete(room); childBetween.delete(room); childPeer.delete(room); roomNames.delete(room); roomPartner.delete(room) // re-served on the next tick
|
|
742
763
|
// Owner-reclaim SENTINEL (75): the grantee child stood down because the owner's bridge
|
|
743
764
|
// returned. Park the room on a cooldown so discovery doesn't respawn it into another
|
|
@@ -790,6 +811,9 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
790
811
|
const stop = (sig) => {
|
|
791
812
|
if (stopping) return; stopping = true
|
|
792
813
|
const code = restarting ? 1 : 0
|
|
814
|
+
|
|
815
|
+
// ── supervisor stand-down logging (cascade brg-instrument) ─────────────────
|
|
816
|
+
console.log(`supervisor_stop sup=${SUP_ID} reason=${sig || 'unknown'} restarting=${restarting} children=${children.size}`)
|
|
793
817
|
clearInterval(iv)
|
|
794
818
|
for (const t of updateTimers) { try { clearInterval(t) } catch { /* noop */ } } // clearInterval clears Timeouts too
|
|
795
819
|
keepFresh.cancel()
|
package/bridge.mjs
CHANGED
|
@@ -397,7 +397,7 @@ if (_superOwn.includes('--supervise') || _superOwn.includes('--keep-alive')) {
|
|
|
397
397
|
child.on('error', () => { /* spawn failure → exit handler retries */ })
|
|
398
398
|
}
|
|
399
399
|
for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { stopping = true; try { child?.kill(sig) } catch { /* noop */ } setTimeout(() => process.exit(0), 400) })
|
|
400
|
-
process.stderr.write(`\n ◆ supervisor up — keeping the bridge for room ${room} alive (auto-restart on crash).\n`)
|
|
400
|
+
process.stderr.write(`\n [b:${BRIDGE_ID} r:${room}] ◆ supervisor up — keeping the bridge for room ${room} alive (auto-restart on crash).\n`)
|
|
401
401
|
spawnChild()
|
|
402
402
|
await new Promise(() => {}) // block forever; the child IS the bridge
|
|
403
403
|
}
|
|
@@ -505,14 +505,15 @@ if (process.stdin.isTTY && !headless && process.env.THINKPOOL_PAIR_AUTOUPDATE !=
|
|
|
505
505
|
const yes = await askYesNo(`\n Run as a background service instead? It survives reboot + auto-updates,\n but runs detached — you'd stop it with 'uninstall-service', not Ctrl-C.\n Install background service? [y/N] `, false)
|
|
506
506
|
if (yes) {
|
|
507
507
|
svc.installService(room, attachedCmd ? [attachedCmd, ...attachedArgs] : [])
|
|
508
|
-
process.stderr.write(`\n ◆ the background service is handling room ${room} now — you can close this terminal.\n ◆ STOP IT with: npx thinkpool-pair@latest uninstall-service ${room}\n (closing the terminal does NOT stop it — it's detached under launchd/systemd.)\n\n`)
|
|
508
|
+
process.stderr.write(`\n [b:${BRIDGE_ID} r:${room}] ◆ the background service is handling room ${room} now — you can close this terminal.\n ◆ STOP IT with: npx thinkpool-pair@latest uninstall-service ${room}\n (closing the terminal does NOT stop it — it's detached under launchd/systemd.)\n\n`)
|
|
509
509
|
process.exit(0) // hand off to the supervised service (no double bridge)
|
|
510
510
|
}
|
|
511
511
|
try { fs.mkdirSync(path.dirname(declineFile), { recursive: true }); fs.writeFileSync(declineFile, new Date().toISOString()) } catch { /* noop */ }
|
|
512
|
-
process.stderr.write(` ◆ running in this terminal. (Auto-update service anytime: npx thinkpool-pair@latest install-service ${room})\n`)
|
|
512
|
+
process.stderr.write(` [b:${BRIDGE_ID} r:${room}] ◆ running in this terminal. (Auto-update service anytime: npx thinkpool-pair@latest install-service ${room})\n`)
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
const name = process.env.TP_NAME || os.userInfo().username || 'host'
|
|
516
|
+
const BRIDGE_ID = (randomUUID?.().slice(0,8)) || 'bridgexx'
|
|
516
517
|
// host: this machine's short label — os.hostname() with any DNS/domain suffix
|
|
517
518
|
// stripped and capped ~24 chars. A /code room can be served by different bridges
|
|
518
519
|
// over time (Max's Mac vs Conrad's Linux box); the client uses this to show WHICH
|
|
@@ -528,6 +529,48 @@ const repoLabel = path.basename(cwd)
|
|
|
528
529
|
// thinkpool-pair's own version — surfaced in the room's welcome banner.
|
|
529
530
|
let VERSION = null
|
|
530
531
|
try { VERSION = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { /* unknown — banner omits it */ }
|
|
532
|
+
|
|
533
|
+
// Stale npx cache check — warn if the running bridge is older than the newest installed.
|
|
534
|
+
// Uses proper semver comparison (not string compare) to avoid false positives like "0.7.150" < "0.7.99".
|
|
535
|
+
try {
|
|
536
|
+
const npxCacheRoot = path.join(os.homedir(), '.npm', '_npx')
|
|
537
|
+
const npxEntries = fs.readdirSync(npxCacheRoot, { withFileTypes: true })
|
|
538
|
+
.filter(dirent => dirent.isDirectory())
|
|
539
|
+
.map(dirent => dirent.name)
|
|
540
|
+
.filter(name => name !== 'cache')
|
|
541
|
+
|
|
542
|
+
// Parse semver into [major, minor, patch] with missing segments as 0
|
|
543
|
+
const parseSemver = (v) => {
|
|
544
|
+
if (!v) return [0, 0, 0]
|
|
545
|
+
return v.split('.').map(p => parseInt(p, 10) || 0)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Compare two semver versions: return true if a < b
|
|
549
|
+
const semverLess = (a, b) => {
|
|
550
|
+
const pa = parseSemver(a)
|
|
551
|
+
const pb = parseSemver(b)
|
|
552
|
+
for (let i = 0; i < 3; i++) {
|
|
553
|
+
if (pa[i] < pb[i]) return true
|
|
554
|
+
if (pa[i] > pb[i]) return false
|
|
555
|
+
}
|
|
556
|
+
return false // equal
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let newestVersion = null
|
|
560
|
+
for (const entry of npxEntries) {
|
|
561
|
+
const pkgPath = path.join(npxCacheRoot, entry, 'node_modules', 'thinkpool-pair', 'package.json')
|
|
562
|
+
try {
|
|
563
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
564
|
+
if (pkg.version) {
|
|
565
|
+
if (!newestVersion || semverLess(newestVersion, pkg.version)) newestVersion = pkg.version
|
|
566
|
+
}
|
|
567
|
+
} catch { /* skip unreadable entries */ }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (newestVersion && VERSION && semverLess(VERSION, newestVersion)) {
|
|
571
|
+
process.stderr.write(` ⚠ stale-cache: running ${VERSION}, newest ${newestVersion}\n`)
|
|
572
|
+
}
|
|
573
|
+
} catch { /* skip on any error — this is a lint, not a hard gate */ }
|
|
531
574
|
// Read the CURRENT branch LIVE (not once at boot) so the room's mode row reflects a
|
|
532
575
|
// branch switch instead of showing whatever branch the dir was on when the bridge spawned
|
|
533
576
|
// (the stale-branch bug). Handles a .git dir (normal checkout) AND a .git FILE (linked
|
|
@@ -685,7 +728,7 @@ let yielding = false // set once when standing down, so reclaim + revoke
|
|
|
685
728
|
process.exit(0)
|
|
686
729
|
}
|
|
687
730
|
iServeAsGrantee = amGrantee({ ownerId, grantUid, myUid: myServeUid })
|
|
688
|
-
if (iServeAsGrantee) process.stderr.write(`\n ◆ serving room ${room} by the owner's grant — will yield if the owner's bridge returns or the grant is revoked.\n`)
|
|
731
|
+
if (iServeAsGrantee) process.stderr.write(`\n [b:${BRIDGE_ID} r:${room}] ◆ serving room ${room} by the owner's grant — will yield if the owner's bridge returns or the grant is revoked.\n`)
|
|
689
732
|
} catch (e) {
|
|
690
733
|
// A THROWN fetchServeRow error is either a definitively-bad auth (401/403) or an
|
|
691
734
|
// availability failure (5xx / transport reject). Only the latter fails OPEN — a stale
|
|
@@ -1006,7 +1049,7 @@ function onPeerBridgeAnnounce (payload) {
|
|
|
1006
1049
|
if (yielding) return
|
|
1007
1050
|
if (!standDownOnPeerAnnounce({ myUid: myServeUid, ownerId: roomOwnerId, peerUid: payload?.uid })) return
|
|
1008
1051
|
yielding = true
|
|
1009
|
-
process.stderr.write(`\n ◆ owner's bridge reconnected — yielding room ${room}.\n`)
|
|
1052
|
+
process.stderr.write(`\n [b:${BRIDGE_ID} r:${room}] ◆ owner's bridge reconnected — yielding room ${room}.\n`)
|
|
1010
1053
|
// Exit with the grant-reclaim SENTINEL (not a clean 0). A supervisor that spawned this
|
|
1011
1054
|
// grantee child reads the code and puts the room on a cooldown, so it does NOT respawn a
|
|
1012
1055
|
// child that will only yield again — the owner-serves/grantee-stands-by coexistence state
|
|
@@ -2782,7 +2825,7 @@ channel
|
|
|
2782
2825
|
.subscribe(status => {
|
|
2783
2826
|
if (status === 'SUBSCRIBED') {
|
|
2784
2827
|
realtimeHealthy = true; brokenSince = 0
|
|
2785
|
-
trackPresence({ name, role: 'bridge' })
|
|
2828
|
+
trackPresence({ name, role: 'bridge', bridge_id: BRIDGE_ID })
|
|
2786
2829
|
const startCmd = attachedCmd || autoAgent // autoAgent: account-mode auto-open (headless)
|
|
2787
2830
|
if (startCmd && !terms.size && !sessions.size) {
|
|
2788
2831
|
// Claude + structured mode → Agent SDK session; everything else → PTY.
|