thinkpool-pair 0.7.129 → 0.7.131
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 +27 -4
- package/claude-session.mjs +12 -3
- package/package.json +1 -1
- package/session-store.mjs +12 -1
package/bridge.mjs
CHANGED
|
@@ -1222,12 +1222,16 @@ function restoredTurnOpen(log) {
|
|
|
1222
1222
|
// relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
|
|
1223
1223
|
// persist to the host file; tool calls round-trip through the perm card; the
|
|
1224
1224
|
// rolling log replays to joiners and survives bridge restarts (session-store).
|
|
1225
|
-
function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rolePrompt, flowSessionId, flowTaskKey, cwd, reviewSliceRoots }) {
|
|
1225
|
+
function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rolePrompt, flowSessionId, flowTaskKey, cwd, reviewSliceRoots, openedAt, defer }) {
|
|
1226
1226
|
if (sessions.has(id)) return
|
|
1227
1227
|
// spawnedBy: set when this lane was Dispatched (spawn_terminal). Restored from the
|
|
1228
1228
|
// session store so the Ensemble flag survives a bridge restart (else a respin
|
|
1229
1229
|
// stripped it and the lane reverted to a plain tab — the t6 "no chip" bug).
|
|
1230
|
-
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, model: model || null, spawnedBy: spawnedBy || undefined, flowSessionId: flowSessionId || null, flowTaskKey: flowTaskKey || null, cwd: cwd || null
|
|
1230
|
+
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, model: model || null, spawnedBy: spawnedBy || undefined, flowSessionId: flowSessionId || null, flowTaskKey: flowTaskKey || null, cwd: cwd || null,
|
|
1231
|
+
// Stable creation order — persisted so a bridge restart restores tabs in the SAME
|
|
1232
|
+
// order (not readdir/filesystem order). Legacy recs (no openedAt) derive it from the
|
|
1233
|
+
// first transcript event ts, so even the first post-fix restart is ordered right.
|
|
1234
|
+
openedAt: openedAt || (Array.isArray(log) ? (log.find((e) => e?.ts)?.ts || 0) : 0) || Date.now() }
|
|
1231
1235
|
// C1 (RT-2): per-term contiguous seq counter. Seeded from the restored log's max
|
|
1232
1236
|
// so a bridge restart resumes ABOVE every persisted seq — fresh events never reuse
|
|
1233
1237
|
// an old seq, which is what keeps the between-turns restart dup-free (see
|
|
@@ -1340,7 +1344,7 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1340
1344
|
// subagent-block; without cwd it runs outside its worktree; without rolePrompt it loses its
|
|
1341
1345
|
// Flow role. blockSubagents / onLaneDone / onReviewVerdict / onSubmitPlan all derive from
|
|
1342
1346
|
// these, so restoring them restores the whole behavior.
|
|
1343
|
-
const sessionData = () => ({ sessionId: entry.session?.sessionId || resume || null, log: entry.log, commands: entry.commands, mode: entry.mode, spawnedBy: entry.spawnedBy, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, cwd: entry.cwd, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [] })
|
|
1347
|
+
const sessionData = () => ({ sessionId: entry.session?.sessionId || resume || null, log: entry.log, commands: entry.commands, mode: entry.mode, spawnedBy: entry.spawnedBy, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, cwd: entry.cwd, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [], openedAt: entry.openedAt || null })
|
|
1344
1348
|
const persist = () => saveSession(room, id, sessionData())
|
|
1345
1349
|
// Synchronous flush of this session's record. Used on open (so a brand-new session
|
|
1346
1350
|
// has a file under its id BEFORE its first event — surviving a restart inside the
|
|
@@ -1623,6 +1627,9 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1623
1627
|
: null
|
|
1624
1628
|
entry.session = startClaudeSession({
|
|
1625
1629
|
cwd: cwd || process.cwd(), model, resume, mode, rolePrompt,
|
|
1630
|
+
// defer: a restored-idle terminal cold-boots its query only on first use — restore is
|
|
1631
|
+
// then instant for display, and only used terminals pay the ~50s MCP boot (Max, 2026-07-02).
|
|
1632
|
+
lazy: !!defer,
|
|
1626
1633
|
reviewGate,
|
|
1627
1634
|
// A Flow CONDUCTOR (flowSessionId set, no flowTaskKey) must DECOMPOSE, not explore via
|
|
1628
1635
|
// a fan-out of Task/Explore subagents, and must not build. Hard-block Task/Bash/Write for
|
|
@@ -2302,11 +2309,20 @@ Output:`
|
|
|
2302
2309
|
// Closed sessions are unlinked by deleteSession, so a file existing on disk ==
|
|
2303
2310
|
// a session that was live at shutdown (never an explicitly closed one).
|
|
2304
2311
|
const all = loadAll(room)
|
|
2312
|
+
// Restore in stable creation order (openedAt) so the tab strip comes back in the
|
|
2313
|
+
// SAME order across a bridge restart — NOT readdir/filesystem order, which reshuffled
|
|
2314
|
+
// the tabs (Max, 2026-07-02). Legacy recs fall back to their first event ts / savedAt.
|
|
2315
|
+
const okey = (r) => r._bt || r.openedAt || r.savedAt || 0
|
|
2316
|
+
all.sort((a, b) => okey(a) - okey(b))
|
|
2305
2317
|
if (all.length) for (const rec of all) {
|
|
2306
2318
|
// FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
|
|
2307
2319
|
// bridge restart: the conductor keeps its subagent-block + plan interception, and
|
|
2308
2320
|
// lanes keep their worktree cwd + the ability to mark done.
|
|
2309
|
-
openStructured({ id: rec.id, resume: canResume(rec) ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, cwd: rec.cwd, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots
|
|
2321
|
+
openStructured({ id: rec.id, resume: canResume(rec) ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, cwd: rec.cwd, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt,
|
|
2322
|
+
// Lazy-boot restored terminals that were IDLE + not part of a flow: their transcript
|
|
2323
|
+
// shows immediately; the query boots on first turn. Mid-turn + flow terminals boot now
|
|
2324
|
+
// (mid-turn needs auto-resume; flow needs its lane live).
|
|
2325
|
+
defer: !rec.flowSessionId && !restoredTurnOpen(rec.log || []) })
|
|
2310
2326
|
const re = sessions.get(rec.id)
|
|
2311
2327
|
if (re && rec.flowReviewTarget) re.flowReviewTarget = rec.flowReviewTarget
|
|
2312
2328
|
// S4 — a resumed re-dispatch lane keeps its surviving revert target across a bridge restart.
|
|
@@ -2785,6 +2801,13 @@ process.on('SIGTERM', () => shutdown(0))
|
|
|
2785
2801
|
// Route both through shutdown() so children are reaped + presence left, then the
|
|
2786
2802
|
// supervisor / launchd / systemd respawns a clean process.
|
|
2787
2803
|
process.on('uncaughtException', (err) => {
|
|
2804
|
+
// Benign write-to-closed-pipe/socket (a broadcast or a child stream that closed
|
|
2805
|
+
// mid-restore) must NOT kill the bridge — that respawn triggered a SECOND full
|
|
2806
|
+
// restore, doubling an already-slow restore (Max, 2026-07-02). Log + keep running.
|
|
2807
|
+
if (err && (err.code === 'EPIPE' || err.code === 'ECONNRESET' || err.code === 'ERR_STREAM_WRITE_AFTER_END' || err.code === 'ERR_STREAM_DESTROYED')) {
|
|
2808
|
+
try { process.stderr.write(`\n ⚠ ignored benign ${err.code} (not fatal): ${err?.message || ''}\n`) } catch { /* noop */ }
|
|
2809
|
+
return
|
|
2810
|
+
}
|
|
2788
2811
|
try { process.stderr.write(`\n ⚠ uncaughtException: ${err?.stack || err}\n ◆ shutting down cleanly for a fresh respawn.\n`) } catch { /* noop */ }
|
|
2789
2812
|
shutdown(1)
|
|
2790
2813
|
})
|
package/claude-session.mjs
CHANGED
|
@@ -181,11 +181,17 @@ export async function oneShotSummary({ prompt, env, cwd, timeoutMs = 25000 }) {
|
|
|
181
181
|
return null
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
export function startClaudeSession({ cwd, model, resume, env, mode: initialMode = 'default', onEvent, requestPermission, mcpServers, crossPostGate, crossRoomPostGate, rolePrompt, blockSubagents = false, onSubmitPlan = null, onLaneDone = null, onReviewVerdict = null, reviewGate = null }) {
|
|
184
|
+
export function startClaudeSession({ cwd, model, resume, env, mode: initialMode = 'default', onEvent, requestPermission, mcpServers, crossPostGate, crossRoomPostGate, rolePrompt, blockSubagents = false, onSubmitPlan = null, onLaneDone = null, onReviewVerdict = null, reviewGate = null, lazy = false }) {
|
|
185
185
|
const ac = new AbortController()
|
|
186
186
|
let input = makeInputStream() // `let`: auto-restart swaps in a fresh stream
|
|
187
187
|
let sessionId = resume || null
|
|
188
188
|
let closed = false
|
|
189
|
+
// Lazy boot (2026-07-02): a RESTORED-IDLE terminal returns a full session object but
|
|
190
|
+
// defers the expensive query() cold-start (MCP + settingSources, ~50s each) until its
|
|
191
|
+
// FIRST turn — so a bridge restart restores every tab's transcript instantly and only
|
|
192
|
+
// the terminals a person actually uses pay the boot. Mid-turn terminals boot eagerly
|
|
193
|
+
// (lazy:false) so auto-resume still works.
|
|
194
|
+
let started = false
|
|
189
195
|
// Cold-start measurement (2026-06-21, spec 2026-06-21-code-session-deploy-stability):
|
|
190
196
|
// stamp when the open begins so we can log spawn→ready (the SDK's first `init`
|
|
191
197
|
// system message — when the terminal becomes usable). New room terminals "take a
|
|
@@ -676,6 +682,7 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
676
682
|
}
|
|
677
683
|
|
|
678
684
|
const runQuery = async () => {
|
|
685
|
+
started = true
|
|
679
686
|
try {
|
|
680
687
|
q = query({ prompt: input.stream, options: { ...opts, ...(sessionId ? { resume: sessionId } : {}) } })
|
|
681
688
|
for await (const m of q) {
|
|
@@ -848,10 +855,12 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
848
855
|
turnActive = false
|
|
849
856
|
}
|
|
850
857
|
}
|
|
851
|
-
runQuery()
|
|
858
|
+
if (!lazy) runQuery()
|
|
852
859
|
|
|
853
860
|
return {
|
|
854
|
-
|
|
861
|
+
// A lazy (restored-idle) session cold-boots the query on its first turn. input.push
|
|
862
|
+
// is queue-backed, so the pushed turn buffers and runs once the query is ready.
|
|
863
|
+
sendTurn(text) { if (!closed) { if (!started) runQuery(); turnActive = true; sawSuggestion = false; if (sugTimer) { clearTimeout(sugTimer); sugTimer = null } lastEvtTs = Date.now(); stalledSent = false; input.push([{ type: 'text', text: String(text) }, { type: 'text', text: `<system-reminder>\n${TP_ROOM_REMINDER}\n</system-reminder>` }]) } },
|
|
855
864
|
// Set the permission mode — Claude Code's ⇧⇥ cycle. setPermissionMode is a
|
|
856
865
|
// streaming control request (drives plan-mode behaviour SDK-side); the local
|
|
857
866
|
// `mode` drives our PreToolUse auto-approve policy. Echo so the room syncs.
|
package/package.json
CHANGED
package/session-store.mjs
CHANGED
|
@@ -42,7 +42,18 @@ function archive(room, file) {
|
|
|
42
42
|
function listRecs(room) {
|
|
43
43
|
try {
|
|
44
44
|
return fs.readdirSync(dir(room)).filter((f) => f.endsWith('.json'))
|
|
45
|
-
.map((f) => {
|
|
45
|
+
.map((f) => {
|
|
46
|
+
const p = path.join(dir(room), f)
|
|
47
|
+
try {
|
|
48
|
+
const r = JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
49
|
+
// _bt: the rec FILE's creation time — a reliable, monotonic proxy for when the
|
|
50
|
+
// terminal was first opened (overwrites on save don't change it). Used to restore
|
|
51
|
+
// tabs in stable creation order; the persisted openedAt is unreliable because a
|
|
52
|
+
// truncated log's first event ts drifts far from real creation. Load-time only.
|
|
53
|
+
try { const s = fs.statSync(p); r._bt = s.birthtimeMs || s.mtimeMs || 0 } catch { r._bt = 0 }
|
|
54
|
+
return r
|
|
55
|
+
} catch { return null }
|
|
56
|
+
})
|
|
46
57
|
.filter(Boolean)
|
|
47
58
|
} catch { return [] }
|
|
48
59
|
}
|