thinkpool-pair 0.7.191 → 0.7.193
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/bridge.mjs +9 -29
- package/codex-session.mjs +10 -11
- package/package.json +1 -1
- package/service.mjs +29 -2
package/bridge.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import os from 'node:os'
|
|
|
32
32
|
import fs from 'node:fs'
|
|
33
33
|
import path from 'node:path'
|
|
34
34
|
import readline from 'node:readline'
|
|
35
|
-
import { spawn,
|
|
35
|
+
import { spawn, execFileSync } from 'node:child_process'
|
|
36
36
|
import { fileURLToPath } from 'node:url'
|
|
37
37
|
import { createRequire } from 'node:module'
|
|
38
38
|
import { randomUUID } from 'node:crypto'
|
|
@@ -50,7 +50,7 @@ import { validateProviderSwitch, providerSwitchPlan, BUILTIN_PROVIDER } from './
|
|
|
50
50
|
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
|
|
51
51
|
import { z } from 'zod'
|
|
52
52
|
import { startClaudeSession } from './claude-session.mjs'
|
|
53
|
-
import { startCodexSession, readCodexDefaultModel } from './codex-session.mjs'
|
|
53
|
+
import { startCodexSession, readCodexDefaultModel, readCodexModels } from './codex-session.mjs'
|
|
54
54
|
import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
|
|
55
55
|
import { normalizePlanOutput, laneModelFor } from './flow-task-graph.mjs' // FL-B1 — validate the conductor's submit_flow_plan task-graph; laneModelFor — per-slice model tier (2026-07-03-flow-lane-model-tiers)
|
|
56
56
|
// S1 (context-offload) — durable digest store. mark_flow_done digests a closed slice in;
|
|
@@ -232,8 +232,9 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
|
232
232
|
if (argv[0] === 'restart-service') {
|
|
233
233
|
const arg1 = argv[1] || ''
|
|
234
234
|
const svcRoom = (!arg1 || arg1.startsWith('-')) ? null : arg1.toUpperCase().trim()
|
|
235
|
-
const svc = await import('./service.mjs')
|
|
236
|
-
|
|
235
|
+
const svc = await import('./service.mjs')
|
|
236
|
+
const ok = argv.includes('--current') ? svc.restartService(svcRoom) : svc.updateService(svcRoom)
|
|
237
|
+
process.exit(ok ? 0 : 1)
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
// Account mode (Phase 1, docs/specs/2026-06-16-account-bridge.md): one bridge per
|
|
@@ -270,7 +271,6 @@ if (argv[0] === 'provider') { const { runProvider } = await import('./provider.m
|
|
|
270
271
|
if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
271
272
|
const { runLauncher, detectState } = await import('./launcher.mjs')
|
|
272
273
|
const svc = await import('./service.mjs')
|
|
273
|
-
const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return 'latest' } })()
|
|
274
274
|
const BRIDGE = fileURLToPath(import.meta.url)
|
|
275
275
|
const io = {
|
|
276
276
|
print: (s) => process.stderr.write(s + '\n'),
|
|
@@ -294,28 +294,8 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
|
294
294
|
installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []) },
|
|
295
295
|
uninstallService: ({ room = null } = {}) => { svc.uninstallService(room) },
|
|
296
296
|
restartService: ({ room = null } = {}) => { svc.restartService(room) },
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
// each start so it can't re-serve an old build). We must NOT trust PKG_VERSION here:
|
|
300
|
-
// npx can run a STALE cached launcher, so this very menu may be an old version — that's
|
|
301
|
-
// the bug that left the bridge stuck on 0.7.75. `npm view` hits the registry live, so it
|
|
302
|
-
// updates correctly even from a stale launcher. Pinned to the resolved number — no auto-roll.
|
|
303
|
-
restartUpdateService: ({ room = null } = {}) => {
|
|
304
|
-
let target = null
|
|
305
|
-
try { target = execSync('npm view thinkpool-pair version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() } catch { /* offline / no npm */ }
|
|
306
|
-
if (!/^\d+\.\d+\.\d+/.test(target || '')) {
|
|
307
|
-
// Can't reach npm to learn the newest version — DON'T silently re-pin this
|
|
308
|
-
// launcher's PKG_VERSION (npx may have run a stale cached launcher, so it could
|
|
309
|
-
// be OLDER than what's installed). Just RESTART the current pinned service (no
|
|
310
|
-
// version change) and say so honestly — never call a same-version restart an
|
|
311
|
-
// "update".
|
|
312
|
-
process.stderr.write(`\n ⚠ couldn't reach npm to check for a newer version — restarting the current version, NO update applied. Try again when online.\n`)
|
|
313
|
-
svc.restartService(room)
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
process.stderr.write(`\n ◆ updating the background service to v${target} (newest published) and restarting…\n`)
|
|
317
|
-
svc.installService(room, [], { version: target, staleProof: true }) // rewrites plist/unit at resolved version + reloads = update + restart
|
|
318
|
-
},
|
|
297
|
+
// Menu and CLI share the same registry-resolve → repin → reload implementation.
|
|
298
|
+
restartUpdateService: ({ room = null } = {}) => { svc.updateService(room) },
|
|
319
299
|
login: async () => { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) },
|
|
320
300
|
// Provider config is NOT terminal — it writes provider.json and returns to the
|
|
321
301
|
// menu so you can pick a model, switch back, or do something else. (It used to
|
|
@@ -1070,7 +1050,7 @@ const announce = () => {
|
|
|
1070
1050
|
// turnActive). Authoritative — the bridge owns the turn — so the /code
|
|
1071
1051
|
// dashboard reads "N agents working" off the roster instead of sniffing
|
|
1072
1052
|
// the live code-event stream (which a passive dashboard channel misses).
|
|
1073
|
-
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, busy: s.session?.turnActive ?? false, commands: s.commands, mode: s.mode || undefined, name: termNames[id] || undefined, model: s.model || undefined, ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
|
|
1053
|
+
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, busy: s.session?.turnActive ?? false, commands: s.commands, mode: s.mode || undefined, name: termNames[id] || undefined, model: s.model || undefined, ...(s.runtime === 'codex' ? { approvalPolicy: 'never', models: s.models || [] } : {}), ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
|
|
1074
1054
|
// provider: the registered LLM-provider this lane runs on, NAME-ONLY {id,name}
|
|
1075
1055
|
// (NEVER the key or baseUrl). Additive; older clients ignore it. Omitted for the
|
|
1076
1056
|
// built-in/default Claude path (no badge). Makes the lane's provider badge +
|
|
@@ -1580,7 +1560,7 @@ function openStructured({ id, runtime = 'claude', model, resume, log, commands,
|
|
|
1580
1560
|
// spawnedBy: set when this lane was Dispatched (spawn_terminal). Restored from the
|
|
1581
1561
|
// session store so the Ensemble flag survives a bridge restart (else a respin
|
|
1582
1562
|
// stripped it and the lane reverted to a plain tab — the t6 "no chip" bug).
|
|
1583
|
-
const entry = { cmd: runtime, runtime, kind: 'structured', log: Array.isArray(log) ? log.slice(-STRUCTURED_LOG_MAX) : [], pending: new Map(), session: null, recovered: false, commands: Array.isArray(commands) ? commands : undefined, mode,
|
|
1563
|
+
const entry = { cmd: runtime, runtime, kind: 'structured', log: Array.isArray(log) ? log.slice(-STRUCTURED_LOG_MAX) : [], pending: new Map(), session: null, recovered: false, commands: Array.isArray(commands) ? commands : undefined, mode, models: runtime === 'codex' ? readCodexModels() : undefined,
|
|
1584
1564
|
// model: truthful active-model label — now the SAME `laneModel` the SDK is given, so the
|
|
1585
1565
|
// chip cannot disagree with the wire. When this lane runs on a custom (non-anthropic)
|
|
1586
1566
|
// registered provider the SDK id is impersonated (see the onEvent guard below), so
|
package/codex-session.mjs
CHANGED
|
@@ -43,6 +43,16 @@ export function readCodexDefaultModel({ home = process.env.CODEX_HOME || path.jo
|
|
|
43
43
|
} catch { return null }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export function readCodexModels({ home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), fsImpl = fs } = {}) {
|
|
47
|
+
try {
|
|
48
|
+
const data = JSON.parse(fsImpl.readFileSync(path.join(home, 'models_cache.json'), 'utf8'))
|
|
49
|
+
return (Array.isArray(data?.models) ? data.models : [])
|
|
50
|
+
.filter((m) => m?.slug && m.visibility !== 'hide')
|
|
51
|
+
.sort((a, b) => (Number(a.priority) || 999) - (Number(b.priority) || 999))
|
|
52
|
+
.map((m) => ({ value: m.slug, displayName: m.display_name || m.slug, description: m.description || '' }))
|
|
53
|
+
} catch { return [] }
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
export function normalizeCodexSandbox(value) {
|
|
47
57
|
return SAFE_SANDBOXES.has(value) ? value : DEFAULT_SANDBOX
|
|
48
58
|
}
|
|
@@ -97,8 +107,6 @@ export function startCodexSession({ cwd, model, resume, env, sandbox = DEFAULT_S
|
|
|
97
107
|
|
|
98
108
|
const mapper = new CodexEventMapper({ onEvent, model: model || null })
|
|
99
109
|
|
|
100
|
-
const note = (text) => { try { onEvent?.({ kind: 'note', text }) } catch { /* noop */ } }
|
|
101
|
-
|
|
102
110
|
function runExec(prompt) {
|
|
103
111
|
// First turn: fresh exec. Later turns: resume THIS lane's captured thread.
|
|
104
112
|
// `--last` is process-global and can cross-wire two concurrently-active Codex
|
|
@@ -123,19 +131,12 @@ export function startCodexSession({ cwd, model, resume, env, sandbox = DEFAULT_S
|
|
|
123
131
|
child = spawn('codex', args, opts)
|
|
124
132
|
if (stdinFd != null) { try { fs.closeSync(stdinFd) } catch { /* child owns its duplicated fd */ } }
|
|
125
133
|
|
|
126
|
-
// G1 mitigation: while codex composes (before the first item event), show life.
|
|
127
|
-
let sawFirst = false
|
|
128
|
-
const composingTimer = setTimeout(() => {
|
|
129
|
-
if (!sawFirst) note('codex composing…')
|
|
130
|
-
}, 1200)
|
|
131
|
-
|
|
132
134
|
const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity })
|
|
133
135
|
rl.on('line', (line) => {
|
|
134
136
|
if (!line.trim()) return
|
|
135
137
|
let ev
|
|
136
138
|
try { ev = JSON.parse(line) } catch { return /* non-JSON progress line */
|
|
137
139
|
}
|
|
138
|
-
if (!sawFirst && ev.type && ev.type.startsWith('item')) sawFirst = true
|
|
139
140
|
if (ev.type === 'thread.started' && ev.thread_id) sessionId = sessionId || ev.thread_id
|
|
140
141
|
if (ev.type === 'turn.completed' || ev.type === 'turn.failed') sawTerminalResult = true
|
|
141
142
|
mapper.push(ev)
|
|
@@ -149,7 +150,6 @@ export function startCodexSession({ cwd, model, resume, env, sandbox = DEFAULT_S
|
|
|
149
150
|
const finish = (code) => {
|
|
150
151
|
if (settled) return
|
|
151
152
|
settled = true
|
|
152
|
-
clearTimeout(composingTimer)
|
|
153
153
|
child = null
|
|
154
154
|
turnActive = false
|
|
155
155
|
if (aborted && !sawTerminalResult) {
|
|
@@ -162,7 +162,6 @@ export function startCodexSession({ cwd, model, resume, env, sandbox = DEFAULT_S
|
|
|
162
162
|
child.on('error', (e) => {
|
|
163
163
|
if (settled) return
|
|
164
164
|
settled = true
|
|
165
|
-
clearTimeout(composingTimer)
|
|
166
165
|
try { onEvent?.({ kind: 'error', message: `codex failed to start: ${e.message}`, recoverable: true }) } catch { /* noop */ }
|
|
167
166
|
child = null
|
|
168
167
|
turnActive = false
|
package/package.json
CHANGED
package/service.mjs
CHANGED
|
@@ -43,7 +43,12 @@ const shq = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`
|
|
|
43
43
|
// which is what left Max's account bridge stuck on 0.7.75 while @latest was 0.7.78.
|
|
44
44
|
// `home` is the platform-correct, runtime-expanded home token ("$HOME" on darwin under
|
|
45
45
|
// /bin/bash; "$$HOME" inside a systemd unit so systemd emits a literal $HOME to bash).
|
|
46
|
-
|
|
46
|
+
// npx caches the temporary execution project as a unit. Removing only the package can
|
|
47
|
+
// leave its generated .bin/package-lock metadata behind, producing the contradictory
|
|
48
|
+
// state where `npm view` sees a version but `npx` reports ETARGET or no executable.
|
|
49
|
+
// The whole _npx directory is disposable; clear its children so the next launch is a
|
|
50
|
+
// genuinely clean install.
|
|
51
|
+
const cacheWipe = (home) => `rm -rf ${home}/.npm/_npx/* 2>/dev/null`
|
|
47
52
|
|
|
48
53
|
// The version installing the service. By DEFAULT the service is pinned to this exact
|
|
49
54
|
// version (not @latest) so a future bad npm publish can't auto-roll to every machine's
|
|
@@ -83,7 +88,7 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
83
88
|
// 0.7.87). `--prefer-online` forces npx to REVALIDATE the packument against the
|
|
84
89
|
// registry every resolve, so `@latest` always means the true latest. Only needed for
|
|
85
90
|
// the @latest (autoUpdate) tier — a pinned exact version is immutable, no staleness.
|
|
86
|
-
const onlineFlag = autoUpdate ? ['--prefer-online'] : []
|
|
91
|
+
const onlineFlag = (autoUpdate || staleProof) ? ['--prefer-online'] : []
|
|
87
92
|
const pkgArgs = room ? ['-y', ...onlineFlag, verSpec, room] : ['-y', ...onlineFlag, verSpec]
|
|
88
93
|
// In-process self-update (account.mjs / bridge.mjs) is gated on this env — only on when
|
|
89
94
|
// the user explicitly opted into auto-update.
|
|
@@ -353,6 +358,28 @@ export function restartService(room) {
|
|
|
353
358
|
}
|
|
354
359
|
}
|
|
355
360
|
|
|
361
|
+
// One authoritative update path for both the interactive menu and `restart-service`.
|
|
362
|
+
// Resolve the registry version first, then rewrite + reload the service pinned to that
|
|
363
|
+
// exact build. If npm is unavailable, fail without pretending an old-version restart
|
|
364
|
+
// was an update.
|
|
365
|
+
export function updateService(room, { exec = execSync, install = installService, active = isServiceInstalled } = {}) {
|
|
366
|
+
if (!active(room)) {
|
|
367
|
+
process.stderr.write(' ⚠ no background service installed to update — install one first.\n')
|
|
368
|
+
return false
|
|
369
|
+
}
|
|
370
|
+
let target = ''
|
|
371
|
+
try {
|
|
372
|
+
target = String(exec('npm view thinkpool-pair version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })).trim()
|
|
373
|
+
} catch { /* reported below */ }
|
|
374
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(target)) {
|
|
375
|
+
process.stderr.write(" ⚠ couldn't reach npm or resolve the published version — bridge left unchanged; NO update or restart was applied.\n")
|
|
376
|
+
return false
|
|
377
|
+
}
|
|
378
|
+
process.stderr.write(`\n ◆ updating the background service to v${target} and restarting…\n`)
|
|
379
|
+
install(room, [], { version: target, staleProof: true })
|
|
380
|
+
return true
|
|
381
|
+
}
|
|
382
|
+
|
|
356
383
|
// Ground truth: is the OS service manager ACTUALLY running our service right now?
|
|
357
384
|
// A leftover plist/unit on disk does NOT mean it's loaded — `launchctl load` can
|
|
358
385
|
// silently no-op (the 2026-06-26 "says installed but it's off" bug), and a unit can
|