thinkpool-pair 0.7.318 → 0.7.319
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 +21 -8
- package/package.json +1 -1
- package/presence.mjs +34 -0
package/account.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { createClient } from '@supabase/supabase-js'
|
|
|
15
15
|
import os from 'node:os'
|
|
16
16
|
import { saveAuth, loadAuth, markAuthReconnectRequired, loadDirs, bindDir, loadServed, rememberServed, loadDefaultDir, saveDefaultDir, claudeRecentProjectDir } from './auth-store.mjs'
|
|
17
17
|
import { isSafeToRestart } from './update-gate.mjs'
|
|
18
|
-
import { makeThrottledTrack, presenceSelfEchoObservation } from './presence.mjs'
|
|
18
|
+
import { makeThrottledTrack, presenceRecoveryEligible, presenceSelfEchoObservation, reducePresenceChannelEvidence } from './presence.mjs'
|
|
19
19
|
import { publicKeyB64, announceProviders, listProviders, addProvider, addProviderModel, removeProvider, unseal } from './providers.mjs'
|
|
20
20
|
import { supervisorServes, supervisorRoomsToStop } from './serve-consent.mjs'
|
|
21
21
|
import { resolveServeDir } from './serve-dir.mjs'
|
|
@@ -679,8 +679,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
679
679
|
// and stamps presenceSyncSeenAt. acctSubscribedAt bounds the startup case where
|
|
680
680
|
// SUBSCRIBED fires but the first sync never arrives: that is a broken Realtime
|
|
681
681
|
// channel, not an unknowable state, once the listener was attached before join.
|
|
682
|
-
let
|
|
683
|
-
let acctSubscribedAt = 0
|
|
682
|
+
let presenceEvidence = reducePresenceChannelEvidence()
|
|
684
683
|
let initialTickDone = false
|
|
685
684
|
let readinessWritten = false
|
|
686
685
|
const publishReadiness = () => {
|
|
@@ -691,14 +690,24 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
691
690
|
readinessWritten = writeSupervisorReady({ version: VERSION, bridgeId: BRIDGE_ID })
|
|
692
691
|
if (!readinessWritten) process.stderr.write('\n ◇ Could not record local supervisor readiness; the bridge remains running but installers will not claim it was confirmed.\n')
|
|
693
692
|
}
|
|
694
|
-
acct.on('presence', { event: 'sync' }, () => {
|
|
693
|
+
acct.on('presence', { event: 'sync' }, () => {
|
|
694
|
+
presenceEvidence = reducePresenceChannelEvidence(presenceEvidence, 'sync')
|
|
695
|
+
publishReadiness()
|
|
696
|
+
})
|
|
695
697
|
|
|
696
698
|
// Re-track on EVERY (re)subscribe, not just the first: a realtime reconnect
|
|
697
699
|
// (network blip, or a token swap mid-flight) rejoins the channel and must
|
|
698
700
|
// re-announce, or the dashboard would read "no bridge" until the next restart.
|
|
699
701
|
await new Promise((res) => acct.subscribe((st) => {
|
|
700
|
-
if (st === 'SUBSCRIBED') {
|
|
702
|
+
if (st === 'SUBSCRIBED') {
|
|
703
|
+
presenceEvidence = reducePresenceChannelEvidence(presenceEvidence, st)
|
|
704
|
+
pushPresence(); res()
|
|
705
|
+
}
|
|
701
706
|
else if (st === 'CLOSED' || st === 'CHANNEL_ERROR' || st === 'TIMED_OUT') {
|
|
707
|
+
// Invalidate the old generation before inspecting presenceState again. The
|
|
708
|
+
// channel retains a local self key across failures; accepting that cache is
|
|
709
|
+
// what left the managed service zombified while a fresh foreground run worked.
|
|
710
|
+
presenceEvidence = reducePresenceChannelEvidence(presenceEvidence, st)
|
|
702
711
|
// Realtime dropped (the 2026-07-08 `realtime CLOSED` blip). The socket normally
|
|
703
712
|
// auto-reconnects, but nudge it so presence — and the claim heartbeat that shares
|
|
704
713
|
// this client — recover instead of the loop standing by against its own stale row.
|
|
@@ -1177,12 +1186,16 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
1177
1186
|
const presenceWatch = setInterval(() => {
|
|
1178
1187
|
if (stopping) return
|
|
1179
1188
|
const now = Date.now()
|
|
1189
|
+
const recoveryEligible = presenceRecoveryEligible({ claimHeld, lastClaimOkAt, now })
|
|
1180
1190
|
let present = false
|
|
1181
1191
|
try { present = !!acct.presenceState?.()[machine] } catch { present = false }
|
|
1182
1192
|
const observation = presenceSelfEchoObservation({
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1193
|
+
// A fresh HTTP claim distinguishes a Realtime-only zombie from a full
|
|
1194
|
+
// connectivity outage. In the latter case, wait in-process; a respawn cannot
|
|
1195
|
+
// restore the network and launchd would otherwise loop indefinitely.
|
|
1196
|
+
tracking: recoveryEligible,
|
|
1197
|
+
subscribedAt: Math.max(presenceEvidence.subscribedAt, lastClaimOkAt),
|
|
1198
|
+
syncSeenAt: presenceEvidence.syncSeenAt,
|
|
1186
1199
|
present,
|
|
1187
1200
|
missingSince: selfEchoMissingSince,
|
|
1188
1201
|
now,
|
package/package.json
CHANGED
package/presence.mjs
CHANGED
|
@@ -57,6 +57,40 @@ export function makeThrottledTrack(channel, { minMs = 5000, onStatus = null } =
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// A RealtimeChannel keeps its last presenceState() locally. After CHANNEL_ERROR /
|
|
61
|
+
// CLOSED that cache can still contain our own key even though the server (and every
|
|
62
|
+
// dashboard subscriber) has already removed it. Treat every subscribe/error edge as
|
|
63
|
+
// a new evidence generation: only a presence sync delivered in that generation may
|
|
64
|
+
// certify the cached self key.
|
|
65
|
+
export function reducePresenceChannelEvidence(state = {}, event, now = Date.now()) {
|
|
66
|
+
const current = {
|
|
67
|
+
subscribedAt: Number(state.subscribedAt) || 0,
|
|
68
|
+
syncSeenAt: Number(state.syncSeenAt) || 0,
|
|
69
|
+
}
|
|
70
|
+
if (event === 'sync') return { ...current, syncSeenAt: now }
|
|
71
|
+
if (event === 'SUBSCRIBED') {
|
|
72
|
+
// A reconnect loop may report SUBSCRIBED repeatedly without ever delivering
|
|
73
|
+
// presence sync. Preserve the first unsynced boundary so retries cannot keep
|
|
74
|
+
// pushing the watchdog deadline out forever.
|
|
75
|
+
if (current.subscribedAt && !current.syncSeenAt) return current
|
|
76
|
+
return { subscribedAt: now, syncSeenAt: 0 }
|
|
77
|
+
}
|
|
78
|
+
if (event === 'CLOSED' || event === 'CHANNEL_ERROR' || event === 'TIMED_OUT') {
|
|
79
|
+
// Likewise, repeated error callbacks are one failure episode until a fresh
|
|
80
|
+
// sync proves recovery.
|
|
81
|
+
if (current.subscribedAt && !current.syncSeenAt) return current
|
|
82
|
+
return { subscribedAt: now, syncSeenAt: 0 }
|
|
83
|
+
}
|
|
84
|
+
return current
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Recycling a websocket is useful only when plain HTTP is healthy enough to prove
|
|
88
|
+
// this is a Realtime-only wedge. During a full network/Supabase outage, respawning a
|
|
89
|
+
// launchd service cannot help and merely creates a restart storm.
|
|
90
|
+
export function presenceRecoveryEligible({ claimHeld, lastClaimOkAt, now, maxClaimSilenceMs = 40_000 }) {
|
|
91
|
+
return !!claimHeld && Number.isFinite(lastClaimOkAt) && (now - lastClaimOkAt) < maxClaimSilenceMs
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
/* ─────────────────────────────────────────────────────────────
|
|
61
95
|
presenceSelfEchoVerdict — the pure trigger for the account
|
|
62
96
|
supervisor's presence self-echo watchdog (2026-07-08 recurrence).
|