thinkpool-pair 0.7.213 → 0.7.215
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 +106 -12
- package/codex-session.mjs +0 -1
- package/package.json +2 -1
- package/side-lane.mjs +42 -0
package/bridge.mjs
CHANGED
|
@@ -104,6 +104,7 @@ import { armInterruptedResume, sendInterruptedContinue, supersedeInterruptedResu
|
|
|
104
104
|
import { turnInFlight } from './update-gate.mjs'
|
|
105
105
|
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage } from './session-store.mjs'
|
|
106
106
|
import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks, usageReportLine, codexUsageReportLine, buildRecapFromLog, RECAP_CAP, trimmedBeforeSeq, firstSeq } from './event-id.mjs'
|
|
107
|
+
import { SIDE_HANDOFF_PROMPT, SIDE_ROLE_PROMPT, appendSideContext, assistantTextSince, sideContextBlock, sideSnapshot } from './side-lane.mjs'
|
|
107
108
|
import { planMeterLine } from './plan-meters.mjs'
|
|
108
109
|
import { makeThrottledTrack } from './presence.mjs'
|
|
109
110
|
import { resolveAnonKey, DEFAULT_SUPABASE_URL } from './supabase-key.mjs'
|
|
@@ -1045,7 +1046,7 @@ const announce = () => {
|
|
|
1045
1046
|
// laneStatusOf: authoritative busy/idle + last-action timestamp/age +
|
|
1046
1047
|
// STUCK/BLOCKED alert. The bridge owns the turn and permission state, so
|
|
1047
1048
|
// every roster consumer reads one status instead of reconstructing it.
|
|
1048
|
-
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, ...laneStatusOf(s), hasTranscript: s.log.length > 0, commands: s.commands, mode: s.mode || undefined, effort: s.effort || undefined, name: termNames[id] || undefined, model: s.model || undefined, ...(s.runtime === 'codex' ? { approvalPolicy: codexConfigForMode(s.mode).approvalPolicy, models: s.models || [], canSteer: s.session?.canSteer ?? false } : {}), ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
|
|
1049
|
+
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, ...laneStatusOf(s), hasTranscript: s.log.length > 0, commands: s.commands, mode: s.mode || undefined, effort: s.effort || undefined, name: termNames[id] || undefined, model: s.model || undefined, ...(s.runtime === 'codex' ? { approvalPolicy: codexConfigForMode(s.mode).approvalPolicy, models: s.models || [], canSteer: s.session?.canSteer ?? false } : {}), ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.sideParent ? { sideParent: s.sideParent, sideTask: s.sideTask || undefined, sideHandback: !!s.sideHandback } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
|
|
1049
1050
|
// provider: the registered LLM-provider this lane runs on, NAME-ONLY {id,name}
|
|
1050
1051
|
// (NEVER the key or baseUrl). Additive; older clients ignore it. Omitted for the
|
|
1051
1052
|
// built-in/default Claude path (no badge). Makes the lane's provider badge +
|
|
@@ -1542,7 +1543,7 @@ function worktreeSnapshot(cwd) {
|
|
|
1542
1543
|
// relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
|
|
1543
1544
|
// persist to the host file; tool calls round-trip through the perm card; the
|
|
1544
1545
|
// rolling log replays to joiners and survives bridge restarts (session-store).
|
|
1545
|
-
function openStructured({ id, runtime = 'claude', model, effort, resume, log, commands, mode, spawnedBy, rolePrompt, flowSessionId, flowTaskKey, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage }) {
|
|
1546
|
+
function openStructured({ id, runtime = 'claude', model, effort, resume, log, commands, mode, spawnedBy, sideParent, sideTask, pendingSideContexts, rolePrompt, flowSessionId, flowTaskKey, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage }) {
|
|
1546
1547
|
if (sessions.has(id)) return
|
|
1547
1548
|
runtime = runtime === 'codex' ? 'codex' : 'claude'
|
|
1548
1549
|
// No explicit mode → a sensible default per runtime (see defaultModeForRuntime):
|
|
@@ -1579,7 +1580,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
1579
1580
|
: (!provider || provider === 'anthropic')
|
|
1580
1581
|
? (laneModel || null)
|
|
1581
1582
|
: (laneModel || providerNameMap()[provider] || provider),
|
|
1582
|
-
provider: provider || null, spawnedBy: spawnedBy || undefined, flowSessionId: flowSessionId || null, flowTaskKey: flowTaskKey || null, cwd: cwd || null, managedWorktree: managedWorktree || null,
|
|
1583
|
+
provider: provider || null, spawnedBy: spawnedBy || undefined, sideParent: sideParent || undefined, sideTask: sideTask || undefined, pendingSideContexts: Array.isArray(pendingSideContexts) ? pendingSideContexts.filter(Boolean).slice(-4) : [], flowSessionId: flowSessionId || null, flowTaskKey: flowTaskKey || null, cwd: cwd || null, managedWorktree: managedWorktree || null,
|
|
1583
1584
|
// Stable creation order — persisted so a bridge restart restores tabs in the SAME
|
|
1584
1585
|
// order (not readdir/filesystem order). Legacy recs (no openedAt) derive it from the
|
|
1585
1586
|
// first transcript event ts, so even the first post-fix restart is ordered right.
|
|
@@ -1773,7 +1774,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
1773
1774
|
// restart. Without this, sessionData omitted it → on restart the resumed session
|
|
1774
1775
|
// re-launched on the host default (Opus) regardless of the last switch, and the
|
|
1775
1776
|
// switch looked like it "never changed the model" (Max 2026-07-02). Restored below.
|
|
1776
|
-
const sessionData = () => ({ sessionId: entry.session?.sessionId || null, runtime: entry.runtime, log: entry.log, commands: entry.commands, mode: entry.mode, effort: entry.effort, model: entry.model || null, provider: entry.provider || null, spawnedBy: entry.spawnedBy, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, cwd: entry.cwd, managedWorktree: entry.managedWorktree, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [], openedAt: entry.openedAt || null, lastUsage: entry.lastUsage || null, carryRecap: entry.pendingRecap || null })
|
|
1777
|
+
const sessionData = () => ({ sessionId: entry.session?.sessionId || null, runtime: entry.runtime, log: entry.log, commands: entry.commands, mode: entry.mode, effort: entry.effort, model: entry.model || null, provider: entry.provider || null, spawnedBy: entry.spawnedBy, sideParent: entry.sideParent, sideTask: entry.sideTask, pendingSideContexts: entry.pendingSideContexts, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, cwd: entry.cwd, managedWorktree: entry.managedWorktree, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [], openedAt: entry.openedAt || null, lastUsage: entry.lastUsage || null, carryRecap: entry.pendingRecap || null })
|
|
1777
1778
|
const persist = () => saveSession(room, id, sessionData())
|
|
1778
1779
|
// Synchronous flush of this session's record. Used on open (so a brand-new session
|
|
1779
1780
|
// has a file under its id BEFORE its first event — surviving a restart inside the
|
|
@@ -2216,7 +2217,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2216
2217
|
try { entry.mockupWatcher?.close() } catch { /* noop */ }
|
|
2217
2218
|
try { void entry.viewport?.stop()?.catch(() => {}) } catch { /* noop */ }
|
|
2218
2219
|
sessions.delete(id)
|
|
2219
|
-
openStructured({ id, runtime: entry.runtime, model, log: entry.log, commands: entry.commands, mode: entry.mode })
|
|
2220
|
+
openStructured({ id, runtime: entry.runtime, model, log: entry.log, commands: entry.commands, mode: entry.mode, provider: entry.provider, spawnedBy: entry.spawnedBy, sideParent: entry.sideParent, sideTask: entry.sideTask, pendingSideContexts: entry.pendingSideContexts, rolePrompt: entry.rolePrompt, cwd: entry.cwd, managedWorktree: entry.managedWorktree })
|
|
2220
2221
|
return
|
|
2221
2222
|
}
|
|
2222
2223
|
// Stamp a wall-clock ts AND a stable cid on every transcript event before
|
|
@@ -2391,6 +2392,26 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2391
2392
|
// _turnStart is cleared unconditionally so a stray post-settle event can't
|
|
2392
2393
|
// re-notify off a stale stamp.
|
|
2393
2394
|
if (evt.kind === 'result' || evt.kind === 'error') {
|
|
2395
|
+
if (entry.sideHandback) {
|
|
2396
|
+
const request = entry.sideHandback
|
|
2397
|
+
entry.sideHandback = null
|
|
2398
|
+
const parent = sessions.get(request.parentId)
|
|
2399
|
+
const successful = evt.kind === 'result' && (!evt.subtype || evt.subtype === 'success')
|
|
2400
|
+
const summary = successful ? (String(evt.resultText || '').trim() || assistantTextSince(entry.log, request.afterSeq)) : ''
|
|
2401
|
+
if (parent && summary) {
|
|
2402
|
+
const context = sideContextBlock({ sideId: id, sideName: termNames[id], task: entry.sideTask, summary })
|
|
2403
|
+
parent.pendingSideContexts = appendSideContext(parent.pendingSideContexts, context)
|
|
2404
|
+
const handoff = { kind: 'side-handback', sideId: id, sideName: termNames[id] || 'Side lane', task: entry.sideTask || '', text: summary, by: request.by }
|
|
2405
|
+
stampEvent(handoff); pushLog(parent, handoff); bcast('code-event', { term: request.parentId, evt: handoff })
|
|
2406
|
+
const done = { kind: 'control', text: 'Brought to main.', by: request.by }
|
|
2407
|
+
stampEvent(done); pushLog(entry, done); bcast('code-event', { term: id, evt: done })
|
|
2408
|
+
parent.flush?.()
|
|
2409
|
+
} else {
|
|
2410
|
+
const failed = { kind: 'control', text: parent ? 'Couldn’t prepare a handoff — try again.' : 'The main terminal is no longer available.', by: request.by }
|
|
2411
|
+
stampEvent(failed); pushLog(entry, failed); bcast('code-event', { term: id, evt: failed })
|
|
2412
|
+
}
|
|
2413
|
+
entry.flush?.(); announce()
|
|
2414
|
+
}
|
|
2394
2415
|
const startedAt = entry._turnStart
|
|
2395
2416
|
entry._turnStart = null
|
|
2396
2417
|
// A canceled/failed App Server request can settle the turn without the
|
|
@@ -2589,7 +2610,7 @@ function respawnStructured(id, provider) {
|
|
|
2589
2610
|
// openStructured seed from the TARGET provider's configured model, which is the
|
|
2590
2611
|
// only model this lane was ever asked for. A same-env model change never reaches
|
|
2591
2612
|
// here — that path is an in-place setModel (see provider-switch).
|
|
2592
|
-
const { runtime, log, commands, mode, spawnedBy, flowSessionId, flowTaskKey, cwd, rolePrompt, reviewSliceRoots, openedAt } = s
|
|
2613
|
+
const { runtime, log, commands, mode, spawnedBy, sideParent, sideTask, pendingSideContexts, flowSessionId, flowTaskKey, cwd, rolePrompt, reviewSliceRoots, openedAt } = s
|
|
2593
2614
|
// Context-carry (2026-07-08): a provider switch is not an SDK resume — the new backend
|
|
2594
2615
|
// starts blank mid-conversation. Synthesize a plain-text recap from the VISIBLE log NOW
|
|
2595
2616
|
// (before teardown) and hand it to the fresh session as its first turn so the agent
|
|
@@ -2608,7 +2629,7 @@ function respawnStructured(id, provider) {
|
|
|
2608
2629
|
// sessionData() (provider included) synchronously on open, so a bridge restart
|
|
2609
2630
|
// restores the lane on its CURRENT provider, not the original — and its next
|
|
2610
2631
|
// announce carries the new provider badge (additive {id,name} projection).
|
|
2611
|
-
openStructured({ id, runtime, provider, log, commands, mode, spawnedBy, flowSessionId, flowTaskKey, cwd, rolePrompt, reviewSliceRoots, openedAt, carryRecap })
|
|
2632
|
+
openStructured({ id, runtime, provider, log, commands, mode, spawnedBy, sideParent, sideTask, pendingSideContexts, flowSessionId, flowTaskKey, cwd, rolePrompt, reviewSliceRoots, openedAt, carryRecap })
|
|
2612
2633
|
return true
|
|
2613
2634
|
}
|
|
2614
2635
|
|
|
@@ -2660,7 +2681,7 @@ function endStructured(id) {
|
|
|
2660
2681
|
// Cascade-close its direct children (spawnedBy === this id — a real terminal id, not a
|
|
2661
2682
|
// `flow:<id>` marker, so flow lanes are untouched; maxHop:1 means no deeper recursion).
|
|
2662
2683
|
for (const [cid, ce] of [...sessions]) {
|
|
2663
|
-
if (ce.spawnedBy === id) endStructured(cid)
|
|
2684
|
+
if (ce.spawnedBy === id || ce.sideParent === id) endStructured(cid)
|
|
2664
2685
|
}
|
|
2665
2686
|
}
|
|
2666
2687
|
deleteSession(room, id) // remove <id>.json so loadAll() won't bring it back
|
|
@@ -2771,11 +2792,60 @@ channel
|
|
|
2771
2792
|
// previous terminal's model (and the first uses the provider default). Falls
|
|
2772
2793
|
// through to the SDK default when absent. Stamped on the entry so the announce
|
|
2773
2794
|
// carries it at open (chip shows the model immediately, not the branch).
|
|
2774
|
-
|
|
2795
|
+
let runtime = structuredRuntime(payload.cmd)
|
|
2796
|
+
let sideParent = null
|
|
2797
|
+
let parent = null
|
|
2798
|
+
if (payload.sideParent) {
|
|
2799
|
+
const requested = sessions.get(payload.sideParent)
|
|
2800
|
+
sideParent = requested?.sideParent || payload.sideParent
|
|
2801
|
+
parent = sessions.get(sideParent)
|
|
2802
|
+
if (!parent || !String(payload.sideTask || '').trim()) {
|
|
2803
|
+
bcast('side-open-failed', { id: payload.id, parent: sideParent, message: parent ? 'Add a task after /side.' : 'The main terminal is not live.' })
|
|
2804
|
+
return
|
|
2805
|
+
}
|
|
2806
|
+
runtime = parent.runtime || runtime
|
|
2807
|
+
if (payload.name) { termNames[payload.id] = String(payload.name).slice(0, 80); saveNames(room, termNames) }
|
|
2808
|
+
}
|
|
2775
2809
|
// Opening a tab is not work. Paint and announce it immediately, then cold-boot the
|
|
2776
2810
|
// expensive Claude/Codex runtime on its first real turn. This keeps a fresh terminal
|
|
2777
2811
|
// instant and prevents abandoned tabs from becoming permanent resident processes.
|
|
2778
|
-
openStructured({
|
|
2812
|
+
openStructured({
|
|
2813
|
+
id: payload.id,
|
|
2814
|
+
runtime,
|
|
2815
|
+
mode: parent?.mode || payload.mode,
|
|
2816
|
+
model: parent?.model || payload.model || undefined,
|
|
2817
|
+
provider: runtime === 'codex' ? undefined : (parent?.provider || payload.provider || undefined),
|
|
2818
|
+
cwd: parent ? (parent.cwd || process.cwd()) : undefined,
|
|
2819
|
+
sideParent: sideParent || undefined,
|
|
2820
|
+
sideTask: sideParent ? String(payload.sideTask).trim() : undefined,
|
|
2821
|
+
rolePrompt: sideParent ? SIDE_ROLE_PROMPT : undefined,
|
|
2822
|
+
defer: true,
|
|
2823
|
+
})
|
|
2824
|
+
if (sideParent) {
|
|
2825
|
+
const child = sessions.get(payload.id)
|
|
2826
|
+
const task = String(payload.sideTask).trim()
|
|
2827
|
+
const by = String(payload.by || 'A room member')
|
|
2828
|
+
const sideName = termNames[payload.id] || 'Side lane'
|
|
2829
|
+
if (!child) {
|
|
2830
|
+
bcast('side-open-failed', { id: payload.id, parent: sideParent, message: 'The side lane could not start.' })
|
|
2831
|
+
return
|
|
2832
|
+
}
|
|
2833
|
+
child.hop = 0
|
|
2834
|
+
const childEvent = { kind: 'you', text: task, by }
|
|
2835
|
+
stampEvent(childEvent); pushLog(child, childEvent); bcast('code-event', { term: payload.id, evt: childEvent })
|
|
2836
|
+
const parentEvent = { kind: 'side-started', sideId: payload.id, sideName, task, by }
|
|
2837
|
+
stampEvent(parentEvent); pushLog(parent, parentEvent); bcast('code-event', { term: sideParent, evt: parentEvent })
|
|
2838
|
+
const snapshot = sideSnapshot(parent.log.filter((event) => event.cid !== parentEvent.cid))
|
|
2839
|
+
const prompt = snapshot ? `${snapshot}\n\n--- side task ---\n${task}` : task
|
|
2840
|
+
try { child.session.sendTurn(prompt) } catch {
|
|
2841
|
+
bcast('side-open-failed', { id: payload.id, parent: sideParent, message: 'The side agent could not start.' })
|
|
2842
|
+
endStructured(payload.id)
|
|
2843
|
+
return
|
|
2844
|
+
}
|
|
2845
|
+
child.flush?.(); parent.flush?.(); announce()
|
|
2846
|
+
process.stderr.write(`\n ◆ ${by} opened side lane ${String(payload.id).slice(0, 8)} under ${String(sideParent).slice(0, 8)}.\n`)
|
|
2847
|
+
return
|
|
2848
|
+
}
|
|
2779
2849
|
process.stderr.write(`\n ◆ web opened a structured "${payload.cmd}" session (${payload.mode || 'default'}${payload.model ? `, model ${payload.model}` : ''}).\n`)
|
|
2780
2850
|
return
|
|
2781
2851
|
}
|
|
@@ -2924,6 +2994,27 @@ channel
|
|
|
2924
2994
|
if (payload?.host && payload.host !== name) return
|
|
2925
2995
|
openStructured({ id: payload?.id || randomUUID(), runtime: payload?.runtime, model: payload?.model, provider: payload?.runtime === 'codex' ? undefined : payload?.provider, resume: payload?.resume, mode: payload?.mode, defer: true })
|
|
2926
2996
|
})
|
|
2997
|
+
.on('broadcast', { event: 'side-handback-request' }, ({ payload }) => {
|
|
2998
|
+
const side = payload?.term && sessions.get(payload.term)
|
|
2999
|
+
if (!side?.sideParent || side.sideParent !== payload?.parent) return
|
|
3000
|
+
const by = String(payload?.by || 'A room member')
|
|
3001
|
+
if (side.sideHandback || side.session?.turnActive) {
|
|
3002
|
+
const busy = { kind: 'control', text: 'Finish or stop the current side-lane turn before bringing it to main.', by }
|
|
3003
|
+
stampEvent(busy); pushLog(side, busy); bcast('code-event', { term: payload.term, evt: busy })
|
|
3004
|
+
return
|
|
3005
|
+
}
|
|
3006
|
+
side.sideHandback = { parentId: side.sideParent, by, afterSeq: maxSeq(side.log) }
|
|
3007
|
+
const preparing = { kind: 'control', text: 'Preparing a compact handoff for main…', by }
|
|
3008
|
+
stampEvent(preparing); pushLog(side, preparing); bcast('code-event', { term: payload.term, evt: preparing })
|
|
3009
|
+
side.flush?.(); announce()
|
|
3010
|
+
try { side.session.sendTurn(SIDE_HANDOFF_PROMPT) }
|
|
3011
|
+
catch {
|
|
3012
|
+
side.sideHandback = null
|
|
3013
|
+
const failed = { kind: 'control', text: 'Couldn’t start the handoff — try again.', by }
|
|
3014
|
+
stampEvent(failed); pushLog(side, failed); bcast('code-event', { term: payload.term, evt: failed })
|
|
3015
|
+
side.flush?.(); announce()
|
|
3016
|
+
}
|
|
3017
|
+
})
|
|
2927
3018
|
.on('broadcast', { event: 'code-turn' }, async ({ payload }) => {
|
|
2928
3019
|
const s = payload?.term && sessions.get(payload.term)
|
|
2929
3020
|
if (!s || payload.text == null) return
|
|
@@ -3069,7 +3160,10 @@ channel
|
|
|
3069
3160
|
// — a separate recap turn would race + double-fire) and clear the flag so the init handler
|
|
3070
3161
|
// no-ops. The visible `you` echo is unchanged; the recap is context for the agent only.
|
|
3071
3162
|
let sendText = text
|
|
3072
|
-
|
|
3163
|
+
const carried = []
|
|
3164
|
+
if (s.pendingRecap) { carried.push(s.pendingRecap); s.pendingRecap = null }
|
|
3165
|
+
if (s.pendingSideContexts?.length) { carried.push(...s.pendingSideContexts); s.pendingSideContexts = [] }
|
|
3166
|
+
if (carried.length) { sendText = `${carried.join('\n\n')}\n\n--- the person's next message ---\n${text}`; s.flush?.() }
|
|
3073
3167
|
// The browser's `hostPath` is display metadata, not host authority. Rebuild
|
|
3074
3168
|
// image paths from the bridge-owned UPDIR + deterministic attachment fields,
|
|
3075
3169
|
// then cover the separate file-put/code-turn download race with a bounded
|
|
@@ -3312,7 +3406,7 @@ channel
|
|
|
3312
3406
|
// FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
|
|
3313
3407
|
// bridge restart: the conductor keeps its subagent-block + plan interception, and
|
|
3314
3408
|
// lanes keep their worktree cwd + the ability to mark done.
|
|
3315
|
-
openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, effort: rec.effort, provider: rec.runtime === 'codex' ? undefined : rec.provider || undefined, 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, managedWorktree: rec.managedWorktree, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt, lastUsage: rec.lastUsage, carryRecap: rec.carryRecap,
|
|
3409
|
+
openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, effort: rec.effort, provider: rec.runtime === 'codex' ? undefined : rec.provider || undefined, resume: canResume(rec) ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, sideParent: rec.sideParent, sideTask: rec.sideTask, pendingSideContexts: rec.pendingSideContexts, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, cwd: rec.cwd, managedWorktree: rec.managedWorktree, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt, lastUsage: rec.lastUsage, carryRecap: rec.carryRecap,
|
|
3316
3410
|
// Lazy-boot restored terminals that were IDLE + not part of a flow: their transcript
|
|
3317
3411
|
// shows immediately; the query boots on first turn. Mid-turn + flow terminals boot now
|
|
3318
3412
|
// (mid-turn needs auto-resume; flow needs its lane live).
|
package/codex-session.mjs
CHANGED
|
@@ -479,7 +479,6 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
479
479
|
})
|
|
480
480
|
appServerThreadReady = true
|
|
481
481
|
mapper.push({ type: 'thread.started', thread_id: sessionId })
|
|
482
|
-
note(`Codex App Server active (${appServerGateResult.version || 'compatible version'}).`)
|
|
483
482
|
return true
|
|
484
483
|
} catch (error) {
|
|
485
484
|
// Fallback is safe only here: no turn/start request has been accepted, so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.215",
|
|
4
4
|
"description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"recap.mjs",
|
|
31
31
|
"transcript-sanitize.mjs",
|
|
32
32
|
"session-store.mjs",
|
|
33
|
+
"side-lane.mjs",
|
|
33
34
|
"cross-terminal.mjs",
|
|
34
35
|
"interrupted-resume.mjs",
|
|
35
36
|
"flow-conductor.mjs",
|
package/side-lane.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { buildRecapFromLog } from './recap.mjs'
|
|
2
|
+
|
|
3
|
+
export const SIDE_RECAP_CAP = 12_000
|
|
4
|
+
|
|
5
|
+
export const SIDE_ROLE_PROMPT = `You are a ThinkPool side lane attached to a main terminal.
|
|
6
|
+
Focus first on reading, searching, comparing, and answering the side task. You are a full agent and share the main terminal's working directory, so coordinate through ThinkPool Peer before changing files that another lane may be editing. Do not make edits unless a person explicitly asks for them in this side lane. Keep the investigation self-contained; the main terminal receives nothing until a person chooses Bring to main.`
|
|
7
|
+
|
|
8
|
+
export const SIDE_HANDOFF_PROMPT = `Prepare a compact handoff for the main terminal now.
|
|
9
|
+
Return only the handoff. Include: conclusion, strongest evidence, files changed or artifacts produced, and any unresolved question or recommended next action. Do not continue the investigation and do not address the reader conversationally.`
|
|
10
|
+
|
|
11
|
+
export function sideSnapshot(log) {
|
|
12
|
+
return buildRecapFromLog(Array.isArray(log) ? log : [], SIDE_RECAP_CAP)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assistantTextSince(log, afterSeq = 0) {
|
|
16
|
+
const chunks = []
|
|
17
|
+
for (const event of Array.isArray(log) ? log : []) {
|
|
18
|
+
if (event?.kind !== 'assistant') continue
|
|
19
|
+
if (Number.isFinite(afterSeq) && Number.isFinite(event.seq) && event.seq <= afterSeq) continue
|
|
20
|
+
for (const block of Array.isArray(event.blocks) ? event.blocks : []) {
|
|
21
|
+
if (block?.type === 'text' && String(block.text || '').trim()) chunks.push(String(block.text).trim())
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return chunks.join('\n\n').trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sideContextBlock({ sideId, sideName, task, summary }) {
|
|
28
|
+
const label = String(sideName || 'Side lane').trim()
|
|
29
|
+
const ref = String(sideId || '').slice(0, 8)
|
|
30
|
+
const lines = [
|
|
31
|
+
`--- side-lane handoff: ${label}${ref ? ` (${ref})` : ''} ---`,
|
|
32
|
+
task ? `Task: ${String(task).trim()}` : '',
|
|
33
|
+
String(summary || '').trim(),
|
|
34
|
+
'--- end side-lane handoff ---',
|
|
35
|
+
]
|
|
36
|
+
return lines.filter(Boolean).join('\n')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function appendSideContext(contexts, context, cap = 4) {
|
|
40
|
+
if (!context) return Array.isArray(contexts) ? contexts.slice(-cap) : []
|
|
41
|
+
return [...(Array.isArray(contexts) ? contexts : []), context].slice(-cap)
|
|
42
|
+
}
|