thinkpool-pair 0.7.240 → 0.7.241

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
@@ -2513,6 +2513,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2513
2513
  // FL-B4 — a review lane records its verdict via FLOW_REVIEW.json (revert-on-fail).
2514
2514
  onReviewVerdict: entry.flowRole === 'reviewer' ? onReviewVerdict : null,
2515
2515
  mcpServers: { thinkpool: peekServer },
2516
+ // Hermes' ACP MCP registration is intentionally non-fatal upstream. Make
2517
+ // ThinkPool's room-tool contract stricter: a lane must prove its actual
2518
+ // role-scoped tools through local /tools before a user turn. Claude/Codex
2519
+ // already fail startup when their required MCP handshake fails.
2520
+ requiredMcpTools: runtime === 'hermes'
2521
+ ? ['read_terminal', ...(canSpawnWorkers ? ['spawn_terminal', 'close_terminal'] : [])]
2522
+ : undefined,
2516
2523
  // Tier C precheck — the PreToolUse gate calls this BEFORE raising a card, so a
2517
2524
  // disabled/looping/over-cap post never bothers a person. Closes over `entry`.
2518
2525
  crossPostGate: () => crossPostDecision({ hop: entry.hop || 0, postCount: entry.postCount || 0, disabled: process.env.TP_CROSSPOST_OFF === '1' }),
@@ -42,7 +42,7 @@ const permissionOutcome = (decision, options = []) => {
42
42
 
43
43
  export function startHermesSession({
44
44
  cwd, model, resume, env = process.env, mode = 'default', onEvent, requestPermission,
45
- roomContext, terminalRolePrompt, rolePrompt, mcpServers, prepareCwd = null,
45
+ roomContext, terminalRolePrompt, rolePrompt, mcpServers, requiredMcpTools = [], prepareCwd = null,
46
46
  command = HERMES_COMMAND, args = ['acp'], clientFactory = createAcpClient,
47
47
  mcpHttpFactory = startCodexMcpHttp, lazy = false,
48
48
  } = {}) {
@@ -66,6 +66,7 @@ export function startHermesSession({
66
66
  const abortedTurns = new Set()
67
67
  let retireClient = () => {}
68
68
  let resuming = false
69
+ let inventoryProbe = null
69
70
  let modelSwitchPending = false
70
71
 
71
72
  const emit = (event) => { try { onEvent?.(event) } catch { /* consumer isolation */ } }
@@ -87,10 +88,33 @@ export function startHermesSession({
87
88
  function onNotification(method, params) {
88
89
  if (method !== 'session/update') return
89
90
  const update = params?.update || params
91
+ if (inventoryProbe) {
92
+ if (update?.sessionUpdate === 'agent_message_chunk' && update?.content?.type === 'text') {
93
+ inventoryProbe.push(String(update.content.text || ''))
94
+ }
95
+ return
96
+ }
90
97
  if (resuming && HERMES_REPLAY_UPDATES.has(update?.sessionUpdate)) return
91
98
  mapper?.push(params)
92
99
  }
93
100
 
101
+ async function probeMcpTools() {
102
+ const required = [...new Set((Array.isArray(requiredMcpTools) ? requiredMcpTools : [])
103
+ .map((name) => String(name || '').trim()).filter(Boolean))]
104
+ if (!required.length) return []
105
+ const chunks = []
106
+ inventoryProbe = chunks
107
+ try {
108
+ await client.request('session/prompt', {
109
+ sessionId,
110
+ prompt: [{ type: 'text', text: '/tools' }],
111
+ messageId: randomUUID(),
112
+ }, 30_000)
113
+ } finally { inventoryProbe = null }
114
+ const inventory = chunks.join('')
115
+ return required.filter((name) => !inventory.includes(`mcp__thinkpool__${name}`))
116
+ }
117
+
94
118
  async function boot() {
95
119
  if (ended) throw new Error('Hermes session is closed')
96
120
  if (crashed) throw new Error('Hermes ACP process crashed; this lane must be restarted before sending another turn')
@@ -151,6 +175,22 @@ export function startHermesSession({
151
175
  } else state = await client.request('session/new', params, 30_000)
152
176
  if (!sessionId) sessionId = state?.sessionId || null
153
177
  if (!sessionId) throw new Error('Hermes ACP did not return a session id')
178
+ let missingMcpTools = await probeMcpTools()
179
+ // Hermes treats ACP-provided MCP registration as non-fatal. A transient
180
+ // first connection can therefore leave an otherwise healthy session with
181
+ // only built-in tools. Re-running the released resume lifecycle retries
182
+ // registration without inventing a new native identity. Bound this to one
183
+ // retry, then fail closed before any user prompt reaches a tool-less lane.
184
+ if (missingMcpTools.length && initialized?.agentCapabilities?.sessionCapabilities?.resume) {
185
+ try {
186
+ resuming = true
187
+ state = await client.request('session/resume', { ...params, sessionId }, 30_000)
188
+ } finally { resuming = false }
189
+ missingMcpTools = await probeMcpTools()
190
+ }
191
+ if (missingMcpTools.length) {
192
+ throw new Error(`Hermes ThinkPool MCP readiness failed; missing ${missingMcpTools.join(', ')}`)
193
+ }
154
194
  const serverModel = state?.models?.currentModelId || null
155
195
  activeModel = requestedModel || serverModel || activeModel
156
196
  mapper.startSession({ sessionId, models: state?.models, modes: state?.modes, commands: [] })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.240",
3
+ "version": "0.7.241",
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": {