thinkpool-pair 0.7.236 → 0.7.237
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 +28 -2
- package/event-id.mjs +1 -1
- package/hermes-session.mjs +10 -5
- package/package.json +1 -1
- package/runtime-registry.mjs +3 -3
package/bridge.mjs
CHANGED
|
@@ -2616,12 +2616,29 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2616
2616
|
// run on different registered providers. (bridge-truth slice.)
|
|
2617
2617
|
{ const onBuiltin = entry.runtime !== 'hermes' && (!entry.provider || entry.provider === 'anthropic')
|
|
2618
2618
|
const m = (entry.runtime === 'hermes' || onBuiltin) ? (evt.model || evt.ctx?.model) : null
|
|
2619
|
-
if (m && m !== entry.model) { entry.model = m; announce() }
|
|
2619
|
+
if (m && m !== entry.model) { entry.model = m; announce(); if (evt.kind === 'model-switch') persist() }
|
|
2620
2620
|
// A custom Anthropic-compatible endpoint may expose its own catalog through the
|
|
2621
2621
|
// same SDK method. Never publish that as the ROOM's Claude catalog: its models
|
|
2622
2622
|
// already render in the provider group and otherwise leak under ANTHROPIC.
|
|
2623
2623
|
if (entry.runtime === 'hermes' && evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { entry.models = evt.models; announce() }
|
|
2624
2624
|
else if (onBuiltin && evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { roomModels = evt.models; announce() } }
|
|
2625
|
+
// Hermes model changes are asynchronous ACP requests. Keep the previous model
|
|
2626
|
+
// authoritative until Hermes acknowledges session/set_model; then settle the
|
|
2627
|
+
// originating control line exactly once. A rejected model never poisons the
|
|
2628
|
+
// saved session or leaves a cross-device chip claiming the optimistic pick.
|
|
2629
|
+
if (evt.kind === 'model-switch') {
|
|
2630
|
+
const request = entry.pendingModelSwitch
|
|
2631
|
+
entry.pendingModelSwitch = null
|
|
2632
|
+
const requested = evt.requested || request?.model || 'requested model'
|
|
2633
|
+
const text = evt.status === 'success'
|
|
2634
|
+
? `switched model → ${evt.model || requested}`
|
|
2635
|
+
: `couldn’t switch to ${requested} — still using ${entry.model || 'the previous model'}`
|
|
2636
|
+
const control = { kind: 'control', text, by: request?.by || null, cid: request?.cid || undefined }
|
|
2637
|
+
pushLog(entry, control)
|
|
2638
|
+
bcast('code-event', { term: id, evt: control })
|
|
2639
|
+
entry.flush?.()
|
|
2640
|
+
announce()
|
|
2641
|
+
}
|
|
2625
2642
|
// Cache the last usage/ctx meter on the entry (persisted + replayed — see
|
|
2626
2643
|
// openStructured). Chrome events bypass pushLog/persist in emitTail below, so
|
|
2627
2644
|
// usage needs an explicit persist() here to survive a bridge restart.
|
|
@@ -2700,7 +2717,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2700
2717
|
// card) and is intentionally NOT chrome, so it persists + replays.
|
|
2701
2718
|
// 'suggestion' is Claude Code's predicted-next-prompt ghost text — ephemeral
|
|
2702
2719
|
// composer UI, broadcast to both clients but never logged/persisted/replayed.
|
|
2703
|
-
const chrome = evt.kind === 'mode' || evt.kind === 'usage' || evt.kind === 'clear' || evt.kind === 'compact' || evt.kind === 'stalled' || evt.kind === 'suggestion' || evt.kind === 'assistant_stream'
|
|
2720
|
+
const chrome = evt.kind === 'mode' || evt.kind === 'usage' || evt.kind === 'clear' || evt.kind === 'compact' || evt.kind === 'stalled' || evt.kind === 'suggestion' || evt.kind === 'assistant_stream' || evt.kind === 'model-switch'
|
|
2704
2721
|
// The compact turn settled with `entry.compacting` STILL TRUE — which, because the
|
|
2705
2722
|
// `compaction` branch above clears the flag the moment the milestone lands, means
|
|
2706
2723
|
// exactly one thing: NO compaction happened. Three ways to get here, all of which
|
|
@@ -3428,10 +3445,19 @@ channel
|
|
|
3428
3445
|
const mm = text.match(/^\/model\b\s*(\S+)?/)
|
|
3429
3446
|
if (mm) {
|
|
3430
3447
|
if (mm[1]) {
|
|
3448
|
+
if (s.runtime === 'hermes' && s.pendingModelSwitch) {
|
|
3449
|
+
ctlLine(`finish the current Hermes model switch before choosing another model`)
|
|
3450
|
+
return
|
|
3451
|
+
}
|
|
3431
3452
|
if (s.session.setModel(mm[1]) === false) {
|
|
3432
3453
|
ctlLine(`finish or stop the current ${structuredRuntimeMetadata(s.runtime)?.label || 'agent'} turn before switching models`)
|
|
3433
3454
|
return
|
|
3434
3455
|
}
|
|
3456
|
+
if (s.runtime === 'hermes') {
|
|
3457
|
+
s.pendingModelSwitch = { model: mm[1], by: payload.by, cid: payload.cid }
|
|
3458
|
+
bcast('code-event', { term: payload.term, evt: { kind: 'model-switch', status: 'pending', requested: mm[1], ts: Date.now() } })
|
|
3459
|
+
return
|
|
3460
|
+
}
|
|
3435
3461
|
// Reflect the switch in the announce IMMEDIATELY (before any turn). The SDK
|
|
3436
3462
|
// only echoes the resolved full id on the next turn, and meta/ctx model go
|
|
3437
3463
|
// stale until then, so without this the chip snapped back to the old model
|
package/event-id.mjs
CHANGED
|
@@ -61,7 +61,7 @@ export function makeSeqCounter(startAfter = 0) {
|
|
|
61
61
|
// consumed a seq + set the durable archive floor while the client dropped them, leaving
|
|
62
62
|
// the client's oldest RENDERED seq above the floor → a phantom "load earlier history"
|
|
63
63
|
// pill on a fresh lane (models fires on every session init) that fetched nothing.
|
|
64
|
-
export const NO_SEQ_KINDS = new Set(['system', 'thinking_tokens', 'effort', 'models', 'queue-add', 'queue-cancel', 'propose-done', 'update-available'])
|
|
64
|
+
export const NO_SEQ_KINDS = new Set(['system', 'thinking_tokens', 'effort', 'models', 'model-switch', 'queue-add', 'queue-cancel', 'propose-done', 'update-available'])
|
|
65
65
|
export const seqable = (evt) => !!evt && typeof evt === 'object' && !NO_SEQ_KINDS.has(evt.kind)
|
|
66
66
|
|
|
67
67
|
// Highest numeric `seq` across a log of events (0 when none) — the floor a restored
|
package/hermes-session.mjs
CHANGED
|
@@ -66,6 +66,7 @@ export function startHermesSession({
|
|
|
66
66
|
const abortedTurns = new Set()
|
|
67
67
|
let retireClient = () => {}
|
|
68
68
|
let resuming = false
|
|
69
|
+
let modelSwitchPending = false
|
|
69
70
|
|
|
70
71
|
const emit = (event) => { try { onEvent?.(event) } catch { /* consumer isolation */ } }
|
|
71
72
|
|
|
@@ -277,12 +278,16 @@ export function startHermesSession({
|
|
|
277
278
|
void mcpHttp?.close().catch(() => {})
|
|
278
279
|
},
|
|
279
280
|
setModel(nextModel) {
|
|
280
|
-
if (!nextModel || turnActive) return false
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
if (!nextModel || turnActive || modelSwitchPending) return false
|
|
282
|
+
const requested = String(nextModel)
|
|
283
|
+
modelSwitchPending = true
|
|
284
|
+
void boot().then(() => client.request('session/set_model', { sessionId, modelId: requested })).then(() => {
|
|
285
|
+
if (ended) return
|
|
286
|
+
activeModel = requested
|
|
283
287
|
if (mapper) mapper.model = activeModel
|
|
284
|
-
emit({ kind: '
|
|
285
|
-
}).catch((error) => emit({ kind: '
|
|
288
|
+
emit({ kind: 'model-switch', status: 'success', model: activeModel, requested })
|
|
289
|
+
}).catch((error) => { if (!ended) emit({ kind: 'model-switch', status: 'failed', model: activeModel, requested, message: `Hermes model switch failed: ${error?.message || error}` }) })
|
|
290
|
+
.finally(() => { modelSwitchPending = false })
|
|
286
291
|
return true
|
|
287
292
|
},
|
|
288
293
|
setMode(nextMode) {
|
package/package.json
CHANGED
package/runtime-registry.mjs
CHANGED
|
@@ -3,17 +3,17 @@ import path from 'node:path'
|
|
|
3
3
|
const RUNTIMES = Object.freeze({
|
|
4
4
|
claude: Object.freeze({
|
|
5
5
|
id: 'claude', command: 'claude', label: 'Claude Code', protocol: 'agent-sdk',
|
|
6
|
-
structured: true, flow: true, canSteer: false, images: true, nativeModelCatalog: false, defaultMode: 'default',
|
|
6
|
+
structured: true, flow: true, canSteer: false, images: true, nativeModelCatalog: false, effortControl: true, defaultMode: 'default',
|
|
7
7
|
modes: Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions']),
|
|
8
8
|
}),
|
|
9
9
|
codex: Object.freeze({
|
|
10
10
|
id: 'codex', command: 'codex', label: 'Codex', protocol: 'codex-app-server',
|
|
11
|
-
structured: true, flow: true, canSteer: true, images: true, nativeModelCatalog: true, defaultMode: 'bypassPermissions',
|
|
11
|
+
structured: true, flow: true, canSteer: true, images: true, nativeModelCatalog: true, effortControl: true, defaultMode: 'bypassPermissions',
|
|
12
12
|
modes: Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions', 'review']),
|
|
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, defaultMode: 'default',
|
|
16
|
+
structured: true, flow: false, canSteer: true, images: true, nativeModelCatalog: true, effortControl: false, defaultMode: 'default',
|
|
17
17
|
modes: Object.freeze(['default', 'acceptEdits']),
|
|
18
18
|
beta: true,
|
|
19
19
|
}),
|