thinkpool-pair 0.7.364 → 0.7.365

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.
Files changed (116) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6266
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -60
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -92
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -79
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +13 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-prompt-contracts.mjs +1 -85
  111. package/thinkpool-room-prompt.mjs +1 -187
  112. package/transcript-sanitize.mjs +1 -332
  113. package/turn-stall.mjs +1 -61
  114. package/update-gate.mjs +1 -53
  115. package/viewport.mjs +1 -810
  116. package/worker-completion.mjs +1 -57
@@ -1,683 +1 @@
1
- // Hermes Agent structured runtime over the official ACP stdio transport.
2
- // Implements the same narrow session contract as claude-session/codex-session.
3
- import fs from 'node:fs'
4
- import path from 'node:path'
5
- import { randomUUID } from 'node:crypto'
6
- import { AcpClient } from './acp-client.mjs'
7
- import { HermesEventMapper, hermesToolFor } from './hermes-event-mapper.mjs'
8
- import { probeHermesRuntime } from './hermes-probe.mjs'
9
- import { HERMES_PLAN_SAFE_MCP_TOOLS, HERMES_QUESTION_MCP_TOOL, hermesExactInventory, hermesPolicyEnv, hermesRoleFor } from './hermes-policy.mjs'
10
- import { startCodexMcpHttp } from './codex-mcp-http.mjs'
11
- import { autoAllow, classifyRisk } from './claude-session.mjs'
12
- import { crossPostNeedsCard } from './cross-terminal.mjs'
13
- import { buildThinkPoolTurnGuidance, createRoomContextSelector, usesFullThinkPoolReminder } from './thinkpool-room-prompt.mjs'
14
- import { createCumulativeEventRelay } from './cumulative-event-relay.mjs'
15
-
16
- export const HERMES_COMMAND = 'thinkpool'
17
- export const HERMES_ACP_PROTOCOL_VERSION = 1
18
- export const HERMES_SUPPORTED_MODES = new Set(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
19
- export const HERMES_EFFORT_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max'])
20
- export const HERMES_EFFORT_CONFIG_ID = 'thinkpool_reasoning_effort'
21
- const HERMES_INITIALIZE_TIMEOUT_MS = 15_000
22
- const PLAN_SAFE_MCP_TOOLS = new Set([...HERMES_PLAN_SAFE_MCP_TOOLS, 'read_review_file'])
23
-
24
- const HERMES_SECRET_ENV_KEY = /(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|ACCESS_KEY|CREDENTIAL)/i
25
- const HERMES_REPLAY_UPDATES = new Set(['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan'])
26
- export const hermesChildEnv = (env = {}) => Object.fromEntries(
27
- Object.entries(env).filter(([key]) => !HERMES_SECRET_ENV_KEY.test(key)),
28
- )
29
-
30
- const imageBlock = (imagePath) => {
31
- const ext = path.extname(imagePath).toLowerCase()
32
- const mimeType = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : ext === '.gif' ? 'image/gif' : 'image/jpeg'
33
- return { type: 'image', mimeType, data: fs.readFileSync(imagePath).toString('base64') }
34
- }
35
-
36
- const modelList = (state) => (state?.availableModels || []).map((item) => ({ value: item.modelId, displayName: item.name || item.modelId, description: item.description || '' }))
37
-
38
- const mcpDescriptor = (url) => ({ type: 'http', name: 'thinkpool', url, headers: [] })
39
- const createAcpClient = (options) => new AcpClient(options)
40
-
41
- export function buildHermesPromptText({ text, firstTurn = false, terminalRolePrompt, rolePrompt, roomContext, promptIndex = 0, forceFullReminder = false } = {}) {
42
- const context = typeof roomContext === 'function' ? roomContext() : roomContext
43
- const inThinkPoolRoom = !!(terminalRolePrompt || rolePrompt || context)
44
- const fullReminder = usesFullThinkPoolReminder({ promptIndex, forceFull: forceFullReminder })
45
- const preamble = [
46
- firstTurn || fullReminder ? terminalRolePrompt : '',
47
- inThinkPoolRoom ? buildThinkPoolTurnGuidance({ text, promptIndex, forceFull: forceFullReminder }) : '',
48
- firstTurn || fullReminder ? rolePrompt : '',
49
- context,
50
- ].filter(Boolean).join('\n\n')
51
- const body = String(text ?? '')
52
- return preamble ? `${preamble}\n\n${body}` : body
53
- }
54
-
55
- const permissionOutcome = (decision, options = []) => {
56
- if (decision === 'deny' || !decision) return { outcome: 'cancelled' }
57
- const preferred = decision === 'always'
58
- ? ['allow_always', 'allow_session', 'allow_once']
59
- : ['allow_once', 'allow_session', 'allow_always']
60
- const option = preferred.map((kind) => options.find((item) => item.kind === kind || item.optionId === kind)).find(Boolean)
61
- || options.find((item) => String(item.kind || '').startsWith('allow'))
62
- return option ? { outcome: 'selected', optionId: option.optionId } : { outcome: 'cancelled' }
63
- }
64
-
65
- export function startHermesSession({
66
- cwd, model, resume, env = process.env, mode = 'default', onEvent, requestPermission,
67
- roomContext, terminalRolePrompt, rolePrompt, mcpServers, requiredMcpTools = [], prepareCwd = null,
68
- command = HERMES_COMMAND, args = ['acp'], clientFactory = createAcpClient,
69
- mcpHttpFactory = startCodexMcpHttp, lazy = false, hermesRole = null,
70
- crossPostGate = null, didSpawnTarget = null, crossRoomPostGate = null, effort = 'high',
71
- admitStart = null, onTurnStart = null,
72
- } = {}) {
73
- let activeCwd = cwd
74
- const requestedModel = model || null
75
- let activeModel = requestedModel
76
- let activeMode = HERMES_SUPPORTED_MODES.has(mode) ? mode : 'default'
77
- let sessionId = resume || null
78
- let client = null
79
- let mapper = null
80
- let mcpHttp = null
81
- let starting = null
82
- let ended = false
83
- let turnActive = false
84
- let started = false
85
- let crashed = false
86
- let firstTurn = true
87
- let userPromptNo = 0
88
- let forceFullReminder = true
89
- const selectRoomContext = createRoomContextSelector(roomContext)
90
- let stderrTail = ''
91
- let promptChain = Promise.resolve()
92
- let activeTurnId = 0
93
- const abortedTurns = new Set()
94
- let retireClient = () => {}
95
- let resuming = false
96
- let inventoryProbe = null
97
- let modelSwitchPending = false
98
- let bootCancelled = false
99
- let suppressNextSessionPublish = false
100
- let activeEffort = effort === null ? null : (HERMES_EFFORT_LEVELS.has(effort) ? effort : 'high')
101
- const queuedTurns = []
102
- let drainingQueuedTurns = false
103
- let queueDrainPending = false
104
- const policyRole = hermesRole || hermesRoleFor({})
105
-
106
- const effectivePolicyRole = () => activeMode === 'plan' ? 'plan' : policyRole
107
- const effectiveMcpTools = () => activeMode === 'plan'
108
- ? requiredMcpTools.filter((name) => PLAN_SAFE_MCP_TOOLS.has(name))
109
- : requiredMcpTools
110
- const acpModeFor = (value) => value === 'acceptEdits'
111
- ? 'accept_edits'
112
- : value === 'bypassPermissions'
113
- ? 'dont_ask'
114
- : 'default'
115
-
116
- const emit = createCumulativeEventRelay((event) => { try { onEvent?.(event) } catch { /* consumer isolation */ } })
117
-
118
- const thinkpoolPeerTool = (name) => {
119
- const lower = String(name || '').toLowerCase()
120
- return [...HERMES_PLAN_SAFE_MCP_TOOLS, 'post_to_terminal', 'post_to_session', HERMES_QUESTION_MCP_TOOL]
121
- .find((candidate) => lower === candidate || lower === `mcp__thinkpool__${candidate}` || lower === `mcp_thinkpool_${candidate}`) || null
122
- }
123
-
124
- async function onRequest(method, params, requestId) {
125
- if (method !== 'session/request_permission') throw Object.assign(new Error(`Unsupported ACP client method: ${method}`), { code: -32601 })
126
- const tool = hermesToolFor(params.toolCall || mapper?.tools.get(params.toolCallId) || {})
127
- const peerTool = thinkpoolPeerTool(tool.name)
128
- const card = {
129
- id: `hermes-perm:${requestId}`,
130
- toolName: peerTool ? `mcp__thinkpool__${peerTool}` : tool.name,
131
- input: tool.input,
132
- risk: peerTool?.startsWith('post_to_') ? 'high' : classifyRisk(tool.name, tool.input),
133
- }
134
- // ACP's native modes currently govern edit proposals only. ThinkPool's
135
- // permission chip is the cross-runtime authority, so apply the same pure
136
- // mode policy Claude uses before raising a durable room card. Structural
137
- // role schemas still win: bypass can auto-allow a request, but it cannot
138
- // restore a tool that the process-local Hermes policy never exposed.
139
- // Match Claude/Codex peer semantics before the generic permission matrix.
140
- // Paired-room reads are always read-only, including in Plan. Same-room posts
141
- // honor bypass/ownership after their bridge gate. Cross-room posts always
142
- // retain sender consent—even in bypass—because the target has a second card.
143
- if (HERMES_PLAN_SAFE_MCP_TOOLS.includes(peerTool)) {
144
- return { outcome: permissionOutcome('allow', params.options) }
145
- }
146
- // The question tool creates its own durable human-input card. Requiring a
147
- // permission card before it can create that card deadlocks the interaction.
148
- if (peerTool === HERMES_QUESTION_MCP_TOOL) {
149
- return { outcome: permissionOutcome('allow', params.options) }
150
- }
151
- if (peerTool === 'post_to_terminal') {
152
- const gate = crossPostGate ? crossPostGate() : { ok: true }
153
- if (!gate.ok) return { outcome: { outcome: 'cancelled' } }
154
- const spawnedByMe = didSpawnTarget ? !!didSpawnTarget(tool.input?.terminal) : false
155
- if (!crossPostNeedsCard({ mode: activeMode, spawnedByMe })) {
156
- return { outcome: permissionOutcome('allow', params.options) }
157
- }
158
- }
159
- if (peerTool === 'post_to_session') {
160
- const gate = crossRoomPostGate ? crossRoomPostGate() : { ok: true }
161
- if (!gate.ok) return { outcome: { outcome: 'cancelled' } }
162
- let decision = 'deny'
163
- try { decision = await requestPermission?.(card) } catch { /* fail closed */ }
164
- return { outcome: permissionOutcome(decision, params.options) }
165
- }
166
- if (activeMode === 'plan') return { outcome: { outcome: 'cancelled' } }
167
- if (autoAllow({ toolName: card.toolName, input: card.input, mode: activeMode })) {
168
- return { outcome: permissionOutcome('allow', params.options) }
169
- }
170
- let decision = 'deny'
171
- try { decision = await requestPermission?.(card) } catch { /* fail closed */ }
172
- return { outcome: permissionOutcome(decision, params.options) }
173
- }
174
-
175
- function onNotification(method, params) {
176
- if (method !== 'session/update') return
177
- const update = params?.update || params
178
- if (inventoryProbe) {
179
- if (update?.sessionUpdate === 'agent_message_chunk' && update?.content?.type === 'text') {
180
- inventoryProbe.push(String(update.content.text || ''))
181
- return
182
- }
183
- // `/tools` is a local readiness transaction, but Hermes can publish its
184
- // command catalog (and other session chrome) concurrently with that
185
- // response. Suppress only the probe's assistant text; dropping every
186
- // notification here made the real native catalog disappear permanently.
187
- }
188
- if (resuming && HERMES_REPLAY_UPDATES.has(update?.sessionUpdate)) return
189
- mapper?.push(params)
190
- }
191
-
192
- async function probeMcpTools() {
193
- const required = [...new Set((Array.isArray(effectiveMcpTools()) ? effectiveMcpTools() : [])
194
- .map((name) => String(name || '').trim()).filter(Boolean))]
195
- // Direct runtime tests and unscoped upstream callers have no bridge role
196
- // contract to prove. Every bridge-created Hermes lane supplies its required
197
- // MCP list; only those lanes enter the exact-inventory transaction.
198
- if (!required.length && effectivePolicyRole() !== 'plan') return { inventory: '', missing: [], forbidden: [] }
199
- const exact = hermesExactInventory(effectivePolicyRole(), { mcpTools: required })
200
- const allowedBuiltins = exact.builtinTools
201
- const requiredBuiltins = exact.requiredBuiltinTools
202
- const expectedMcp = exact.mcpTools
203
- const chunks = []
204
- inventoryProbe = chunks
205
- try {
206
- await client.request('session/prompt', {
207
- sessionId,
208
- prompt: [{ type: 'text', text: '/tools' }],
209
- messageId: randomUUID(),
210
- }, 30_000)
211
- } finally { inventoryProbe = null }
212
- const inventory = chunks.join('')
213
- const has = (name) => new RegExp(`(?:^|[^a-z0-9_])${name}(?:$|[^a-z0-9_])`, 'i').test(inventory)
214
- const mcpNames = [...inventory.matchAll(/\bmcp(?:__|_)[a-z0-9_]+/gi)].map((item) => item[0])
215
- const expectedMcpNames = new Set(expectedMcp.flatMap((name) => [`mcp__thinkpool__${name}`, `mcp_thinkpool_${name}`]))
216
- const unexpectedMcp = [...new Set(mcpNames.filter((name) => !expectedMcpNames.has(name)))]
217
- // This is a zero-inference ACP inventory command. It proves the complete
218
- // role schema (required built-ins plus exactly this lane's ThinkPool MCP
219
- // set). Optional provider/browser capabilities may be absent, but any
220
- // presented builtin outside the role allowlist is still a hard failure.
221
- return {
222
- inventory,
223
- missing: [
224
- ...requiredBuiltins.filter((name) => !has(name)),
225
- ...expectedMcp.filter((name) => !has(`mcp__thinkpool__${name}`) && !has(`mcp_thinkpool_${name}`)),
226
- ],
227
- forbidden: [
228
- ...exact.allBuiltinTools.filter((name) => !allowedBuiltins.includes(name) && has(name)),
229
- ...unexpectedMcp,
230
- ],
231
- }
232
- }
233
-
234
- async function assertMcpReadiness() {
235
- const probe = await probeMcpTools()
236
- if (!probe.missing.length && !probe.forbidden.length) return probe
237
- const details = [probe.missing.length ? `missing ${probe.missing.join(', ')}` : '', probe.forbidden.length ? `forbidden ${probe.forbidden.join(', ')}` : ''].filter(Boolean).join('; ')
238
- throw new Error(`Hermes ThinkPool MCP readiness failed; ${details}`)
239
- }
240
-
241
- async function boot() {
242
- if (ended) throw new Error('Hermes session is closed')
243
- if (crashed) throw new Error('Hermes ACP process crashed; this lane must be restarted before sending another turn')
244
- if (client?.alive && mapper) return
245
- if (starting) return starting
246
- const gate = typeof admitStart === 'function' ? admitStart() : { ok: true }
247
- if (gate?.ok === false) throw new Error(gate.reason || 'Host memory is critically low. This Hermes runtime was not started.')
248
- starting = (async () => {
249
- if (!started && prepareCwd) {
250
- try { activeCwd = prepareCwd() || activeCwd } catch { /* retain original cwd */ }
251
- }
252
- const thinkpool = mcpServers?.thinkpool
253
- if (thinkpool && !mcpHttp) mcpHttp = await mcpHttpFactory({ sdkServer: thinkpool })
254
- let childEnv = hermesChildEnv(env)
255
- let launchCommand = command
256
- let launchArgs = args
257
- // The bridge advertises Hermes only after this probe, but lazy lanes may
258
- // boot much later. Re-check immediately before every real ACP process so
259
- // a removed/changed delegation hook cannot ride a stale startup verdict.
260
- if (command === HERMES_COMMAND && clientFactory === createAcpClient) {
261
- const profile = probeHermesRuntime({ command, env: childEnv, strictBootstrap: true })
262
- if (!profile.available) throw new Error(`Hermes profile safety check failed: ${profile.reason || 'runtime unavailable'}`)
263
- // The executable is the installed venv Python, never the mutable
264
- // profile wrapper. HERMES_HOME is the probe-verified isolated profile.
265
- launchCommand = profile.python
266
- launchArgs = [profile.bootstrap]
267
- childEnv = { ...childEnv, HERMES_HOME: profile.profile, THINKPOOL_HERMES_ACP_POLICY: hermesPolicyEnv(effectivePolicyRole(), { mcpTools: effectiveMcpTools() }) }
268
- }
269
- let retired = false
270
- retireClient = () => { retired = true }
271
- const initializeParams = {
272
- protocolVersion: HERMES_ACP_PROTOCOL_VERSION,
273
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
274
- clientInfo: { name: 'thinkpool-pair', title: 'ThinkPool Code', version: '1' },
275
- }
276
- let initialized
277
- // Cold Hermes imports take 6–9 seconds on the supported host, and the
278
- // production ENOSPC incident pushed one child just beyond the old 10s
279
- // cliff. Initialization is pre-session and pre-inference, so one fresh
280
- // process retry is safe: no prompt or tool can have executed yet.
281
- for (let attempt = 0; attempt < 2; attempt++) {
282
- retired = false
283
- retireClient = () => { retired = true }
284
- client = clientFactory({
285
- command: launchCommand, args: launchArgs, cwd: activeCwd, env: childEnv,
286
- onNotification,
287
- onRequest,
288
- onStderr: (text) => { stderrTail = (stderrTail + text).slice(-2000) },
289
- onClose: (error) => {
290
- if (ended || crashed || retired) return
291
- crashed = true
292
- turnActive = false
293
- emit({ kind: 'error', message: `Hermes ACP closed unexpectedly: ${error?.message || error}`, recoverable: true })
294
- },
295
- })
296
- client.start()
297
- try {
298
- initialized = await client.request('initialize', initializeParams, HERMES_INITIALIZE_TIMEOUT_MS)
299
- break
300
- } catch (error) {
301
- const retryable = !bootCancelled && attempt === 0 && /^initialize timed out\b/i.test(String(error?.message || error))
302
- if (!retryable) throw error
303
- retireClient()
304
- const timedOutClient = client
305
- client = null
306
- timedOutClient?.end()
307
- stderrTail = ''
308
- }
309
- }
310
- if (initialized?.protocolVersion !== HERMES_ACP_PROTOCOL_VERSION) {
311
- throw new Error(`Unsupported Hermes ACP protocol ${initialized?.protocolVersion ?? 'unknown'}; expected ${HERMES_ACP_PROTOCOL_VERSION}`)
312
- }
313
- const params = { cwd: path.resolve(activeCwd), mcpServers: mcpHttp ? [mcpDescriptor(mcpHttp.url)] : [] }
314
- mapper = new HermesEventMapper({ onEvent: emit, sessionId, model: activeModel })
315
- let state
316
- if (sessionId) {
317
- if (!initialized?.agentCapabilities?.sessionCapabilities?.resume) {
318
- throw new Error(`Hermes session ${sessionId} cannot be resumed by this ACP release; no fresh session was substituted`)
319
- }
320
- try {
321
- resuming = true
322
- state = await client.request('session/resume', { ...params, sessionId }, 30_000)
323
- }
324
- catch (error) {
325
- throw new Error(`Hermes session ${sessionId} is not available; no fresh session was substituted (${error?.message || error})`)
326
- }
327
- finally { resuming = false }
328
- } else state = await client.request('session/new', params, 30_000)
329
- if (!sessionId) sessionId = state?.sessionId || null
330
- if (!sessionId) throw new Error('Hermes ACP did not return a session id')
331
- let toolProbe = await probeMcpTools()
332
- let missingMcpTools = toolProbe.missing
333
- // Hermes treats ACP-provided MCP registration as non-fatal. A transient
334
- // first connection can therefore leave an otherwise healthy session with
335
- // only built-in tools. Re-running the released resume lifecycle retries
336
- // registration without inventing a new native identity. Bound this to one
337
- // retry, then fail closed before any user prompt reaches a tool-less lane.
338
- if (missingMcpTools.length && initialized?.agentCapabilities?.sessionCapabilities?.resume) {
339
- try {
340
- resuming = true
341
- state = await client.request('session/resume', { ...params, sessionId }, 30_000)
342
- } finally { resuming = false }
343
- toolProbe = await probeMcpTools()
344
- missingMcpTools = toolProbe.missing
345
- }
346
- if (missingMcpTools.length || toolProbe.forbidden.length) {
347
- const details = [missingMcpTools.length ? `missing ${missingMcpTools.join(', ')}` : '', toolProbe.forbidden.length ? `forbidden ${toolProbe.forbidden.join(', ')}` : ''].filter(Boolean).join('; ')
348
- throw new Error(`Hermes ThinkPool MCP readiness failed; ${details}`)
349
- }
350
- const serverModel = state?.models?.currentModelId || null
351
- activeModel = requestedModel || serverModel || activeModel
352
- // A spawned Hermes worker may be pinned to an exact model from its parent's
353
- // ACP catalog. Acknowledge that model BEFORE publishing the initial system/
354
- // models events; otherwise the worker runs the requested model while the UI
355
- // and persisted resume record lie that it still uses the profile default.
356
- if (requestedModel) {
357
- await client.request('session/set_model', { sessionId, modelId: activeModel })
358
- await assertMcpReadiness()
359
- }
360
- // The bootstrap owns this config id and applies it to the process-local
361
- // agent. Send it on new, resume and reconstructed sessions; it never
362
- // touches Hermes' shared profile/config.yaml.
363
- if (activeEffort) await client.request('session/set_config_option', { sessionId, configId: HERMES_EFFORT_CONFIG_ID, value: activeEffort })
364
- const publishedModels = state?.models
365
- ? { ...state.models, currentModelId: activeModel }
366
- : activeModel ? { currentModelId: activeModel, availableModels: [] } : state?.models
367
- if (!suppressNextSessionPublish) {
368
- mapper.startSession({ sessionId, models: publishedModels, modes: state?.modes, commands: [] })
369
- } else {
370
- // A Plan transition recreates only the ACP process so its tool schema
371
- // can become structurally read-only. Keep the room transcript/catalog
372
- // stable while the fresh mapper resumes the same native session.
373
- mapper.sessionId = sessionId
374
- mapper.model = activeModel
375
- suppressNextSessionPublish = false
376
- }
377
- started = true
378
- const acpMode = acpModeFor(activeMode)
379
- if (state?.modes?.availableModes?.some((item) => item.id === acpMode) && state.modes.currentModeId !== acpMode) {
380
- await client.request('session/set_mode', { sessionId, modeId: acpMode })
381
- }
382
- emit({ kind: 'capabilities', runtime: 'hermes', protocol: 'acp', protocolVersion: initialized?.protocolVersion, capabilities: initialized?.agentCapabilities || {}, models: modelList(state?.models), flow: command === HERMES_COMMAND && clientFactory === createAcpClient })
383
- })().catch(async (error) => {
384
- crashed = true
385
- client?.end()
386
- try { await mcpHttp?.close() } catch { /* startup cleanup */ }
387
- mcpHttp = null
388
- if (!bootCancelled) emit({ kind: 'error', message: `Hermes ACP startup failed: ${error?.message || error}${stderrTail ? `: ${stderrTail.trim().slice(-240)}` : ''}`, recoverable: true })
389
- throw error
390
- }).finally(() => { starting = null; bootCancelled = false })
391
- return starting
392
- }
393
-
394
- function reviveAfterExplicitRetry() {
395
- if (!crashed || ended) return
396
- retireClient()
397
- client?.end()
398
- client = null
399
- mapper = null
400
- crashed = false
401
- stderrTail = ''
402
- }
403
-
404
- function finishAbortedTurn() {
405
- if (mapper) mapper.finishTurn({ stopReason: 'cancelled' })
406
- else emit({ kind: 'result', subtype: 'aborted', sessionId, model: activeModel, costUsd: null, usage: null, numTurns: 1, durationMs: undefined, denials: 0, resultText: null })
407
- }
408
-
409
- async function restartAfterCancelledResponseFailure() {
410
- const oldClient = client
411
- retireClient()
412
- client = null
413
- mapper = null
414
- oldClient?.end()
415
- await boot()
416
- }
417
-
418
- function promptBlocks(text, options = {}, { promptIndex = 0, forceFull = false } = {}) {
419
- const rawText = String(text)
420
- // Hermes handles /steer, /queue, /compact and /reset only when the slash is
421
- // the first character of a pure-text prompt.
422
- if (/^\s*\//.test(rawText)) return [{ type: 'text', text: rawText }]
423
- const fullReminder = usesFullThinkPoolReminder({ promptIndex, forceFull })
424
- const blocks = [{ type: 'text', text: buildHermesPromptText({
425
- text: rawText,
426
- firstTurn,
427
- terminalRolePrompt,
428
- rolePrompt,
429
- roomContext: () => selectRoomContext({ force: fullReminder }),
430
- promptIndex,
431
- forceFullReminder: forceFull,
432
- }) }]
433
- for (const imagePath of (Array.isArray(options.images) ? options.images : [])) {
434
- try { blocks.push(imageBlock(imagePath)) } catch { /* quoted host path remains in text */ }
435
- }
436
- return blocks
437
- }
438
-
439
- async function runPrompt(text, options = {}, { steering = false, turnId = activeTurnId, promptIndex = 0, forceFull = false } = {}) {
440
- try { await boot() }
441
- catch (error) {
442
- if (abortedTurns.has(turnId)) return { stopReason: 'cancelled' }
443
- throw error
444
- }
445
- if (abortedTurns.has(turnId)) return { stopReason: 'cancelled' }
446
- const body = steering ? `/steer ${String(text)}` : String(text)
447
- let result
448
- try {
449
- result = await client.request('session/prompt', {
450
- sessionId,
451
- prompt: promptBlocks(body, options, { promptIndex, forceFull }),
452
- messageId: randomUUID(),
453
- }, 0)
454
- } catch (error) {
455
- // Hermes 0.18.2 successfully interrupts the model/tool, then its ACP
456
- // prompt handler can reject with Internal error while constructing the
457
- // cancelled response. The bridge already owns the canonical aborted
458
- // boundary below, so a late response failure for that exact turn is not a
459
- // second turn failure. Process-close errors still surface through onClose.
460
- if (abortedTurns.has(turnId)) {
461
- // Hermes 0.18.2 throws before clearing its transient is_running flag.
462
- // Resume the same persisted session in a fresh per-lane ACP process so
463
- // the next queued prompt cannot disappear into that stale runtime queue.
464
- if (!ended) await restartAfterCancelledResponseFailure()
465
- return { stopReason: 'cancelled' }
466
- }
467
- throw error
468
- }
469
- if (!steering) {
470
- if (abortedTurns.has(turnId)) return result
471
- turnActive = false
472
- firstTurn = false
473
- const compact = /^\s*\/compact\b/i.test(String(text || '')) ? mapper.compactOutcome() : null
474
- if (compact) emit({ kind: 'compaction', trigger: 'manual', ...compact })
475
- const reasoning = String(text || '').match(/^\s*\/reasoning\s+(low|medium|high|xhigh|max|none|reset)\s*$/i)
476
- if (reasoning) {
477
- const requested = reasoning[1].toLowerCase()
478
- const confirmed = requested === 'reset'
479
- ? /Session reasoning reset to model default/i.test(mapper.text)
480
- : new RegExp(`Session reasoning set to ${requested}`, 'i').test(mapper.text)
481
- if (confirmed) {
482
- activeEffort = requested === 'reset' ? null : requested
483
- emit({ kind: 'effort', level: activeEffort })
484
- }
485
- }
486
- // Reserve the idle-looking result boundary for the existing FIFO before
487
- // publishing it. An onEvent consumer can synchronously submit a new turn
488
- // from the result callback; without this latch that turn starts ahead of
489
- // already-queued work and violates /queue order.
490
- queueDrainPending = queuedTurns.length > 0 && !ended && !abortedTurns.has(turnId)
491
- mapper.finishTurn({ stopReason: result?.stopReason, usage: result?.usage })
492
- if (queueDrainPending) void drainQueuedTurns()
493
- }
494
- return result
495
- }
496
-
497
- async function drainQueuedTurns() {
498
- if (drainingQueuedTurns || ended || turnActive) return
499
- drainingQueuedTurns = true
500
- try {
501
- while (queuedTurns.length && !ended && !turnActive) {
502
- queueDrainPending = false
503
- const next = queuedTurns.shift()
504
- const turnId = ++activeTurnId
505
- turnActive = true
506
- try { onTurnStart?.(next.options || {}) } catch { /* observer cannot break the FIFO */ }
507
- try { await runPrompt(next.text, next.options, { steering: false, turnId, promptIndex: next.promptIndex, forceFull: next.forceFull }) }
508
- catch (error) { turnActive = false; emit({ kind: 'error', message: `Hermes queued turn failed: ${error?.message || error}`, recoverable: true }) }
509
- }
510
- } finally { drainingQueuedTurns = false; queueDrainPending = false }
511
- }
512
-
513
- if (!lazy) queueMicrotask(() => { void boot().catch(() => {}) })
514
-
515
- return {
516
- get sessionId() { return sessionId },
517
- get turnActive() { return turnActive },
518
- get queuedDepth() { return queuedTurns.length },
519
- get canSteer() { return !!client?.alive },
520
- get started() { return started },
521
- get models() { return [] },
522
- get effort() { return activeEffort },
523
- sendTurn(text, options = {}) {
524
- if (ended) return false
525
- if (!client?.alive && !starting) {
526
- const gate = typeof admitStart === 'function' ? admitStart() : { ok: true }
527
- if (gate?.ok === false) {
528
- emit({ kind: 'error', message: gate.reason || 'Host memory is critically low. This Hermes runtime was not started.', recoverable: true })
529
- return false
530
- }
531
- }
532
- const promptIndex = userPromptNo++
533
- const thisTurnForceFull = forceFullReminder
534
- forceFullReminder = /^\s*\/(?:compact|reset|clear)\b/i.test(String(text || ''))
535
- // A crash never replays work by itself. A later explicit turn is the
536
- // authority to launch a fresh ACP process and resume the same native
537
- // session id; this is the recovery path the old permanent latch blocked.
538
- reviveAfterExplicitRetry()
539
- const queued = String(text || '').match(/^\s*\/queue\s+([\s\S]+)$/i)
540
- // A result callback may arrive after turnActive fell but before the FIFO
541
- // drain claimed the next queued item. Keep any new human turn behind the
542
- // already-visible queue instead of letting that micro-window reorder it.
543
- if (queueDrainPending) {
544
- queuedTurns.push({ text: queued ? queued[1] : String(text || ''), options, promptIndex, forceFull: thisTurnForceFull })
545
- emit({ kind: 'queue-add', queued: true, depth: queuedTurns.length })
546
- return true
547
- }
548
- // A busy prompt is a genuine ACP /steer call and may run concurrently.
549
- if (turnActive) {
550
- if (queued) {
551
- queuedTurns.push({ text: queued[1], options, promptIndex, forceFull: thisTurnForceFull })
552
- emit({ kind: 'queue-add', queued: true, depth: queuedTurns.length })
553
- return true
554
- }
555
- const turnId = activeTurnId
556
- void runPrompt(text, options, { steering: true, turnId, promptIndex, forceFull: thisTurnForceFull }).catch((error) => emit({ kind: 'error', message: `Hermes steering failed: ${error?.message || error}`, recoverable: true }))
557
- return true
558
- }
559
- const turnId = ++activeTurnId
560
- // Claim the turn synchronously, before the cold safety probe/import. The
561
- // room can now show Thinking + Stop for the whole accepted lifecycle.
562
- turnActive = true
563
- promptChain = promptChain.then(() => runPrompt(text, options, { steering: false, turnId, promptIndex, forceFull: thisTurnForceFull })).catch((error) => {
564
- turnActive = false
565
- emit({ kind: 'error', message: `Hermes turn failed: ${error?.message || error}`, recoverable: true })
566
- })
567
- return true
568
- },
569
- async abort() {
570
- if (!turnActive) return
571
- queuedTurns.length = 0
572
- queueDrainPending = false
573
- const turnId = activeTurnId
574
- abortedTurns.add(turnId)
575
- // Bound retained turn ids while preserving any concurrent /steer request
576
- // that may settle after the primary prompt response.
577
- if (abortedTurns.size > 32) abortedTurns.delete(abortedTurns.values().next().value)
578
- turnActive = false
579
- firstTurn = false
580
- if (starting) {
581
- bootCancelled = true
582
- retireClient()
583
- client?.end()
584
- finishAbortedTurn()
585
- await promptChain.catch(() => {})
586
- return
587
- }
588
- if (!sessionId || !client?.alive) {
589
- finishAbortedTurn()
590
- await promptChain.catch(() => {})
591
- return
592
- }
593
- finishAbortedTurn()
594
- await client.notify('session/cancel', { sessionId }).catch((error) => {
595
- // Never display a stopped lane while the un-cancelled Hermes process
596
- // might still be executing. A transport failure tears down the ACP
597
- // process and makes the lane honestly non-resumable until restart.
598
- crashed = true
599
- client?.end()
600
- emit({ kind: 'error', message: `Hermes cancellation delivery failed; ACP process stopped: ${error?.message || error}`, recoverable: true })
601
- })
602
- // The ACP cancellation notification is not itself the completion edge.
603
- // Wait for the active prompt promise to unwind (including the 0.18.2
604
- // cancelled-response recovery) before a bridge queue flush starts anew.
605
- await promptChain.catch(() => {})
606
- },
607
- end() {
608
- ended = true
609
- turnActive = false
610
- emit.cancel()
611
- queuedTurns.length = 0
612
- queueDrainPending = false
613
- if (sessionId && client?.alive) void client.request('session/close', { sessionId }, 1500).catch(() => {}).finally(() => client?.end())
614
- else client?.end()
615
- void mcpHttp?.close().catch(() => {})
616
- },
617
- setModel(nextModel) {
618
- if (!nextModel || turnActive || modelSwitchPending) return false
619
- const requested = String(nextModel)
620
- modelSwitchPending = true
621
- void boot().then(() => client.request('session/set_model', { sessionId, modelId: requested })).then(async () => {
622
- // 0.18.2 reconstructs state.agent during set_model. The bootstrap
623
- // re-registers its MCP; this independent zero-inference /tools check is
624
- // the transaction's acknowledgement boundary.
625
- await assertMcpReadiness()
626
- if (ended) return
627
- activeModel = requested
628
- if (mapper) mapper.model = activeModel
629
- emit({ kind: 'model-switch', status: 'success', model: activeModel, requested })
630
- }).catch((error) => { if (!ended) emit({ kind: 'model-switch', status: 'failed', model: activeModel, requested, message: `Hermes model switch failed: ${error?.message || error}` }) })
631
- .finally(() => { modelSwitchPending = false })
632
- return true
633
- },
634
- setMode(nextMode) {
635
- if (turnActive || !HERMES_SUPPORTED_MODES.has(nextMode)) return false
636
- const priorMode = activeMode
637
- const changesPolicySchema = (priorMode === 'plan') !== (nextMode === 'plan')
638
- if (changesPolicySchema) {
639
- // Plan is stronger than an approval preference: it removes terminal,
640
- // process, write, patch, browser, and execution schemas. ACP modes alone
641
- // cannot do that, so resume the exact native session in a fresh process
642
- // under the bridge-owned read-only policy.
643
- activeMode = nextMode
644
- suppressNextSessionPublish = started
645
- if (started) {
646
- retireClient()
647
- const oldClient = client
648
- client = null
649
- mapper = null
650
- started = false
651
- crashed = false
652
- stderrTail = ''
653
- oldClient?.end()
654
- }
655
- void boot().then(() => emit({ kind: 'mode', mode: activeMode })).catch((error) => {
656
- activeMode = priorMode
657
- suppressNextSessionPublish = false
658
- emit({ kind: 'error', message: `Hermes mode switch failed: ${error?.message || error}`, recoverable: true })
659
- })
660
- return true
661
- }
662
- void boot().then(() => client.request('session/set_mode', { sessionId, modeId: acpModeFor(nextMode) })).then(() => {
663
- activeMode = nextMode
664
- emit({ kind: 'mode', mode: activeMode })
665
- }).catch((error) => emit({ kind: 'error', message: `Hermes mode switch failed: ${error?.message || error}`, recoverable: true }))
666
- return true
667
- },
668
- setEffort(nextEffort) {
669
- if (turnActive || !HERMES_EFFORT_LEVELS.has(nextEffort)) return false
670
- const requested = String(nextEffort)
671
- void boot().then(() => client.request('session/set_config_option', { sessionId, configId: HERMES_EFFORT_CONFIG_ID, value: requested })).then(() => {
672
- activeEffort = requested
673
- emit({ kind: 'effort', level: activeEffort })
674
- }).catch((error) => emit({ kind: 'error', message: `Hermes reasoning change failed: ${error?.message || error}`, recoverable: true }))
675
- return true
676
- },
677
- clearContext() {
678
- if (turnActive) return false
679
- return this.sendTurn('/reset')
680
- },
681
- warm() { void boot().catch(() => {}) },
682
- }
683
- }
1
+ import e from"node:fs";import t from"node:path";import{randomUUID as o}from"node:crypto";import{AcpClient as n}from"./acp-client.mjs";import{HermesEventMapper as s,hermesToolFor as r}from"./hermes-event-mapper.mjs";import{probeHermesRuntime as i}from"./hermes-probe.mjs";import{HERMES_PLAN_SAFE_MCP_TOOLS as l,HERMES_QUESTION_MCP_TOOL as a,hermesExactInventory as c,hermesPolicyEnv as d,hermesRoleFor as m}from"./hermes-policy.mjs";import{startCodexMcpHttp as u}from"./codex-mcp-http.mjs";import{autoAllow as p,classifyRisk as f}from"./claude-session.mjs";import{crossPostNeedsCard as h}from"./cross-terminal.mjs";import{buildThinkPoolTurnGuidance as g,createRoomContextSelector as w,usesFullThinkPoolReminder as _}from"./thinkpool-room-prompt.mjs";import{createCumulativeEventRelay as y}from"./cumulative-event-relay.mjs";export const HERMES_COMMAND="thinkpool";export const HERMES_ACP_PROTOCOL_VERSION=1;export const HERMES_SUPPORTED_MODES=new Set(["default","acceptEdits","plan","bypassPermissions"]);export const HERMES_EFFORT_LEVELS=new Set(["none","low","medium","high","xhigh","max"]);export const HERMES_EFFORT_CONFIG_ID="thinkpool_reasoning_effort";const E=new Set([...l,"read_review_file"]),I=/(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|ACCESS_KEY|CREDENTIAL)/i,S=new Set(["agent_message_chunk","agent_thought_chunk","tool_call","tool_call_update","plan"]);export const hermesChildEnv=(e={})=>Object.fromEntries(Object.entries(e).filter(([e])=>!I.test(e)));const b=o=>{const n=t.extname(o).toLowerCase();return{type:"image",mimeType:".png"===n?"image/png":".webp"===n?"image/webp":".gif"===n?"image/gif":"image/jpeg",data:e.readFileSync(o).toString("base64")}},k=e=>(e?.availableModels||[]).map(e=>({value:e.modelId,displayName:e.name||e.modelId,description:e.description||""})),x=e=>new n(e);export function buildHermesPromptText({text:e,firstTurn:t=!1,terminalRolePrompt:o,rolePrompt:n,roomContext:s,promptIndex:r=0,forceFullReminder:i=!1}={}){const l="function"==typeof s?s():s,a=!!(o||n||l),c=_({promptIndex:r,forceFull:i}),d=[t||c?o:"",a?g({text:e,promptIndex:r,forceFull:i}):"",t||c?n:"",l].filter(Boolean).join("\n\n"),m=String(e??"");return d?`${d}\n\n${m}`:m}const v=(e,t=[])=>{if("deny"===e||!e)return{outcome:"cancelled"};const o=("always"===e?["allow_always","allow_session","allow_once"]:["allow_once","allow_session","allow_always"]).map(e=>t.find(t=>t.kind===e||t.optionId===e)).find(Boolean)||t.find(e=>String(e.kind||"").startsWith("allow"));return o?{outcome:"selected",optionId:o.optionId}:{outcome:"cancelled"}};export function startHermesSession({cwd:e,model:n,resume:g,env:I=process.env,mode:R="default",onEvent:C,requestPermission:H,roomContext:T,terminalRolePrompt:P,rolePrompt:$,mcpServers:M,requiredMcpTools:F=[],prepareCwd:O=null,command:q="thinkpool",args:A=["acp"],clientFactory:j=x,mcpHttpFactory:D=u,lazy:L=!1,hermesRole:N=null,crossPostGate:V=null,didSpawnTarget:B=null,crossRoomPostGate:U=null,effort:z="high",admitStart:G=null,onTurnStart:K=null}={}){let Y=e;const W=n||null;let J=W,Q=HERMES_SUPPORTED_MODES.has(R)?R:"default",X=g||null,Z=null,ee=null,te=null,oe=null,ne=!1,se=!1,re=!1,ie=!1,le=!0,ae=0,ce=!0;const de=w(T);let me="",ue=Promise.resolve(),pe=0;const fe=new Set;let he=()=>{},ge=!1,we=null,_e=!1,ye=!1,Ee=!1,Ie=null===z?null:HERMES_EFFORT_LEVELS.has(z)?z:"high";const Se=[];let be=!1,ke=!1;const xe=N||m({}),ve=()=>"plan"===Q?"plan":xe,Re=()=>"plan"===Q?F.filter(e=>E.has(e)):F,Ce=e=>"acceptEdits"===e?"accept_edits":"bypassPermissions"===e?"dont_ask":"default",He=y(e=>{try{C?.(e)}catch{}});async function Te(e,t,o){if("session/request_permission"!==e)throw Object.assign(new Error(`Unsupported ACP client method: ${e}`),{code:-32601});const n=r(t.toolCall||ee?.tools.get(t.toolCallId)||{}),s=(e=>{const t=String(e||"").toLowerCase();return[...l,"post_to_terminal","post_to_session",a].find(e=>t===e||t===`mcp__thinkpool__${e}`||t===`mcp_thinkpool_${e}`)||null})(n.name),i={id:`hermes-perm:${o}`,toolName:s?`mcp__thinkpool__${s}`:n.name,input:n.input,risk:s?.startsWith("post_to_")?"high":f(n.name,n.input)};if(l.includes(s))return{outcome:v("allow",t.options)};if(s===a)return{outcome:v("allow",t.options)};if("post_to_terminal"===s){if(!(V?V():{ok:!0}).ok)return{outcome:{outcome:"cancelled"}};const e=!!B&&!!B(n.input?.terminal);if(!h({mode:Q,spawnedByMe:e}))return{outcome:v("allow",t.options)}}if("post_to_session"===s){if(!(U?U():{ok:!0}).ok)return{outcome:{outcome:"cancelled"}};let e="deny";try{e=await(H?.(i))}catch{}return{outcome:v(e,t.options)}}if("plan"===Q)return{outcome:{outcome:"cancelled"}};if(p({toolName:i.toolName,input:i.input,mode:Q}))return{outcome:v("allow",t.options)};let c="deny";try{c=await(H?.(i))}catch{}return{outcome:v(c,t.options)}}function Pe(e,t){if("session/update"!==e)return;const o=t?.update||t;we&&"agent_message_chunk"===o?.sessionUpdate&&"text"===o?.content?.type?we.push(String(o.content.text||"")):ge&&S.has(o?.sessionUpdate)||ee?.push(t)}async function $e(){const e=[...new Set((Array.isArray(Re())?Re():[]).map(e=>String(e||"").trim()).filter(Boolean))];if(!e.length&&"plan"!==ve())return{inventory:"",missing:[],forbidden:[]};const t=c(ve(),{mcpTools:e}),n=t.builtinTools,s=t.requiredBuiltinTools,r=t.mcpTools,i=[];we=i;try{await Z.request("session/prompt",{sessionId:X,prompt:[{type:"text",text:"/tools"}],messageId:o()},3e4)}finally{we=null}const l=i.join(""),a=e=>new RegExp(`(?:^|[^a-z0-9_])${e}(?:$|[^a-z0-9_])`,"i").test(l),d=[...l.matchAll(/\bmcp(?:__|_)[a-z0-9_]+/gi)].map(e=>e[0]),m=new Set(r.flatMap(e=>[`mcp__thinkpool__${e}`,`mcp_thinkpool_${e}`])),u=[...new Set(d.filter(e=>!m.has(e)))];return{inventory:l,missing:[...s.filter(e=>!a(e)),...r.filter(e=>!a(`mcp__thinkpool__${e}`)&&!a(`mcp_thinkpool_${e}`))],forbidden:[...t.allBuiltinTools.filter(e=>!n.includes(e)&&a(e)),...u]}}async function Me(){const e=await $e();if(!e.missing.length&&!e.forbidden.length)return e;const t=[e.missing.length?`missing ${e.missing.join(", ")}`:"",e.forbidden.length?`forbidden ${e.forbidden.join(", ")}`:""].filter(Boolean).join("; ");throw new Error(`Hermes ThinkPool MCP readiness failed; ${t}`)}async function Fe(){if(ne)throw new Error("Hermes session is closed");if(ie)throw new Error("Hermes ACP process crashed; this lane must be restarted before sending another turn");if(Z?.alive&&ee)return;if(oe)return oe;const e="function"==typeof G?G():{ok:!0};if(!1===e?.ok)throw new Error(e.reason||"Host memory is critically low. This Hermes runtime was not started.");return oe=(async()=>{if(!re&&O)try{Y=O()||Y}catch{}const e=M?.thinkpool;e&&!te&&(te=await D({sdkServer:e}));let o=hermesChildEnv(I),n=q,r=A;if("thinkpool"===q&&j===x){const e=i({command:q,env:o,strictBootstrap:!0});if(!e.available)throw new Error(`Hermes profile safety check failed: ${e.reason||"runtime unavailable"}`);n=e.python,r=[e.bootstrap],o={...o,HERMES_HOME:e.profile,THINKPOOL_HERMES_ACP_POLICY:d(ve(),{mcpTools:Re()})}}let l=!1;he=()=>{l=!0};const a={protocolVersion:1,clientCapabilities:{fs:{readTextFile:!1,writeTextFile:!1},terminal:!1},clientInfo:{name:"thinkpool-pair",title:"ThinkPool Code",version:"1"}};let c;for(let e=0;e<2;e++){l=!1,he=()=>{l=!0},Z=j({command:n,args:r,cwd:Y,env:o,onNotification:Pe,onRequest:Te,onStderr:e=>{me=(me+e).slice(-2e3)},onClose:e=>{ne||ie||l||(ie=!0,se=!1,He({kind:"error",message:`Hermes ACP closed unexpectedly: ${e?.message||e}`,recoverable:!0}))}}),Z.start();try{c=await Z.request("initialize",a,15e3);break}catch(t){if(ye||0!==e||!/^initialize timed out\b/i.test(String(t?.message||t)))throw t;he();const o=Z;Z=null,o?.end(),me=""}}if(1!==c?.protocolVersion)throw new Error(`Unsupported Hermes ACP protocol ${c?.protocolVersion??"unknown"}; expected 1`);const m={cwd:t.resolve(Y),mcpServers:te?[(u=te.url,{type:"http",name:"thinkpool",url:u,headers:[]})]:[]};var u;let p;if(ee=new s({onEvent:He,sessionId:X,model:J}),X){if(!c?.agentCapabilities?.sessionCapabilities?.resume)throw new Error(`Hermes session ${X} cannot be resumed by this ACP release; no fresh session was substituted`);try{ge=!0,p=await Z.request("session/resume",{...m,sessionId:X},3e4)}catch(e){throw new Error(`Hermes session ${X} is not available; no fresh session was substituted (${e?.message||e})`)}finally{ge=!1}}else p=await Z.request("session/new",m,3e4);if(X||(X=p?.sessionId||null),!X)throw new Error("Hermes ACP did not return a session id");let f=await $e(),h=f.missing;if(h.length&&c?.agentCapabilities?.sessionCapabilities?.resume){try{ge=!0,p=await Z.request("session/resume",{...m,sessionId:X},3e4)}finally{ge=!1}f=await $e(),h=f.missing}if(h.length||f.forbidden.length){const e=[h.length?`missing ${h.join(", ")}`:"",f.forbidden.length?`forbidden ${f.forbidden.join(", ")}`:""].filter(Boolean).join("; ");throw new Error(`Hermes ThinkPool MCP readiness failed; ${e}`)}J=W||p?.models?.currentModelId||null||J,W&&(await Z.request("session/set_model",{sessionId:X,modelId:J}),await Me()),Ie&&await Z.request("session/set_config_option",{sessionId:X,configId:HERMES_EFFORT_CONFIG_ID,value:Ie});const g=p?.models?{...p.models,currentModelId:J}:J?{currentModelId:J,availableModels:[]}:p?.models;Ee?(ee.sessionId=X,ee.model=J,Ee=!1):ee.startSession({sessionId:X,models:g,modes:p?.modes,commands:[]}),re=!0;const w=Ce(Q);p?.modes?.availableModes?.some(e=>e.id===w)&&p.modes.currentModeId!==w&&await Z.request("session/set_mode",{sessionId:X,modeId:w}),He({kind:"capabilities",runtime:"hermes",protocol:"acp",protocolVersion:c?.protocolVersion,capabilities:c?.agentCapabilities||{},models:k(p?.models),flow:"thinkpool"===q&&j===x})})().catch(async e=>{ie=!0,Z?.end();try{await(te?.close())}catch{}throw te=null,ye||He({kind:"error",message:`Hermes ACP startup failed: ${e?.message||e}${me?`: ${me.trim().slice(-240)}`:""}`,recoverable:!0}),e}).finally(()=>{oe=null,ye=!1}),oe}function Oe(){ee?ee.finishTurn({stopReason:"cancelled"}):He({kind:"result",subtype:"aborted",sessionId:X,model:J,costUsd:null,usage:null,numTurns:1,durationMs:void 0,denials:0,resultText:null})}function qe(e,t={},{promptIndex:o=0,forceFull:n=!1}={}){const s=String(e);if(/^\s*\//.test(s))return[{type:"text",text:s}];const r=_({promptIndex:o,forceFull:n}),i=[{type:"text",text:buildHermesPromptText({text:s,firstTurn:le,terminalRolePrompt:P,rolePrompt:$,roomContext:()=>de({force:r}),promptIndex:o,forceFullReminder:n})}];for(const e of Array.isArray(t.images)?t.images:[])try{i.push(b(e))}catch{}return i}async function Ae(e,t={},{steering:n=!1,turnId:s=pe,promptIndex:r=0,forceFull:i=!1}={}){try{await Fe()}catch(e){if(fe.has(s))return{stopReason:"cancelled"};throw e}if(fe.has(s))return{stopReason:"cancelled"};const l=n?`/steer ${String(e)}`:String(e);let a;try{a=await Z.request("session/prompt",{sessionId:X,prompt:qe(l,t,{promptIndex:r,forceFull:i}),messageId:o()},0)}catch(e){if(fe.has(s))return ne||await async function(){const e=Z;he(),Z=null,ee=null,e?.end(),await Fe()}(),{stopReason:"cancelled"};throw e}if(!n){if(fe.has(s))return a;se=!1,le=!1;const t=/^\s*\/compact\b/i.test(String(e||""))?ee.compactOutcome():null;t&&He({kind:"compaction",trigger:"manual",...t});const o=String(e||"").match(/^\s*\/reasoning\s+(low|medium|high|xhigh|max|none|reset)\s*$/i);if(o){const e=o[1].toLowerCase();("reset"===e?/Session reasoning reset to model default/i.test(ee.text):new RegExp(`Session reasoning set to ${e}`,"i").test(ee.text))&&(Ie="reset"===e?null:e,He({kind:"effort",level:Ie}))}ke=Se.length>0&&!ne&&!fe.has(s),ee.finishTurn({stopReason:a?.stopReason,usage:a?.usage}),ke&&async function(){if(!(be||ne||se)){be=!0;try{for(;Se.length&&!ne&&!se;){ke=!1;const e=Se.shift(),t=++pe;se=!0;try{K?.(e.options||{})}catch{}try{await Ae(e.text,e.options,{steering:!1,turnId:t,promptIndex:e.promptIndex,forceFull:e.forceFull})}catch(e){se=!1,He({kind:"error",message:`Hermes queued turn failed: ${e?.message||e}`,recoverable:!0})}}}finally{be=!1,ke=!1}}}()}return a}return L||queueMicrotask(()=>{Fe().catch(()=>{})}),{get sessionId(){return X},get turnActive(){return se},get queuedDepth(){return Se.length},get canSteer(){return!!Z?.alive},get started(){return re},get models(){return[]},get effort(){return Ie},sendTurn(e,t={}){if(ne)return!1;if(!Z?.alive&&!oe){const e="function"==typeof G?G():{ok:!0};if(!1===e?.ok)return He({kind:"error",message:e.reason||"Host memory is critically low. This Hermes runtime was not started.",recoverable:!0}),!1}const o=ae++,n=ce;ce=/^\s*\/(?:compact|reset|clear)\b/i.test(String(e||"")),ie&&!ne&&(he(),Z?.end(),Z=null,ee=null,ie=!1,me="");const s=String(e||"").match(/^\s*\/queue\s+([\s\S]+)$/i);if(ke)return Se.push({text:s?s[1]:String(e||""),options:t,promptIndex:o,forceFull:n}),He({kind:"queue-add",queued:!0,depth:Se.length}),!0;if(se)return s?(Se.push({text:s[1],options:t,promptIndex:o,forceFull:n}),He({kind:"queue-add",queued:!0,depth:Se.length}),!0):(Ae(e,t,{steering:!0,turnId:pe,promptIndex:o,forceFull:n}).catch(e=>He({kind:"error",message:`Hermes steering failed: ${e?.message||e}`,recoverable:!0})),!0);const r=++pe;return se=!0,ue=ue.then(()=>Ae(e,t,{steering:!1,turnId:r,promptIndex:o,forceFull:n})).catch(e=>{se=!1,He({kind:"error",message:`Hermes turn failed: ${e?.message||e}`,recoverable:!0})}),!0},async abort(){if(!se)return;Se.length=0,ke=!1;const e=pe;return fe.add(e),fe.size>32&&fe.delete(fe.values().next().value),se=!1,le=!1,oe?(ye=!0,he(),Z?.end(),Oe(),void await ue.catch(()=>{})):X&&Z?.alive?(Oe(),await Z.notify("session/cancel",{sessionId:X}).catch(e=>{ie=!0,Z?.end(),He({kind:"error",message:`Hermes cancellation delivery failed; ACP process stopped: ${e?.message||e}`,recoverable:!0})}),void await ue.catch(()=>{})):(Oe(),void await ue.catch(()=>{}))},end(){ne=!0,se=!1,He.cancel(),Se.length=0,ke=!1,X&&Z?.alive?Z.request("session/close",{sessionId:X},1500).catch(()=>{}).finally(()=>Z?.end()):Z?.end(),te?.close().catch(()=>{})},setModel(e){if(!e||se||_e)return!1;const t=String(e);return _e=!0,Fe().then(()=>Z.request("session/set_model",{sessionId:X,modelId:t})).then(async()=>{await Me(),ne||(J=t,ee&&(ee.model=J),He({kind:"model-switch",status:"success",model:J,requested:t}))}).catch(e=>{ne||He({kind:"model-switch",status:"failed",model:J,requested:t,message:`Hermes model switch failed: ${e?.message||e}`})}).finally(()=>{_e=!1}),!0},setMode(e){if(se||!HERMES_SUPPORTED_MODES.has(e))return!1;const t=Q;if("plan"===t!=("plan"===e)){if(Q=e,Ee=re,re){he();const e=Z;Z=null,ee=null,re=!1,ie=!1,me="",e?.end()}return Fe().then(()=>He({kind:"mode",mode:Q})).catch(e=>{Q=t,Ee=!1,He({kind:"error",message:`Hermes mode switch failed: ${e?.message||e}`,recoverable:!0})}),!0}return Fe().then(()=>Z.request("session/set_mode",{sessionId:X,modeId:Ce(e)})).then(()=>{Q=e,He({kind:"mode",mode:Q})}).catch(e=>He({kind:"error",message:`Hermes mode switch failed: ${e?.message||e}`,recoverable:!0})),!0},setEffort(e){if(se||!HERMES_EFFORT_LEVELS.has(e))return!1;const t=String(e);return Fe().then(()=>Z.request("session/set_config_option",{sessionId:X,configId:HERMES_EFFORT_CONFIG_ID,value:t})).then(()=>{Ie=t,He({kind:"effort",level:Ie})}).catch(e=>He({kind:"error",message:`Hermes reasoning change failed: ${e?.message||e}`,recoverable:!0})),!0},clearContext(){return!se&&this.sendTurn("/reset")},warm(){Fe().catch(()=>{})}}}