thinkpool-pair 0.7.104 → 0.7.106
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 +11 -2
- package/claude-session.mjs +10 -1
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -1322,6 +1322,11 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1322
1322
|
})
|
|
1323
1323
|
entry.session = startClaudeSession({
|
|
1324
1324
|
cwd: cwd || process.cwd(), model, resume, mode, rolePrompt,
|
|
1325
|
+
// A Flow CONDUCTOR (flowSessionId set, no flowTaskKey) must DECOMPOSE, not explore via
|
|
1326
|
+
// a fan-out of Task/Explore subagents — those raise permission cards in plan mode (which
|
|
1327
|
+
// now land on the hidden conductor terminal) and the model ignores the prompt rule not
|
|
1328
|
+
// to. Hard-block the Task tool for conductors so it reads with Read/Grep/Glob + ExitPlanMode.
|
|
1329
|
+
blockSubagents: !!flowSessionId && !flowTaskKey,
|
|
1325
1330
|
mcpServers: { thinkpool: peekServer },
|
|
1326
1331
|
// Tier C precheck — the PreToolUse gate calls this BEFORE raising a card, so a
|
|
1327
1332
|
// disabled/looping/over-cap post never bothers a person. Closes over `entry`.
|
|
@@ -1834,7 +1839,11 @@ flowChannel
|
|
|
1834
1839
|
const cid = randomUUID()
|
|
1835
1840
|
termNames[cid] = `Flow · ${String(payload.flowId).slice(0, 6)}`
|
|
1836
1841
|
saveNames(room, termNames)
|
|
1837
|
-
|
|
1842
|
+
// spawnedBy = the SAME synthetic `flow:<id>` the lanes carry, so the conductor + its
|
|
1843
|
+
// lanes form ONE Ensemble group (same ensembleRoot) — the drilled flow shows a row of
|
|
1844
|
+
// all its terminals to hop between, with a working Back. (No flowTaskKey → it's still
|
|
1845
|
+
// the conductor, excluded from the lane cap below.)
|
|
1846
|
+
openStructured({ id: cid, mode: 'plan', rolePrompt: FLOW_CONDUCTOR_PROMPT, flowSessionId: payload.flowId, spawnedBy: `flow:${payload.flowId}` })
|
|
1838
1847
|
const ce = sessions.get(cid)
|
|
1839
1848
|
if (ce?.session) { try { ce.session.sendTurn(payload.prompt || '') } catch { /* session still starting */ } }
|
|
1840
1849
|
process.stderr.write(`\n ${A.mag}◆ flow conductor launched — flow ${String(payload.flowId).slice(0, 8)} (plan mode).${A.rst}\n`)
|
|
@@ -1851,7 +1860,7 @@ flowChannel
|
|
|
1851
1860
|
const assignments = []
|
|
1852
1861
|
// Step 6 — cap concurrent Flow lanes (invariant: ≤ FLOW_LIMITS.maxConcurrentLanes).
|
|
1853
1862
|
// Overflow tasks stay pending; the room re-dispatches them in the next wave.
|
|
1854
|
-
let liveLanes = [...sessions.values()].filter(e => typeof e.spawnedBy === 'string' && e.spawnedBy.startsWith('flow:')).length
|
|
1863
|
+
let liveLanes = [...sessions.values()].filter(e => typeof e.spawnedBy === 'string' && e.spawnedBy.startsWith('flow:') && e.flowTaskKey).length // lanes only — conductor has no flowTaskKey
|
|
1855
1864
|
for (const t of payload.tasks) {
|
|
1856
1865
|
const gate = canDispatch({ mode: payload.mode || 'guide', liveLanes, budget: { capTokens: null, spentTokens: 0, killed: false } })
|
|
1857
1866
|
if (!gate.ok) { process.stderr.write(`\n ${A.yel}◆ flow dispatch held (${gate.reason}, ${liveLanes}/${FLOW_LIMITS.maxConcurrentLanes} lanes) — ${assignments.length} spawned this wave.${A.rst}\n`); break }
|
package/claude-session.mjs
CHANGED
|
@@ -120,7 +120,7 @@ const simplifyBlocks = (blocks = []) => blocks.map((b) => {
|
|
|
120
120
|
*/
|
|
121
121
|
const MODES = new Set(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
|
|
122
122
|
|
|
123
|
-
export function startClaudeSession({ cwd, model, resume, env, mode: initialMode = 'default', onEvent, requestPermission, mcpServers, crossPostGate, crossRoomPostGate, rolePrompt }) {
|
|
123
|
+
export function startClaudeSession({ cwd, model, resume, env, mode: initialMode = 'default', onEvent, requestPermission, mcpServers, crossPostGate, crossRoomPostGate, rolePrompt, blockSubagents = false }) {
|
|
124
124
|
const ac = new AbortController()
|
|
125
125
|
let input = makeInputStream() // `let`: auto-restart swaps in a fresh stream
|
|
126
126
|
let sessionId = resume || null
|
|
@@ -248,6 +248,15 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
248
248
|
// Surface it to the room when it changes; absent on models without effort.
|
|
249
249
|
const lvl = hookInput.effort?.level
|
|
250
250
|
if (lvl && lvl !== effort) { effort = lvl; emit({ kind: 'effort', level: effort }) }
|
|
251
|
+
// Flow CONDUCTOR hard-stop: a conductor must DECOMPOSE, never fan out Task/Explore
|
|
252
|
+
// subagents. The prompt asks it not to, but the model's parallelize instinct wins and
|
|
253
|
+
// it spawns Explore agents anyway — which (in plan mode) raise permission cards on the
|
|
254
|
+
// now-hidden conductor terminal and hang the whole flow. Deny the Task tool outright and
|
|
255
|
+
// tell it to read with Read/Grep/Glob + ExitPlanMode. (blockSubagents is set only for
|
|
256
|
+
// the conductor session — lanes are governed by their own one-hop prompt rule.)
|
|
257
|
+
if (blockSubagents && (toolName === 'Task' || toolName === 'Agent')) {
|
|
258
|
+
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'Flow conductors do NOT spawn subagents — it stalls the flow on a permission card. If you must inspect the codebase, read it yourself with Read/Grep/Glob (allowed here, no prompt). Then DECOMPOSE and call ExitPlanMode once. Do not call Task/Agent again.' } }
|
|
259
|
+
}
|
|
251
260
|
// ThinkPool cross-terminal READ (B2) is read-only + within-room — never
|
|
252
261
|
// prompt, in any mode. It still surfaces as a tool card so the room sees the peek.
|
|
253
262
|
if (toolName === 'mcp__thinkpool__read_terminal') {
|