thinkpool-pair 0.7.197 → 0.7.199

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/account.mjs CHANGED
@@ -39,25 +39,26 @@ function pidAlive(pid) {
39
39
  try { process.kill(pid, 0); return true } catch (e) { return e.code === 'EPERM' }
40
40
  }
41
41
 
42
- // Returns true if we now hold the lock, false if another LIVE supervisor holds it.
43
- // A stale lock (holder dead / garbage / ours) is taken over. fs errors fail-OPEN
44
- // (return true) — a lock we can't manage must never block the real bridge.
42
+ // Returns true only if we demonstrably hold the lock. A stale lock (holder dead /
43
+ // garbage / ours) is taken over. Filesystem errors fail CLOSED: serving without a
44
+ // lock can start a second bridge, and duplicate bridges can race terminal reconcile
45
+ // and delete room history. A temporarily unmanageable lock is safer than that.
45
46
  export function acquireSingleton(lockPath = LOCK) {
46
- try { fs.mkdirSync(path.dirname(lockPath), { recursive: true }) } catch { /* noop */ }
47
+ try { fs.mkdirSync(path.dirname(lockPath), { recursive: true }) } catch { return false }
47
48
  for (let i = 0; i < 3; i++) {
48
49
  try {
49
50
  const fd = fs.openSync(lockPath, 'wx') // atomic create-exclusive
50
51
  fs.writeSync(fd, String(process.pid)); fs.closeSync(fd)
51
52
  return true
52
53
  } catch (e) {
53
- if (e.code !== 'EEXIST') return true // can't create for another reason → fail open
54
+ if (e.code !== 'EEXIST') return false // cannot prove ownership → refuse to serve
54
55
  let pid = 0
55
- try { pid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10) || 0 } catch { /* unreadable */ }
56
+ try { pid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10) || 0 } catch { return false }
56
57
  if (pidAlive(pid)) return false // a live supervisor owns it → refuse
57
- try { fs.rmSync(lockPath, { force: true }) } catch { return true } // stale → drop + retry
58
+ try { fs.rmSync(lockPath, { force: true }) } catch { return false } // stale but unmanageable
58
59
  }
59
60
  }
60
- return true
61
+ return false
61
62
  }
62
63
 
63
64
  // Release only if WE own it (don't unlink a lock another instance took over).
package/bridge.mjs CHANGED
@@ -1053,7 +1053,7 @@ const announce = () => {
1053
1053
  // turnActive). Authoritative — the bridge owns the turn — so the /code
1054
1054
  // dashboard reads "N agents working" off the roster instead of sniffing
1055
1055
  // the live code-event stream (which a passive dashboard channel misses).
1056
- ...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, busy: s.session?.turnActive ?? false, commands: s.commands, mode: s.mode || undefined, name: termNames[id] || undefined, model: s.model || undefined, ...(s.runtime === 'codex' ? { approvalPolicy: codexConfigForMode(s.mode).approvalPolicy, models: s.models || [] } : {}), ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
1056
+ ...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, busy: s.session?.turnActive ?? false, 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 || [] } : {}), ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
1057
1057
  // provider: the registered LLM-provider this lane runs on, NAME-ONLY {id,name}
1058
1058
  // (NEVER the key or baseUrl). Additive; older clients ignore it. Omitted for the
1059
1059
  // built-in/default Claude path (no badge). Makes the lane's provider badge +
@@ -1546,7 +1546,7 @@ function worktreeSnapshot(cwd) {
1546
1546
  // relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
1547
1547
  // persist to the host file; tool calls round-trip through the perm card; the
1548
1548
  // rolling log replays to joiners and survives bridge restarts (session-store).
1549
- function openStructured({ id, runtime = 'claude', model, resume, log, commands, mode, spawnedBy, rolePrompt, flowSessionId, flowTaskKey, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage }) {
1549
+ function openStructured({ id, runtime = 'claude', model, effort, resume, log, commands, mode, spawnedBy, rolePrompt, flowSessionId, flowTaskKey, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage }) {
1550
1550
  if (sessions.has(id)) return
1551
1551
  runtime = runtime === 'codex' ? 'codex' : 'claude'
1552
1552
  mode = STRUCTURED_MODES.has(mode) ? mode : 'default'
@@ -1563,7 +1563,8 @@ function openStructured({ id, runtime = 'claude', model, resume, log, commands,
1563
1563
  // spawnedBy: set when this lane was Dispatched (spawn_terminal). Restored from the
1564
1564
  // session store so the Ensemble flag survives a bridge restart (else a respin
1565
1565
  // stripped it and the lane reverted to a plain tab — the t6 "no chip" bug).
1566
- 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, models: runtime === 'codex' ? readCodexModels() : undefined,
1566
+ effort = new Set(['low', 'medium', 'high', 'xhigh', 'max']).has(effort) ? effort : 'high'
1567
+ 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' ? readCodexModels() : undefined,
1567
1568
  // model: truthful active-model label — now the SAME `laneModel` the SDK is given, so the
1568
1569
  // chip cannot disagree with the wire. When this lane runs on a custom (non-anthropic)
1569
1570
  // registered provider the SDK id is impersonated (see the onEvent guard below), so
@@ -1767,7 +1768,7 @@ function openStructured({ id, runtime = 'claude', model, resume, log, commands,
1767
1768
  // restart. Without this, sessionData omitted it → on restart the resumed session
1768
1769
  // re-launched on the host default (Opus) regardless of the last switch, and the
1769
1770
  // switch looked like it "never changed the model" (Max 2026-07-02). Restored below.
1770
- const sessionData = () => ({ sessionId: entry.session?.sessionId || resume || null, runtime: entry.runtime, log: entry.log, commands: entry.commands, mode: entry.mode, 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 })
1771
+ const sessionData = () => ({ sessionId: entry.session?.sessionId || resume || 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 })
1771
1772
  const persist = () => saveSession(room, id, sessionData())
1772
1773
  // Synchronous flush of this session's record. Used on open (so a brand-new session
1773
1774
  // has a file under its id BEFORE its first event — surviving a restart inside the
@@ -2114,7 +2115,7 @@ function openStructured({ id, runtime = 'claude', model, resume, log, commands,
2114
2115
  // laneModel, NOT the raw `model` param: the SDK's `model` option OVERRIDES the
2115
2116
  // ANTHROPIC_MODEL supplied by resolveProviderEnv() in `env` below, so an inherited
2116
2117
  // Claude id here defeats the whole per-provider env. undefined → option omitted.
2117
- cwd: cwd || process.cwd(), model: laneModel, resume, mode, rolePrompt,
2118
+ cwd: cwd || process.cwd(), model: laneModel, effort, resume, mode, rolePrompt,
2118
2119
  // Idle ordinary tabs create nothing. Immediately before the FIRST real
2119
2120
  // turn (human prompt, dispatch, or cascade task), the runtime calls this
2120
2121
  // while still cold; subsequent turns reuse the persisted worktree.
@@ -3006,6 +3007,23 @@ channel
3006
3007
  else if (payload.mode === 'acceptEdits') acceptEditsPending(s)
3007
3008
  }
3008
3009
  })
3010
+ .on('broadcast', { event: 'code-effort' }, ({ payload }) => {
3011
+ const s = payload?.term && sessions.get(payload.term)
3012
+ const valid = new Set(['low', 'medium', 'high', 'xhigh', 'max'])
3013
+ if (!s || !valid.has(payload.level)) return
3014
+ if (s.session?.turnActive) {
3015
+ const evt = { kind: 'control', text: 'finish or stop the current turn before switching thinking power' }
3016
+ pushLog(s, evt)
3017
+ bcast('code-event', { term: payload.term, evt })
3018
+ return
3019
+ }
3020
+ Promise.resolve(s.session.setEffort?.(payload.level)).then((ok) => {
3021
+ if (ok === false) return
3022
+ s.effort = payload.level
3023
+ s.flush?.()
3024
+ announce()
3025
+ }).catch(() => {})
3026
+ })
3009
3027
  .on('broadcast', { event: 'code-close' }, ({ payload }) => {
3010
3028
  endStructured(payload?.id)
3011
3029
  })
@@ -3165,7 +3183,7 @@ channel
3165
3183
  // FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
3166
3184
  // bridge restart: the conductor keeps its subagent-block + plan interception, and
3167
3185
  // lanes keep their worktree cwd + the ability to mark done.
3168
- openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, 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,
3186
+ 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,
3169
3187
  // Lazy-boot restored terminals that were IDLE + not part of a flow: their transcript
3170
3188
  // shows immediately; the query boots on first turn. Mid-turn + flow terminals boot now
3171
3189
  // (mid-turn needs auto-resume; flow needs its lane live).
@@ -205,7 +205,7 @@ const TP_ROOM_REMINDER = [
205
205
  'Verify before claiming done — show runtime evidence you produced, not "should work, go test it".',
206
206
  ].join(' ')
207
207
 
208
- export function startClaudeSession({ cwd, model, 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 }) {
208
+ 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 }) {
209
209
  // Per-turn reminder + live ROOM NOW tail. roomContext (bridge-supplied) returns the
210
210
  // room's CURRENT state — sibling lanes, active git worktrees — or null. The static
211
211
  // rules keep the agent aware of the room's FEATURES; the live tail keeps it aware of
@@ -251,7 +251,8 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
251
251
  let mode = MODES.has(initialMode) ? initialMode : 'default' // mirrors ⇧⇥ cycle
252
252
  const alwaysAllow = new Set() // tool:risk signatures the user chose "don't ask again" for
253
253
  const toolStart = new Map() // tool_use id → start time, for the duration badge
254
- let effort = null // active reasoning effort for the turn (from the hook input)
254
+ const effortLevels = new Set(['low', 'medium', 'high', 'xhigh', 'max'])
255
+ let effort = effortLevels.has(initialEffort) ? initialEffort : 'high'
255
256
  // Live token count for the thinking indicator — mirrors Claude Code's
256
257
  // "↓ N tokens": the turn's billed OUTPUT tokens (thinking is billed as
257
258
  // output, so this is the full count, not just the reasoning estimate). A
@@ -736,6 +737,7 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
736
737
  if (mcpServers && Object.keys(mcpServers).length) opts.mcpServers = { ...(opts.mcpServers || {}), ...mcpServers }
737
738
  if (cwd) opts.cwd = cwd
738
739
  if (model) opts.model = model
740
+ opts.effort = effort
739
741
  // NB: resume is NOT baked into opts — runQuery sets it per-run from the live `sessionId`
740
742
  // (seeded from `resume`). That makes the resume target controllable: a re-create can point
741
743
  // it at persistedSessionId, and the "No conversation found" recovery can DROP it (clearing
@@ -1197,6 +1199,16 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
1197
1199
  if (turnActive) { pendingSwitch = true; return }
1198
1200
  await recreateForSwitch()
1199
1201
  },
1202
+ async setEffort(level) {
1203
+ if (!effortLevels.has(level) || closed) return false
1204
+ effort = level
1205
+ opts.effort = level
1206
+ emit({ kind: 'effort', level })
1207
+ if (!started) return true
1208
+ if (turnActive) { pendingSwitch = true; return true }
1209
+ await recreateForSwitch()
1210
+ return true
1211
+ },
1200
1212
  async listModels() {
1201
1213
  try {
1202
1214
  const ms = await q?.supportedModels?.()
package/codex-session.mjs CHANGED
@@ -32,6 +32,7 @@ import { startCodexMcpHttp } from './codex-mcp-http.mjs'
32
32
 
33
33
  const DEFAULT_SANDBOX = 'workspace-write'
34
34
  const SAFE_SANDBOXES = new Set(['read-only', 'workspace-write', 'danger-full-access'])
35
+ export const EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max'])
35
36
  export const CODEX_MODE_CONFIG = {
36
37
  default: { sandbox: 'workspace-write', approvalPolicy: 'untrusted' },
37
38
  acceptEdits: { sandbox: 'workspace-write', approvalPolicy: 'on-request' },
@@ -96,7 +97,7 @@ export function buildCodexPrompt({ text, rolePrompt, roomContext }) {
96
97
  return `<thinkpool-context>\n${additions.join('\n\n')}\n</thinkpool-context>\n\n${String(text ?? '')}`
97
98
  }
98
99
 
99
- export function buildCodexExecArgs({ sessionId, model, sandbox, approvalPolicy = 'untrusted', providerConfig, mcpUrl, writableDirs = [], prompt }) {
100
+ export function buildCodexExecArgs({ sessionId, model, effort, sandbox, approvalPolicy = 'untrusted', providerConfig, mcpUrl, writableDirs = [], prompt }) {
100
101
  const safeSandbox = normalizeCodexSandbox(sandbox)
101
102
  const args = sessionId
102
103
  // `codex exec resume` deliberately has no --sandbox option. The resumed
@@ -106,6 +107,7 @@ export function buildCodexExecArgs({ sessionId, model, sandbox, approvalPolicy =
106
107
  : ['exec', '--json', '--sandbox', safeSandbox, '--skip-git-repo-check']
107
108
  if (model) args.push('-m', model)
108
109
  args.push('-c', `approval_policy="${approvalPolicy}"`)
110
+ if (EFFORT_LEVELS.has(effort)) args.push('-c', `model_reasoning_effort="${effort}"`)
109
111
  if (providerConfig) args.push('-c', providerConfig)
110
112
  // `resume` retains the roots selected by the fresh rollout and does not
111
113
  // accept --add-dir itself. Supply these only when creating the thread.
@@ -129,7 +131,7 @@ export function buildCodexExecArgs({ sessionId, model, sandbox, approvalPolicy =
129
131
  * @param {string} [o.providerConfig] optional -c overrides / provider block (M2: from the provider registry)
130
132
  * @returns {{ sendTurn(text), abort(), end(), readonly sessionId }}
131
133
  */
132
- export function startCodexSession({ cwd, model, resume, env, sandbox, mode = 'default', onEvent, providerConfig, rolePrompt, roomContext, mcpServers, prepareCwd = null }) {
134
+ export function startCodexSession({ cwd, model, effort: initialEffort = 'high', resume, env, sandbox, mode = 'default', onEvent, providerConfig, rolePrompt, roomContext, mcpServers, prepareCwd = null }) {
133
135
  let activeMode = CODEX_MODE_CONFIG[mode] ? mode : 'default'
134
136
  let modeConfig = codexConfigForMode(activeMode)
135
137
  sandbox = normalizeCodexSandbox(sandbox || modeConfig.sandbox)
@@ -142,6 +144,7 @@ export function startCodexSession({ cwd, model, resume, env, sandbox, mode = 'de
142
144
  let ended = false
143
145
  let turnActive = false
144
146
  let activeModel = model || null
147
+ let activeEffort = EFFORT_LEVELS.has(initialEffort) ? initialEffort : 'high'
145
148
  let mcpHttpPromise = null
146
149
  // serialize turns: one exec at a time
147
150
  let chain = Promise.resolve()
@@ -168,6 +171,7 @@ export function startCodexSession({ cwd, model, resume, env, sandbox, mode = 'de
168
171
  const args = buildCodexExecArgs({
169
172
  sessionId: turnNo === 0 ? null : sessionId,
170
173
  model: activeModel,
174
+ effort: activeEffort,
171
175
  sandbox,
172
176
  approvalPolicy: modeConfig.approvalPolicy,
173
177
  providerConfig,
@@ -286,6 +290,12 @@ export function startCodexSession({ cwd, model, resume, env, sandbox, mode = 'de
286
290
  turnNo = 0
287
291
  return true
288
292
  },
293
+ setEffort(nextEffort) {
294
+ if (turnActive || !EFFORT_LEVELS.has(nextEffort)) return false
295
+ activeEffort = nextEffort
296
+ try { onEvent?.({ kind: 'effort', level: activeEffort }) } catch { /* noop */ }
297
+ return true
298
+ },
289
299
  clearContext() {
290
300
  if (turnActive) return false
291
301
  sessionId = null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.197",
3
+ "version": "0.7.199",
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": {