thinkpool-pair 0.7.258 → 0.7.259
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/claude-session.mjs +24 -13
- package/codex-session.mjs +33 -9
- package/cross-terminal.mjs +3 -1
- package/hermes-session.mjs +27 -11
- package/package.json +1 -1
- package/thinkpool-room-prompt.mjs +60 -2
package/claude-session.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { sanitizeSession } from './transcript-sanitize.mjs'
|
|
|
21
21
|
import { reviewGatePreToolDecision } from './flow-review-gate.mjs'
|
|
22
22
|
import { crossPostNeedsCard } from './cross-terminal.mjs'
|
|
23
23
|
import { correctContext } from './context-windows.mjs'
|
|
24
|
-
import { THINKPOOL_REMOTE_DELIVERY_RULES, THINKPOOL_RUNTIME_TURN_REMINDER } from './thinkpool-room-prompt.mjs'
|
|
24
|
+
import { THINKPOOL_REMOTE_DELIVERY_RULES, THINKPOOL_RUNTIME_TURN_REMINDER, buildThinkPoolTurnGuidance, createRoomContextSelector, usesFullThinkPoolReminder } from './thinkpool-room-prompt.mjs'
|
|
25
25
|
import { stallDecision, stallEvent, isCompactTurn } from './turn-stall.mjs'
|
|
26
26
|
|
|
27
27
|
// The caret-pulled SDK's real version (^0.3.x auto-upgrades on restart). Resolved
|
|
@@ -183,18 +183,19 @@ const simplifyBlocks = (blocks = []) => blocks.map((b) => {
|
|
|
183
183
|
*/
|
|
184
184
|
const MODES = new Set(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
|
|
185
185
|
|
|
186
|
-
//
|
|
186
|
+
// Periodic salience reminder. The full ThinkPool Code ruleset lives in
|
|
187
187
|
// `appendSystemPrompt` (baked once at session start), but on a long session — heavy
|
|
188
188
|
// tool output, and especially after auto-compaction — the model's attention drifts
|
|
189
189
|
// off a system prompt that sits behind the large host CLAUDE.md. The classic tell:
|
|
190
190
|
// the agent hands the room a host-local path (`open …`, "it's in ~/claude-shots/")
|
|
191
191
|
// or narrates instead of showing, forgetting it's driven from a phone. So we re-state
|
|
192
|
-
// the highest-drift rules
|
|
192
|
+
// the highest-drift rules in a <system-reminder> at session start, after recovery,
|
|
193
|
+
// and every fifth prompt. Ordinary turns get only the compact invariant and any
|
|
194
|
+
// route-specific guidance implied by the request.
|
|
193
195
|
// adjacent to where the model's attention actually is — the same trick the host
|
|
194
196
|
// harness uses to keep CLAUDE.md alive. The shared capability router deliberately
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// the bridge's roomContext callback — see roomReminder() inside startClaudeSession.
|
|
197
|
+
// A live "ROOM NOW" tail (sibling lanes + active worktrees) is appended when it
|
|
198
|
+
// changes and on the same periodic refresh cadence.
|
|
198
199
|
const TP_ROOM_REMINDER = [
|
|
199
200
|
'You are Claude in a ThinkPool Code room, driven live from a phone or browser — NOT a local terminal. Keep using the room\'s features.',
|
|
200
201
|
THINKPOOL_RUNTIME_TURN_REMINDER,
|
|
@@ -210,11 +211,15 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
210
211
|
// rules keep the agent aware of the room's FEATURES; the live tail keeps it aware of
|
|
211
212
|
// the room's STATE (2026-07-02 ask: "aware at all points that it's in a ThinkPool
|
|
212
213
|
// Code session"). A broken snapshot must never break a turn — fail-quiet to static.
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
const selectRoomContext = createRoomContextSelector(roomContext)
|
|
215
|
+
let userPromptNo = 0
|
|
216
|
+
let forceFullReminder = true
|
|
217
|
+
const roomReminder = (text, { promptIndex = 0, forceFull = false } = {}) => {
|
|
218
|
+
const fullReminder = usesFullThinkPoolReminder({ promptIndex, forceFull })
|
|
219
|
+
const live = selectRoomContext({ force: fullReminder })
|
|
216
220
|
const role = String(terminalRolePrompt || '').trim()
|
|
217
|
-
|
|
221
|
+
const guidance = fullReminder ? TP_ROOM_REMINDER : buildThinkPoolTurnGuidance({ text, promptIndex, forceFull })
|
|
222
|
+
return `<system-reminder>\n${fullReminder && role ? `${role}\n\n` : ''}${guidance}${live ? `\n\n${live}` : ''}\n</system-reminder>`
|
|
218
223
|
}
|
|
219
224
|
const ac = new AbortController()
|
|
220
225
|
let input = makeInputStream() // `let`: auto-restart swaps in a fresh stream
|
|
@@ -226,6 +231,7 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
226
231
|
// "No conversation found" (Max 2026-07-02). This is always a resumable id.
|
|
227
232
|
let persistedSessionId = resume || null
|
|
228
233
|
let lastTurnText = null // the most recent turn text, so a bad-resume recovery can re-deliver it
|
|
234
|
+
let lastTurnReminder = null
|
|
229
235
|
let closed = false
|
|
230
236
|
// Lazy boot (2026-07-02): a RESTORED-IDLE terminal returns a full session object but
|
|
231
237
|
// defers the expensive query() cold-start (MCP + settingSources, ~50s each) until its
|
|
@@ -849,6 +855,7 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
849
855
|
// old heuristic): trigger ('manual' for /compact vs 'auto') + the token
|
|
850
856
|
// count before compaction. Emit a real recap card the room can pin.
|
|
851
857
|
if (m.subtype === 'compact_boundary') {
|
|
858
|
+
forceFullReminder = true
|
|
852
859
|
emit({ kind: 'compaction', trigger: m.compact_metadata?.trigger || 'auto', preTokens: m.compact_metadata?.pre_tokens ?? null })
|
|
853
860
|
// Refresh the ctx% meter RIGHT AFTER compaction — it otherwise only updates at
|
|
854
861
|
// turn-end, so the mode row kept showing the stale PRE-compaction window
|
|
@@ -970,7 +977,7 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
970
977
|
restartTimer = null
|
|
971
978
|
if (closed) return
|
|
972
979
|
runQuery()
|
|
973
|
-
if (replay != null) { turnActive = true; lastEvtTs = Date.now(); stalledSent = false; input.push([{ type: 'text', text: replay }, { type: 'text', text: roomReminder() }]) }
|
|
980
|
+
if (replay != null) { turnActive = true; lastEvtTs = Date.now(); stalledSent = false; input.push([{ type: 'text', text: replay }, { type: 'text', text: lastTurnReminder || roomReminder(replay, { forceFull: true }) }]) }
|
|
974
981
|
}, 300)
|
|
975
982
|
break
|
|
976
983
|
}
|
|
@@ -1144,7 +1151,7 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
1144
1151
|
if (replay != null) {
|
|
1145
1152
|
turnActive = true; lastEvtTs = Date.now(); stalledSent = false // re-arm liveness for the retried turn
|
|
1146
1153
|
// Match sendTurn's block shape: a slash command goes clean; a normal turn keeps the reminder.
|
|
1147
|
-
input.push(/^\s*\//.test(replay) ? [{ type: 'text', text: replay }] : [{ type: 'text', text: replay }, { type: 'text', text: roomReminder() }])
|
|
1154
|
+
input.push(/^\s*\//.test(replay) ? [{ type: 'text', text: replay }] : [{ type: 'text', text: replay }, { type: 'text', text: lastTurnReminder || roomReminder(replay, { forceFull: true }) }])
|
|
1148
1155
|
emitRaw({ kind: 'note', text: 'retrying the stalled turn on a fresh connection' })
|
|
1149
1156
|
}
|
|
1150
1157
|
}
|
|
@@ -1155,6 +1162,10 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
1155
1162
|
// is queue-backed, so the pushed turn buffers and runs once the query is ready.
|
|
1156
1163
|
sendTurn(text) { if (!closed) { if (!started && prepareCwd) { try { const next = prepareCwd(); if (next) { cwd = next; opts.cwd = next } } catch { /* keep original cwd */ } } if (!started) runQuery(); turnActive = true; sawSuggestion = false; lastTurnText = String(text); if (sugTimer) { clearTimeout(sugTimer); sugTimer = null } lastEvtTs = Date.now(); stalledSent = false; stallRetried = false; forceStopped = false;
|
|
1157
1164
|
const t = String(text)
|
|
1165
|
+
const promptIndex = userPromptNo++
|
|
1166
|
+
const thisTurnForceFull = forceFullReminder
|
|
1167
|
+
forceFullReminder = /^\s*\/(?:compact|clear|reset)\b/i.test(t)
|
|
1168
|
+
lastTurnReminder = /^\s*\//.test(t) ? null : roomReminder(t, { promptIndex, forceFull: thisTurnForceFull })
|
|
1158
1169
|
// Arm the compaction window BEFORE the push: from here until the `compaction` milestone
|
|
1159
1170
|
// (or the turn's result) the SDK is allowed to be silent for minutes without the stall
|
|
1160
1171
|
// watchdog aborting it. See turn-stall.mjs — aborting a compaction is what produced
|
|
@@ -1168,7 +1179,7 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
1168
1179
|
// recognized). So a slash command goes CLEAN; conversational turns keep the reminder.
|
|
1169
1180
|
// Normal turns never start with "/" (composeAgentStdin prepends the preamble), and the
|
|
1170
1181
|
// web already routes "/"-prefixed input as a command (pane.jsx), so this matches intent.
|
|
1171
|
-
input.push(/^\s*\//.test(t) ? [{ type: 'text', text: t }] : [{ type: 'text', text: t }, { type: 'text', text:
|
|
1182
|
+
input.push(/^\s*\//.test(t) ? [{ type: 'text', text: t }] : [{ type: 'text', text: t }, { type: 'text', text: lastTurnReminder }])
|
|
1172
1183
|
} },
|
|
1173
1184
|
// Cold-boot the query WITHOUT sending a turn — the background warmer calls this on
|
|
1174
1185
|
// lazily-restored idle terminals so they're ready before the user clicks them.
|
package/codex-session.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import readline from 'node:readline'
|
|
|
30
30
|
import { buildCodexAppServerArgs, codexAppServerGate, createCodexAppServer } from './codex-app-server.mjs'
|
|
31
31
|
import { CodexEventMapper } from './codex-event-mapper.mjs'
|
|
32
32
|
import { startCodexMcpHttp } from './codex-mcp-http.mjs'
|
|
33
|
-
import { CODEX_THINKPOOL_FIRST_TURN_PREAMBLE,
|
|
33
|
+
import { CODEX_THINKPOOL_FIRST_TURN_PREAMBLE, buildThinkPoolTurnGuidance, createRoomContextSelector, usesFullThinkPoolReminder } from './thinkpool-room-prompt.mjs'
|
|
34
34
|
|
|
35
35
|
const DEFAULT_SANDBOX = 'workspace-write'
|
|
36
36
|
const SAFE_SANDBOXES = new Set(['read-only', 'workspace-write', 'danger-full-access'])
|
|
@@ -214,13 +214,14 @@ export const CODEX_ROOM_CASCADE_REMINDER = [
|
|
|
214
214
|
'THINKPOOL ROOM WORKFLOW: obey the authoritative TERMINAL ROLE above. Conductors are independent main terminals; Ensemble lanes are workers only. When a person explicitly asks to open, launch, start, or spawn a separate Cascade/conductor terminal, use open_main_terminal — NEVER spawn_terminal. Use spawn_terminal only for worker slices from a role permitted to delegate. If open_main_terminal is unavailable, say so; never substitute an Ensemble child. Leaf, worker, Side, and managed Flow lanes work directly and must not delegate. Never use hidden in-process subagents. Each worker owns its own worktree. For a genuinely decomposable task in a conductor-capable role, state a short plan in chat, use sliceType scaffold for mechanical work, feature/fix for builders, and review for adversarial verification; review must use a balanced model tier. Read results, verify the integrated outcome, and close every worker you spawned.',
|
|
215
215
|
].join(' ')
|
|
216
216
|
|
|
217
|
-
export function buildCodexPrompt({ text, terminalRolePrompt, rolePrompt, roomContext, firstTurn = false }) {
|
|
217
|
+
export function buildCodexPrompt({ text, terminalRolePrompt, rolePrompt, roomContext, firstTurn = false, promptIndex = 0, forceFullReminder = false }) {
|
|
218
218
|
const context = typeof roomContext === 'function' ? roomContext() : roomContext
|
|
219
|
+
const fullReminder = usesFullThinkPoolReminder({ promptIndex, forceFull: forceFullReminder })
|
|
219
220
|
const additions = [
|
|
220
221
|
firstTurn ? CODEX_THINKPOOL_FIRST_TURN_PREAMBLE : '',
|
|
221
|
-
terminalRolePrompt,
|
|
222
|
-
|
|
223
|
-
rolePrompt || CODEX_ROOM_CASCADE_REMINDER,
|
|
222
|
+
fullReminder ? terminalRolePrompt : '',
|
|
223
|
+
buildThinkPoolTurnGuidance({ text, promptIndex, forceFull: forceFullReminder }),
|
|
224
|
+
fullReminder ? (rolePrompt || CODEX_ROOM_CASCADE_REMINDER) : '',
|
|
224
225
|
context,
|
|
225
226
|
].map((v) => String(v || '').trim()).filter(Boolean)
|
|
226
227
|
if (!additions.length) return String(text ?? '')
|
|
@@ -396,6 +397,9 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
396
397
|
})
|
|
397
398
|
let sessionId = resumeUsable ? requestedResume : null
|
|
398
399
|
let turnNo = sessionId ? 1 : 0
|
|
400
|
+
let userPromptNo = 0
|
|
401
|
+
let forceFullReminder = true
|
|
402
|
+
const selectRoomContext = createRoomContextSelector(roomContext)
|
|
399
403
|
let child = null
|
|
400
404
|
let aborted = false
|
|
401
405
|
let ended = false
|
|
@@ -689,7 +693,16 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
689
693
|
// Claude receives these through the Agent SDK's system-reminder path.
|
|
690
694
|
// Codex is one-shot per turn, so give it the same lane identity and live
|
|
691
695
|
// room awareness in a compact envelope before the person's text.
|
|
692
|
-
const
|
|
696
|
+
const fullReminder = usesFullThinkPoolReminder({ promptIndex: next.promptIndex, forceFull: next.forceFullReminder })
|
|
697
|
+
const prompt = buildCodexPrompt({
|
|
698
|
+
text: next.text,
|
|
699
|
+
terminalRolePrompt,
|
|
700
|
+
rolePrompt,
|
|
701
|
+
roomContext: () => selectRoomContext({ force: fullReminder }),
|
|
702
|
+
firstTurn: turnNo === 0,
|
|
703
|
+
promptIndex: next.promptIndex,
|
|
704
|
+
forceFullReminder: next.forceFullReminder,
|
|
705
|
+
})
|
|
693
706
|
const usedAppServer = await runAppServer(prompt, next.options)
|
|
694
707
|
if (!usedAppServer) await runExec(prompt, next.options)
|
|
695
708
|
turnNo++
|
|
@@ -705,11 +718,22 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
705
718
|
get started() { return turnNo > 0 || turnActive },
|
|
706
719
|
sendTurn(text, options = {}) {
|
|
707
720
|
if (ended) return false
|
|
721
|
+
const promptIndex = userPromptNo++
|
|
722
|
+
const thisTurnForceFull = forceFullReminder
|
|
723
|
+
forceFullReminder = /^\s*\/(?:compact|reset|clear)\b/i.test(String(text || ''))
|
|
708
724
|
if (!turnActive && turnNo === 0 && prepareCwd) {
|
|
709
725
|
try { cwd = prepareCwd() || cwd } catch { /* keep original cwd */ }
|
|
710
726
|
}
|
|
711
727
|
if (turnActive && appServer && activeTurnId) {
|
|
712
|
-
const
|
|
728
|
+
const fullReminder = usesFullThinkPoolReminder({ promptIndex, forceFull: thisTurnForceFull })
|
|
729
|
+
const prompt = buildCodexPrompt({
|
|
730
|
+
text,
|
|
731
|
+
terminalRolePrompt,
|
|
732
|
+
rolePrompt,
|
|
733
|
+
roomContext: () => selectRoomContext({ force: fullReminder }),
|
|
734
|
+
promptIndex,
|
|
735
|
+
forceFullReminder: thisTurnForceFull,
|
|
736
|
+
})
|
|
713
737
|
const targetTurnId = activeTurnId
|
|
714
738
|
// Serialize authored follow-ups: parallel RPCs can complete out of order.
|
|
715
739
|
steerChain = steerChain.then(() => appServer.steer({ threadId: sessionId, turnId: targetTurnId, input: prompt, images: Array.isArray(options.images) ? options.images : [] }))
|
|
@@ -718,7 +742,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
718
742
|
// pre-write failure. Timeout/close after write is delivery-uncertain;
|
|
719
743
|
// replaying it as a new turn can execute the same instruction twice.
|
|
720
744
|
if (error?.delivery === 'rejected' || error?.delivery === 'not_sent') {
|
|
721
|
-
queue.push({ text, options })
|
|
745
|
+
queue.push({ text, options, promptIndex, forceFullReminder: thisTurnForceFull })
|
|
722
746
|
if (!turnActive && queue.length === 1) pump()
|
|
723
747
|
} else {
|
|
724
748
|
try { onEvent?.({ kind: 'error', message: 'Codex steering delivery is uncertain; the message was not replayed automatically.', recoverable: true }) } catch { /* noop */ }
|
|
@@ -726,7 +750,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
726
750
|
})
|
|
727
751
|
return true
|
|
728
752
|
}
|
|
729
|
-
queue.push({ text, options })
|
|
753
|
+
queue.push({ text, options, promptIndex, forceFullReminder: thisTurnForceFull })
|
|
730
754
|
// start the pump if idle
|
|
731
755
|
if (queue.length === 1) pump()
|
|
732
756
|
return true
|
package/cross-terminal.mjs
CHANGED
|
@@ -14,7 +14,9 @@ import { BLOCKED_REASON, projectLaneLifecycle } from './lane-lifecycle.mjs'
|
|
|
14
14
|
// Bounds — the pull tool is self-limiting (the agent only calls it when it needs
|
|
15
15
|
// to), but a runaway loop must still be capped (the cost-guard / C10 class).
|
|
16
16
|
export const PEEK = {
|
|
17
|
-
|
|
17
|
+
// Keep the implicit pull concise for consumer turns. Agents can request more
|
|
18
|
+
// explicitly when a diagnosis genuinely needs deeper sibling history.
|
|
19
|
+
defaultLines: 20,
|
|
18
20
|
maxLines: 200,
|
|
19
21
|
perTurnCap: 10, // read_terminal calls allowed per user turn (bridge resets it)
|
|
20
22
|
lineCap: 200, // per-line truncation
|
package/hermes-session.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { HERMES_PLAN_SAFE_MCP_TOOLS, HERMES_QUESTION_MCP_TOOL, hermesExactInvent
|
|
|
10
10
|
import { startCodexMcpHttp } from './codex-mcp-http.mjs'
|
|
11
11
|
import { autoAllow, classifyRisk } from './claude-session.mjs'
|
|
12
12
|
import { crossPostNeedsCard } from './cross-terminal.mjs'
|
|
13
|
-
import {
|
|
13
|
+
import { buildThinkPoolTurnGuidance, createRoomContextSelector, usesFullThinkPoolReminder } from './thinkpool-room-prompt.mjs'
|
|
14
14
|
|
|
15
15
|
export const HERMES_COMMAND = 'thinkpool'
|
|
16
16
|
export const HERMES_ACP_PROTOCOL_VERSION = 1
|
|
@@ -35,13 +35,14 @@ const modelList = (state) => (state?.availableModels || []).map((item) => ({ val
|
|
|
35
35
|
const mcpDescriptor = (url) => ({ type: 'http', name: 'thinkpool', url, headers: [] })
|
|
36
36
|
const createAcpClient = (options) => new AcpClient(options)
|
|
37
37
|
|
|
38
|
-
export function buildHermesPromptText({ text, firstTurn = false, terminalRolePrompt, rolePrompt, roomContext } = {}) {
|
|
38
|
+
export function buildHermesPromptText({ text, firstTurn = false, terminalRolePrompt, rolePrompt, roomContext, promptIndex = 0, forceFullReminder = false } = {}) {
|
|
39
39
|
const context = typeof roomContext === 'function' ? roomContext() : roomContext
|
|
40
40
|
const inThinkPoolRoom = !!(terminalRolePrompt || rolePrompt || context)
|
|
41
|
+
const fullReminder = usesFullThinkPoolReminder({ promptIndex, forceFull: forceFullReminder })
|
|
41
42
|
const preamble = [
|
|
42
|
-
firstTurn ? terminalRolePrompt : '',
|
|
43
|
-
inThinkPoolRoom ?
|
|
44
|
-
firstTurn ? rolePrompt : '',
|
|
43
|
+
firstTurn || fullReminder ? terminalRolePrompt : '',
|
|
44
|
+
inThinkPoolRoom ? buildThinkPoolTurnGuidance({ text, promptIndex, forceFull: forceFullReminder }) : '',
|
|
45
|
+
firstTurn || fullReminder ? rolePrompt : '',
|
|
45
46
|
context,
|
|
46
47
|
].filter(Boolean).join('\n\n')
|
|
47
48
|
const body = String(text ?? '')
|
|
@@ -79,6 +80,9 @@ export function startHermesSession({
|
|
|
79
80
|
let started = false
|
|
80
81
|
let crashed = false
|
|
81
82
|
let firstTurn = true
|
|
83
|
+
let userPromptNo = 0
|
|
84
|
+
let forceFullReminder = true
|
|
85
|
+
const selectRoomContext = createRoomContextSelector(roomContext)
|
|
82
86
|
let stderrTail = ''
|
|
83
87
|
let promptChain = Promise.resolve()
|
|
84
88
|
let activeTurnId = 0
|
|
@@ -393,19 +397,28 @@ export function startHermesSession({
|
|
|
393
397
|
await boot()
|
|
394
398
|
}
|
|
395
399
|
|
|
396
|
-
function promptBlocks(text, options = {}) {
|
|
400
|
+
function promptBlocks(text, options = {}, { promptIndex = 0, forceFull = false } = {}) {
|
|
397
401
|
const rawText = String(text)
|
|
398
402
|
// Hermes handles /steer, /queue, /compact and /reset only when the slash is
|
|
399
403
|
// the first character of a pure-text prompt.
|
|
400
404
|
if (/^\s*\//.test(rawText)) return [{ type: 'text', text: rawText }]
|
|
401
|
-
const
|
|
405
|
+
const fullReminder = usesFullThinkPoolReminder({ promptIndex, forceFull })
|
|
406
|
+
const blocks = [{ type: 'text', text: buildHermesPromptText({
|
|
407
|
+
text: rawText,
|
|
408
|
+
firstTurn,
|
|
409
|
+
terminalRolePrompt,
|
|
410
|
+
rolePrompt,
|
|
411
|
+
roomContext: () => selectRoomContext({ force: fullReminder }),
|
|
412
|
+
promptIndex,
|
|
413
|
+
forceFullReminder: forceFull,
|
|
414
|
+
}) }]
|
|
402
415
|
for (const imagePath of (Array.isArray(options.images) ? options.images : [])) {
|
|
403
416
|
try { blocks.push(imageBlock(imagePath)) } catch { /* quoted host path remains in text */ }
|
|
404
417
|
}
|
|
405
418
|
return blocks
|
|
406
419
|
}
|
|
407
420
|
|
|
408
|
-
async function runPrompt(text, options = {}, { steering = false, turnId = activeTurnId } = {}) {
|
|
421
|
+
async function runPrompt(text, options = {}, { steering = false, turnId = activeTurnId, promptIndex = 0, forceFull = false } = {}) {
|
|
409
422
|
try { await boot() }
|
|
410
423
|
catch (error) {
|
|
411
424
|
if (abortedTurns.has(turnId)) return { stopReason: 'cancelled' }
|
|
@@ -417,7 +430,7 @@ export function startHermesSession({
|
|
|
417
430
|
try {
|
|
418
431
|
result = await client.request('session/prompt', {
|
|
419
432
|
sessionId,
|
|
420
|
-
prompt: promptBlocks(body, options),
|
|
433
|
+
prompt: promptBlocks(body, options, { promptIndex, forceFull }),
|
|
421
434
|
messageId: randomUUID(),
|
|
422
435
|
}, 0)
|
|
423
436
|
} catch (error) {
|
|
@@ -454,6 +467,9 @@ export function startHermesSession({
|
|
|
454
467
|
get models() { return [] },
|
|
455
468
|
sendTurn(text, options = {}) {
|
|
456
469
|
if (ended) return false
|
|
470
|
+
const promptIndex = userPromptNo++
|
|
471
|
+
const thisTurnForceFull = forceFullReminder
|
|
472
|
+
forceFullReminder = /^\s*\/(?:compact|reset|clear)\b/i.test(String(text || ''))
|
|
457
473
|
// A crash never replays work by itself. A later explicit turn is the
|
|
458
474
|
// authority to launch a fresh ACP process and resume the same native
|
|
459
475
|
// session id; this is the recovery path the old permanent latch blocked.
|
|
@@ -461,14 +477,14 @@ export function startHermesSession({
|
|
|
461
477
|
// A busy prompt is a genuine ACP /steer call and may run concurrently.
|
|
462
478
|
if (turnActive) {
|
|
463
479
|
const turnId = activeTurnId
|
|
464
|
-
void runPrompt(text, options, { steering: true, turnId }).catch((error) => emit({ kind: 'error', message: `Hermes steering failed: ${error?.message || error}`, recoverable: true }))
|
|
480
|
+
void runPrompt(text, options, { steering: true, turnId, promptIndex, forceFull: thisTurnForceFull }).catch((error) => emit({ kind: 'error', message: `Hermes steering failed: ${error?.message || error}`, recoverable: true }))
|
|
465
481
|
return true
|
|
466
482
|
}
|
|
467
483
|
const turnId = ++activeTurnId
|
|
468
484
|
// Claim the turn synchronously, before the cold safety probe/import. The
|
|
469
485
|
// room can now show Thinking + Stop for the whole accepted lifecycle.
|
|
470
486
|
turnActive = true
|
|
471
|
-
promptChain = promptChain.then(() => runPrompt(text, options, { steering: false, turnId })).catch((error) => {
|
|
487
|
+
promptChain = promptChain.then(() => runPrompt(text, options, { steering: false, turnId, promptIndex, forceFull: thisTurnForceFull })).catch((error) => {
|
|
472
488
|
turnActive = false
|
|
473
489
|
emit({ kind: 'error', message: `Hermes turn failed: ${error?.message || error}`, recoverable: true })
|
|
474
490
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Shared delivery contract for structured agents running inside ThinkPool Code.
|
|
2
|
-
// Claude receives this in its system prompt
|
|
3
|
-
//
|
|
2
|
+
// Claude receives this in its system prompt; all runtimes receive a compact
|
|
3
|
+
// per-turn invariant plus the full contract at session start, after a reset, and
|
|
4
|
+
// every fifth user prompt. Keep runtime-specific
|
|
4
5
|
// orchestration rules in their own drivers — these are the cross-runtime truths
|
|
5
6
|
// about available capabilities and how finished work reaches the people.
|
|
6
7
|
// One canonical intent → capability router for every structured runtime. Tool
|
|
@@ -90,6 +91,63 @@ export const THINKPOOL_RUNTIME_TURN_REMINDER = [
|
|
|
90
91
|
'VERIFY BEFORE CLAIMING: run or serve what changed and show the real response, passing output, or rendered evidence; state exactly what remains unverified.',
|
|
91
92
|
].join(' ')
|
|
92
93
|
|
|
94
|
+
// Repeating the complete router on every turn made a durable operating contract
|
|
95
|
+
// compete with the user's actual request. The model still gets a small invariant
|
|
96
|
+
// on ordinary turns, while periodic and recovery turns refresh the complete map.
|
|
97
|
+
export const THINKPOOL_FULL_REMINDER_INTERVAL = 5
|
|
98
|
+
|
|
99
|
+
export const THINKPOOL_RUNTIME_SALIENCE_REMINDER = 'THINKPOOL: prefer relevant exposed room tools unless the user explicitly opts out; obey the durable terminal role and consent rules. Assume the people are remote, do host work yourself, and deliver reachable verified results.'
|
|
100
|
+
|
|
101
|
+
const ROUTE_TRIGGERS = Object.freeze({
|
|
102
|
+
'room-awareness': /\b(other|another|sibling|peer)\s+(lane|terminal)|\bread_terminal\b/i,
|
|
103
|
+
'cross-room-awareness': /\b(other|another|cross[- ]?room|cross[- ]?session)\s+(room|session)|\b(list_sessions|read_session)\b/i,
|
|
104
|
+
'visible-handoff': /\b(hand[ -]?off|tell|send|post)\b.{0,40}\b(lane|terminal|room|session|agent)\b|\b(post_to_terminal|post_to_session)\b/i,
|
|
105
|
+
'work-routing': /\b(parallel|delegate|worker|sub[- ]?terminal|conductor|cascade|spawn_terminal|open_main_terminal|close_terminal)\b/i,
|
|
106
|
+
'room-question': /\b(request_user_input)\b/i,
|
|
107
|
+
'visual-proof': /\b(ui|ux|visual|design|frontend|html|css|page|route|mockup|screenshot|responsive|desktop|mobile|preview)\b/i,
|
|
108
|
+
'external-research': /\b(research|latest|current|look up|browse|web search|online source|verify online)\b/i,
|
|
109
|
+
'flow-completion': /\b(flow|submit_flow_plan|mark_flow_done|submit_flow_review|read_review_file|run_review_check)\b/i,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const DESIGN_ROUTE_REMINDER = 'DESIGN ROUTE: authored HTML must produce a source-backed Thinkpool Design card/popup with verified desktop and mobile renders. The popup is where users edit; do not duplicate its PNGs inline. Use inline PNG evidence only when no interactive Design artifact is available.'
|
|
113
|
+
|
|
114
|
+
export function usesFullThinkPoolReminder({ promptIndex = 0, forceFull = false } = {}) {
|
|
115
|
+
const index = Math.max(0, Number.isFinite(Number(promptIndex)) ? Math.trunc(Number(promptIndex)) : 0)
|
|
116
|
+
return !!forceFull || index % THINKPOOL_FULL_REMINDER_INTERVAL === 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildThinkPoolTurnGuidance({ text = '', promptIndex = 0, forceFull = false } = {}) {
|
|
120
|
+
if (usesFullThinkPoolReminder({ promptIndex, forceFull })) return THINKPOOL_RUNTIME_TURN_REMINDER
|
|
121
|
+
const body = String(text || '')
|
|
122
|
+
const routed = THINKPOOL_CAPABILITY_ROUTES
|
|
123
|
+
.filter((route) => ROUTE_TRIGGERS[route.id]?.test(body))
|
|
124
|
+
.map((route) => `${route.id}: ${route.rule}`)
|
|
125
|
+
if (ROUTE_TRIGGERS['visual-proof'].test(body)) routed.push(DESIGN_ROUTE_REMINDER)
|
|
126
|
+
return [THINKPOOL_RUNTIME_SALIENCE_REMINDER, ...routed].join(' ')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function roomContextFingerprint(value) {
|
|
130
|
+
return String(value || '')
|
|
131
|
+
// Relative ages change while the underlying lane state does not. Ignore
|
|
132
|
+
// that clock churn when deciding whether a room snapshot is new information.
|
|
133
|
+
.replace(/ · (?:\d+[smhd] ago|no activity)(?=\s+—)/g, ' · age')
|
|
134
|
+
.trim()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createRoomContextSelector(roomContext) {
|
|
138
|
+
let previousFingerprint = null
|
|
139
|
+
return ({ force = false } = {}) => {
|
|
140
|
+
let context = null
|
|
141
|
+
try { context = typeof roomContext === 'function' ? roomContext() : roomContext } catch { context = null }
|
|
142
|
+
const value = String(context || '').trim()
|
|
143
|
+
if (!value) return ''
|
|
144
|
+
const fingerprint = roomContextFingerprint(value)
|
|
145
|
+
const changed = fingerprint !== previousFingerprint
|
|
146
|
+
previousFingerprint = fingerprint
|
|
147
|
+
return force || changed ? value : ''
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
93
151
|
// One authoritative terminal-identity preamble for every structured runtime.
|
|
94
152
|
// The bridge derives this from durable structural metadata — never from the text
|
|
95
153
|
// of the task handed to the model. That distinction matters because a spawned
|