thinkpool-pair 0.7.204 → 0.7.205
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 +37 -9
- package/codex-event-mapper.mjs +16 -4
- package/codex-session.mjs +18 -1
- package/package.json +1 -1
- package/recap.mjs +4 -0
package/bridge.mjs
CHANGED
|
@@ -1759,7 +1759,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
1759
1759
|
// restart. Without this, sessionData omitted it → on restart the resumed session
|
|
1760
1760
|
// re-launched on the host default (Opus) regardless of the last switch, and the
|
|
1761
1761
|
// switch looked like it "never changed the model" (Max 2026-07-02). Restored below.
|
|
1762
|
-
const sessionData = () => ({ sessionId: entry.session?.sessionId ||
|
|
1762
|
+
const sessionData = () => ({ sessionId: entry.session?.sessionId || null, runtime: entry.runtime, log: entry.log, commands: entry.commands, mode: entry.mode, effort: entry.effort, model: entry.model || null, provider: entry.provider || null, spawnedBy: entry.spawnedBy, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, cwd: entry.cwd, managedWorktree: entry.managedWorktree, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [], openedAt: entry.openedAt || null, lastUsage: entry.lastUsage || null, carryRecap: entry.pendingRecap || null })
|
|
1763
1763
|
const persist = () => saveSession(room, id, sessionData())
|
|
1764
1764
|
// Synchronous flush of this session's record. Used on open (so a brand-new session
|
|
1765
1765
|
// has a file under its id BEFORE its first event — surviving a restart inside the
|
|
@@ -2098,7 +2098,8 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2098
2098
|
// Context-carry (2026-07-08): a non-resumable re-open (provider switch / non-resumable
|
|
2099
2099
|
// restart) hands the FRESH session a plain-text recap of the visible log as its first
|
|
2100
2100
|
// input, so the agent picks up the work instead of starting blank mid-conversation. Held
|
|
2101
|
-
// here
|
|
2101
|
+
// here and persist it until consumed, so a restart between /compact and the next
|
|
2102
|
+
// human turn cannot strand a fresh Codex thread without its recap. It is fired
|
|
2102
2103
|
// on the init `system` event (see onEvent) — a sendTurn before the input stream is consumed
|
|
2103
2104
|
// is silently lost (the 2026-07-02 auto-resume bug). Empty string → nothing to carry.
|
|
2104
2105
|
entry.pendingRecap = (typeof carryRecap === 'string' && carryRecap.trim()) ? carryRecap : null
|
|
@@ -2286,6 +2287,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2286
2287
|
if (evt.kind === 'system' && entry.pendingRecap) {
|
|
2287
2288
|
const recap = entry.pendingRecap
|
|
2288
2289
|
entry.pendingRecap = null
|
|
2290
|
+
entry.flush?.()
|
|
2289
2291
|
try { entry.session?.sendTurn(recap) } catch { /* not live — stays idle, user can continue manually */ }
|
|
2290
2292
|
process.stderr.write(`\n ◆ carried ${recap.length}c context recap into fresh session (${id.slice(0, 8)}) after provider/restore switch.\n`)
|
|
2291
2293
|
}
|
|
@@ -2879,11 +2881,11 @@ channel
|
|
|
2879
2881
|
pushLog(s, evt)
|
|
2880
2882
|
bcast('code-event', { term: payload.term, evt })
|
|
2881
2883
|
}
|
|
2882
|
-
// Session controls (/model, /clear) are NOT agent turns — they change the
|
|
2884
|
+
// Session controls (/model, /clear, /usage, and Codex /compact) are NOT agent turns — they change the
|
|
2883
2885
|
// shared session config. Emitting a `you` line for them strands the reader's
|
|
2884
2886
|
// inferred "working…" state (no turn → no result to clear it; the old bug).
|
|
2885
|
-
// Instead announce a ◆ control line both readers see. /compact + any
|
|
2886
|
-
// slash still flow through as a real, self-terminating turn.
|
|
2887
|
+
// Instead announce a ◆ control line both readers see. Claude /compact + any
|
|
2888
|
+
// other slash still flow through as a real, self-terminating turn.
|
|
2887
2889
|
const ctlLine = (ctlText) => {
|
|
2888
2890
|
const evt = { kind: 'control', text: ctlText, by: payload.by, cid: payload.cid }
|
|
2889
2891
|
pushLog(s, evt)
|
|
@@ -2936,7 +2938,33 @@ channel
|
|
|
2936
2938
|
// No persisted ctl line — the live indicator + the CompactionCard are the record.
|
|
2937
2939
|
if (/^\/compact\b/.test(text)) {
|
|
2938
2940
|
if (s.runtime === 'codex') {
|
|
2939
|
-
|
|
2941
|
+
if (s.session.turnActive) {
|
|
2942
|
+
ctlLine('finish or stop the current Codex turn before compacting context')
|
|
2943
|
+
return
|
|
2944
|
+
}
|
|
2945
|
+
const recap = buildRecapFromLog(s.log, RECAP_CAP, { reason: 'compact' })
|
|
2946
|
+
if (!recap) {
|
|
2947
|
+
ctlLine('nothing to compact — context unchanged')
|
|
2948
|
+
return
|
|
2949
|
+
}
|
|
2950
|
+
if (s.session.clearContext?.() === false) {
|
|
2951
|
+
ctlLine('Codex context compaction unavailable right now')
|
|
2952
|
+
return
|
|
2953
|
+
}
|
|
2954
|
+
s.pendingRecap = recap
|
|
2955
|
+
const ce = { kind: 'compaction', trigger: 'manual', preTokens: s.lastUsage?.ctx?.used || null, by: payload.by }
|
|
2956
|
+
pushLog(s, ce)
|
|
2957
|
+
bcast('code-event', { term: payload.term, evt: ce })
|
|
2958
|
+
// The new native thread has only the bounded recap queued for the next
|
|
2959
|
+
// human turn. Reset the visible meter to that conservative text estimate.
|
|
2960
|
+
if (s.lastUsage?.ctx?.max) {
|
|
2961
|
+
const used = Math.ceil(recap.length / 4)
|
|
2962
|
+
const max = s.lastUsage.ctx.max
|
|
2963
|
+
const usage = { kind: 'usage', ctx: { ...s.lastUsage.ctx, used, max, pct: Math.min(100, Math.round((used / max) * 100)), over: used > max } }
|
|
2964
|
+
s.lastUsage = usage
|
|
2965
|
+
bcast('code-event', { term: payload.term, evt: usage })
|
|
2966
|
+
}
|
|
2967
|
+
s.flush?.()
|
|
2940
2968
|
return
|
|
2941
2969
|
}
|
|
2942
2970
|
s.compacting = true
|
|
@@ -2956,7 +2984,7 @@ channel
|
|
|
2956
2984
|
// it degrades to "plan meters unavailable" rather than printing an inferred reset.
|
|
2957
2985
|
if (/^\/usage\s*$/.test(text)) {
|
|
2958
2986
|
ctlLine(usageReportLine(s.model, s.log))
|
|
2959
|
-
planMeterLine().then((l) => { if (l) ctlLine(l) }).catch(() => { /* meters are never load-bearing */ })
|
|
2987
|
+
if (s.runtime !== 'codex') planMeterLine().then((l) => { if (l) ctlLine(l) }).catch(() => { /* meters are never load-bearing */ })
|
|
2960
2988
|
return
|
|
2961
2989
|
}
|
|
2962
2990
|
// Context-carry (2026-07-08) point 4: a real human turn arrived BEFORE the post-switch/
|
|
@@ -2964,7 +2992,7 @@ channel
|
|
|
2964
2992
|
// — a separate recap turn would race + double-fire) and clear the flag so the init handler
|
|
2965
2993
|
// no-ops. The visible `you` echo is unchanged; the recap is context for the agent only.
|
|
2966
2994
|
let sendText = text
|
|
2967
|
-
if (s.pendingRecap) { sendText = `${s.pendingRecap}\n\n--- the person's next message ---\n${text}`; s.pendingRecap = null }
|
|
2995
|
+
if (s.pendingRecap) { sendText = `${s.pendingRecap}\n\n--- the person's next message ---\n${text}`; s.pendingRecap = null; s.flush?.() }
|
|
2968
2996
|
s.session.sendTurn(sendText)
|
|
2969
2997
|
echoYou()
|
|
2970
2998
|
// Pool-order fix (2026-07-02): emit the "dispatched to agent" marker HERE, right after
|
|
@@ -3193,7 +3221,7 @@ channel
|
|
|
3193
3221
|
// FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
|
|
3194
3222
|
// bridge restart: the conductor keeps its subagent-block + plan interception, and
|
|
3195
3223
|
// lanes keep their worktree cwd + the ability to mark done.
|
|
3196
|
-
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,
|
|
3224
|
+
openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, effort: rec.effort, provider: rec.runtime === 'codex' ? undefined : rec.provider || undefined, resume: canResume(rec) ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, cwd: rec.cwd, managedWorktree: rec.managedWorktree, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt, lastUsage: rec.lastUsage, carryRecap: rec.carryRecap,
|
|
3197
3225
|
// Lazy-boot restored terminals that were IDLE + not part of a flow: their transcript
|
|
3198
3226
|
// shows immediately; the query boots on first turn. Mid-turn + flow terminals boot now
|
|
3199
3227
|
// (mid-turn needs auto-resume; flow needs its lane live).
|
package/codex-event-mapper.mjs
CHANGED
|
@@ -42,7 +42,7 @@ export class CodexEventMapper {
|
|
|
42
42
|
/**
|
|
43
43
|
* @param {{onEvent:(evt)=>void, model?:string|null, sessionId?:string|null}} opts
|
|
44
44
|
*/
|
|
45
|
-
constructor({ onEvent, model = null, sessionId = null, fsImpl = fs }) {
|
|
45
|
+
constructor({ onEvent, model = null, sessionId = null, fsImpl = fs, contextWindowForModel = () => null }) {
|
|
46
46
|
this._onEvent = onEvent
|
|
47
47
|
this._model = model || null
|
|
48
48
|
this._sessionId = sessionId || null
|
|
@@ -50,6 +50,7 @@ export class CodexEventMapper {
|
|
|
50
50
|
this._fileBefore = new Map() // item.id → [{ path, kind, content }]
|
|
51
51
|
this._fs = fsImpl
|
|
52
52
|
this._systemSent = false
|
|
53
|
+
this._contextWindowForModel = contextWindowForModel
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** Emit a room-event; never let a consumer throw into the stream. */
|
|
@@ -143,7 +144,15 @@ export class CodexEventMapper {
|
|
|
143
144
|
const u = ev.usage || {}
|
|
144
145
|
// Map Codex usage → Claude-shape usage (what event-id.mjs folds into cost).
|
|
145
146
|
// reasoning_output_tokens are billed as output → fold in (conservative).
|
|
146
|
-
const out = (u.output_tokens || 0) + (u.reasoning_output_tokens || 0)
|
|
147
|
+
const out = (Number(u.output_tokens) || 0) + (Number(u.reasoning_output_tokens) || 0)
|
|
148
|
+
const totalInput = Number(u.input_tokens) || 0
|
|
149
|
+
const cachedInput = Math.min(totalInput, Number(u.cached_input_tokens) || 0)
|
|
150
|
+
let max = null
|
|
151
|
+
try { max = Number(this._contextWindowForModel(this._model)) } catch { /* unknown catalog → omit meter */ }
|
|
152
|
+
if (Number.isFinite(max) && max > 0) {
|
|
153
|
+
const used = totalInput + out
|
|
154
|
+
this._emit({ kind: 'usage', ctx: { used, max, pct: Math.min(100, Math.max(0, Math.round((used / max) * 100))), over: used > max, model: this._model } })
|
|
155
|
+
}
|
|
147
156
|
this._emit({
|
|
148
157
|
kind: 'result',
|
|
149
158
|
subtype: 'success',
|
|
@@ -151,8 +160,11 @@ export class CodexEventMapper {
|
|
|
151
160
|
model: this._model,
|
|
152
161
|
costUsd: null,
|
|
153
162
|
usage: {
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
// Claude-shape fields are mutually exclusive buckets. Codex reports
|
|
164
|
+
// cached_input_tokens as a subset of input_tokens, so subtract it here;
|
|
165
|
+
// usageReportLine adds the buckets and must not double-count cache hits.
|
|
166
|
+
input_tokens: totalInput - cachedInput,
|
|
167
|
+
cache_read_input_tokens: cachedInput,
|
|
156
168
|
cache_creation_input_tokens: 0,
|
|
157
169
|
output_tokens: out,
|
|
158
170
|
},
|
package/codex-session.mjs
CHANGED
|
@@ -65,6 +65,19 @@ export function readCodexModels({ home = process.env.CODEX_HOME || path.join(os.
|
|
|
65
65
|
} catch { return [] }
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// The CLI's current model catalog is the authority for the runtime's usable model
|
|
69
|
+
// window. Keep this separate from readCodexModels so the public announce shape stays
|
|
70
|
+
// stable while the driver can emit a truthful context meter for every Codex model.
|
|
71
|
+
export function readCodexContextWindow(model, { home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), fsImpl = fs } = {}) {
|
|
72
|
+
if (!model) return null
|
|
73
|
+
try {
|
|
74
|
+
const data = JSON.parse(fsImpl.readFileSync(path.join(home, 'models_cache.json'), 'utf8'))
|
|
75
|
+
const hit = (Array.isArray(data?.models) ? data.models : []).find((m) => m?.slug === model)
|
|
76
|
+
const max = Number(hit?.context_window)
|
|
77
|
+
return Number.isFinite(max) && max > 0 ? Math.floor(max) : null
|
|
78
|
+
} catch { return null }
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
export function normalizeCodexSandbox(value) {
|
|
69
82
|
return SAFE_SANDBOXES.has(value) ? value : DEFAULT_SANDBOX
|
|
70
83
|
}
|
|
@@ -150,7 +163,11 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
150
163
|
let chain = Promise.resolve()
|
|
151
164
|
const queue = []
|
|
152
165
|
|
|
153
|
-
const mapper = new CodexEventMapper({
|
|
166
|
+
const mapper = new CodexEventMapper({
|
|
167
|
+
onEvent,
|
|
168
|
+
model: model || null,
|
|
169
|
+
contextWindowForModel: (id) => readCodexContextWindow(id),
|
|
170
|
+
})
|
|
154
171
|
|
|
155
172
|
const note = (text) => { try { onEvent?.({ kind: 'note', text }) } catch { /* noop */ } }
|
|
156
173
|
|
package/package.json
CHANGED
package/recap.mjs
CHANGED
|
@@ -42,6 +42,10 @@ const FRAMING = {
|
|
|
42
42
|
header: '[CONTEXT CARRY — this lane kept talking while your machine was asleep]',
|
|
43
43
|
body: "Your bridge was offline. The lane stayed open and the people in this room kept planning here with Pool — a chat-only assistant with no file access and no tools. Everything below already happened and they have all seen it. NOTHING WAS RUN ON YOUR BEHALF: no commands were executed, no files were changed, and no plan below has been carried out. Do NOT re-greet and do NOT re-answer what is already answered. Read the recap, then wait for the people to tell you what to do with it — the plan below is a plan, not an instruction you have already been given.",
|
|
44
44
|
},
|
|
45
|
+
compact: {
|
|
46
|
+
header: '[CONTEXT COMPACTED — continue this Codex lane from its bounded recap]',
|
|
47
|
+
body: "The person intentionally compacted this lane. Your prior native thread was closed and the visible conversation below is its bounded continuation context. Do NOT re-greet, do NOT re-answer completed work, and do NOT repeat tool calls that already succeeded. Continue from the person's next message using this recap as memory.",
|
|
48
|
+
},
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/* Context-carry recap for a lane whose SDK memory CAN'T be resumed — a cross-provider
|