thinkpool-pair 0.7.231 → 0.7.232
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 +39 -17
- package/claude-session.mjs +9 -7
- package/codex-session.mjs +6 -5
- package/package.json +1 -1
- package/thinkpool-room-prompt.mjs +34 -0
package/bridge.mjs
CHANGED
|
@@ -111,6 +111,7 @@ import { SIDE_HANDOFF_PROMPT, SIDE_ROLE_PROMPT, appendSideContext, assistantText
|
|
|
111
111
|
import { planMeterLine } from './plan-meters.mjs'
|
|
112
112
|
import { makeThrottledTrack } from './presence.mjs'
|
|
113
113
|
import { resolveAnonKey, DEFAULT_SUPABASE_URL } from './supabase-key.mjs'
|
|
114
|
+
import { buildTerminalRolePrompt } from './thinkpool-room-prompt.mjs'
|
|
114
115
|
|
|
115
116
|
// Public client creds (the same anon values the web app ships — safe to embed).
|
|
116
117
|
// Override with TP_SUPABASE_URL / TP_SUPABASE_ANON if you ever need to.
|
|
@@ -1654,7 +1655,7 @@ function worktreeSnapshot(cwd) {
|
|
|
1654
1655
|
// relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
|
|
1655
1656
|
// persist to the host file; tool calls round-trip through the perm card; the
|
|
1656
1657
|
// rolling log replays to joiners and survives bridge restarts (session-store).
|
|
1657
|
-
function openStructured({ id, runtime = 'claude', model, effort, resume, log, commands, mode, spawnedBy, sideParent, sideTask, pendingSideContexts, rolePrompt, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage }) {
|
|
1658
|
+
function openStructured({ id, runtime = 'claude', model, effort, resume, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, rolePrompt, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage }) {
|
|
1658
1659
|
if (sessions.has(id)) return
|
|
1659
1660
|
runtime = runtime === 'codex' ? 'codex' : 'claude'
|
|
1660
1661
|
// No explicit mode → a sensible default per runtime (see defaultModeForRuntime):
|
|
@@ -1683,6 +1684,14 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
1683
1684
|
// session store so the Ensemble flag survives a bridge restart (else a respin
|
|
1684
1685
|
// stripped it and the lane reverted to a plain tab — the t6 "no chip" bug).
|
|
1685
1686
|
effort = new Set(['low', 'medium', 'high', 'xhigh', 'max']).has(effort) ? effort : 'high'
|
|
1687
|
+
// Structural role is durable and independent of the per-turn hop breaker. A
|
|
1688
|
+
// human can speak into a spawned lane (resetting hop to 0) without magically
|
|
1689
|
+
// turning that lane into a top-level terminal. Legacy records predate
|
|
1690
|
+
// spawnDepth; infer only the first level from their ownership marker.
|
|
1691
|
+
const structuralDepth = Number.isInteger(spawnDepth) && spawnDepth >= 0
|
|
1692
|
+
? spawnDepth
|
|
1693
|
+
: (sideParent || (spawnedBy && !String(spawnedBy).startsWith('flow:')) ? 1 : 0)
|
|
1694
|
+
const initialHop = Number.isInteger(hop) && hop >= 0 ? hop : structuralDepth
|
|
1686
1695
|
const entry = { cmd: runtime, runtime, 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, effort, models: runtime === 'codex' ? codexModels : undefined,
|
|
1687
1696
|
// model: truthful active-model label — now the SAME `laneModel` the SDK is given, so the
|
|
1688
1697
|
// chip cannot disagree with the wire. When this lane runs on a custom (non-anthropic)
|
|
@@ -1696,7 +1705,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
1696
1705
|
: (!provider || provider === 'anthropic')
|
|
1697
1706
|
? (laneModel || null)
|
|
1698
1707
|
: (laneModel || providerNameMap()[provider] || provider),
|
|
1699
|
-
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,
|
|
1708
|
+
provider: provider || null, spawnedBy: spawnedBy || undefined, spawnDepth: structuralDepth, cascadeRole: cascadeRole === 'conductor' || cascadeRole === 'worker' ? cascadeRole : null, hop: initialHop, 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,
|
|
1700
1709
|
flowRole: flowRole || (flowSessionId ? (flowTaskKey ? ((flowReviewTargets?.length || reviewSliceRoots?.length) ? 'reviewer' : 'builder') : 'conductor') : null),
|
|
1701
1710
|
flowReviewTarget: flowReviewTarget || null,
|
|
1702
1711
|
flowReviewTargets: Array.isArray(flowReviewTargets) && flowReviewTargets.length ? flowReviewTargets.filter(Boolean) : (flowReviewTarget ? [flowReviewTarget] : []),
|
|
@@ -1946,7 +1955,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
1946
1955
|
// restart. Without this, sessionData omitted it → on restart the resumed session
|
|
1947
1956
|
// re-launched on the host default (Opus) regardless of the last switch, and the
|
|
1948
1957
|
// switch looked like it "never changed the model" (Max 2026-07-02). Restored below.
|
|
1949
|
-
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, flowRole: entry.flowRole || null, flowReviewTargets: entry.flowReviewTargets || [], flowReviewRound: entry.flowReviewRound || 0, dispatchBaseSha: entry.dispatchBaseSha || null, 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 })
|
|
1958
|
+
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, spawnDepth: entry.spawnDepth || 0, cascadeRole: entry.cascadeRole || null, hop: entry.hop || 0, sideParent: entry.sideParent, sideTask: entry.sideTask, pendingSideContexts: entry.pendingSideContexts, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, flowRole: entry.flowRole || null, flowReviewTargets: entry.flowReviewTargets || [], flowReviewRound: entry.flowReviewRound || 0, dispatchBaseSha: entry.dispatchBaseSha || null, 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 })
|
|
1950
1959
|
const persist = () => saveSession(room, id, sessionData())
|
|
1951
1960
|
// Synchronous flush of this session's record. Used on open (so a brand-new session
|
|
1952
1961
|
// has a file under its id BEFORE its first event — surviving a restart inside the
|
|
@@ -2109,12 +2118,13 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2109
2118
|
// fork-bomb breaker, now one level deeper (2026-07-10 cascade-spawn-depth spec).
|
|
2110
2119
|
...(!entry.flowRole ? [tool(
|
|
2111
2120
|
'spawn_terminal',
|
|
2112
|
-
'Open a NEW agent terminal (Claude or Codex) in this ThinkPool Code room — YOUR lane to use, not a sibling that is already working. The runtime defaults to inheriting this lane, or pass `runtime` to dispatch across runtimes. Choose `model` explicitly from advertised metadata when practical: for Codex use Luna/Mini for mechanical work, Terra for ordinary feature/fix work, and Sol only for repeated-failure, architecture/conductor, or adversarial final-review work; never inherit Sol by default, and omit `model` for runtime fallback when the named tier is unavailable. For cascades, pass sliceType=scaffold for mechanical work, feature/fix for builders, and review for adversarial verification (review never routes to the cheap tier); an explicit model overrides slice tiering. Omit sliceType to preserve ordinary model inheritance/default behavior. Give the lane an initial `task`, collect its result with read_terminal, then ALWAYS close_terminal it — a finished lane left open holds a slot against the room cap and blocks the next fan-out. By default the new lane INHERITS your current permission mode. ALWAYS prefer spawning a fresh lane over post_to_terminal into one that is already busy. Bounded by cascade depth, the room terminal cap, and per-turn limits.',
|
|
2121
|
+
'Open a NEW agent terminal (Claude or Codex) in this ThinkPool Code room — YOUR lane to use, not a sibling that is already working. ALWAYS declare cascadeRole="conductor" when this new terminal must decompose and spawn its own worker sub-terminals; declare cascadeRole="worker" for an assigned slice that must work directly. The bridge injects that identity into the child\'s first system preamble. The runtime defaults to inheriting this lane, or pass `runtime` to dispatch across runtimes. Choose `model` explicitly from advertised metadata when practical: for Codex use Luna/Mini for mechanical work, Terra for ordinary feature/fix work, and Sol only for repeated-failure, architecture/conductor, or adversarial final-review work; never inherit Sol by default, and omit `model` for runtime fallback when the named tier is unavailable. For cascades, pass sliceType=scaffold for mechanical work, feature/fix for builders, and review for adversarial verification (review never routes to the cheap tier); an explicit model overrides slice tiering. Omit sliceType to preserve ordinary model inheritance/default behavior. Give the lane an initial `task`, collect its result with read_terminal, then ALWAYS close_terminal it — a finished lane left open holds a slot against the room cap and blocks the next fan-out. By default the new lane INHERITS your current permission mode. ALWAYS prefer spawning a fresh lane over post_to_terminal into one that is already busy. Bounded by cascade depth, the room terminal cap, and per-turn limits.',
|
|
2113
2122
|
{
|
|
2114
2123
|
name: z.string().max(80).optional().describe('a short label for the new lane so the room (and you) can find it, e.g. "Research: topologies"'),
|
|
2115
2124
|
task: z.string().optional().describe('an initial task to hand the new lane immediately; omit to open it idle'),
|
|
2116
2125
|
model: z.string().optional().describe('optional model for the chosen runtime, e.g. opus / sonnet / gpt-5.6-sol'),
|
|
2117
2126
|
sliceType: z.enum(['scaffold', 'feature', 'fix', 'review']).optional().describe('optional cascade slice tier; omitted preserves normal inheritance/default, explicit model wins'),
|
|
2127
|
+
cascadeRole: z.enum(['conductor', 'worker']).optional().describe('structural Cascade identity injected into the child system preamble: conductor may dispatch one worker tier; worker does the assigned slice directly'),
|
|
2118
2128
|
runtime: z.enum(['claude', 'codex']).optional().describe('agent runtime for the new lane; defaults to inheriting this lane'),
|
|
2119
2129
|
provider: z.string().optional().describe('optional registered LLM provider id to run this lane on (from the account\'s provider registry); omit for the default Claude/Anthropic path'),
|
|
2120
2130
|
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('permission mode for the new lane; defaults to inheriting YOUR current mode. Raising a lane to bypassPermissions from a non-bypass lane asks the room to confirm once.'),
|
|
@@ -2167,18 +2177,23 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2167
2177
|
// plan-mode lane would stall on ExitPlanMode cards. An explicit args.mode still wins;
|
|
2168
2178
|
// otherwise inherit, but fall plan → default (an autonomous worker doesn't plan-gate).
|
|
2169
2179
|
const childMode = args?.mode || (entry.mode === 'plan' ? 'default' : entry.mode)
|
|
2170
|
-
|
|
2180
|
+
const childSpawnDepth = (entry.spawnDepth || 0) + 1
|
|
2181
|
+
const childHop = (entry.hop || 0) + 1
|
|
2182
|
+
// Stamp ownership/depth BEFORE the runtime starts so its first system
|
|
2183
|
+
// preamble is truthful. Mutating ne.spawnedBy after openStructured was
|
|
2184
|
+
// too late: Codex/Claude had already booted with the top-level wording.
|
|
2185
|
+
openStructured({ id: newId, runtime: childRuntime, model: childModel, provider: args?.provider, mode: childMode, spawnedBy: id, spawnDepth: childSpawnDepth, cascadeRole: args?.cascadeRole, hop: childHop })
|
|
2171
2186
|
const ne = sessions.get(newId)
|
|
2172
2187
|
if (!ne) { if (args?.name) { delete termNames[newId]; saveNames(room, termNames) } return okText('Could not open a new lane — the terminal cap may have just been reached. Close one and retry.') }
|
|
2173
|
-
ne.
|
|
2188
|
+
ne.peekCount = 0; ne.postCount = 0; ne.spawnTimes = []
|
|
2174
2189
|
announce() // (defensive) ensure the web renders the new tab live
|
|
2175
2190
|
if (args?.task) {
|
|
2176
|
-
//
|
|
2177
|
-
//
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
const msg = `[Task from terminal ${fromRef}'s agent — relayed via ThinkPool cross-terminal;
|
|
2191
|
+
// The user-visible relay reinforces (but never defines) the structural
|
|
2192
|
+
// role already injected by the system preamble above.
|
|
2193
|
+
const roleRelay = args?.cascadeRole === 'conductor'
|
|
2194
|
+
? 'you are its Cascade conductor terminal, responsible for worker sub-terminals'
|
|
2195
|
+
: 'you are its worker sub-terminal, not a top-level room terminal'
|
|
2196
|
+
const msg = `[Task from terminal ${fromRef}'s agent — relayed via ThinkPool cross-terminal; ${roleRelay}]\n${args.task}`
|
|
2182
2197
|
const evt = { kind: 'you', text: msg, by: `terminal ${fromRef} (agent)`, crosspost: true, relaySourceName: termNames[id] || undefined }
|
|
2183
2198
|
stampEvent(evt); pushLog(ne, evt); bcast('code-event', { term: newId, evt })
|
|
2184
2199
|
try { ne.session.sendTurn(msg) } catch { return okText(`Opened lane ${newRef}${args?.name ? ` ("${args.name}")` : ''}, but it may still be starting — could not hand off the task. Try post_to_terminal shortly.`) }
|
|
@@ -2311,11 +2326,18 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2311
2326
|
// on the init `system` event (see onEvent) — a sendTurn before the input stream is consumed
|
|
2312
2327
|
// is silently lost (the 2026-07-02 auto-resume bug). Empty string → nothing to carry.
|
|
2313
2328
|
entry.pendingRecap = (typeof carryRecap === 'string' && carryRecap.trim()) ? carryRecap : null
|
|
2329
|
+
const terminalRolePrompt = buildTerminalRolePrompt({
|
|
2330
|
+
spawnedBy: entry.spawnedBy,
|
|
2331
|
+
spawnDepth: entry.spawnDepth,
|
|
2332
|
+
cascadeRole: entry.cascadeRole,
|
|
2333
|
+
flowRole: entry.flowRole,
|
|
2334
|
+
sideParent: entry.sideParent,
|
|
2335
|
+
})
|
|
2314
2336
|
entry.session = (runtime === 'codex' ? startCodexSession : startClaudeSession)({
|
|
2315
2337
|
// laneModel, NOT the raw `model` param: the SDK's `model` option OVERRIDES the
|
|
2316
2338
|
// ANTHROPIC_MODEL supplied by resolveProviderEnv() in `env` below, so an inherited
|
|
2317
2339
|
// Claude id here defeats the whole per-provider env. undefined → option omitted.
|
|
2318
|
-
cwd: cwd || process.cwd(), model: laneModel, effort, resume, mode, rolePrompt,
|
|
2340
|
+
cwd: cwd || process.cwd(), model: laneModel, effort, resume, mode, terminalRolePrompt, rolePrompt,
|
|
2319
2341
|
// Idle ordinary tabs create nothing. Immediately before the FIRST real
|
|
2320
2342
|
// turn (human prompt, dispatch, or cascade task), the runtime calls this
|
|
2321
2343
|
// while still cold; subsequent turns reuse the persisted worktree.
|
|
@@ -2413,7 +2435,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2413
2435
|
openStructured({
|
|
2414
2436
|
id, runtime: entry.runtime, model: entry.model || model, effort: entry.effort,
|
|
2415
2437
|
provider: entry.provider, log: entry.log, commands: entry.commands, mode: entry.mode,
|
|
2416
|
-
spawnedBy: entry.spawnedBy, sideParent: entry.sideParent, sideTask: entry.sideTask, pendingSideContexts: entry.pendingSideContexts, flowSessionId: entry.flowSessionId,
|
|
2438
|
+
spawnedBy: entry.spawnedBy, spawnDepth: entry.spawnDepth, cascadeRole: entry.cascadeRole, hop: entry.hop, sideParent: entry.sideParent, sideTask: entry.sideTask, pendingSideContexts: entry.pendingSideContexts, flowSessionId: entry.flowSessionId,
|
|
2417
2439
|
flowTaskKey: entry.flowTaskKey, flowRole: entry.flowRole, flowReviewTarget: entry.flowReviewTarget,
|
|
2418
2440
|
flowReviewTargets: entry.flowReviewTargets, flowReviewRound: entry.flowReviewRound,
|
|
2419
2441
|
dispatchBaseSha: entry.dispatchBaseSha, revertTarget: entry.revertTarget, cwd: entry.cwd,
|
|
@@ -2830,7 +2852,7 @@ function respawnStructured(id, provider) {
|
|
|
2830
2852
|
// openStructured seed from the TARGET provider's configured model, which is the
|
|
2831
2853
|
// only model this lane was ever asked for. A same-env model change never reaches
|
|
2832
2854
|
// here — that path is an in-place setModel (see provider-switch).
|
|
2833
|
-
const { runtime, log, commands, mode, spawnedBy, sideParent, sideTask, pendingSideContexts, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, rolePrompt, reviewSliceRoots, openedAt, lastUsage } = s
|
|
2855
|
+
const { runtime, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, rolePrompt, reviewSliceRoots, openedAt, lastUsage } = s
|
|
2834
2856
|
// Context-carry (2026-07-08): a provider switch is not an SDK resume — the new backend
|
|
2835
2857
|
// starts blank mid-conversation. Synthesize a plain-text recap from the VISIBLE log NOW
|
|
2836
2858
|
// (before teardown) and hand it to the fresh session as its first turn so the agent
|
|
@@ -2849,7 +2871,7 @@ function respawnStructured(id, provider) {
|
|
|
2849
2871
|
// sessionData() (provider included) synchronously on open, so a bridge restart
|
|
2850
2872
|
// restores the lane on its CURRENT provider, not the original — and its next
|
|
2851
2873
|
// announce carries the new provider badge (additive {id,name} projection).
|
|
2852
|
-
openStructured({ id, runtime, provider, log, commands, mode, spawnedBy, sideParent, sideTask, pendingSideContexts, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, rolePrompt, reviewSliceRoots, openedAt, lastUsage, carryRecap })
|
|
2874
|
+
openStructured({ id, runtime, provider, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, rolePrompt, reviewSliceRoots, openedAt, lastUsage, carryRecap })
|
|
2853
2875
|
return true
|
|
2854
2876
|
}
|
|
2855
2877
|
|
|
@@ -3642,7 +3664,7 @@ channel
|
|
|
3642
3664
|
// FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
|
|
3643
3665
|
// bridge restart: the conductor keeps its subagent-block + plan interception, and
|
|
3644
3666
|
// lanes keep their worktree cwd + the ability to mark done.
|
|
3645
|
-
openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, effort: rec.effort, provider: rec.runtime === 'codex' ? undefined : rec.provider || undefined, resume: resumable ? 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, flowRole: rec.flowRole, flowReviewTarget: rec.flowReviewTarget, flowReviewTargets: rec.flowReviewTargets, flowReviewRound: rec.flowReviewRound, dispatchBaseSha: rec.dispatchBaseSha, revertTarget: rec.revertTarget, cwd: rec.cwd, managedWorktree: rec.managedWorktree, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt, lastUsage: rec.lastUsage, carryRecap: wasInterrupted ? recoveryRecap : rec.carryRecap,
|
|
3667
|
+
openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, effort: rec.effort, provider: rec.runtime === 'codex' ? undefined : rec.provider || undefined, resume: resumable ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, spawnDepth: rec.spawnDepth, cascadeRole: rec.cascadeRole, hop: rec.hop, sideParent: rec.sideParent, sideTask: rec.sideTask, pendingSideContexts: rec.pendingSideContexts, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, flowRole: rec.flowRole, flowReviewTarget: rec.flowReviewTarget, flowReviewTargets: rec.flowReviewTargets, flowReviewRound: rec.flowReviewRound, dispatchBaseSha: rec.dispatchBaseSha, revertTarget: rec.revertTarget, cwd: rec.cwd, managedWorktree: rec.managedWorktree, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt, lastUsage: rec.lastUsage, carryRecap: wasInterrupted ? recoveryRecap : rec.carryRecap,
|
|
3646
3668
|
// Lazy-boot restored terminals that were IDLE + not part of a flow: their transcript
|
|
3647
3669
|
// shows immediately; the query boots on first turn. Mid-turn + flow terminals boot now
|
|
3648
3670
|
// (mid-turn needs auto-resume; flow needs its lane live).
|
package/claude-session.mjs
CHANGED
|
@@ -200,14 +200,14 @@ const TP_ROOM_REMINDER = [
|
|
|
200
200
|
THINKPOOL_DESIGN_DELIVERY_RULE,
|
|
201
201
|
'REMOTE USER: the people here may have no terminal and no access to this host — never ask them to run a local command, open a local file, or check something on the machine. Anything host-side, YOU run, and show the result.',
|
|
202
202
|
'SHOW, don\'t narrate: a picture of the result beats a wall of text on a phone — err toward more screenshots.',
|
|
203
|
-
'ENSEMBLE:
|
|
203
|
+
'ENSEMBLE: obey your authoritative TERMINAL ROLE. Only conductor-capable roles fan work out with spawn_terminal (visible and steerable here); leaf, Side, and managed Flow lanes work directly. When opening a Cascade conductor, pass cascadeRole="conductor"; that conductor passes cascadeRole="worker" for slice lanes. Never use built-in invisible Task/Agent subagents or hijack a busy sibling.',
|
|
204
204
|
'PEER: before substantive work, check what the other lanes are doing (the ROOM NOW snapshot below, read_terminal for detail; list_sessions/read_session across rooms) — coordinate on shared files/branches instead of colliding.',
|
|
205
205
|
'WORKTREES: parallel lanes share one repo — before code edits run `git worktree list`; if linked worktrees exist, take your OWN worktree + branch, never the shared checkout or a branch another lane is on.',
|
|
206
|
-
'BUILD WORKFLOW (default, no magic word): right-size — a trivial ask you just do; a
|
|
206
|
+
'BUILD WORKFLOW (default, no magic word): right-size within your TERMINAL ROLE — a trivial ask or delegated slice you just do; a conductor-capable role with a genuinely decomposable build FIRST writes a short plan in chat, THEN fans independent slices into visible spawn_terminal lanes and verifies them. Leaf/Side/managed Flow roles do not fan out. Never plan-mode/ExitPlanMode; plans live in chat and lanes in the existing list.',
|
|
207
207
|
'Verify before claiming done — show runtime evidence you produced, not "should work, go test it".',
|
|
208
208
|
].join(' ')
|
|
209
209
|
|
|
210
|
-
export function startClaudeSession({ cwd, model, effort: initialEffort = 'high', resume, env, mode: initialMode = 'default', onEvent, requestPermission, mcpServers, crossPostGate, crossRoomPostGate, didSpawnTarget = null, rolePrompt, blockSubagents = false, onSubmitPlan = null, onLaneDone = null, onReviewVerdict = null, reviewGate = null, lazy = false, roomContext = null, suggest = true, prepareCwd = null }) {
|
|
210
|
+
export function startClaudeSession({ cwd, model, effort: initialEffort = 'high', resume, env, mode: initialMode = 'default', onEvent, requestPermission, mcpServers, crossPostGate, crossRoomPostGate, didSpawnTarget = null, terminalRolePrompt, rolePrompt, blockSubagents = false, onSubmitPlan = null, onLaneDone = null, onReviewVerdict = null, reviewGate = null, lazy = false, roomContext = null, suggest = true, prepareCwd = null }) {
|
|
211
211
|
// Per-turn reminder + live ROOM NOW tail. roomContext (bridge-supplied) returns the
|
|
212
212
|
// room's CURRENT state — sibling lanes, active git worktrees — or null. The static
|
|
213
213
|
// rules keep the agent aware of the room's FEATURES; the live tail keeps it aware of
|
|
@@ -216,7 +216,8 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
216
216
|
const roomReminder = () => {
|
|
217
217
|
let live = null
|
|
218
218
|
try { live = roomContext ? roomContext() : null } catch { live = null }
|
|
219
|
-
|
|
219
|
+
const role = String(terminalRolePrompt || '').trim()
|
|
220
|
+
return `<system-reminder>\n${role ? `${role}\n\n` : ''}${TP_ROOM_REMINDER}${live ? `\n\n${live}` : ''}\n</system-reminder>`
|
|
220
221
|
}
|
|
221
222
|
const ac = new AbortController()
|
|
222
223
|
let input = makeInputStream() // `let`: auto-restart swaps in a fresh stream
|
|
@@ -690,6 +691,7 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
690
691
|
// a bypass session still planned. Bias the room session to DO the work; only
|
|
691
692
|
// plan when the room is actually in Plan mode (⇧⇥) or the user explicitly asks.
|
|
692
693
|
appendSystemPrompt: [
|
|
694
|
+
...(terminalRolePrompt ? [terminalRolePrompt] : []),
|
|
693
695
|
...(rolePrompt ? [rolePrompt] : []),
|
|
694
696
|
'ENVIRONMENT (authoritative — overrides any user-global CLAUDE.md or memory that claims otherwise): You are Claude running inside a ThinkPool Code room, driven live by a user (and possibly a partner) from a phone or browser, via the thinkpool-pair bridge.',
|
|
695
697
|
'You are NOT in cmux. cmux may be installed on this machine and appear in $PATH, but it did not spawn you and its automation/orchestration rules do not apply. If any loaded memory says "Claude Code lives inside cmux," it is describing direct terminal use, not this room — disregard it here. The runtime truth is the env: THINKPOOL_PAIR_ACCOUNT_CHILD / npm_lifecycle_script=thinkpool-pair mark a ThinkPool Code session.',
|
|
@@ -700,15 +702,15 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
700
702
|
// no summon word. The agent INFERS build intent and runs plan→(optional)fan-out→build→self-verify,
|
|
701
703
|
// right-sized to the task, surfaced in the room's EXISTING surfaces (chat + lane list) — never a new panel,
|
|
702
704
|
// never plan-mode's approval card. Trivial asks stay single-lane with zero ceremony.
|
|
703
|
-
'DEFAULT BUILD WORKFLOW (this IS your standard behaviour — no keyword and no "flow" word is ever required to trigger it; you infer it from the request itself). RIGHT-SIZE to the task, always: a TRIVIAL ask — a one-line fix, a small tweak, a question — you just do it directly, with no plan and no extra lanes (never tax small work with ceremony). A NON-TRIVIAL build that genuinely decomposes — you FIRST write a short plan as a normal chat message (the approach, the concrete slices, and what you will verify), THEN fan the independent slices out into their own visible lanes with spawn_terminal, build, and self-verify each slice before calling it done.',
|
|
705
|
+
'DEFAULT BUILD WORKFLOW (this IS your standard behaviour — no keyword and no "flow" word is ever required to trigger it; you infer it from the request itself). RIGHT-SIZE to the task within the authoritative TERMINAL ROLE, always: a TRIVIAL ask — a one-line fix, a small tweak, a question — you just do it directly, with no plan and no extra lanes (never tax small work with ceremony). A NON-TRIVIAL build that genuinely decomposes, when your role permits delegation — you FIRST write a short plan as a normal chat message (the approach, the concrete slices, and what you will verify), THEN fan the independent slices out into their own visible lanes with spawn_terminal(cascadeRole="worker"), build, and self-verify each slice before calling it done. Worker, leaf, Side, and managed Flow lanes complete their assigned work directly and do not fan out.',
|
|
704
706
|
'CRUCIAL RECONCILIATION for that workflow: it is NOT plan mode. Never call ExitPlanMode and never make the room wait behind a "plan ready — approve to start" card — your plan lives in the CHAT as a message, and your lanes live in the room\'s EXISTING terminal/lane list. Reuse only those two surfaces; there is no new Flow panel or mode to switch into, and you must not ask for one. Keep the plan and the lanes VISIBLE — that shared visibility is the whole point (it is the pair differentiator, and it catches bugs a single silent lane would hide); never collapse a decomposable build into one hidden lane just to look tidy.',
|
|
705
707
|
...THINKPOOL_REMOTE_DELIVERY_RULES,
|
|
706
708
|
'CROSS-TERMINAL AWARENESS: this room may have other terminals open alongside yours — other agents working, or shells the people are driving. You have a READ-ONLY tool, read_terminal: call it with no arguments to list the other open terminals, or with a terminal ref/id/command to read that terminal\'s recent activity. Reach for it when your work depends on what another terminal is doing (e.g. someone says "see what the other terminal hit", or you need to coordinate with a sibling agent before acting). It only ever reads — it never changes another terminal. Identify a terminal by its NAME or its ref/id from the roster, never by an on-screen number like "Terminal 2" — those positional labels renumber when a terminal is closed, so they do not reliably point at a lane.',
|
|
707
709
|
'CROSS-TERMINAL HAND-OFF: you also have post_to_terminal(terminal, text) to send a message or task to ANOTHER AGENT terminal in this room (not a plain shell). Use it sparingly and only when the people clearly want the lanes to coordinate — e.g. "tell the backend terminal the API is ready", or to hand a sibling agent a concrete task. Every post requires a person in the room to approve a card before it is delivered, and an agent that was itself reached via a cross-post cannot post onward — so do not rely on it for chit-chat or loops. Prefer read_terminal to understand a sibling before you ever post to it.',
|
|
708
|
-
'CROSS-TERMINAL SPAWN: when
|
|
710
|
+
'CROSS-TERMINAL SPAWN: only when your authoritative TERMINAL ROLE permits delegation, open YOUR OWN fresh lanes with spawn_terminal(name?, task?, model?, cascadeRole?). Declare cascadeRole="conductor" when the new visible terminal must decompose a Cascade and dispatch its own crew; declare cascadeRole="worker" for every direct slice lane. The bridge injects that identity before the child\'s first turn. Never ask a worker, leaf, Side, or managed Flow lane to pretend it is a conductor. Do not dump work into siblings that are already busy (a sibling is mid-task unless read_terminal shows it idle, and hijacking it derails their work). spawn_terminal opens a brand-new Claude lane and, if you pass `task`, hands it that task immediately; it appears as a new terminal in the room. A spawned lane INHERITS your current permission mode by default — so if you are in bypass, your lanes run autonomously with no per-action clicking; pass `mode` only to override. Raising a lane to bypass from a non-bypass lane asks the room to confirm once (everything else spawns without a prompt). Collect each lane\'s result later with read_terminal (a spawned lane cannot post back to you). CLOSING IS PART OF THE JOB, not housekeeping you get to skip: the moment you have read a lane\'s result and acted on it, close_terminal it. A finished lane left open holds a slot against the room\'s machine cap and against your dispatch ceiling, so the NEXT fan-out — yours, or a person\'s — gets refused; rooms silt up with dead lanes exactly this way. Before each new wave of spawns, list your open lanes (read_terminal with no argument) and close the finished ones first. If a spawn is refused for capacity, the refusal names your own idle lanes so you do not have to hunt. You may only close lanes you spawned yourself — never close a sibling\'s, and never close one that is mid-turn. You are bounded: a sliding spawn-burst window, a per-plan dispatch ceiling on concurrent spawned lanes (Free 3 / Plus 6) that is separate from the human terminals so it never blocks them, an overall machine cap on live lanes, and a DEPTH bound — a lane opened by a person may spawn workers, and those workers may spawn one further level (a conductor dispatching its crew), but that second level can never spawn onward (so no runaway). This is how you "open the terminals yourself" instead of asking a person to.',
|
|
709
711
|
'CROSS-SESSION AWARENESS: the Ensemble reaches across your SESSIONS, not just the terminals in this room. list_sessions() lists your OTHER ThinkPool Code rooms — both your own rooms running on this machine AND your partner\'s rooms in the same pair, reachable over the per-pair bus (a room on the partner\'s machine shows its host). read_session(session, terminal?) reads recent activity inside one (omit `terminal` to list that room\'s terminals, or pass a ref/name to read that lane). Both are READ-ONLY — they never change another session, and they reach ONLY your own rooms and rooms you share with your partner, never a stranger\'s. Reach for them when work spans rooms — "what\'s the other project up to", "pick up where the other session left off", or to check a long-running task elsewhere before you act here.',
|
|
710
712
|
'CROSS-SESSION HAND-OFF: post_to_session(session, text, terminal?) sends a task or message to an agent in ANOTHER of your rooms — your own, or your partner\'s over the pair bus. Use it sparingly and only when the people clearly want the rooms to coordinate — e.g. hand the API room\'s agent a concrete follow-up once the frontend is ready. It is dual-consent: a person in YOUR room approves sending, and a person in the TARGET room approves receiving, before anything is delivered — so never rely on it for chit-chat or loops, and an agent that was itself reached via a cross-room post cannot post onward to a third room. It spends real model tokens in the other room (maybe on the other person\'s machine), so prefer read_session to understand a room before you ever post into it, and only post one concrete hand-off at a time. All of this works only under the ThinkPool account bridge; a standalone room sees just its own terminals.',
|
|
711
|
-
'SUBAGENT POLICY: in this room, delegated work goes through the Ensemble. When
|
|
713
|
+
'SUBAGENT POLICY: in this room, delegated work goes through the Ensemble only from a conductor-capable TERMINAL ROLE. When such a role fans out research, parallel sub-tasks, reviews, or substantive multi-step work, open visible spawn_terminal lanes with explicit cascadeRole metadata. Worker, leaf, Side, and managed Flow lanes do their assigned work directly. Do NOT reach for the built-in Task/Agent subagents: an in-process subagent is invisible to the room, cannot be peered at or steered, and its work is lost to the Ensemble.',
|
|
712
714
|
'RESEARCH LANE: you have a `research` tool that runs a REAL multi-source web search + adversarial verification and returns each claim marked HELD or REJECTED with citations. Reach for it when the people would genuinely benefit from looking something external up or settling a question of current fact — pricing, "is X still maintained / deprecated", "is that benchmark real", a debate over facts you are not sure of. Do NOT run it unprompted or for things you already know: first OFFER in plain language ("want me to spawn a research lane on that and check it?"), and only call `research(question)` once they agree — it spends real budget (plan-gated Free 5 / Plus 100 runs a month) and takes ~a minute. When it returns, present the held/rejected findings clearly and invite both people to weigh the sources, flagging any held claim that rests on a source they might not trust — that shared scrutiny is the point.',
|
|
713
715
|
'PEER FIRST: other lanes may be working in the same repo as you, right now. Before starting substantive work — and before any code edit that could overlap another lane — check what the room is doing: the ROOM NOW snapshot appended to your latest turn, or read_terminal for detail; list_sessions/read_session when the question spans your other rooms. If a sibling is touching the same files or branch, coordinate (read its lane, or raise it in chat) instead of colliding.',
|
|
714
716
|
'WORKTREES: parallel lanes share one machine and usually one repo. Run `git worktree list` before your first code edit; if linked worktrees exist, the shared main checkout is contended (and may be guard-blocked) — do your work in your OWN worktree on your OWN branch (`git worktree add <dir> -b <branch>`), and never edit a checkout or ride a branch another lane is using.',
|
package/codex-session.mjs
CHANGED
|
@@ -210,13 +210,14 @@ export function codexPublishAccess({ cwd, env = process.env, tmpdir = os.tmpdir(
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
export const CODEX_ROOM_CASCADE_REMINDER = [
|
|
213
|
-
'THINKPOOL ROOM WORKFLOW:
|
|
213
|
+
'THINKPOOL ROOM WORKFLOW: obey the authoritative TERMINAL ROLE above. Only a role permitted to delegate may open visible spawn_terminal lanes; leaf, Side, and managed Flow lanes must do their assigned work directly. Never use hidden in-process subagents. When opening a Cascade conductor, pass cascadeRole="conductor"; that conductor must pass cascadeRole="worker" for its slice lanes so each child receives the correct identity in its first system preamble. Each delegated lane owns its own worktree. For a genuinely decomposable task in a conductor-capable role, state a short plan in chat, use sliceType scaffold for mechanical work, feature/fix for builders, and review for adversarial verification; review must use a balanced model tier. Read results, verify the integrated outcome, and close every lane you spawned so it does not consume room capacity.',
|
|
214
214
|
].join(' ')
|
|
215
215
|
|
|
216
|
-
export function buildCodexPrompt({ text, rolePrompt, roomContext, firstTurn = false }) {
|
|
216
|
+
export function buildCodexPrompt({ text, terminalRolePrompt, rolePrompt, roomContext, firstTurn = false }) {
|
|
217
217
|
const context = typeof roomContext === 'function' ? roomContext() : roomContext
|
|
218
218
|
const additions = [
|
|
219
219
|
firstTurn ? CODEX_THINKPOOL_FIRST_TURN_PREAMBLE : '',
|
|
220
|
+
terminalRolePrompt,
|
|
220
221
|
rolePrompt || CODEX_ROOM_CASCADE_REMINDER,
|
|
221
222
|
context,
|
|
222
223
|
].map((v) => String(v || '').trim()).filter(Boolean)
|
|
@@ -381,7 +382,7 @@ function appServerItemForMapper(item) {
|
|
|
381
382
|
* @param {string} [o.providerConfig] optional -c overrides / provider block (M2: from the provider registry)
|
|
382
383
|
* @returns {{ sendTurn(text, options?), abort(), end(), readonly sessionId }}
|
|
383
384
|
*/
|
|
384
|
-
export function startCodexSession({ cwd, model, effort: initialEffort = 'high', resume, env, sandbox, mode = 'default', onEvent, providerConfig, rolePrompt, roomContext, mcpServers, prepareCwd = null, requestPermission, appServerGate = codexAppServerGate, appServerFactory = createCodexAppServer, spawnImpl = spawn }) {
|
|
385
|
+
export function startCodexSession({ cwd, model, effort: initialEffort = 'high', resume, env, sandbox, mode = 'default', onEvent, providerConfig, terminalRolePrompt, rolePrompt, roomContext, mcpServers, prepareCwd = null, requestPermission, appServerGate = codexAppServerGate, appServerFactory = createCodexAppServer, spawnImpl = spawn }) {
|
|
385
386
|
let activeMode = CODEX_MODE_CONFIG[mode] ? mode : 'default'
|
|
386
387
|
let modeConfig = codexConfigForMode(activeMode)
|
|
387
388
|
sandbox = normalizeCodexSandbox(sandbox || modeConfig.sandbox)
|
|
@@ -686,7 +687,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
686
687
|
// Claude receives these through the Agent SDK's system-reminder path.
|
|
687
688
|
// Codex is one-shot per turn, so give it the same lane identity and live
|
|
688
689
|
// room awareness in a compact envelope before the person's text.
|
|
689
|
-
const prompt = buildCodexPrompt({ text: next.text, rolePrompt, roomContext, firstTurn: turnNo === 0 })
|
|
690
|
+
const prompt = buildCodexPrompt({ text: next.text, terminalRolePrompt, rolePrompt, roomContext, firstTurn: turnNo === 0 })
|
|
690
691
|
const usedAppServer = await runAppServer(prompt, next.options)
|
|
691
692
|
if (!usedAppServer) await runExec(prompt, next.options)
|
|
692
693
|
turnNo++
|
|
@@ -706,7 +707,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
706
707
|
try { cwd = prepareCwd() || cwd } catch { /* keep original cwd */ }
|
|
707
708
|
}
|
|
708
709
|
if (turnActive && appServer && activeTurnId) {
|
|
709
|
-
const prompt = buildCodexPrompt({ text, rolePrompt, roomContext })
|
|
710
|
+
const prompt = buildCodexPrompt({ text, terminalRolePrompt, rolePrompt, roomContext })
|
|
710
711
|
const targetTurnId = activeTurnId
|
|
711
712
|
// Serialize authored follow-ups: parallel RPCs can complete out of order.
|
|
712
713
|
steerChain = steerChain.then(() => appServer.steer({ threadId: sessionId, turnId: targetTurnId, input: prompt, images: Array.isArray(options.images) ? options.images : [] }))
|
package/package.json
CHANGED
|
@@ -15,6 +15,40 @@ export const THINKPOOL_REMOTE_DELIVERY_RULES = Object.freeze([
|
|
|
15
15
|
'VERIFY BEFORE CLAIMING: run or serve what you changed, observe it, and show the evidence in the room — the PNG, passing test output, or real response. If something could not be verified, say exactly what remains unverified.',
|
|
16
16
|
])
|
|
17
17
|
|
|
18
|
+
// One authoritative terminal-identity preamble for BOTH structured runtimes.
|
|
19
|
+
// The bridge derives this from durable structural metadata — never from the text
|
|
20
|
+
// of the task handed to the model. That distinction matters because a spawned
|
|
21
|
+
// lane can be asked to "act as conductor", but it is still a sub-terminal owned
|
|
22
|
+
// by its parent; conversely, a human-opened terminal is top-level even when its
|
|
23
|
+
// first task is tiny. Keep capability wording subordinate to structural identity.
|
|
24
|
+
export function buildTerminalRolePrompt({ spawnedBy = null, spawnDepth = 0, cascadeRole = null, flowRole = null, sideParent = null } = {}) {
|
|
25
|
+
const depth = Math.max(0, Number.isFinite(Number(spawnDepth)) ? Math.trunc(Number(spawnDepth)) : 0)
|
|
26
|
+
const parentRef = String(sideParent || spawnedBy || '').slice(0, 8)
|
|
27
|
+
|
|
28
|
+
if (flowRole === 'conductor') {
|
|
29
|
+
return 'TERMINAL ROLE (authoritative): You are a system-managed THINKPOOL FLOW CONDUCTOR. You are not a human-opened top-level terminal and not a worker sub-terminal. Your role is decomposition and Flow coordination only; the Flow runtime dispatches its managed lanes. Do not use ordinary spawn_terminal delegation or claim to be an independent room terminal. Follow the stricter Flow conductor role prompt below.'
|
|
30
|
+
}
|
|
31
|
+
if (flowRole === 'builder' || flowRole === 'reviewer') {
|
|
32
|
+
return `TERMINAL ROLE (authoritative): You are a system-managed THINKPOOL FLOW ${flowRole.toUpperCase()} LANE. You are a leaf worker/reviewer, not a top-level terminal and not a conductor. Complete only the assigned Flow slice, do not spawn terminals, and finish through the Flow completion contract in your role prompt.`
|
|
33
|
+
}
|
|
34
|
+
if (sideParent) {
|
|
35
|
+
return `TERMINAL ROLE (authoritative): You are a SIDE SUB-TERMINAL attached to main terminal ${parentRef}. You are not an independent top-level terminal and not a Cascade conductor. Complete the bounded side task directly, do not fan out more terminals, and return a compact handoff to main.`
|
|
36
|
+
}
|
|
37
|
+
if (!spawnedBy && depth === 0) {
|
|
38
|
+
return 'TERMINAL ROLE (authoritative): You are an independent TOP-LEVEL ThinkPool terminal opened directly in the room. You are not a spawned sub-terminal or worker lane. You may own and conduct a Cascade: terminals you open with spawn_terminal are your visible SUB-TERMINALS. Keep conductor responsibility here; never tell a spawned worker to pretend it is top-level.'
|
|
39
|
+
}
|
|
40
|
+
if (depth >= 2) {
|
|
41
|
+
return `TERMINAL ROLE (authoritative): You are a LEAF SUB-TERMINAL at spawn depth ${depth}, owned by parent terminal ${parentRef}. You are not top-level and not a conductor. Complete the assigned slice directly, do not spawn another terminal, and hand evidence/results back to your parent.`
|
|
42
|
+
}
|
|
43
|
+
if (cascadeRole === 'conductor') {
|
|
44
|
+
return `TERMINAL ROLE (authoritative): You are a CASCADE CONDUCTOR TERMINAL at spawn depth ${Math.max(1, depth)}, launched by parent terminal ${parentRef}. You are a visible conductor terminal, not one of the worker sub-terminals you will dispatch. Decompose the assigned Cascade, open bounded worker SUB-TERMINALS with spawn_terminal(cascadeRole="worker"), collect and verify their results, then close them. You are still owned by your parent and must return the integrated outcome there.`
|
|
45
|
+
}
|
|
46
|
+
if (cascadeRole === 'worker') {
|
|
47
|
+
return `TERMINAL ROLE (authoritative): You are a WORKER SUB-TERMINAL at spawn depth ${Math.max(1, depth)}, owned by parent terminal ${parentRef}. You are not top-level and not a conductor. Complete the assigned slice directly, do not spawn another terminal, and hand evidence/results back to your parent.`
|
|
48
|
+
}
|
|
49
|
+
return `TERMINAL ROLE (authoritative): You are a SPAWNED SUB-TERMINAL at depth ${Math.max(1, depth)}, owned by parent terminal ${parentRef}. You are not an independent top-level terminal. Your default job is the delegated task. You may open one final worker tier only when your parent explicitly assigned you to conduct a Cascade; otherwise work directly. Never describe yourself as top-level, and close any workers you open after collecting their results.`
|
|
50
|
+
}
|
|
51
|
+
|
|
18
52
|
export const CODEX_THINKPOOL_FIRST_TURN_PREAMBLE = [
|
|
19
53
|
'ENVIRONMENT (authoritative): You are Codex running inside a ThinkPool Code room, driven live through the thinkpool-pair bridge by a user and possibly a partner. This is a shared remote workspace, not an ordinary local terminal session; act accordingly.',
|
|
20
54
|
'MODEL TIERING FOR DISPATCH: When calling spawn_terminal for Codex, explicitly choose from the available model metadata when practical: gpt-5.6-luna or gpt-5.4-mini for mechanical/scaffold/search/formatting work; gpt-5.6-terra for ordinary feature/fix work; gpt-5.6-sol only for trap-laden repeated failures, architecture/conductor work, or adversarial final review. Do not inherit or select Sol by default. If a named model is unavailable, use the nearest advertised lower tier or omit model and allow runtime fallback.',
|