thinkpool-pair 0.7.6 → 0.7.8
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 +1 -1
- package/bridge.mjs +19 -13
- package/claude-session.mjs +26 -7
- package/package.json +1 -1
package/account.mjs
CHANGED
|
@@ -132,7 +132,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
132
132
|
}
|
|
133
133
|
warned.delete(room)
|
|
134
134
|
process.stderr.write(`\n ◆ serving ${room}${r.name ? ` "${r.name}"` : ''} → ${dir}${dirs[room] ? '' : ' (default)'}\n`)
|
|
135
|
-
const child = spawn(process.execPath, [BRIDGE, room, '--headless'], { cwd: dir, stdio: 'inherit' })
|
|
135
|
+
const child = spawn(process.execPath, [BRIDGE, room, '--headless', '--auto=claude'], { cwd: dir, stdio: 'inherit' })
|
|
136
136
|
children.set(room, child)
|
|
137
137
|
child.on('exit', () => { children.delete(room) }) // re-served on the next tick
|
|
138
138
|
}
|
package/bridge.mjs
CHANGED
|
@@ -175,6 +175,9 @@ if (_superOwn.includes('--supervise') || _superOwn.includes('--keep-alive')) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
const headless = argv.includes('--headless')
|
|
178
|
+
// Account-mode children pass --auto=<agent> so a served session opens a live
|
|
179
|
+
// terminal automatically (headless, no TTY/picker) instead of an empty room.
|
|
180
|
+
const autoAgent = (argv.find(a => a.startsWith('--auto=')) || '').slice(7) || null
|
|
178
181
|
// Structured mode (Phase 2, opt-in): Claude Code runs through the Agent SDK
|
|
179
182
|
// (structured events + risk-tiered permission gate) instead of the PTY byte
|
|
180
183
|
// relay. Default OFF — the PTY path is untouched. Only applies to `claude`.
|
|
@@ -454,14 +457,16 @@ function printLocal(evt) {
|
|
|
454
457
|
// relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
|
|
455
458
|
// persist to the host file; tool calls round-trip through the perm card; the
|
|
456
459
|
// rolling log replays to joiners and survives bridge restarts (session-store).
|
|
457
|
-
function openStructured({ id, model, resume, log, commands }) {
|
|
460
|
+
function openStructured({ id, model, resume, log, commands, mode }) {
|
|
458
461
|
if (sessions.has(id)) return
|
|
459
|
-
const entry = { cmd: 'claude', kind: 'structured', log: Array.isArray(log) ? log.slice(-STRUCTURED_LOG_MAX) : [], pending: new Map(), session: null, recovered: false, commands: Array.isArray(commands) ? commands : undefined }
|
|
462
|
+
const entry = { cmd: 'claude', kind: 'structured', log: Array.isArray(log) ? log.slice(-STRUCTURED_LOG_MAX) : [], pending: new Map(), session: null, recovered: false, commands: Array.isArray(commands) ? commands : undefined, mode }
|
|
460
463
|
sessions.set(id, entry)
|
|
461
464
|
if (entry.log.length) process.stderr.write(`\n ◆ restored ${entry.log.length} prior events (${id.slice(0, 8)})${resume ? ' + resuming live context' : ''}.\n`)
|
|
462
|
-
|
|
465
|
+
// Persist the permission mode alongside the transcript so a bridge restart
|
|
466
|
+
// restores the session in the SAME mode (a bypass room stays bypass on resume).
|
|
467
|
+
const persist = () => saveSession(room, id, { sessionId: entry.session?.sessionId || resume || null, log: entry.log, commands: entry.commands, mode: entry.mode })
|
|
463
468
|
entry.session = startClaudeSession({
|
|
464
|
-
cwd: process.cwd(), model, resume,
|
|
469
|
+
cwd: process.cwd(), model, resume, mode,
|
|
465
470
|
onEvent: (evt) => {
|
|
466
471
|
// Self-heal a stale resume — the saved SDK session expired. Reopen fresh,
|
|
467
472
|
// keeping the transcript (scrollback survives; live context is gone).
|
|
@@ -470,7 +475,7 @@ function openStructured({ id, model, resume, log, commands }) {
|
|
|
470
475
|
process.stderr.write(`\n ◆ saved session expired — starting fresh (transcript kept).\n`)
|
|
471
476
|
try { entry.session?.end() } catch { /* noop */ }
|
|
472
477
|
sessions.delete(id)
|
|
473
|
-
openStructured({ id, model, log: entry.log, commands: entry.commands })
|
|
478
|
+
openStructured({ id, model, log: entry.log, commands: entry.commands, mode: entry.mode })
|
|
474
479
|
return
|
|
475
480
|
}
|
|
476
481
|
// Stamp a wall-clock ts on every transcript event so the web client can
|
|
@@ -577,8 +582,8 @@ channel
|
|
|
577
582
|
if (agent?.resume) { try { if (agent.resume.probe()) args = [...agent.resume.args] } catch { /* fresh */ } }
|
|
578
583
|
}
|
|
579
584
|
if (wantStructured(payload.cmd)) {
|
|
580
|
-
openStructured({ id: payload.id })
|
|
581
|
-
process.stderr.write(`\n ◆ web opened a structured "${payload.cmd}" session.\n`)
|
|
585
|
+
openStructured({ id: payload.id, mode: payload.mode })
|
|
586
|
+
process.stderr.write(`\n ◆ web opened a structured "${payload.cmd}" session (${payload.mode || 'default'}).\n`)
|
|
582
587
|
return
|
|
583
588
|
}
|
|
584
589
|
openTerm({ id: payload.id, cmd: payload.cmd, args })
|
|
@@ -629,7 +634,7 @@ channel
|
|
|
629
634
|
// ── structured-session control (Phase 2) ──
|
|
630
635
|
.on('broadcast', { event: 'code-open' }, ({ payload }) => {
|
|
631
636
|
if (payload?.host && payload.host !== name) return
|
|
632
|
-
openStructured({ id: payload?.id || randomUUID(), model: payload?.model, resume: payload?.resume })
|
|
637
|
+
openStructured({ id: payload?.id || randomUUID(), model: payload?.model, resume: payload?.resume, mode: payload?.mode })
|
|
633
638
|
})
|
|
634
639
|
.on('broadcast', { event: 'code-turn' }, ({ payload }) => {
|
|
635
640
|
const s = payload?.term && sessions.get(payload.term)
|
|
@@ -683,7 +688,7 @@ channel
|
|
|
683
688
|
})
|
|
684
689
|
.on('broadcast', { event: 'code-mode' }, ({ payload }) => {
|
|
685
690
|
const s = payload?.term && sessions.get(payload.term)
|
|
686
|
-
if (s) s.session.setMode(payload.mode)
|
|
691
|
+
if (s) { s.mode = payload.mode; s.session.setMode(payload.mode) } // track for persist/restore
|
|
687
692
|
})
|
|
688
693
|
.on('broadcast', { event: 'code-close' }, ({ payload }) => {
|
|
689
694
|
const s = payload?.id && sessions.get(payload.id)
|
|
@@ -694,16 +699,17 @@ channel
|
|
|
694
699
|
if (status === 'SUBSCRIBED') {
|
|
695
700
|
realtimeHealthy = true; brokenSince = 0
|
|
696
701
|
channel.track({ name, role: 'bridge' })
|
|
697
|
-
|
|
702
|
+
const startCmd = attachedCmd || autoAgent // autoAgent: account-mode auto-open (headless)
|
|
703
|
+
if (startCmd && !terms.size && !sessions.size) {
|
|
698
704
|
// Claude + structured mode → Agent SDK session; everything else → PTY.
|
|
699
705
|
// On restart, restore the latest saved structured session for this room:
|
|
700
706
|
// replay its transcript + resume the live SDK context if recent enough.
|
|
701
|
-
if (wantStructured(
|
|
707
|
+
if (wantStructured(startCmd)) {
|
|
702
708
|
const prev = loadLatest(room)
|
|
703
|
-
if (prev && (prev.log?.length || prev.sessionId)) openStructured({ id: prev.id, resume: canResume(prev) ? prev.sessionId : undefined, log: prev.log, commands: prev.commands })
|
|
709
|
+
if (prev && (prev.log?.length || prev.sessionId)) openStructured({ id: prev.id, resume: canResume(prev) ? prev.sessionId : undefined, log: prev.log, commands: prev.commands, mode: prev.mode })
|
|
704
710
|
else openStructured({ id: randomUUID() })
|
|
705
711
|
}
|
|
706
|
-
else openTerm({ id: randomUUID(), cmd:
|
|
712
|
+
else openTerm({ id: randomUUID(), cmd: startCmd, args: attachedArgs, attached: !autoAgent })
|
|
707
713
|
}
|
|
708
714
|
announce()
|
|
709
715
|
process.stderr.write(headless
|
package/claude-session.mjs
CHANGED
|
@@ -113,20 +113,37 @@ const simplifyBlocks = (blocks = []) => blocks.map((b) => {
|
|
|
113
113
|
*/
|
|
114
114
|
const MODES = new Set(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
|
|
115
115
|
|
|
116
|
-
export function startClaudeSession({ cwd, model, resume, onEvent, requestPermission }) {
|
|
116
|
+
export function startClaudeSession({ cwd, model, resume, mode: initialMode = 'default', onEvent, requestPermission }) {
|
|
117
117
|
const ac = new AbortController()
|
|
118
118
|
const input = makeInputStream()
|
|
119
119
|
let sessionId = resume || null
|
|
120
120
|
let closed = false
|
|
121
121
|
let q = null // the live Query — control requests (interrupt /
|
|
122
122
|
// setPermissionMode) route through it once streaming.
|
|
123
|
-
|
|
123
|
+
// The session is CREATED in the caller's chosen mode (not hard-coded default):
|
|
124
|
+
// a bypass session must start in bypass so the agent never enters plan mode and
|
|
125
|
+
// never throws an ExitPlanMode card. Falls back to 'default' for unknown values.
|
|
126
|
+
let mode = MODES.has(initialMode) ? initialMode : 'default' // mirrors ⇧⇥ cycle
|
|
124
127
|
const alwaysAllow = new Set() // tool:risk signatures the user chose "don't ask again" for
|
|
125
128
|
const toolStart = new Map() // tool_use id → start time, for the duration badge
|
|
126
129
|
let effort = null // active reasoning effort for the turn (from the hook input)
|
|
127
130
|
|
|
128
131
|
const emit = (evt) => { try { onEvent?.(evt) } catch { /* never let a consumer throw into the loop */ } }
|
|
129
132
|
|
|
133
|
+
// Apply a permission-mode change to the live SDK *outside* any hook callback.
|
|
134
|
+
// setPermissionMode is a streaming control request; awaiting it from INSIDE the
|
|
135
|
+
// PreToolUse hook (which the SDK is itself awaiting) jams the control channel —
|
|
136
|
+
// the call throws, and when that was silently swallowed the SDK stayed in plan
|
|
137
|
+
// mode, so the agent got bounced back to planning after every approved edit
|
|
138
|
+
// (the "PLAN READY every turn" loop). Deferring to the next tick lets the hook
|
|
139
|
+
// return first, freeing the channel. Errors are logged to the bridge, not eaten.
|
|
140
|
+
const scheduleSdkMode = (next) => {
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
Promise.resolve(q?.setPermissionMode?.(next)).catch((e) =>
|
|
143
|
+
console.error(`[claude-session] setPermissionMode(${next}) failed:`, e?.message || e))
|
|
144
|
+
}, 0)
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
// PreToolUse — fires on EVERY tool call (the universal gate). The mode policy
|
|
131
148
|
// mirrors Claude Code exactly: reads never prompt (any mode); Auto-accept
|
|
132
149
|
// edits auto-approves non-destructive writes; Bash / network / destructive
|
|
@@ -150,8 +167,8 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
|
|
|
150
167
|
catch { choice = 'keep' }
|
|
151
168
|
if (choice === 'run' || choice === 'accept') {
|
|
152
169
|
const next = choice === 'accept' ? 'acceptEdits' : 'default'
|
|
153
|
-
mode = next
|
|
154
|
-
|
|
170
|
+
mode = next // our auto-allow gate updates immediately
|
|
171
|
+
scheduleSdkMode(next) // flip the SDK AFTER this hook returns (see above)
|
|
155
172
|
emit({ kind: 'mode', mode: next })
|
|
156
173
|
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', permissionDecisionReason: `Plan approved in the ThinkPool room — proceed (${next} mode).` } }
|
|
157
174
|
}
|
|
@@ -211,7 +228,7 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
|
|
|
211
228
|
|
|
212
229
|
const opts = {
|
|
213
230
|
abortController: ac,
|
|
214
|
-
permissionMode:
|
|
231
|
+
permissionMode: mode,
|
|
215
232
|
hooks: { PreToolUse: [{ hooks: [preTool] }] },
|
|
216
233
|
// Load the host's REAL Claude environment — user + project + local settings —
|
|
217
234
|
// so custom slash commands (.claude/commands/*.md), CLAUDE.md and agents work
|
|
@@ -298,8 +315,10 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
|
|
|
298
315
|
// `mode` drives our PreToolUse auto-approve policy. Echo so the room syncs.
|
|
299
316
|
async setMode(m) {
|
|
300
317
|
if (!MODES.has(m) || closed) return
|
|
301
|
-
mode = m
|
|
302
|
-
|
|
318
|
+
mode = m // gate is authoritative immediately
|
|
319
|
+
scheduleSdkMode(m) // defer the SDK control request — a mode toggle can
|
|
320
|
+
// land while a perm card is pending (hook in flight),
|
|
321
|
+
// which is exactly the re-entrancy that jams it.
|
|
303
322
|
emit({ kind: 'mode', mode })
|
|
304
323
|
},
|
|
305
324
|
// /model is disabled in headless SDK — switch via the setModel control
|