thinkpool-pair 0.7.281 → 0.7.283

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 CHANGED
@@ -68,7 +68,7 @@ import { hostMemoryAdmission } from './host-memory.mjs'
68
68
  const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'review', 'bypassPermissions'])
69
69
  import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, FLOW_CODEX_CONDUCTOR_PROMPT, FLOW_CODEX_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
70
70
  import { legacyBuilderCompletionAllowed, normalizePlanOutput, validatePlanForRuntime, validReviewTargetShape } from './flow-task-graph.mjs'
71
- import { normalizeFlowRuntime, flowLaneModelFor, flowConductorModelFor, spawnedLaneModelFor, modelCatalogValues, resolveCodexModel, assertRuntimeModelCompatible } from './flow-models.mjs'
71
+ import { normalizeFlowRuntime, flowLaneModelFor, flowConductorModelFor, spawnedLaneModelFor, modelCatalogValues, resolveCodexModel, resolveHermesOpenModel, assertRuntimeModelCompatible } from './flow-models.mjs'
72
72
  // S1 (context-offload) — durable digest store. mark_flow_done digests a closed slice in;
73
73
  // the dispatch loop reads the bounded cross-wave context back out (via assembleCrossWaveContext,
74
74
  // which enforces the CEILING). Consume the store — the internals live in flow-context-store.mjs.
@@ -2079,15 +2079,17 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2079
2079
  const runtime = args?.runtime || entry.runtime || 'claude'
2080
2080
  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.' }
2081
2081
  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.' }
2082
- 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.' }
2083
- const catalog = runtime === 'codex' ? readCodexModels() : runtime === 'hermes' ? (entry.models || []) : []
2082
+ const hermesCatalog = entry.runtime === 'hermes' && entry.models?.length
2083
+ ? entry.models
2084
+ : [...sessions.values()].flatMap((session) => session.runtime === 'hermes' ? (session.models || []) : [])
2085
+ const catalog = runtime === 'codex' ? readCodexModels() : runtime === 'hermes' ? hermesCatalog : []
2084
2086
  if (runtime === 'codex' && args?.model && !modelCatalogValues(catalog).has(args.model)) {
2085
2087
  return { error: `Could not open a Codex terminal on ${JSON.stringify(args.model)} — that model is not in this host's visible Codex catalog.` }
2086
2088
  }
2087
- if (runtime === 'hermes' && !args?.model) return { error: 'Opening a Hermes worker requires an explicit exact model ID from this session\'s ACP catalog.' }
2088
- if (runtime === 'hermes' && !modelCatalogValues(catalog).has(args.model)) {
2089
- 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.` }
2090
- }
2089
+ const hermesModel = runtime === 'hermes'
2090
+ ? resolveHermesOpenModel(args?.model, catalog, { strictCatalog: entry.runtime === 'hermes' })
2091
+ : null
2092
+ if (hermesModel && !hermesModel.ok) return { error: hermesModel.error }
2091
2093
  const provider = runtime === 'claude' && args?.provider ? resolveProviderRef(args.provider) : args?.provider
2092
2094
  if (runtime === 'claude' && args?.provider && !provider) {
2093
2095
  const choices = registeredProviderChoices()
@@ -2098,7 +2100,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2098
2100
  }
2099
2101
  return {
2100
2102
  runtime,
2101
- model: args?.model || (worker && args?.sliceType
2103
+ model: hermesModel?.model || args?.model || (worker && args?.sliceType
2102
2104
  ? spawnedLaneModelFor({ sliceType: args.sliceType, runtime, catalog })
2103
2105
  : undefined),
2104
2106
  // Worker lanes are the autonomous execution unit. They never inherit a
@@ -2359,13 +2361,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2359
2361
  // conductor mains, spawned workers, Side lanes, and Flow lanes cannot use it.
2360
2362
  ...(canOpenMainConductor ? [tool(
2361
2363
  'open_main_terminal',
2362
- 'Open a separate independent MAIN terminal to conduct a Cascade. Use this whenever a person explicitly asks to open, launch, start, or spawn a new Cascade/conductor terminal. This is the top-level room lifecycle, NOT Ensemble dispatch: the new terminal has no spawnedBy owner and appears as a main terminal. Never substitute spawn_terminal, which creates worker sub-terminals only. Give the conductor its full initial task; it will use spawn_terminal for its workers.',
2364
+ 'Open a separate independent MAIN terminal to conduct a Cascade on any supported runtime: Claude (including a connected Anthropic-compatible BYOK provider), Codex, or Hermes. Use this whenever a person explicitly asks to open, launch, start, or spawn a new Cascade/conductor terminal. This is the top-level room lifecycle, NOT Ensemble dispatch: the new terminal has no spawnedBy owner and appears as a main terminal. Never substitute spawn_terminal, which creates worker sub-terminals only. Give the conductor its full initial task; it will use spawn_terminal for its workers.',
2363
2365
  {
2364
2366
  name: z.string().max(80).optional().describe('short label for the main conductor, e.g. "Cascade · Multi-device chaos"'),
2365
2367
  task: z.string().min(1).describe('complete initial Cascade brief for the conductor'),
2366
- model: z.string().optional().describe('optional conductor model, e.g. opus / gpt-5.6-sol'),
2367
- runtime: z.enum(['claude', 'codex']).optional().describe('agent runtime; defaults to inheriting this main terminal'),
2368
- provider: z.string().optional().describe('optional registered LLM provider id for a Claude conductor'),
2368
+ model: z.string().optional().describe('optional conductor model, e.g. opus / gpt-5.6-sol / nous:z-ai/glm-5.2'),
2369
+ runtime: z.enum(['claude', 'codex', 'hermes']).optional().describe('agent runtime; every top-level terminal can open every supported runtime, defaulting to its own'),
2370
+ provider: z.string().optional().describe(`optional registered Claude-compatible provider name or id; omit for built-in Anthropic${registeredProviderChoices() ? `. Available now: ${registeredProviderChoices()}` : ''}`),
2369
2371
  mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('permission mode; defaults to inheriting this main terminal, except plan falls back to default'),
2370
2372
  },
2371
2373
  async (args) => {
@@ -2416,13 +2418,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2416
2418
  // not receive this tool, so the hierarchy is capability-enforced.
2417
2419
  ...(canSpawnWorkers ? [tool(
2418
2420
  'spawn_terminal',
2419
- '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. For ordinary workers use an appropriate non-Sol tier: gpt-5.6-luna or gpt-5.4-mini for scaffold/search, gpt-5.6-terra for feature/fix, and a balanced tier for adversarial review; never select Sol for routine workers. A Hermes sliceType=review lane is structurally reviewer-scoped before ACP startup: it can inspect files and use the bridge review-check tool, but has no terminal, write, patch, delegate, browser, skill, memory, or session-history schema. 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.',
2421
+ 'Open a visible WORKER SUB-TERMINAL on any supported runtime—Claude (including a connected Anthropic-compatible BYOK provider), Codex, or Hermes—from any top-level terminal. spawn_terminal is worker-only: it can never create a Cascade conductor or main terminal. For ordinary workers use an appropriate non-Sol tier: gpt-5.6-luna or gpt-5.4-mini for scaffold/search, gpt-5.6-terra for feature/fix, and a balanced tier for adversarial review; never select Sol for routine workers. A Hermes sliceType=review lane is structurally reviewer-scoped before ACP startup: it can inspect files and use the bridge review-check tool, but has no terminal, write, patch, delegate, browser, skill, memory, or session-history schema. 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.',
2420
2422
  {
2421
2423
  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"'),
2422
2424
  task: z.string().optional().describe('an initial task to hand the new lane immediately; omit to open it idle'),
2423
- 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'),
2425
+ model: z.string().optional().describe('model for the chosen runtime; Hermes accepts its profile default or an ACP ID such as nous:z-ai/glm-5.2'),
2424
2426
  sliceType: z.enum(['scaffold', 'feature', 'fix', 'review']).optional().describe('optional cascade slice tier; omitted preserves normal inheritance/default, explicit model wins'),
2425
- 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'),
2427
+ runtime: z.enum(['claude', 'codex', 'hermes']).optional().describe('agent runtime for the new lane; every top-level terminal can open every supported runtime'),
2426
2428
  provider: z.string().optional().describe(`optional registered Claude-compatible provider name or id; omit for built-in Anthropic${registeredProviderChoices() ? `. Available now: ${registeredProviderChoices()}` : ''}`),
2427
2429
  mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('accepted for compatibility; spawned workers always run autonomously in bypassPermissions'),
2428
2430
  },
@@ -2675,7 +2677,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2675
2677
  cascadeRole: entry.cascadeRole,
2676
2678
  flowRole: entry.flowRole,
2677
2679
  sideParent: entry.sideParent,
2678
- }), runtime === 'hermes' && canSpawnWorkers ? HERMES_VISIBLE_WORKER_FALLBACK_RULE : ''].filter(Boolean).join('\n\n')
2680
+ }), canSpawnWorkers ? HERMES_VISIBLE_WORKER_FALLBACK_RULE : ''].filter(Boolean).join('\n\n')
2679
2681
  entry.session = startStructuredSession(runtime, {
2680
2682
  // laneModel, NOT the raw `model` param: the SDK's `model` option OVERRIDES the
2681
2683
  // ANTHROPIC_MODEL supplied by resolveProviderEnv() in `env` below, so an inherited
@@ -431,7 +431,7 @@ export const spawnDecision = ({ hop = 0, spawnTimes = [], now = 0, spawnedLive =
431
431
  // host path, or mutable process object is copied into the preview.
432
432
  const DISPATCH_ARG_KEYS = Object.freeze(['mode', 'model', 'name', 'provider', 'runtime', 'sliceType', 'task'])
433
433
  const DISPATCH_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan'])
434
- const DISPATCH_RUNTIMES = new Set(['claude', 'codex'])
434
+ const DISPATCH_RUNTIMES = new Set(['claude', 'codex', 'hermes'])
435
435
  const DISPATCH_SLICES = new Set(['scaffold', 'feature', 'fix', 'review'])
436
436
  const secretValue = /((?<![a-z0-9])sk-[a-z0-9_-]{8,}|(?<![a-z0-9])gsk_[a-z0-9_-]{8,}|(?<![a-z0-9])AIza[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,})/ig
437
437
  const hostPath = /(?:\/home\/|\/users\/|\/private\/|\/tmp\/|[a-z]:\\|\\\\)/ig
@@ -487,7 +487,9 @@ export function buildDispatchPreview ({ args, initiatorTerminalId, roomCode, bri
487
487
  // durable resolution/delivery-attempt events.
488
488
  const authority = Object.freeze({ initiatorTerminalId: String(initiatorTerminalId), roomCode: String(roomCode).toUpperCase(), bridgeAuthorityId: String(bridgeAuthorityId), localAuthorityId: String(localAuthorityId) })
489
489
  const fingerprint = sha256({ authority, exactArgs, caps })
490
- const provider = exactArgs.provider || (exactArgs.runtime === 'codex' ? 'OpenAI / Codex login' : 'Anthropic / host default')
490
+ const provider = exactArgs.provider || (exactArgs.runtime === 'codex'
491
+ ? 'OpenAI / Codex login'
492
+ : exactArgs.runtime === 'hermes' ? 'Hermes ACP profile' : 'Anthropic / host default')
491
493
  const model = exactArgs.model || context.resolvedModel || 'runtime default'
492
494
  const purpose = safeCopy(exactArgs.name || exactArgs.task?.split(/\r?\n/)[0] || 'Open one worker lane', 120)
493
495
  return Object.freeze({
package/flow-models.mjs CHANGED
@@ -24,6 +24,38 @@ export function modelCatalogValues(catalog = []) {
24
24
  )).filter(Boolean))
25
25
  }
26
26
 
27
+ const exactHermesModelId = (value) => /^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+\/[A-Za-z0-9._:/-]+$/.test(value)
28
+
29
+ const expandHermesModelAlias = (value) => {
30
+ if (/^glm[-_.]/i.test(value)) return `nous:z-ai/${value.toLowerCase()}`
31
+ const shorthand = value.match(/^([^:/]+):([^/]+)$/)
32
+ if (!shorthand) return value
33
+ const provider = shorthand[1].toLowerCase() === 'zai' ? 'z-ai' : shorthand[1].toLowerCase()
34
+ return `nous:${provider}/${shorthand[2]}`
35
+ }
36
+
37
+ /**
38
+ * Resolve a Hermes model for any bridge opener. A Hermes parent has a live ACP
39
+ * catalog and remains strict. Other runtimes may use a full id or unambiguous
40
+ * shorthand; the child session acknowledges session/set_model before inference.
41
+ * Omitting a model intentionally selects the isolated Hermes profile default.
42
+ */
43
+ export function resolveHermesOpenModel(model, catalog = [], { strictCatalog = false } = {}) {
44
+ const requested = String(model || '').trim()
45
+ if (!requested) return { ok: true, model: undefined, validatedBy: 'default' }
46
+ const values = [...modelCatalogValues(catalog)]
47
+ if (values.includes(requested)) return { ok: true, model: requested, validatedBy: 'catalog' }
48
+
49
+ const expanded = expandHermesModelAlias(requested)
50
+ const normalized = (value) => String(value).toLowerCase().replace(/[^a-z0-9]+/g, '')
51
+ const requestedKey = normalized(expanded).replace(/^nous/, '')
52
+ const aliases = values.filter((value) => normalized(value).replace(/^nous/, '') === requestedKey)
53
+ if (aliases.length === 1) return { ok: true, model: aliases[0], validatedBy: 'catalog' }
54
+ if (strictCatalog && values.length) return { ok: false, error: `Could not open a Hermes terminal on ${JSON.stringify(requested)} — that model is not in this session's ACP catalog.` }
55
+ if (!exactHermesModelId(expanded)) return { ok: false, error: `Could not resolve Hermes model ${JSON.stringify(requested)}. Use a full ACP model ID such as "nous:z-ai/glm-5.2".` }
56
+ return { ok: true, model: expanded, validatedBy: 'child-runtime' }
57
+ }
58
+
27
59
  function firstVisible(candidates, catalog) {
28
60
  const visible = modelCatalogValues(catalog)
29
61
  for (const candidate of candidates) if (visible.has(candidate)) return candidate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.281",
3
+ "version": "0.7.283",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bundleVersion": 7,
3
+ "bundleVersion": 8,
4
4
  "contracts": [
5
5
  {
6
6
  "id": "room-coordination",
@@ -41,8 +41,9 @@
41
41
  },
42
42
  {
43
43
  "id": "work-routing",
44
- "version": 3,
45
- "providerRoutingContract": "A Claude worker may select a connected Anthropic-compatible provider by durable id, unique display name, or unique configured model. Unknown or ambiguous references fail closed and must never fall through to built-in Claude.",
44
+ "version": 4,
45
+ "providerRoutingContract": "A Claude lane may select a connected Anthropic-compatible provider by durable id, unique display name, or unique configured model. Unknown or ambiguous references fail closed and must never fall through to built-in Claude.",
46
+ "openerParityContract": "Every top-level terminal may use spawn_terminal or open_main_terminal to open every supported structured runtime: Claude, Codex, or Hermes. Runtime-specific provider/model validation remains authoritative and occurs before inference.",
46
47
  "routes": [
47
48
  {
48
49
  "id": "work-routing",
@@ -152,8 +152,7 @@ export const CODEX_THINKPOOL_FIRST_TURN_PREAMBLE = [
152
152
  ...THINKPOOL_REMOTE_DELIVERY_RULES,
153
153
  ].join(' ')
154
154
 
155
- // Hermes mains can dispatch visible ordinary workers, including a Claude-family
156
- // model served through the parent's Nous Portal catalog when native Claude Code
157
- // account access is unavailable. This is manual orchestration only: ACP 0.18.2
158
- // does not expose structural read-only review policy, and Flow remains disabled.
159
- 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.'
155
+ // Every top-level main can open every supported runtime. Hermes model selection
156
+ // is acknowledged by the child ACP session before inference, so it is safe for a
157
+ // Claude or Codex parent to request a visible Hermes lane.
158
+ export const HERMES_VISIBLE_WORKER_FALLBACK_RULE = 'UNIVERSAL OPENER ROUTING: a top-level terminal of any runtime may use spawn_terminal or open_main_terminal to open Claude (including a connected Anthropic-compatible BYOK provider), Codex, or Hermes. For Hermes, omit model to use the isolated profile default or pass an ACP ID such as model="nous:z-ai/glm-5.2"; common GLM shorthand is normalized and the child acknowledges the selected model before inference. 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.'