thinkpool-pair 0.7.303 → 0.7.305

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 CHANGED
@@ -15,7 +15,7 @@ import { createClient } from '@supabase/supabase-js'
15
15
  import os from 'node:os'
16
16
  import { saveAuth, loadAuth, loadDirs, bindDir, loadServed, rememberServed, loadDefaultDir, saveDefaultDir, claudeRecentProjectDir } from './auth-store.mjs'
17
17
  import { isSafeToRestart } from './update-gate.mjs'
18
- import { makeThrottledTrack, presenceSelfEchoVerdict } from './presence.mjs'
18
+ import { makeThrottledTrack, presenceSelfEchoObservation } 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'
@@ -603,16 +603,18 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
603
603
  // respawn the supervisor every ~2.5 min ("the bridge disconnects and reconnects
604
604
  // randomly", 2026-07-09 — nine respawns in an hour, track=ok on every one).
605
605
  // Binding sync does two jobs: turns presence delivery ON (the echo becomes real)
606
- // and stamps presenceSyncSeenAt — the watchdog's fail-safe (no proven delivery →
607
- // no escalation, ever). Proof: /tmp echo-proof (no listener → {}, listener → key).
606
+ // and stamps presenceSyncSeenAt. acctSubscribedAt bounds the startup case where
607
+ // SUBSCRIBED fires but the first sync never arrives: that is a broken Realtime
608
+ // channel, not an unknowable state, once the listener was attached before join.
608
609
  let presenceSyncSeenAt = 0
610
+ let acctSubscribedAt = 0
609
611
  acct.on('presence', { event: 'sync' }, () => { presenceSyncSeenAt = Date.now() })
610
612
 
611
613
  // Re-track on EVERY (re)subscribe, not just the first: a realtime reconnect
612
614
  // (network blip, or a token swap mid-flight) rejoins the channel and must
613
615
  // re-announce, or the dashboard would read "no bridge" until the next restart.
614
616
  await new Promise((res) => acct.subscribe((st) => {
615
- if (st === 'SUBSCRIBED') { pushPresence(); res() }
617
+ if (st === 'SUBSCRIBED') { if (!acctSubscribedAt) acctSubscribedAt = Date.now(); pushPresence(); res() }
616
618
  else if (st === 'CLOSED' || st === 'CHANNEL_ERROR' || st === 'TIMED_OUT') {
617
619
  // Realtime dropped (the 2026-07-08 `realtime CLOSED` blip). The socket normally
618
620
  // auto-reconnects, but nudge it so presence — and the claim heartbeat that shares
@@ -1080,18 +1082,19 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
1080
1082
  const presenceWatch = setInterval(() => {
1081
1083
  if (stopping) return
1082
1084
  const now = Date.now()
1083
- // FAIL-SAFE: escalation requires PROVEN presence delivery on this channel (≥1
1084
- // sync ever received). An empty presenceState() without it means WE can't see —
1085
- // not that the track failed; respawning on that is the 0.7.174 false-positive
1086
- // loop. A real zombie (the bug this watchdog exists for) still escalates:
1087
- // delivery was proven earlier, then our key vanished from the server's state.
1088
- if (!presenceSyncSeenAt) { selfEchoMissingSince = 0; return }
1089
1085
  let present = false
1090
1086
  try { present = !!acct.presenceState?.()[machine] } catch { present = false }
1091
- if (present) { selfEchoMissingSince = 0; presenceRecycledForMissing = 0; return }
1092
- if (!claimHeld) { selfEchoMissingSince = 0; return } // a standby bridge doesn't announce — never escalate
1093
- if (!selfEchoMissingSince) selfEchoMissingSince = now
1094
- const verdict = presenceSelfEchoVerdict({ tracking: claimHeld, selfEchoMissingSince, now })
1087
+ const observation = presenceSelfEchoObservation({
1088
+ tracking: claimHeld,
1089
+ subscribedAt: acctSubscribedAt,
1090
+ syncSeenAt: presenceSyncSeenAt,
1091
+ present,
1092
+ missingSince: selfEchoMissingSince,
1093
+ now,
1094
+ })
1095
+ selfEchoMissingSince = observation.missingSince
1096
+ if (!selfEchoMissingSince) { presenceRecycledForMissing = 0; return }
1097
+ const verdict = observation.verdict
1095
1098
  if (verdict === 'recycle') {
1096
1099
  if (presenceRecycledForMissing === selfEchoMissingSince) return // already recycled this episode — wait out the respawn window
1097
1100
  presenceRecycledForMissing = selfEchoMissingSince
package/flow-models.mjs CHANGED
@@ -12,6 +12,9 @@ const CLAUDE_TIERS = {
12
12
  const CODEX_SCAFFOLD = ['gpt-5.6-luna', 'gpt-5.4-mini', 'gpt-5.3-codex-spark']
13
13
  const CODEX_BALANCED = ['gpt-5.6-terra', 'gpt-5.4']
14
14
  const HERMES_REVIEW = ['nous:anthropic/claude-sonnet-4.6', 'nous:anthropic/claude-sonnet-4.5', 'nous:anthropic/claude-sonnet-4']
15
+ const HERMES_MODEL_ALIASES = Object.freeze({
16
+ fable: 'nous:anthropic/claude-fable-5',
17
+ })
15
18
 
16
19
  export function normalizeFlowRuntime(runtime, fallback = null) {
17
20
  if (runtime === 'claude' || runtime === 'codex' || runtime === 'hermes') return runtime
@@ -27,6 +30,8 @@ export function modelCatalogValues(catalog = []) {
27
30
  const exactHermesModelId = (value) => /^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+\/[A-Za-z0-9._:/-]+$/.test(value)
28
31
 
29
32
  const expandHermesModelAlias = (value) => {
33
+ const named = HERMES_MODEL_ALIASES[value.toLowerCase()]
34
+ if (named) return named
30
35
  if (/^glm[-_.]/i.test(value)) return `nous:z-ai/${value.toLowerCase()}`
31
36
  const shorthand = value.match(/^([^:/]+):([^/]+)$/)
32
37
  if (!shorthand) return value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.303",
3
+ "version": "0.7.305",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {
package/presence.mjs CHANGED
@@ -90,3 +90,37 @@ export function presenceSelfEchoVerdict({ tracking, selfEchoMissingSince, now, g
90
90
  if (missingMs >= graceMs) return 'recycle'
91
91
  return 'ok'
92
92
  }
93
+
94
+ // Turn the supervisor's raw account-channel observation into the state consumed by
95
+ // presenceSelfEchoVerdict. A successful SUBSCRIBED callback is enough to start a
96
+ // bounded startup grace period even when Supabase never sends the first presence
97
+ // sync. Without that bound, `syncSeenAt === 0` disabled the watchdog forever: the
98
+ // HTTP claim stayed healthy while account presence and every private pair bus were
99
+ // dead, so a genuinely live partner room was reported as offline indefinitely.
100
+ //
101
+ // Once a sync has been seen, the original fail-safe still applies normally: a
102
+ // present self key clears the episode; a missing key starts at the observation tick.
103
+ export function presenceSelfEchoObservation({
104
+ tracking,
105
+ subscribedAt,
106
+ syncSeenAt,
107
+ present,
108
+ missingSince,
109
+ now,
110
+ graceMs = 60_000,
111
+ respawnMs = 90_000,
112
+ }) {
113
+ if (!tracking || !subscribedAt) return { missingSince: 0, verdict: 'ok' }
114
+ if (syncSeenAt && present) return { missingSince: 0, verdict: 'ok' }
115
+ const nextMissingSince = missingSince || (syncSeenAt ? now : subscribedAt)
116
+ return {
117
+ missingSince: nextMissingSince,
118
+ verdict: presenceSelfEchoVerdict({
119
+ tracking,
120
+ selfEchoMissingSince: nextMissingSince,
121
+ now,
122
+ graceMs,
123
+ respawnMs,
124
+ }),
125
+ }
126
+ }