thinkpool-pair 0.7.128 → 0.7.130
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 +32 -11
- package/package.json +1 -1
- package/session-store.mjs +11 -0
package/bridge.mjs
CHANGED
|
@@ -83,7 +83,7 @@ const flowRedispatch = new Map()
|
|
|
83
83
|
const flowBudgets = new Map()
|
|
84
84
|
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, CROSSROOM, formatPairRoster, crossRoomPostDecision } from './cross-terminal.mjs'
|
|
85
85
|
import { turnInFlight } from './update-gate.mjs'
|
|
86
|
-
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq } from './session-store.mjs'
|
|
86
|
+
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, loadSummary, saveSummary } from './session-store.mjs'
|
|
87
87
|
import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks } from './event-id.mjs'
|
|
88
88
|
import { makeThrottledTrack } from './presence.mjs'
|
|
89
89
|
|
|
@@ -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 }) {
|
|
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
|
|
@@ -2213,6 +2217,13 @@ channel
|
|
|
2213
2217
|
// re-open within a page load).
|
|
2214
2218
|
.on('broadcast', { event: 'code-summarize' }, async ({ payload }) => {
|
|
2215
2219
|
const by = payload?.by || null
|
|
2220
|
+
// Peek — any device opening the panel fetches the cached recap (cross-device +
|
|
2221
|
+
// cross-restart persistence) WITHOUT spending a model call. No cache → silent.
|
|
2222
|
+
if (payload?.peek) {
|
|
2223
|
+
const cached = loadSummary(room)
|
|
2224
|
+
if (cached && cached.text) bcast('code-summary', { status: 'done', ...cached })
|
|
2225
|
+
return
|
|
2226
|
+
}
|
|
2216
2227
|
bcast('code-summary', { status: 'generating', by })
|
|
2217
2228
|
try {
|
|
2218
2229
|
// Pull assistant/user text out of a transcript-event array, keep the tail.
|
|
@@ -2253,18 +2264,23 @@ channel
|
|
|
2253
2264
|
? `\n\nThere are also ${closedIds.length} CLOSED terminal(s). Recent excerpts (do NOT name them individually):\n${(closedDigests.join('\n---\n') || '(no retained detail)').slice(0, 1400)}`
|
|
2254
2265
|
: ''
|
|
2255
2266
|
|
|
2256
|
-
const prompt = `
|
|
2257
|
-
-
|
|
2258
|
-
-
|
|
2259
|
-
|
|
2267
|
+
const prompt = `Summarize this multi-terminal coding session as a STRUCTURED LIST. Output ONLY these lines, nothing else:
|
|
2268
|
+
- ONE line per LIVE terminal, EXACTLY: <terminal name> | <working or done> | <one clause, max 12 words, no trailing period>
|
|
2269
|
+
- If there are CLOSED terminals, ONE final line, EXACTLY: CLOSED | <count> | <one aggregate clause about what they handled — do NOT name them>
|
|
2270
|
+
Use the pipe "|" as the separator. No headers, no markdown, no bullets, no extra prose. Example:
|
|
2271
|
+
Bridge Whip | working | SDK 0.7.128, detection gates live
|
|
2272
|
+
Reddit | done | GTM strategy mapped
|
|
2273
|
+
CLOSED | 52 | flow-trigger churn and @pool integration
|
|
2260
2274
|
|
|
2261
2275
|
${liveBlock}${closedBlock}
|
|
2262
2276
|
|
|
2263
|
-
|
|
2277
|
+
Output:`
|
|
2264
2278
|
|
|
2265
2279
|
const res = await oneShotSummary({ prompt, env: env || process.env, cwd: cwd || process.cwd() })
|
|
2266
2280
|
if (!res || !res.text) { bcast('code-summary', { status: 'error', error: 'Summary model unavailable on this provider.', by }); return }
|
|
2267
|
-
|
|
2281
|
+
const summary = { text: res.text, model: res.model, generatedAt: Date.now() }
|
|
2282
|
+
try { saveSummary(room, summary) } catch { /* cache is best-effort */ }
|
|
2283
|
+
bcast('code-summary', { status: 'done', ...summary, by })
|
|
2268
2284
|
process.stderr.write(`\n ◆ session summary generated (${res.model}) for ${by || 'someone'}.\n`)
|
|
2269
2285
|
} catch (e) {
|
|
2270
2286
|
bcast('code-summary', { status: 'error', error: String(e?.message || e).slice(0, 140), by })
|
|
@@ -2290,11 +2306,16 @@ Recap:`
|
|
|
2290
2306
|
// Closed sessions are unlinked by deleteSession, so a file existing on disk ==
|
|
2291
2307
|
// a session that was live at shutdown (never an explicitly closed one).
|
|
2292
2308
|
const all = loadAll(room)
|
|
2309
|
+
// Restore in stable creation order (openedAt) so the tab strip comes back in the
|
|
2310
|
+
// SAME order across a bridge restart — NOT readdir/filesystem order, which reshuffled
|
|
2311
|
+
// the tabs (Max, 2026-07-02). Legacy recs fall back to their first event ts / savedAt.
|
|
2312
|
+
const okey = (r) => r.openedAt || (Array.isArray(r.log) ? (r.log.find((e) => e?.ts)?.ts || 0) : 0) || r.savedAt || 0
|
|
2313
|
+
all.sort((a, b) => okey(a) - okey(b))
|
|
2293
2314
|
if (all.length) for (const rec of all) {
|
|
2294
2315
|
// FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
|
|
2295
2316
|
// bridge restart: the conductor keeps its subagent-block + plan interception, and
|
|
2296
2317
|
// lanes keep their worktree cwd + the ability to mark done.
|
|
2297
|
-
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 })
|
|
2318
|
+
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 })
|
|
2298
2319
|
const re = sessions.get(rec.id)
|
|
2299
2320
|
if (re && rec.flowReviewTarget) re.flowReviewTarget = rec.flowReviewTarget
|
|
2300
2321
|
// S4 — a resumed re-dispatch lane keeps its surviving revert target across a bridge restart.
|
package/package.json
CHANGED
package/session-store.mjs
CHANGED
|
@@ -191,3 +191,14 @@ export function loadNames(room) {
|
|
|
191
191
|
export function saveNames(room, names) {
|
|
192
192
|
try { ensureDir(room); fs.writeFileSync(namesFile(room), JSON.stringify(names || {})) } catch { /* noop */ }
|
|
193
193
|
}
|
|
194
|
+
|
|
195
|
+
// Session recap cache (2026-07-02, Session Info panel). The last on-demand summary,
|
|
196
|
+
// stored as a dotfile (NOT *.json → never in loadAll) so it survives a bridge restart
|
|
197
|
+
// and any device that opens the panel can fetch it (persists across devices). One per room.
|
|
198
|
+
const summaryFile = (room) => path.join(dir(room), '.summary')
|
|
199
|
+
export function loadSummary(room) {
|
|
200
|
+
try { return JSON.parse(fs.readFileSync(summaryFile(room), 'utf8')) || null } catch { return null }
|
|
201
|
+
}
|
|
202
|
+
export function saveSummary(room, summary) {
|
|
203
|
+
try { ensureDir(room); fs.writeFileSync(summaryFile(room), JSON.stringify(summary || null)) } catch { /* noop */ }
|
|
204
|
+
}
|