thinkpool-pair 0.7.237 → 0.7.238

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
@@ -51,7 +51,7 @@ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
51
51
  import { z } from 'zod'
52
52
  import { readCodexDefaultModel, readCodexModels, codexConfigForMode, codexThreadCanResume } from './codex-session.mjs'
53
53
  import { startStructuredSession } from './runtime-session.mjs'
54
- import { defaultStructuredMode, structuredRuntimeForCommand, structuredRuntimeMetadata, structuredRuntimeSupportsMode } from './runtime-registry.mjs'
54
+ import { defaultStructuredMode, shouldDeferStructuredRuntime, structuredRuntimeForCommand, structuredRuntimeMetadata, structuredRuntimeSupportsMode } from './runtime-registry.mjs'
55
55
  import { probeHermesRuntime } from './hermes-probe.mjs'
56
56
  import { canonicalRoomFilePath, waitForNativeImages } from './codex-images.mjs'
57
57
  import { createManagedLaneWorktree, removeManagedLaneWorktree } from './lane-worktree.mjs'
@@ -1694,7 +1694,7 @@ function worktreeSnapshot(cwd) {
1694
1694
  // relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
1695
1695
  // persist to the host file; tool calls round-trip through the perm card; the
1696
1696
  // rolling log replays to joiners and survives bridge restarts (session-store).
1697
- function openStructured({ id, runtime = 'claude', model, effort, resume, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, rolePrompt, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage }) {
1697
+ function openStructured({ id, runtime = 'claude', model, models, effort, resume, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, rolePrompt, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage }) {
1698
1698
  if (sessions.has(id)) return
1699
1699
  runtime = structuredRuntimeMetadata(runtime) ? runtime : 'claude'
1700
1700
  // No explicit mode → a sensible default per runtime (see defaultModeForRuntime):
@@ -1733,7 +1733,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
1733
1733
  ? spawnDepth
1734
1734
  : (sideParent || (spawnedBy && !String(spawnedBy).startsWith('flow:')) ? 1 : 0)
1735
1735
  const initialHop = Number.isInteger(hop) && hop >= 0 ? hop : structuralDepth
1736
- 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, effort, models: runtime === 'codex' ? codexModels : undefined,
1736
+ 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, effort, models: runtime === 'codex' ? codexModels : runtime === 'hermes' && Array.isArray(models) ? models : undefined,
1737
1737
  // model: truthful active-model label — now the SAME `laneModel` the SDK is given, so the
1738
1738
  // chip cannot disagree with the wire. When this lane runs on a custom (non-anthropic)
1739
1739
  // registered provider the SDK id is impersonated (see the onEvent guard below), so
@@ -1996,7 +1996,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
1996
1996
  // restart. Without this, sessionData omitted it → on restart the resumed session
1997
1997
  // re-launched on the host default (Opus) regardless of the last switch, and the
1998
1998
  // switch looked like it "never changed the model" (Max 2026-07-02). Restored below.
1999
- const sessionData = () => ({ sessionId: entry.session?.sessionId || null, runtime: entry.runtime, log: entry.log, commands: entry.commands, mode: entry.mode, effort: entry.effort, model: entry.model || null, provider: entry.provider || null, spawnedBy: entry.spawnedBy, spawnDepth: entry.spawnDepth || 0, cascadeRole: entry.cascadeRole || null, hop: entry.hop || 0, sideParent: entry.sideParent, sideTask: entry.sideTask, pendingSideContexts: entry.pendingSideContexts, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, flowRole: entry.flowRole || null, flowReviewTargets: entry.flowReviewTargets || [], flowReviewRound: entry.flowReviewRound || 0, dispatchBaseSha: entry.dispatchBaseSha || null, cwd: entry.cwd, managedWorktree: entry.managedWorktree, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [], openedAt: entry.openedAt || null, lastUsage: entry.lastUsage || null, carryRecap: entry.pendingRecap || null })
1999
+ const sessionData = () => ({ sessionId: entry.session?.sessionId || null, runtime: entry.runtime, log: entry.log, commands: entry.commands, mode: entry.mode, effort: entry.effort, model: entry.model || null, models: entry.runtime === 'hermes' ? (entry.models || []) : undefined, provider: entry.provider || null, spawnedBy: entry.spawnedBy, spawnDepth: entry.spawnDepth || 0, cascadeRole: entry.cascadeRole || null, hop: entry.hop || 0, sideParent: entry.sideParent, sideTask: entry.sideTask, pendingSideContexts: entry.pendingSideContexts, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, flowRole: entry.flowRole || null, flowReviewTargets: entry.flowReviewTargets || [], flowReviewRound: entry.flowReviewRound || 0, dispatchBaseSha: entry.dispatchBaseSha || null, cwd: entry.cwd, managedWorktree: entry.managedWorktree, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [], openedAt: entry.openedAt || null, lastUsage: entry.lastUsage || null, carryRecap: entry.pendingRecap || null })
2000
2000
  const persist = () => saveSession(room, id, sessionData())
2001
2001
  // Synchronous flush of this session's record. Used on open (so a brand-new session
2002
2002
  // has a file under its id BEFORE its first event — surviving a restart inside the
@@ -2486,7 +2486,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
2486
2486
  },
2487
2487
  // defer: a restored-idle terminal cold-boots its query only on first use — restore is
2488
2488
  // then instant for display, and only used terminals pay the ~50s MCP boot (Max, 2026-07-02).
2489
- lazy: !!defer || (!cwd && !flowSessionId && !flowTaskKey),
2489
+ lazy: shouldDeferStructuredRuntime({ runtime, defer, cwd, flowSessionId, flowTaskKey, models: entry.models }),
2490
2490
  reviewGate,
2491
2491
  // A Flow CONDUCTOR (flowSessionId set, no flowTaskKey) must DECOMPOSE, not explore via
2492
2492
  // a fan-out of Task/Explore subagents, and must not build. Hard-block Task/Bash/Write for
@@ -3808,7 +3808,7 @@ channel
3808
3808
  // FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
3809
3809
  // bridge restart: the conductor keeps its subagent-block + plan interception, and
3810
3810
  // lanes keep their worktree cwd + the ability to mark done.
3811
- openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, effort: rec.effort, provider: rec.runtime === 'claude' ? rec.provider || undefined : undefined, resume: resumable ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, spawnDepth: rec.spawnDepth, cascadeRole: rec.cascadeRole, hop: rec.hop, sideParent: rec.sideParent, sideTask: rec.sideTask, pendingSideContexts: rec.pendingSideContexts, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, flowRole: rec.flowRole, flowReviewTarget: rec.flowReviewTarget, flowReviewTargets: rec.flowReviewTargets, flowReviewRound: rec.flowReviewRound, dispatchBaseSha: rec.dispatchBaseSha, revertTarget: rec.revertTarget, cwd: rec.cwd, managedWorktree: rec.managedWorktree, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt, lastUsage: rec.lastUsage, carryRecap: wasInterrupted ? recoveryRecap : rec.carryRecap,
3811
+ openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, models: rec.models, effort: rec.effort, provider: rec.runtime === 'claude' ? rec.provider || undefined : undefined, resume: resumable ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, spawnDepth: rec.spawnDepth, cascadeRole: rec.cascadeRole, hop: rec.hop, sideParent: rec.sideParent, sideTask: rec.sideTask, pendingSideContexts: rec.pendingSideContexts, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, flowRole: rec.flowRole, flowReviewTarget: rec.flowReviewTarget, flowReviewTargets: rec.flowReviewTargets, flowReviewRound: rec.flowReviewRound, dispatchBaseSha: rec.dispatchBaseSha, revertTarget: rec.revertTarget, cwd: rec.cwd, managedWorktree: rec.managedWorktree, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt, lastUsage: rec.lastUsage, carryRecap: wasInterrupted ? recoveryRecap : rec.carryRecap,
3812
3812
  // Lazy-boot restored terminals that were IDLE + not part of a flow: their transcript
3813
3813
  // shows immediately; the query boots on first turn. Mid-turn + flow terminals boot now
3814
3814
  // (mid-turn needs auto-resume; flow needs its lane live).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.237",
3
+ "version": "0.7.238",
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": {
@@ -13,7 +13,7 @@ const RUNTIMES = Object.freeze({
13
13
  }),
14
14
  hermes: Object.freeze({
15
15
  id: 'hermes', command: 'thinkpool', label: 'Hermes Agent', protocol: 'acp',
16
- structured: true, flow: false, canSteer: true, images: true, nativeModelCatalog: true, effortControl: false, defaultMode: 'default',
16
+ structured: true, flow: false, canSteer: true, images: true, nativeModelCatalog: true, catalogRequiresSession: true, effortControl: false, defaultMode: 'default',
17
17
  modes: Object.freeze(['default', 'acceptEdits']),
18
18
  beta: true,
19
19
  }),
@@ -28,3 +28,12 @@ export const structuredRuntimeForCommand = (command) => {
28
28
  export const defaultStructuredMode = (runtime) => structuredRuntimeMetadata(runtime)?.defaultMode || 'default'
29
29
  export const structuredRuntimeSupportsMode = (runtime, mode) => structuredRuntimeMetadata(runtime)?.modes?.includes(mode) === true
30
30
  export const structuredRuntimeSupportsFlow = (runtime) => structuredRuntimeMetadata(runtime)?.flow === true
31
+ export const shouldDeferStructuredRuntime = ({ runtime, defer, cwd, flowSessionId, flowTaskKey, models } = {}) => {
32
+ const metadata = structuredRuntimeMetadata(runtime)
33
+ // Some ACP agents publish their model catalog only from session/new or
34
+ // session/resume. A brand-new lane must perform that zero-inference handshake
35
+ // before its first prompt or the model picker has no truthful choices to show.
36
+ // Once the exact catalog is persisted, the normal lazy-start policy resumes.
37
+ if (metadata?.catalogRequiresSession && !(Array.isArray(models) && models.length)) return false
38
+ return !!defer || (!cwd && !flowSessionId && !flowTaskKey)
39
+ }