thinkpool-pair 0.7.251 → 0.7.252
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 +10 -1
- package/hermes-session.mjs +93 -29
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -3833,8 +3833,17 @@ channel
|
|
|
3833
3833
|
const nativeImages = s.runtime === 'codex' || s.runtime === 'hermes'
|
|
3834
3834
|
? await waitForNativeImages(payload.files, { updir: UPDIR })
|
|
3835
3835
|
: []
|
|
3836
|
-
s.session.sendTurn(sendText, nativeImages.length ? { images: nativeImages } : undefined)
|
|
3836
|
+
const accepted = s.session.sendTurn(sendText, nativeImages.length ? { images: nativeImages } : undefined)
|
|
3837
3837
|
echoYou()
|
|
3838
|
+
if (accepted === false) {
|
|
3839
|
+
// A runtime that did not accept a turn must still close the optimistic
|
|
3840
|
+
// user-line lifecycle. Without this boundary the client truthfully shows
|
|
3841
|
+
// what the person sent but falsely leaves Thinking/Stop pinned forever.
|
|
3842
|
+
const evt = { kind: 'error', message: `${structuredRuntimeMetadata(s.runtime)?.label || 'Agent'} did not accept this turn; retry after the lane is available.`, recoverable: true }
|
|
3843
|
+
pushLog(s, evt)
|
|
3844
|
+
bcast('code-event', { term: payload.term, evt })
|
|
3845
|
+
}
|
|
3846
|
+
announce()
|
|
3838
3847
|
// Pool-order fix (2026-07-02): emit the "dispatched to agent" marker HERE, right after
|
|
3839
3848
|
// the @pool you-line, so it inherits the NEXT seq (seqable('pool') is true) and orders
|
|
3840
3849
|
// correctly for EVERY viewer. It used to be pushed web-side with the FIRER's wall-clock
|
package/hermes-session.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { classifyRisk } from './claude-session.mjs'
|
|
|
13
13
|
export const HERMES_COMMAND = 'thinkpool'
|
|
14
14
|
export const HERMES_ACP_PROTOCOL_VERSION = 1
|
|
15
15
|
export const HERMES_SUPPORTED_MODES = new Set(['default', 'acceptEdits'])
|
|
16
|
+
const HERMES_INITIALIZE_TIMEOUT_MS = 15_000
|
|
16
17
|
|
|
17
18
|
const HERMES_SECRET_ENV_KEY = /(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|ACCESS_KEY|CREDENTIAL)/i
|
|
18
19
|
const HERMES_REPLAY_UPDATES = new Set(['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan'])
|
|
@@ -69,6 +70,7 @@ export function startHermesSession({
|
|
|
69
70
|
let resuming = false
|
|
70
71
|
let inventoryProbe = null
|
|
71
72
|
let modelSwitchPending = false
|
|
73
|
+
let bootCancelled = false
|
|
72
74
|
const policyRole = hermesRole || hermesRoleFor({})
|
|
73
75
|
|
|
74
76
|
const emit = (event) => { try { onEvent?.(event) } catch { /* consumer isolation */ } }
|
|
@@ -177,24 +179,45 @@ export function startHermesSession({
|
|
|
177
179
|
}
|
|
178
180
|
let retired = false
|
|
179
181
|
retireClient = () => { retired = true }
|
|
180
|
-
|
|
181
|
-
command: launchCommand, args: launchArgs, cwd: activeCwd, env: childEnv,
|
|
182
|
-
onNotification,
|
|
183
|
-
onRequest,
|
|
184
|
-
onStderr: (text) => { stderrTail = (stderrTail + text).slice(-2000) },
|
|
185
|
-
onClose: (error) => {
|
|
186
|
-
if (ended || crashed || retired) return
|
|
187
|
-
crashed = true
|
|
188
|
-
turnActive = false
|
|
189
|
-
emit({ kind: 'error', message: `Hermes ACP closed unexpectedly: ${error?.message || error}`, recoverable: true })
|
|
190
|
-
},
|
|
191
|
-
})
|
|
192
|
-
client.start()
|
|
193
|
-
const initialized = await client.request('initialize', {
|
|
182
|
+
const initializeParams = {
|
|
194
183
|
protocolVersion: HERMES_ACP_PROTOCOL_VERSION,
|
|
195
184
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
196
185
|
clientInfo: { name: 'thinkpool-pair', title: 'ThinkPool Code', version: '1' },
|
|
197
|
-
}
|
|
186
|
+
}
|
|
187
|
+
let initialized
|
|
188
|
+
// Cold Hermes imports take 6–9 seconds on the supported host, and the
|
|
189
|
+
// production ENOSPC incident pushed one child just beyond the old 10s
|
|
190
|
+
// cliff. Initialization is pre-session and pre-inference, so one fresh
|
|
191
|
+
// process retry is safe: no prompt or tool can have executed yet.
|
|
192
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
193
|
+
retired = false
|
|
194
|
+
retireClient = () => { retired = true }
|
|
195
|
+
client = clientFactory({
|
|
196
|
+
command: launchCommand, args: launchArgs, cwd: activeCwd, env: childEnv,
|
|
197
|
+
onNotification,
|
|
198
|
+
onRequest,
|
|
199
|
+
onStderr: (text) => { stderrTail = (stderrTail + text).slice(-2000) },
|
|
200
|
+
onClose: (error) => {
|
|
201
|
+
if (ended || crashed || retired) return
|
|
202
|
+
crashed = true
|
|
203
|
+
turnActive = false
|
|
204
|
+
emit({ kind: 'error', message: `Hermes ACP closed unexpectedly: ${error?.message || error}`, recoverable: true })
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
client.start()
|
|
208
|
+
try {
|
|
209
|
+
initialized = await client.request('initialize', initializeParams, HERMES_INITIALIZE_TIMEOUT_MS)
|
|
210
|
+
break
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const retryable = !bootCancelled && attempt === 0 && /^initialize timed out\b/i.test(String(error?.message || error))
|
|
213
|
+
if (!retryable) throw error
|
|
214
|
+
retireClient()
|
|
215
|
+
const timedOutClient = client
|
|
216
|
+
client = null
|
|
217
|
+
timedOutClient?.end()
|
|
218
|
+
stderrTail = ''
|
|
219
|
+
}
|
|
220
|
+
}
|
|
198
221
|
if (initialized?.protocolVersion !== HERMES_ACP_PROTOCOL_VERSION) {
|
|
199
222
|
throw new Error(`Unsupported Hermes ACP protocol ${initialized?.protocolVersion ?? 'unknown'}; expected ${HERMES_ACP_PROTOCOL_VERSION}`)
|
|
200
223
|
}
|
|
@@ -255,16 +278,32 @@ export function startHermesSession({
|
|
|
255
278
|
await client.request('session/set_mode', { sessionId, modeId: acpMode })
|
|
256
279
|
}
|
|
257
280
|
emit({ kind: 'capabilities', runtime: 'hermes', protocol: 'acp', protocolVersion: initialized?.protocolVersion, capabilities: initialized?.agentCapabilities || {}, models: modelList(state?.models), flow: command === HERMES_COMMAND && clientFactory === createAcpClient })
|
|
258
|
-
})().catch((error) => {
|
|
281
|
+
})().catch(async (error) => {
|
|
259
282
|
crashed = true
|
|
260
283
|
client?.end()
|
|
261
|
-
|
|
262
|
-
|
|
284
|
+
try { await mcpHttp?.close() } catch { /* startup cleanup */ }
|
|
285
|
+
mcpHttp = null
|
|
286
|
+
if (!bootCancelled) emit({ kind: 'error', message: `Hermes ACP startup failed: ${error?.message || error}${stderrTail ? `: ${stderrTail.trim().slice(-240)}` : ''}`, recoverable: true })
|
|
263
287
|
throw error
|
|
264
|
-
}).finally(() => { starting = null })
|
|
288
|
+
}).finally(() => { starting = null; bootCancelled = false })
|
|
265
289
|
return starting
|
|
266
290
|
}
|
|
267
291
|
|
|
292
|
+
function reviveAfterExplicitRetry() {
|
|
293
|
+
if (!crashed || ended) return
|
|
294
|
+
retireClient()
|
|
295
|
+
client?.end()
|
|
296
|
+
client = null
|
|
297
|
+
mapper = null
|
|
298
|
+
crashed = false
|
|
299
|
+
stderrTail = ''
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function finishAbortedTurn() {
|
|
303
|
+
if (mapper) mapper.finishTurn({ stopReason: 'cancelled' })
|
|
304
|
+
else emit({ kind: 'result', subtype: 'aborted', sessionId, model: activeModel, costUsd: null, usage: null, numTurns: 1, durationMs: undefined, denials: 0, resultText: null })
|
|
305
|
+
}
|
|
306
|
+
|
|
268
307
|
async function restartAfterCancelledResponseFailure() {
|
|
269
308
|
const oldClient = client
|
|
270
309
|
retireClient()
|
|
@@ -288,12 +327,14 @@ export function startHermesSession({
|
|
|
288
327
|
return blocks
|
|
289
328
|
}
|
|
290
329
|
|
|
291
|
-
async function runPrompt(text, options = {}) {
|
|
292
|
-
await boot()
|
|
293
|
-
|
|
294
|
-
|
|
330
|
+
async function runPrompt(text, options = {}, { steering = false, turnId = activeTurnId } = {}) {
|
|
331
|
+
try { await boot() }
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (abortedTurns.has(turnId)) return { stopReason: 'cancelled' }
|
|
334
|
+
throw error
|
|
335
|
+
}
|
|
336
|
+
if (abortedTurns.has(turnId)) return { stopReason: 'cancelled' }
|
|
295
337
|
const body = steering ? `/steer ${String(text)}` : String(text)
|
|
296
|
-
if (!steering) turnActive = true
|
|
297
338
|
let result
|
|
298
339
|
try {
|
|
299
340
|
result = await client.request('session/prompt', {
|
|
@@ -334,17 +375,29 @@ export function startHermesSession({
|
|
|
334
375
|
get started() { return started },
|
|
335
376
|
get models() { return [] },
|
|
336
377
|
sendTurn(text, options = {}) {
|
|
337
|
-
if (ended
|
|
378
|
+
if (ended) return false
|
|
379
|
+
// A crash never replays work by itself. A later explicit turn is the
|
|
380
|
+
// authority to launch a fresh ACP process and resume the same native
|
|
381
|
+
// session id; this is the recovery path the old permanent latch blocked.
|
|
382
|
+
reviveAfterExplicitRetry()
|
|
338
383
|
// A busy prompt is a genuine ACP /steer call and may run concurrently.
|
|
339
|
-
if (turnActive) {
|
|
340
|
-
|
|
384
|
+
if (turnActive) {
|
|
385
|
+
const turnId = activeTurnId
|
|
386
|
+
void runPrompt(text, options, { steering: true, turnId }).catch((error) => emit({ kind: 'error', message: `Hermes steering failed: ${error?.message || error}`, recoverable: true }))
|
|
387
|
+
return true
|
|
388
|
+
}
|
|
389
|
+
const turnId = ++activeTurnId
|
|
390
|
+
// Claim the turn synchronously, before the cold safety probe/import. The
|
|
391
|
+
// room can now show Thinking + Stop for the whole accepted lifecycle.
|
|
392
|
+
turnActive = true
|
|
393
|
+
promptChain = promptChain.then(() => runPrompt(text, options, { steering: false, turnId })).catch((error) => {
|
|
341
394
|
turnActive = false
|
|
342
395
|
emit({ kind: 'error', message: `Hermes turn failed: ${error?.message || error}`, recoverable: true })
|
|
343
396
|
})
|
|
344
397
|
return true
|
|
345
398
|
},
|
|
346
399
|
abort() {
|
|
347
|
-
if (!
|
|
400
|
+
if (!turnActive) return
|
|
348
401
|
const turnId = activeTurnId
|
|
349
402
|
abortedTurns.add(turnId)
|
|
350
403
|
// Bound retained turn ids while preserving any concurrent /steer request
|
|
@@ -352,7 +405,18 @@ export function startHermesSession({
|
|
|
352
405
|
if (abortedTurns.size > 32) abortedTurns.delete(abortedTurns.values().next().value)
|
|
353
406
|
turnActive = false
|
|
354
407
|
firstTurn = false
|
|
355
|
-
|
|
408
|
+
if (starting) {
|
|
409
|
+
bootCancelled = true
|
|
410
|
+
retireClient()
|
|
411
|
+
client?.end()
|
|
412
|
+
finishAbortedTurn()
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
if (!sessionId || !client?.alive) {
|
|
416
|
+
finishAbortedTurn()
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
finishAbortedTurn()
|
|
356
420
|
void client.notify('session/cancel', { sessionId }).catch((error) => {
|
|
357
421
|
// Never display a stopped lane while the un-cancelled Hermes process
|
|
358
422
|
// might still be executing. A transport failure tears down the ACP
|