thinkpool-pair 0.7.241 → 0.7.242
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 +21 -8
- package/cross-terminal.mjs +41 -0
- package/hermes-session.mjs +10 -3
- package/package.json +1 -1
- package/thinkpool-room-prompt.mjs +6 -0
package/bridge.mjs
CHANGED
|
@@ -105,7 +105,7 @@ const flowRedispatch = new Map()
|
|
|
105
105
|
// wave BEFORE overrun. Lives bridge-side because waves dispatch across separate
|
|
106
106
|
// broadcasts; without persistent state the cap can never bite.
|
|
107
107
|
const flowBudgets = new Map()
|
|
108
|
-
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, laneStatusOf } from './cross-terminal.mjs'
|
|
108
|
+
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, laneStatusOf, nativeClaudeFallbackHint } from './cross-terminal.mjs'
|
|
109
109
|
import { dispatchPendingRecap, recoverInterruptedTurn, recoverMissingResumeOnce, sendInterruptedContinue, shouldAttemptNativeResume, supersedeInterruptedResume } from './interrupted-resume.mjs'
|
|
110
110
|
import { turnInFlight } from './update-gate.mjs'
|
|
111
111
|
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage } from './session-store.mjs'
|
|
@@ -114,7 +114,7 @@ import { SIDE_HANDOFF_PROMPT, SIDE_ROLE_PROMPT, appendSideContext, assistantText
|
|
|
114
114
|
import { planMeterLine } from './plan-meters.mjs'
|
|
115
115
|
import { makeThrottledTrack } from './presence.mjs'
|
|
116
116
|
import { resolveAnonKey, DEFAULT_SUPABASE_URL } from './supabase-key.mjs'
|
|
117
|
-
import { buildTerminalRolePrompt } from './thinkpool-room-prompt.mjs'
|
|
117
|
+
import { buildTerminalRolePrompt, HERMES_VISIBLE_WORKER_FALLBACK_RULE } from './thinkpool-room-prompt.mjs'
|
|
118
118
|
|
|
119
119
|
// Public client creds (the same anon values the web app ships — safe to embed).
|
|
120
120
|
// Override with TP_SUPABASE_URL / TP_SUPABASE_ANON if you ever need to.
|
|
@@ -2014,10 +2014,19 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2014
2014
|
const resolveAgentOpen = (args, { worker = false } = {}) => {
|
|
2015
2015
|
const runtime = args?.runtime || entry.runtime || 'claude'
|
|
2016
2016
|
if (runtime === 'codex' && args?.provider) return { error: 'A Codex terminal uses its Codex/OpenAI login in v1; custom bridge providers are not wired to Codex yet. Omit provider or open a Claude terminal for that provider.' }
|
|
2017
|
-
|
|
2017
|
+
if (runtime === 'hermes' && args?.provider) return { error: 'A Hermes terminal uses its isolated Hermes profile and ACP model catalog; bridge provider IDs do not apply. Omit provider and choose an exact Hermes model ID.' }
|
|
2018
|
+
if (runtime === 'hermes' && (!worker || entry.runtime !== 'hermes')) return { error: 'A Hermes worker can only be opened by a top-level Hermes terminal with a live ACP model catalog.' }
|
|
2019
|
+
const catalog = runtime === 'codex' ? readCodexModels() : runtime === 'hermes' ? (entry.models || []) : []
|
|
2018
2020
|
if (runtime === 'codex' && args?.model && !modelCatalogValues(catalog).has(args.model)) {
|
|
2019
2021
|
return { error: `Could not open a Codex terminal on ${JSON.stringify(args.model)} — that model is not in this host's visible Codex catalog.` }
|
|
2020
2022
|
}
|
|
2023
|
+
if (runtime === 'hermes' && !args?.model) return { error: 'Opening a Hermes worker requires an explicit exact model ID from this session\'s ACP catalog.' }
|
|
2024
|
+
if (runtime === 'hermes' && !modelCatalogValues(catalog).has(args.model)) {
|
|
2025
|
+
return { error: `Could not open a Hermes worker on ${JSON.stringify(args.model)} — that exact model is not in this parent session's ACP catalog.` }
|
|
2026
|
+
}
|
|
2027
|
+
if (runtime === 'hermes' && args?.mode && !['default', 'acceptEdits'].includes(args.mode)) {
|
|
2028
|
+
return { error: `Hermes ACP does not structurally support ${JSON.stringify(args.mode)} mode. Use default or acceptEdits; Flow/plan/review safety remains disabled.` }
|
|
2029
|
+
}
|
|
2021
2030
|
if (runtime === 'claude' && !args?.provider && args?.model && /^gpt-/i.test(args.model)) {
|
|
2022
2031
|
return { error: `Could not open a Claude terminal on Codex model ${JSON.stringify(args.model)}. Choose runtime="codex" or a Claude model.` }
|
|
2023
2032
|
}
|
|
@@ -2070,7 +2079,11 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2070
2079
|
if (entry.peekCount > PEEK.perTurnCap) {
|
|
2071
2080
|
return { content: [{ type: 'text', text: `Cross-terminal read limit reached for this turn (${PEEK.perTurnCap}). Continue with what you have, or ask the people in the room.` }] }
|
|
2072
2081
|
}
|
|
2082
|
+
const sib = siblingsOf({ selfId: id, sessions, terms, names: termNames })
|
|
2083
|
+
const target = args?.terminal ? resolveSibling(sib, args.terminal) : null
|
|
2084
|
+
const targetEntry = target?.kind === 'agent' ? sessions.get(target.id) : null
|
|
2073
2085
|
const text = formatPeek({ selfId: id, sessions, terms, names: termNames, terminal: args?.terminal, lines: args?.lines })
|
|
2086
|
+
+ nativeClaudeFallbackHint({ parentRuntime: entry.runtime, parentModels: entry.models, targetRuntime: targetEntry?.runtime, targetLog: targetEntry?.log })
|
|
2074
2087
|
return { content: [{ type: 'text', text }] }
|
|
2075
2088
|
},
|
|
2076
2089
|
),
|
|
@@ -2252,13 +2265,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2252
2265
|
// not receive this tool, so the hierarchy is capability-enforced.
|
|
2253
2266
|
...(canSpawnWorkers ? [tool(
|
|
2254
2267
|
'spawn_terminal',
|
|
2255
|
-
'Open a WORKER SUB-TERMINAL (Claude or
|
|
2268
|
+
'Open a visible WORKER SUB-TERMINAL (Claude, Codex, or—only from a Hermes parent—Hermes) in the ThinkPool Ensemble. spawn_terminal is worker-only: it can never create a Cascade conductor or main terminal. When a person asks for a separate Cascade/conductor terminal, use open_main_terminal instead. The runtime defaults to inheriting this main/conductor terminal, or pass `runtime`. Choose `model` explicitly when practical: Codex Luna/Mini for mechanical work, Terra for ordinary feature/fix work, and a balanced tier for adversarial review; never select Sol for routine workers. A Hermes worker requires an explicit exact model ID from the parent ACP catalog. After a native Claude account/provider failure, do not retry Claude aliases on that provider; follow read_terminal\'s one-time visible Hermes fallback guidance. A Hermes review worker in default mode is permission-gated, not structurally read-only—deny/stop writes and never claim Flow safety. Pass sliceType=scaffold for mechanical work, feature/fix for builders, and review for adversarial verification. Give the worker a bounded initial task, collect its result with read_terminal, then ALWAYS close_terminal it.',
|
|
2256
2269
|
{
|
|
2257
2270
|
name: z.string().max(80).optional().describe('a short label for the new lane so the room (and you) can find it, e.g. "Research: topologies"'),
|
|
2258
2271
|
task: z.string().optional().describe('an initial task to hand the new lane immediately; omit to open it idle'),
|
|
2259
|
-
model: z.string().optional().describe('
|
|
2272
|
+
model: z.string().optional().describe('model for the chosen runtime; Hermes workers require an exact ACP catalog ID such as nous:anthropic/claude-sonnet-4.6'),
|
|
2260
2273
|
sliceType: z.enum(['scaffold', 'feature', 'fix', 'review']).optional().describe('optional cascade slice tier; omitted preserves normal inheritance/default, explicit model wins'),
|
|
2261
|
-
runtime: z.enum(['claude', 'codex']).optional().describe('agent runtime for the new lane;
|
|
2274
|
+
runtime: z.enum(['claude', 'codex', 'hermes']).optional().describe('agent runtime for the new lane; Hermes is allowed only from a top-level Hermes parent with an explicit catalog model'),
|
|
2262
2275
|
provider: z.string().optional().describe('optional registered LLM provider id to run this lane on (from the account\'s provider registry); omit for the default Claude/Anthropic path'),
|
|
2263
2276
|
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('permission mode for the new lane; defaults to inheriting YOUR current mode. Raising a lane to bypassPermissions from a non-bypass lane asks the room to confirm once.'),
|
|
2264
2277
|
},
|
|
@@ -2446,13 +2459,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2446
2459
|
// on the init `system` event (see onEvent) — a sendTurn before the input stream is consumed
|
|
2447
2460
|
// is silently lost (the 2026-07-02 auto-resume bug). Empty string → nothing to carry.
|
|
2448
2461
|
entry.pendingRecap = (typeof carryRecap === 'string' && carryRecap.trim()) ? carryRecap : null
|
|
2449
|
-
const terminalRolePrompt = buildTerminalRolePrompt({
|
|
2462
|
+
const terminalRolePrompt = [buildTerminalRolePrompt({
|
|
2450
2463
|
spawnedBy: entry.spawnedBy,
|
|
2451
2464
|
spawnDepth: entry.spawnDepth,
|
|
2452
2465
|
cascadeRole: entry.cascadeRole,
|
|
2453
2466
|
flowRole: entry.flowRole,
|
|
2454
2467
|
sideParent: entry.sideParent,
|
|
2455
|
-
})
|
|
2468
|
+
}), runtime === 'hermes' && canSpawnWorkers ? HERMES_VISIBLE_WORKER_FALLBACK_RULE : ''].filter(Boolean).join('\n\n')
|
|
2456
2469
|
entry.session = startStructuredSession(runtime, {
|
|
2457
2470
|
// laneModel, NOT the raw `model` param: the SDK's `model` option OVERRIDES the
|
|
2458
2471
|
// ANTHROPIC_MODEL supplied by resolveProviderEnv() in `env` below, so an inherited
|
package/cross-terminal.mjs
CHANGED
|
@@ -223,6 +223,47 @@ export const formatPeek = ({ selfId, sessions, terms, names = {}, terminal, line
|
|
|
223
223
|
return `Terminal ${label(hit)} (${hit.kind} · ${hit.cmd}) — last ${n} lines:\n${body}`
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
// A native Claude Code account failure is provider-wide, not model-specific. Retrying
|
|
227
|
+
// Sonnet → Opus → Haiku on the same built-in Anthropic path only repeats the same
|
|
228
|
+
// failure and burns orchestration time. Keep this classifier deliberately narrow:
|
|
229
|
+
// transient model/rate/overload errors must not disable the whole provider.
|
|
230
|
+
export const nativeClaudeProviderAccessFailed = (events) => {
|
|
231
|
+
let text = ''
|
|
232
|
+
try { text = typeof events === 'string' ? events : JSON.stringify(events || '') }
|
|
233
|
+
catch { text = String(events || '') }
|
|
234
|
+
return /organization has disabled (?:claude )?subscription access|(?:claude )?subscription access (?:has been |is )?disabled|invalid anthropic (?:api )?key|invalid x-api-key|not logged in to claude|(?:run|use) \/login[^\n]{0,80}claude|no valid anthropic (?:api )?key/i.test(text)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const HERMES_CLAUDE_REVIEW_PREFERENCE = Object.freeze([
|
|
238
|
+
'nous:anthropic/claude-sonnet-4.6',
|
|
239
|
+
'nous:anthropic/claude-sonnet-4.5',
|
|
240
|
+
'nous:anthropic/claude-sonnet-4',
|
|
241
|
+
])
|
|
242
|
+
|
|
243
|
+
export const preferredHermesClaudeReviewModel = (models = []) => {
|
|
244
|
+
const values = new Set((Array.isArray(models) ? models : []).map((model) => (
|
|
245
|
+
typeof model === 'string' ? model : model?.value || model?.slug
|
|
246
|
+
)).filter(Boolean))
|
|
247
|
+
for (const model of HERMES_CLAUDE_REVIEW_PREFERENCE) if (values.has(model)) return model
|
|
248
|
+
for (const model of values) if (/^nous:anthropic\/claude-sonnet-/i.test(model)) return model
|
|
249
|
+
for (const model of values) if (/^nous:anthropic\/claude-haiku-/i.test(model)) return model
|
|
250
|
+
for (const model of values) if (/^nous:anthropic\/claude-opus-/i.test(model)) return model
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Returned alongside read_terminal output so the orchestrator gets a structural
|
|
255
|
+
// provider verdict at the exact moment it observes the failed worker. A Hermes
|
|
256
|
+
// fallback is a visible ordinary worker, never hidden delegate_task and never Flow.
|
|
257
|
+
// Hermes ACP 0.18.2 cannot enforce read-only review, so the note states the honest
|
|
258
|
+
// permission-gated boundary instead of pretending mode="default" is structural plan.
|
|
259
|
+
export const nativeClaudeFallbackHint = ({ parentRuntime, parentModels, targetRuntime, targetLog } = {}) => {
|
|
260
|
+
if (targetRuntime !== 'claude' || !nativeClaudeProviderAccessFailed(targetLog)) return ''
|
|
261
|
+
const stop = 'ThinkPool provider status: native Claude access failed at the account/provider level. Do not retry Sonnet, Opus, Haiku, or another model alias on the same native Claude provider during this task.'
|
|
262
|
+
const fallback = parentRuntime === 'hermes' ? preferredHermesClaudeReviewModel(parentModels) : null
|
|
263
|
+
if (!fallback) return `\n\n${stop} Use a different available runtime/provider once, or report the blocker.`
|
|
264
|
+
return `\n\n${stop} For a visible manual review fallback, spawn exactly one worker with runtime="hermes", model=${JSON.stringify(fallback)}, mode="default", and sliceType="review". This Hermes lane is permission-gated, not structurally read-only: instruct it not to edit, deny/stop any write request, and do not treat its review as a Flow safety proof. Never use Hermes delegate_task.`
|
|
265
|
+
}
|
|
266
|
+
|
|
226
267
|
// ── Live per-turn room snapshot ("ROOM NOW") ─────────────────────────────────
|
|
227
268
|
// The compact tail claude-session appends to EVERY user turn's <system-reminder>
|
|
228
269
|
// (via the bridge's roomContext callback), so a lane sees the room's CURRENT
|
package/hermes-session.mjs
CHANGED
|
@@ -193,11 +193,18 @@ export function startHermesSession({
|
|
|
193
193
|
}
|
|
194
194
|
const serverModel = state?.models?.currentModelId || null
|
|
195
195
|
activeModel = requestedModel || serverModel || activeModel
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
// A spawned Hermes worker may be pinned to an exact model from its parent's
|
|
197
|
+
// ACP catalog. Acknowledge that model BEFORE publishing the initial system/
|
|
198
|
+
// models events; otherwise the worker runs the requested model while the UI
|
|
199
|
+
// and persisted resume record lie that it still uses the profile default.
|
|
200
|
+
if (requestedModel && requestedModel !== serverModel) {
|
|
199
201
|
await client.request('session/set_model', { sessionId, modelId: activeModel })
|
|
200
202
|
}
|
|
203
|
+
const publishedModels = state?.models
|
|
204
|
+
? { ...state.models, currentModelId: activeModel }
|
|
205
|
+
: activeModel ? { currentModelId: activeModel, availableModels: [] } : state?.models
|
|
206
|
+
mapper.startSession({ sessionId, models: publishedModels, modes: state?.modes, commands: [] })
|
|
207
|
+
started = true
|
|
201
208
|
const acpMode = activeMode === 'acceptEdits' ? 'accept_edits' : 'default'
|
|
202
209
|
if (state?.modes?.availableModes?.some((item) => item.id === acpMode) && state.modes.currentModeId !== acpMode) {
|
|
203
210
|
await client.request('session/set_mode', { sessionId, modeId: acpMode })
|
package/package.json
CHANGED
|
@@ -57,3 +57,9 @@ export const CODEX_THINKPOOL_FIRST_TURN_PREAMBLE = [
|
|
|
57
57
|
'MODEL TIERING FOR TERMINALS: When calling open_main_terminal for a genuinely complex Cascade conductor, gpt-5.6-sol may be appropriate. When calling spawn_terminal for worker slices, explicitly choose gpt-5.6-luna or gpt-5.4-mini for mechanical/scaffold/search/formatting work, gpt-5.6-terra for ordinary feature/fix work, and a balanced tier for adversarial review. Never select Sol for routine workers. If a named model is unavailable, use the nearest advertised lower tier or omit model and allow runtime fallback.',
|
|
58
58
|
...THINKPOOL_REMOTE_DELIVERY_RULES,
|
|
59
59
|
].join(' ')
|
|
60
|
+
|
|
61
|
+
// Hermes mains can dispatch visible ordinary workers, including a Claude-family
|
|
62
|
+
// model served through the parent's Nous Portal catalog when native Claude Code
|
|
63
|
+
// account access is unavailable. This is manual orchestration only: ACP 0.18.2
|
|
64
|
+
// does not expose structural read-only review policy, and Flow remains disabled.
|
|
65
|
+
export const HERMES_VISIBLE_WORKER_FALLBACK_RULE = 'HERMES VISIBLE-WORKER FALLBACK: spawn_terminal may open a visible Hermes worker only from a top-level Hermes terminal and only with an explicit exact model ID advertised in this session\'s ACP catalog. If a native Claude Code worker reports an account/provider authentication or subscription-access failure, treat the entire native Claude provider as unavailable for the rest of the task—do not retry Sonnet, Opus, or Haiku aliases on it. For a manual Claude-family review fallback, use runtime="hermes", the exact Nous Claude model suggested by read_terminal, mode="default", and sliceType="review". This fallback is permission-gated, not structurally read-only: instruct it not to edit, stop/deny any write request, and never call it Flow/reviewer safety. Never use hidden Hermes delegate_task.'
|