thinkpool-pair 0.7.106 → 0.7.107
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/account.mjs +14 -4
- package/claude-session.mjs +13 -9
- package/package.json +1 -1
package/account.mjs
CHANGED
|
@@ -605,11 +605,19 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
605
605
|
try { w.reply(m) } catch { /* reply path gone */ }
|
|
606
606
|
}
|
|
607
607
|
})
|
|
608
|
-
|
|
608
|
+
const dropRoom = () => {
|
|
609
609
|
children.delete(room); childIdle.delete(room); childBetween.delete(room); childPeer.delete(room); roomNames.delete(room); roomPartner.delete(room) // re-served on the next tick
|
|
610
610
|
// Fail any in-flight cross-room peeks/posts this dead room had ASKED (bus-origin
|
|
611
611
|
// entries have askerRoom null and just time out on their own).
|
|
612
612
|
for (const [reqId, w] of pairPending) { if (w?.askerRoom === room) pairPending.delete(reqId) }
|
|
613
|
+
}
|
|
614
|
+
child.on('exit', dropRoom)
|
|
615
|
+
// A spawn that never starts (execPath/cwd vanished between the existsSync check and
|
|
616
|
+
// spawn) emits 'error', NOT 'exit'. Without this handler the stale children-map entry
|
|
617
|
+
// would block re-serving forever AND an unhandled 'error' would crash the supervisor.
|
|
618
|
+
child.on('error', (e) => {
|
|
619
|
+
dropRoom()
|
|
620
|
+
try { process.stderr.write(`\n ◇ ${room}: failed to launch its bridge (${e?.message || e}) — retrying next tick.\n`) } catch { /* noop */ }
|
|
613
621
|
})
|
|
614
622
|
// If an update is already pending, tell the freshly-served child immediately so
|
|
615
623
|
// its room shows the "update ready" chip without waiting for the next poll.
|
|
@@ -634,6 +642,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
634
642
|
|
|
635
643
|
await tick()
|
|
636
644
|
const iv = setInterval(tick, 15000)
|
|
645
|
+
const updateTimers = [] // auto-update poll/apply timers — cleared in stop() so none fire post-exit
|
|
637
646
|
let stopping = false
|
|
638
647
|
// restart-vs-stop is the ONLY difference in exit code: a web "restart" sets this so
|
|
639
648
|
// we exit non-zero (every supervisor respawns); a normal stop/shutdown exits 0.
|
|
@@ -641,6 +650,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
641
650
|
if (stopping) return; stopping = true
|
|
642
651
|
const code = restarting ? 1 : 0
|
|
643
652
|
clearInterval(iv)
|
|
653
|
+
for (const t of updateTimers) { try { clearInterval(t) } catch { /* noop */ } } // clearInterval clears Timeouts too
|
|
644
654
|
keepFresh.cancel()
|
|
645
655
|
releaseSingleton()
|
|
646
656
|
for (const c of children.values()) { try { c.kill(sig || 'SIGTERM') } catch { /* noop */ } }
|
|
@@ -703,9 +713,9 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
703
713
|
applyIfIdle() // unattended fallback fires here when no room has a web peer
|
|
704
714
|
}
|
|
705
715
|
}
|
|
706
|
-
setInterval(check, POLL_MS).unref()
|
|
707
|
-
setInterval(applyIfIdle, 15000).unref()
|
|
708
|
-
setTimeout(check, 60000).unref()
|
|
716
|
+
updateTimers.push(setInterval(check, POLL_MS).unref())
|
|
717
|
+
updateTimers.push(setInterval(applyIfIdle, 15000).unref())
|
|
718
|
+
updateTimers.push(setTimeout(check, 60000).unref())
|
|
709
719
|
}
|
|
710
720
|
|
|
711
721
|
await new Promise(() => {})
|
package/claude-session.mjs
CHANGED
|
@@ -248,14 +248,18 @@ 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
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
251
|
+
// Flow CONDUCTOR hard-stop: a conductor must DECOMPOSE using ONLY Read/Grep/Glob (which
|
|
252
|
+
// are auto-allowed, no card) then ExitPlanMode. Two things otherwise stall it on a
|
|
253
|
+
// permission card on the now-hidden conductor terminal: (1) Task/Agent subagents — the
|
|
254
|
+
// model's parallelize instinct fans out Explore agents despite the prompt; (2) Bash —
|
|
255
|
+
// the model reaches for `find`/`grep`/`wc`, and Bash is medium-risk → a card in plan
|
|
256
|
+
// mode. Deny BOTH and tell it to use Grep/Glob/Read instead. (blockSubagents is set only
|
|
257
|
+
// for the conductor session — lanes build under their own bypass/one-hop rules.)
|
|
258
|
+
if (blockSubagents && (toolName === 'Task' || toolName === 'Agent' || toolName === 'Bash')) {
|
|
259
|
+
const reason = (toolName === 'Bash')
|
|
260
|
+
? 'Flow conductors do NOT run Bash — it pops a permission card and stalls the flow. Use the Grep tool (not `grep`/`rg`), the Glob tool (not `find`/`ls`), and Read (not `cat`/`head`) — those run with no prompt. Then DECOMPOSE and call ExitPlanMode once.'
|
|
261
|
+
: 'Flow conductors do NOT spawn subagents — it stalls the flow on a permission card. Inspect the codebase yourself with Read/Grep/Glob (no prompt). Then DECOMPOSE and call ExitPlanMode once. Do not call Task/Agent again.'
|
|
262
|
+
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } }
|
|
259
263
|
}
|
|
260
264
|
// ThinkPool cross-terminal READ (B2) is read-only + within-room — never
|
|
261
265
|
// prompt, in any mode. It still surfaces as a tool card so the room sees the peek.
|
|
@@ -388,7 +392,7 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
388
392
|
let decision = 'allow'
|
|
389
393
|
if (!auto) {
|
|
390
394
|
try {
|
|
391
|
-
decision = await requestPermission?.({ id: randomUUID(), toolName, input: toolInput, risk }) ?? '
|
|
395
|
+
decision = await requestPermission?.({ id: randomUUID(), toolName, input: toolInput, risk }) ?? 'deny'
|
|
392
396
|
} catch { decision = 'deny' } // a broken permission path must fail safe (deny)
|
|
393
397
|
// "Allow & don't ask again" — remember the signature, then allow.
|
|
394
398
|
if (decision === 'always') { alwaysAllow.add(sig); decision = 'allow' }
|