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 +7 -0
- package/hermes-session.mjs +41 -1
- package/package.json +1 -1
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' }),
|
package/hermes-session.mjs
CHANGED
|
@@ -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: [] })
|