thinkpool-pair 0.7.198 → 0.7.200
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 +2 -1
- package/agent-detect.mjs +28 -0
- package/bridge.mjs +26 -19
- package/claude-session.mjs +14 -2
- package/codex-event-mapper.mjs +43 -7
- package/codex-session.mjs +12 -2
- package/package.json +2 -1
package/account.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { publicKeyB64, announceProviders, listProviders, addProvider, addProvide
|
|
|
20
20
|
import { supervisorServes } from './serve-consent.mjs'
|
|
21
21
|
import { resolveServeDir } from './serve-dir.mjs'
|
|
22
22
|
import { pairKeyFor, pairTopic, CROSSROOM_BUS } from './cross-terminal.mjs'
|
|
23
|
+
import { installedAgentCommands } from './agent-detect.mjs'
|
|
23
24
|
|
|
24
25
|
const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return null } })()
|
|
25
26
|
|
|
@@ -557,7 +558,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
557
558
|
// to spawn against — re-read each announce so an add/remove reflects immediately.
|
|
558
559
|
// Both are additive; older clients ignore them (feature: multi-provider BYOK, slice 1).
|
|
559
560
|
const PROVIDER_PUBKEY = publicKeyB64()
|
|
560
|
-
const pushPresence = () => trackPresence({ name: machine, version: VERSION, rooms: [...children.keys()], refused: [...refused].map(([code, reason]) => ({ code, reason })), service: runsAsService(), providerPubKey: PROVIDER_PUBKEY, providers: announceProviders(), ts: Date.now() })
|
|
561
|
+
const pushPresence = () => trackPresence({ name: machine, version: VERSION, rooms: [...children.keys()], refused: [...refused].map(([code, reason]) => ({ code, reason })), service: runsAsService(), providerPubKey: PROVIDER_PUBKEY, providers: announceProviders(), agents: installedAgentCommands().map((cmd) => ({ cmd })), ts: Date.now() })
|
|
561
562
|
|
|
562
563
|
// ── Provider registry — the multi-BYOK wire contract (slice 1). ─────────────
|
|
563
564
|
// AUTH: the account channel `tpacct:<uid>` is created WITHOUT config.private:true,
|
package/agent-detect.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// One installed-agent detector shared by the room bridge and account supervisor.
|
|
2
|
+
// Both surfaces announce the same machine; they must not disagree about which
|
|
3
|
+
// coding CLIs it can open merely because they publish on different channels.
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
export const KNOWN_AGENT_COMMANDS = [
|
|
8
|
+
'claude', 'codex', 'gemini', 'aider', 'cursor-agent',
|
|
9
|
+
'opencode', 'copilot', 'goose', 'crush', 'qwen',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export function commandOnPath(command, { env = process.env, platform = process.platform, fsImpl = fs } = {}) {
|
|
13
|
+
const exts = platform === 'win32'
|
|
14
|
+
? (env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
15
|
+
: ['']
|
|
16
|
+
const delimiter = platform === 'win32' ? ';' : path.delimiter
|
|
17
|
+
for (const dir of (env.PATH || '').split(delimiter)) {
|
|
18
|
+
if (!dir) continue
|
|
19
|
+
for (const ext of exts) {
|
|
20
|
+
try { if (fsImpl.existsSync(path.join(dir, command + ext))) return true } catch { /* unreadable path entry */ }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function installedAgentCommands(options) {
|
|
27
|
+
return KNOWN_AGENT_COMMANDS.filter((command) => commandOnPath(command, options))
|
|
28
|
+
}
|
package/bridge.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import { z } from 'zod'
|
|
|
52
52
|
import { startClaudeSession } from './claude-session.mjs'
|
|
53
53
|
import { startCodexSession, readCodexDefaultModel, readCodexModels, codexConfigForMode } from './codex-session.mjs'
|
|
54
54
|
import { createManagedLaneWorktree, removeManagedLaneWorktree } from './lane-worktree.mjs'
|
|
55
|
+
import { commandOnPath } from './agent-detect.mjs'
|
|
55
56
|
|
|
56
57
|
const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
|
|
57
58
|
import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
|
|
@@ -178,19 +179,7 @@ function claudeHasSession() {
|
|
|
178
179
|
} catch { return false }
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
const onPath = (c) => {
|
|
183
|
-
const exts = process.platform === 'win32'
|
|
184
|
-
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
185
|
-
: ['']
|
|
186
|
-
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
187
|
-
if (!dir) continue
|
|
188
|
-
for (const ext of exts) {
|
|
189
|
-
try { if (fs.existsSync(path.join(dir, c + ext))) return true } catch { /* noop */ }
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return false
|
|
193
|
-
}
|
|
182
|
+
const onPath = (command) => commandOnPath(command)
|
|
194
183
|
|
|
195
184
|
// Pick which installed agent to share → resolves to a command string. A single
|
|
196
185
|
// match (or non-TTY stdin, e.g. piped) auto-selects with no prompt.
|
|
@@ -1053,7 +1042,7 @@ const announce = () => {
|
|
|
1053
1042
|
// turnActive). Authoritative — the bridge owns the turn — so the /code
|
|
1054
1043
|
// dashboard reads "N agents working" off the roster instead of sniffing
|
|
1055
1044
|
// 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' } : {}),
|
|
1045
|
+
...[...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
1046
|
// provider: the registered LLM-provider this lane runs on, NAME-ONLY {id,name}
|
|
1058
1047
|
// (NEVER the key or baseUrl). Additive; older clients ignore it. Omitted for the
|
|
1059
1048
|
// built-in/default Claude path (no badge). Makes the lane's provider badge +
|
|
@@ -1546,7 +1535,7 @@ function worktreeSnapshot(cwd) {
|
|
|
1546
1535
|
// relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
|
|
1547
1536
|
// persist to the host file; tool calls round-trip through the perm card; the
|
|
1548
1537
|
// 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 }) {
|
|
1538
|
+
function openStructured({ id, runtime = 'claude', model, effort, resume, log, commands, mode, spawnedBy, rolePrompt, flowSessionId, flowTaskKey, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage }) {
|
|
1550
1539
|
if (sessions.has(id)) return
|
|
1551
1540
|
runtime = runtime === 'codex' ? 'codex' : 'claude'
|
|
1552
1541
|
mode = STRUCTURED_MODES.has(mode) ? mode : 'default'
|
|
@@ -1563,7 +1552,8 @@ function openStructured({ id, runtime = 'claude', model, resume, log, commands,
|
|
|
1563
1552
|
// spawnedBy: set when this lane was Dispatched (spawn_terminal). Restored from the
|
|
1564
1553
|
// session store so the Ensemble flag survives a bridge restart (else a respin
|
|
1565
1554
|
// stripped it and the lane reverted to a plain tab — the t6 "no chip" bug).
|
|
1566
|
-
|
|
1555
|
+
effort = new Set(['low', 'medium', 'high', 'xhigh', 'max']).has(effort) ? effort : 'high'
|
|
1556
|
+
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
1557
|
// model: truthful active-model label — now the SAME `laneModel` the SDK is given, so the
|
|
1568
1558
|
// chip cannot disagree with the wire. When this lane runs on a custom (non-anthropic)
|
|
1569
1559
|
// registered provider the SDK id is impersonated (see the onEvent guard below), so
|
|
@@ -1767,7 +1757,7 @@ function openStructured({ id, runtime = 'claude', model, resume, log, commands,
|
|
|
1767
1757
|
// restart. Without this, sessionData omitted it → on restart the resumed session
|
|
1768
1758
|
// re-launched on the host default (Opus) regardless of the last switch, and the
|
|
1769
1759
|
// 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 })
|
|
1760
|
+
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
1761
|
const persist = () => saveSession(room, id, sessionData())
|
|
1772
1762
|
// Synchronous flush of this session's record. Used on open (so a brand-new session
|
|
1773
1763
|
// has a file under its id BEFORE its first event — surviving a restart inside the
|
|
@@ -2114,7 +2104,7 @@ function openStructured({ id, runtime = 'claude', model, resume, log, commands,
|
|
|
2114
2104
|
// laneModel, NOT the raw `model` param: the SDK's `model` option OVERRIDES the
|
|
2115
2105
|
// ANTHROPIC_MODEL supplied by resolveProviderEnv() in `env` below, so an inherited
|
|
2116
2106
|
// Claude id here defeats the whole per-provider env. undefined → option omitted.
|
|
2117
|
-
cwd: cwd || process.cwd(), model: laneModel, resume, mode, rolePrompt,
|
|
2107
|
+
cwd: cwd || process.cwd(), model: laneModel, effort, resume, mode, rolePrompt,
|
|
2118
2108
|
// Idle ordinary tabs create nothing. Immediately before the FIRST real
|
|
2119
2109
|
// turn (human prompt, dispatch, or cascade task), the runtime calls this
|
|
2120
2110
|
// while still cold; subsequent turns reuse the persisted worktree.
|
|
@@ -3006,6 +2996,23 @@ channel
|
|
|
3006
2996
|
else if (payload.mode === 'acceptEdits') acceptEditsPending(s)
|
|
3007
2997
|
}
|
|
3008
2998
|
})
|
|
2999
|
+
.on('broadcast', { event: 'code-effort' }, ({ payload }) => {
|
|
3000
|
+
const s = payload?.term && sessions.get(payload.term)
|
|
3001
|
+
const valid = new Set(['low', 'medium', 'high', 'xhigh', 'max'])
|
|
3002
|
+
if (!s || !valid.has(payload.level)) return
|
|
3003
|
+
if (s.session?.turnActive) {
|
|
3004
|
+
const evt = { kind: 'control', text: 'finish or stop the current turn before switching thinking power' }
|
|
3005
|
+
pushLog(s, evt)
|
|
3006
|
+
bcast('code-event', { term: payload.term, evt })
|
|
3007
|
+
return
|
|
3008
|
+
}
|
|
3009
|
+
Promise.resolve(s.session.setEffort?.(payload.level)).then((ok) => {
|
|
3010
|
+
if (ok === false) return
|
|
3011
|
+
s.effort = payload.level
|
|
3012
|
+
s.flush?.()
|
|
3013
|
+
announce()
|
|
3014
|
+
}).catch(() => {})
|
|
3015
|
+
})
|
|
3009
3016
|
.on('broadcast', { event: 'code-close' }, ({ payload }) => {
|
|
3010
3017
|
endStructured(payload?.id)
|
|
3011
3018
|
})
|
|
@@ -3165,7 +3172,7 @@ channel
|
|
|
3165
3172
|
// FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
|
|
3166
3173
|
// bridge restart: the conductor keeps its subagent-block + plan interception, and
|
|
3167
3174
|
// 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,
|
|
3175
|
+
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
3176
|
// Lazy-boot restored terminals that were IDLE + not part of a flow: their transcript
|
|
3170
3177
|
// shows immediately; the query boots on first turn. Mid-turn + flow terminals boot now
|
|
3171
3178
|
// (mid-turn needs auto-resume; flow needs its lane live).
|
package/claude-session.mjs
CHANGED
|
@@ -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
|
-
|
|
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-event-mapper.mjs
CHANGED
|
@@ -28,16 +28,27 @@
|
|
|
28
28
|
// the existing risk classifier (claude-session.classifyRisk) and room rendering
|
|
29
29
|
// apply unchanged. Codex v1 is observe-only (no per-tool card) — classification
|
|
30
30
|
// is for display accent + future gating, not blocking (spec §tradeoff).
|
|
31
|
+
import fs from 'node:fs'
|
|
32
|
+
|
|
33
|
+
const safeMcpPart = (value) => String(value || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
34
|
+
const mcpToolName = (item) => `mcp__${safeMcpPart(item?.server)}__${safeMcpPart(item?.tool)}`
|
|
35
|
+
const resultText = (value) => {
|
|
36
|
+
if (typeof value === 'string') return value
|
|
37
|
+
if (value == null) return ''
|
|
38
|
+
try { return JSON.stringify(value) } catch { return String(value) }
|
|
39
|
+
}
|
|
31
40
|
|
|
32
41
|
export class CodexEventMapper {
|
|
33
42
|
/**
|
|
34
43
|
* @param {{onEvent:(evt)=>void, model?:string|null, sessionId?:string|null}} opts
|
|
35
44
|
*/
|
|
36
|
-
constructor({ onEvent, model = null, sessionId = null }) {
|
|
45
|
+
constructor({ onEvent, model = null, sessionId = null, fsImpl = fs }) {
|
|
37
46
|
this._onEvent = onEvent
|
|
38
47
|
this._model = model || null
|
|
39
48
|
this._sessionId = sessionId || null
|
|
40
49
|
this._starts = new Map() // item.id → startMs (for durationMs)
|
|
50
|
+
this._fileBefore = new Map() // item.id → [{ path, kind, content }]
|
|
51
|
+
this._fs = fsImpl
|
|
41
52
|
this._systemSent = false
|
|
42
53
|
}
|
|
43
54
|
|
|
@@ -68,10 +79,18 @@ export class CodexEventMapper {
|
|
|
68
79
|
if (it.type === 'command_execution') {
|
|
69
80
|
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: 'Bash', input: { command: it.command || '' } }], parentToolUseId: null })
|
|
70
81
|
} else if (it.type === 'file_change') {
|
|
71
|
-
const
|
|
72
|
-
|
|
82
|
+
const before = (it.changes || []).map((c) => {
|
|
83
|
+
let content = ''
|
|
84
|
+
if (c.kind !== 'add' && c.path) {
|
|
85
|
+
try { content = this._fs.readFileSync(c.path, 'utf8') } catch { /* missing/unreadable → empty */ }
|
|
86
|
+
}
|
|
87
|
+
return { path: c.path || '', kind: c.kind || 'update', content }
|
|
88
|
+
})
|
|
89
|
+
this._fileBefore.set(it.id, before)
|
|
73
90
|
} else if (it.type === 'web_search') {
|
|
74
91
|
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: 'WebSearch', input: { query: it.query || '' } }], parentToolUseId: null })
|
|
92
|
+
} else if (it.type === 'mcp_tool_call') {
|
|
93
|
+
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: mcpToolName(it), input: it.arguments || {} }], parentToolUseId: null })
|
|
75
94
|
}
|
|
76
95
|
// agent_message / reasoning have no item.started (burst on completion).
|
|
77
96
|
return
|
|
@@ -92,13 +111,30 @@ export class CodexEventMapper {
|
|
|
92
111
|
const bad = it.exit_code != null ? it.exit_code !== 0 : false
|
|
93
112
|
this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: it.aggregated_output || '' }], isError: bad, durationMs, parentToolUseId: null })
|
|
94
113
|
} else if (it.type === 'file_change') {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
const before = this._fileBefore.get(it.id) || (it.changes || []).map((c) => ({ path: c.path || '', kind: c.kind || 'update', content: '' }))
|
|
115
|
+
this._fileBefore.delete(it.id)
|
|
116
|
+
const edits = before.map((c) => {
|
|
117
|
+
let next = ''
|
|
118
|
+
if (c.kind !== 'delete' && c.path) {
|
|
119
|
+
try { next = this._fs.readFileSync(c.path, 'utf8') } catch { /* deleted/unreadable → empty */ }
|
|
120
|
+
}
|
|
121
|
+
return { file_path: c.path, path: c.path, old_string: c.content, new_string: next, kind: c.kind }
|
|
122
|
+
})
|
|
123
|
+
const first = edits[0] || { file_path: '', path: '', old_string: '', new_string: '', kind: 'update' }
|
|
124
|
+
const name = edits.length > 1 ? 'MultiEdit' : first.kind === 'add' ? 'Write' : 'Edit'
|
|
125
|
+
const input = name === 'MultiEdit'
|
|
126
|
+
? { edits }
|
|
127
|
+
: name === 'Write'
|
|
128
|
+
? { file_path: first.file_path, path: first.path, content: first.new_string }
|
|
129
|
+
: first
|
|
130
|
+
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name, input }], parentToolUseId: null })
|
|
131
|
+
this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: `${name === 'Write' ? 'wrote' : 'edited'} ${first.path}` }], isError: false, durationMs, parentToolUseId: null })
|
|
98
132
|
} else if (it.type === 'web_search') {
|
|
99
133
|
this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: it.query || 'search completed' }], isError: false, durationMs, parentToolUseId: null })
|
|
134
|
+
} else if (it.type === 'mcp_tool_call') {
|
|
135
|
+
this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: it.error?.message || resultText(it.result) }], isError: it.status === 'failed' || !!it.error, durationMs, parentToolUseId: null })
|
|
100
136
|
}
|
|
101
|
-
//
|
|
137
|
+
// plan_update remains an additive future event shape. Unknown
|
|
102
138
|
// items are ignored rather than risking a malformed transcript row.
|
|
103
139
|
return
|
|
104
140
|
}
|
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.
|
|
3
|
+
"version": "0.7.200",
|
|
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": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"turn-stall.mjs",
|
|
21
21
|
"update-gate.mjs",
|
|
22
22
|
"agent-notify.mjs",
|
|
23
|
+
"agent-detect.mjs",
|
|
23
24
|
"event-id.mjs",
|
|
24
25
|
"plan-meters.mjs",
|
|
25
26
|
"recap.mjs",
|