thinkpool-pair 0.7.335 → 0.7.336
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 +17 -1
- package/bridge.mjs +7 -2
- package/hermes-model-cache.mjs +54 -0
- package/package.json +2 -1
package/account.mjs
CHANGED
|
@@ -526,6 +526,11 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
526
526
|
// never impersonate a completed restart.
|
|
527
527
|
const BRIDGE_ID = randomUUID()
|
|
528
528
|
let restarting = false // set by the "restart" broadcast → stop() exits non-zero so a supervisor respawns us
|
|
529
|
+
// Account-control handlers are registered before the discovery loop is
|
|
530
|
+
// initialized. Route early requests through a harmless placeholder, then
|
|
531
|
+
// replace it once `tick` exists; this avoids a temporal-dead-zone crash during
|
|
532
|
+
// supervisor bootstrap while still letting a live web room request discovery.
|
|
533
|
+
let requestDiscoveryTick = () => {}
|
|
529
534
|
// Web "attach this session" card → bind an unbound/new room to a local dir (and
|
|
530
535
|
// optionally set it as the account default) so it serves WITHOUT a terminal command.
|
|
531
536
|
// Rides the private, owner-gated control topic; the dir must exist on THIS machine.
|
|
@@ -538,7 +543,17 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
538
543
|
bindDir(code, dir)
|
|
539
544
|
if (payload?.setDefault) { saveDefaultDir(dir); process.stderr.write(`\n ◆ default project dir set → ${dir}\n`) }
|
|
540
545
|
process.stderr.write(`\n ◆ ${code} bound → ${dir} (from the web). Serving…\n`)
|
|
541
|
-
|
|
546
|
+
requestDiscoveryTick()
|
|
547
|
+
})
|
|
548
|
+
// A newly-created web room already knows its owner and has an authenticated,
|
|
549
|
+
// owner-private account-control channel. Nudge discovery immediately instead
|
|
550
|
+
// of waiting for the supervisor's 15-second reconciliation interval. `tick`
|
|
551
|
+
// still re-queries owner-owned active rooms, so the browser cannot make this
|
|
552
|
+
// host serve an arbitrary code.
|
|
553
|
+
acctControl.on('broadcast', { event: 'serve-room' }, ({ payload }) => {
|
|
554
|
+
const code = String(payload?.code || '').toUpperCase().trim()
|
|
555
|
+
if (!/^[A-Z0-9]{4,12}$/.test(code)) return
|
|
556
|
+
requestDiscoveryTick()
|
|
542
557
|
})
|
|
543
558
|
// Dashboard "Disconnect" → broadcast shutdown → clean exit (presence leaves, bar flips live).
|
|
544
559
|
// If we're the auto-restarting background service (launchd KeepAlive / systemd
|
|
@@ -906,6 +921,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
906
921
|
tickingSince = now
|
|
907
922
|
try { await tickBody() } finally { if (tickingSince === now) tickingSince = 0 }
|
|
908
923
|
}
|
|
924
|
+
requestDiscoveryTick = () => { void tick().catch(() => {}) }
|
|
909
925
|
// Expose for the watchdog to force-unwedge the guard after a reconnect.
|
|
910
926
|
const clearTickGuard = () => { tickingSince = 0 }
|
|
911
927
|
const tickBody = async () => {
|
package/bridge.mjs
CHANGED
|
@@ -130,6 +130,7 @@ import { fetchPairControlDeliveryAuthority } from './pair-control-authority.mjs'
|
|
|
130
130
|
import { SIDE_HANDOFF_PROMPT, SIDE_ROLE_PROMPT, appendSideContext, assistantTextSince, dispatchSideContexts, sideContextBlock, sideSnapshot } from './side-lane.mjs'
|
|
131
131
|
import { planMeterLine } from './plan-meters.mjs'
|
|
132
132
|
import { priceForModel } from './model-prices.mjs'
|
|
133
|
+
import { loadHermesModelCache, saveHermesModelCache } from './hermes-model-cache.mjs'
|
|
133
134
|
import { makeThrottledTrack, recoveryBackoffMs } from './presence.mjs'
|
|
134
135
|
import { MockupDeliveryQueue, completeMockupManifest, isMockupDeliveryBoundary } from './mockup-delivery.mjs'
|
|
135
136
|
import { resolveAnonKey, DEFAULT_SUPABASE_URL } from './supabase-key.mjs'
|
|
@@ -140,6 +141,10 @@ import { buildTerminalRolePrompt, HERMES_VISIBLE_WORKER_FALLBACK_RULE, THINKPOOL
|
|
|
140
141
|
const SUPABASE_URL = process.env.TP_SUPABASE_URL || DEFAULT_SUPABASE_URL
|
|
141
142
|
const WEB_BASE = process.env.TP_WEB_BASE || 'https://thinkpool.io'
|
|
142
143
|
const IMAGE_QUEUE_CONFIG = imageQueueConfig()
|
|
144
|
+
// Shared by every Hermes lane in this room child and persisted across room
|
|
145
|
+
// children. Without this, each new room must boot ACP (up to 30s) merely to
|
|
146
|
+
// discover the same public model list before its first real turn.
|
|
147
|
+
let hostHermesModels = loadHermesModelCache()
|
|
143
148
|
|
|
144
149
|
// The anon key is RESOLVED, not baked. It used to be a string literal here, and
|
|
145
150
|
// that literal now sits in 224 published tarballs on 224 users' laptops: disable
|
|
@@ -2139,7 +2144,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2139
2144
|
? spawnDepth
|
|
2140
2145
|
: (sideParent || (spawnedBy && !String(spawnedBy).startsWith('flow:')) ? 1 : 0)
|
|
2141
2146
|
const initialHop = Number.isInteger(hop) && hop >= 0 ? hop : structuralDepth
|
|
2142
|
-
const entry = { cmd: runtime, runtime, kind: 'structured', log: Array.isArray(log) ? log.slice(-STRUCTURED_LOG_MAX) : [], pending: new Map(), session: null, recovered: false, commands: commandCatalogForRuntime(runtime, commands), mode, effort, models: runtime === 'codex' ? codexModels : runtime === 'hermes'
|
|
2147
|
+
const entry = { cmd: runtime, runtime, kind: 'structured', log: Array.isArray(log) ? log.slice(-STRUCTURED_LOG_MAX) : [], pending: new Map(), session: null, recovered: false, commands: commandCatalogForRuntime(runtime, commands), mode, effort, models: runtime === 'codex' ? codexModels : runtime === 'hermes' ? (Array.isArray(models) && models.length ? models : hostHermesModels) : undefined,
|
|
2143
2148
|
// model: truthful active-model label — now the SAME `laneModel` the SDK is given, so the
|
|
2144
2149
|
// chip cannot disagree with the wire. When this lane runs on a custom (non-anthropic)
|
|
2145
2150
|
// registered provider the SDK id is impersonated (see the onEvent guard below), so
|
|
@@ -3311,7 +3316,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
3311
3316
|
// A custom Anthropic-compatible endpoint may expose its own catalog through the
|
|
3312
3317
|
// same SDK method. Never publish that as the ROOM's Claude catalog: its models
|
|
3313
3318
|
// already render in the provider group and otherwise leak under ANTHROPIC.
|
|
3314
|
-
if (entry.runtime === 'hermes' && evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { entry.models = evt.models; announce() }
|
|
3319
|
+
if (entry.runtime === 'hermes' && evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { entry.models = saveHermesModelCache(evt.models); hostHermesModels = entry.models; announce() }
|
|
3315
3320
|
else if (onBuiltin && evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { roomModels = evt.models; announce() } }
|
|
3316
3321
|
// Hermes model changes are asynchronous ACP requests. Keep the previous model
|
|
3317
3322
|
// authoritative until Hermes acknowledges session/set_model; then settle the
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Hermes exposes its model catalog only after `session/new`, which may take up
|
|
2
|
+
// to 30 seconds while the ACP process boots. The catalog is public metadata and
|
|
3
|
+
// identical for every room served by this Hermes profile, so keep the last
|
|
4
|
+
// validated copy at the host level. A live Hermes session always replaces it.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import os from 'node:os'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
|
|
10
|
+
const MAX_MODELS = 1000
|
|
11
|
+
const MAX_BYTES = 512 * 1024
|
|
12
|
+
const MAX_FIELD = 512
|
|
13
|
+
|
|
14
|
+
const cachePath = () => process.env.TP_HERMES_MODEL_CACHE
|
|
15
|
+
|| path.join(process.env.TP_PAIR_ROOT || path.join(os.homedir(), '.thinkpool-pair'), 'hermes-models.json')
|
|
16
|
+
|
|
17
|
+
export function normalizeHermesModelCache(models) {
|
|
18
|
+
if (!Array.isArray(models)) return []
|
|
19
|
+
const seen = new Set()
|
|
20
|
+
const normalized = []
|
|
21
|
+
for (const raw of models.slice(0, MAX_MODELS)) {
|
|
22
|
+
const value = String(raw?.value || '').trim().slice(0, MAX_FIELD)
|
|
23
|
+
if (!value || seen.has(value)) continue
|
|
24
|
+
seen.add(value)
|
|
25
|
+
normalized.push({
|
|
26
|
+
value,
|
|
27
|
+
displayName: String(raw?.displayName || value).trim().slice(0, MAX_FIELD) || value,
|
|
28
|
+
description: String(raw?.description || '').trim().slice(0, MAX_FIELD),
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
return normalized
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadHermesModelCache() {
|
|
35
|
+
try {
|
|
36
|
+
const file = cachePath()
|
|
37
|
+
if ((Number(fs.statSync(file).size) || 0) > MAX_BYTES) return []
|
|
38
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
39
|
+
return normalizeHermesModelCache(parsed?.models)
|
|
40
|
+
} catch { return [] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function saveHermesModelCache(models) {
|
|
44
|
+
const normalized = normalizeHermesModelCache(models)
|
|
45
|
+
if (!normalized.length) return []
|
|
46
|
+
try {
|
|
47
|
+
const file = cachePath()
|
|
48
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
49
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
50
|
+
fs.writeFileSync(temp, JSON.stringify({ savedAt: Date.now(), models: normalized }), { mode: 0o600 })
|
|
51
|
+
fs.renameSync(temp, file)
|
|
52
|
+
} catch { /* cache is opportunistic; live discovery remains the fallback */ }
|
|
53
|
+
return normalized
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.336",
|
|
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": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"codex-commands.mjs",
|
|
37
37
|
"acp-client.mjs",
|
|
38
38
|
"hermes-session.mjs",
|
|
39
|
+
"hermes-model-cache.mjs",
|
|
39
40
|
"hermes-policy.mjs",
|
|
40
41
|
"hermes-acp-bootstrap.py",
|
|
41
42
|
"hermes-event-mapper.mjs",
|