thinkpool-pair 0.7.201 → 0.7.203
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 +13 -5
- package/interrupted-resume.mjs +29 -0
- package/package.json +2 -1
package/bridge.mjs
CHANGED
|
@@ -98,6 +98,7 @@ const flowRedispatch = new Map()
|
|
|
98
98
|
// broadcasts; without persistent state the cap can never bite.
|
|
99
99
|
const flowBudgets = new Map()
|
|
100
100
|
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint } from './cross-terminal.mjs'
|
|
101
|
+
import { armInterruptedResume, sendInterruptedContinue, supersedeInterruptedResume } from './interrupted-resume.mjs'
|
|
101
102
|
import { turnInFlight } from './update-gate.mjs'
|
|
102
103
|
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage } from './session-store.mjs'
|
|
103
104
|
import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks, usageReportLine, buildRecapFromLog, RECAP_CAP, trimmedBeforeSeq, firstSeq } from './event-id.mjs'
|
|
@@ -2238,7 +2239,10 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2238
2239
|
{ const onBuiltin = !entry.provider || entry.provider === 'anthropic'
|
|
2239
2240
|
const m = onBuiltin ? (evt.model || evt.ctx?.model) : null
|
|
2240
2241
|
if (m && m !== entry.model) { entry.model = m; announce() }
|
|
2241
|
-
|
|
2242
|
+
// A custom Anthropic-compatible endpoint may expose its own catalog through the
|
|
2243
|
+
// same SDK method. Never publish that as the ROOM's Claude catalog: its models
|
|
2244
|
+
// already render in the provider group and otherwise leak under ANTHROPIC.
|
|
2245
|
+
if (onBuiltin && evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { roomModels = evt.models; announce() } }
|
|
2242
2246
|
// Cache the last usage/ctx meter on the entry (persisted + replayed — see
|
|
2243
2247
|
// openStructured). Chrome events bypass pushLog/persist in emitTail below, so
|
|
2244
2248
|
// usage needs an explicit persist() here to survive a bridge restart.
|
|
@@ -2271,9 +2275,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2271
2275
|
// resume gets its single "continue" HERE — not on a blind timer that raced the ~40s
|
|
2272
2276
|
// resume and dropped the turn into a dead input stream. Fires exactly once.
|
|
2273
2277
|
if (evt.kind === 'system' && entry.pendingAutoResume) {
|
|
2274
|
-
entry.
|
|
2275
|
-
try { entry.session?.sendTurn('continue') } catch { /* not resumable — stays idle, user can continue manually */ }
|
|
2276
|
-
process.stderr.write(`\n ◆ auto-resumed interrupted turn (${id.slice(0, 8)}) — sent continue.\n`)
|
|
2278
|
+
if (sendInterruptedContinue(entry)) process.stderr.write(`\n ◆ auto-resumed interrupted turn (${id.slice(0, 8)}) — sent continue.\n`)
|
|
2277
2279
|
}
|
|
2278
2280
|
// Context-carry (2026-07-08): a lane re-opened WITHOUT an SDK resume (provider switch,
|
|
2279
2281
|
// non-resumable restart) starts blank. Fire the text-layer recap as its first turn the
|
|
@@ -2851,6 +2853,11 @@ channel
|
|
|
2851
2853
|
s.hop = 0
|
|
2852
2854
|
s.roomHop = 0 // a human turn is room-hop 0 — clears any injected cross-room hop depth
|
|
2853
2855
|
const text = String(payload.text)
|
|
2856
|
+
// A human turn supersedes a restart's still-pending auto-continue. Without this,
|
|
2857
|
+
// their message starts the cold runtime; its init event then queues a second stale
|
|
2858
|
+
// `continue` behind the request. Codex normally resumes immediately at restore, while
|
|
2859
|
+
// Claude holds this flag only until its input stream is live.
|
|
2860
|
+
supersedeInterruptedResume(s)
|
|
2854
2861
|
// Echo the turn so BOTH readers (and late joiners, via the log) show who
|
|
2855
2862
|
// said what — UNLESS silent (an @pool fire, which renders its own line).
|
|
2856
2863
|
const echoYou = () => {
|
|
@@ -3203,7 +3210,8 @@ channel
|
|
|
3203
3210
|
// instant it's live. openStructured already closed the interrupted turn idle, so a
|
|
3204
3211
|
// continue that can't be delivered just leaves it idle (recoverable) — never a hang.
|
|
3205
3212
|
if (re && !rec.flowSessionId && canResume(rec) && restoredTurnOpen(rec.log || [])) {
|
|
3206
|
-
|
|
3213
|
+
const recovery = armInterruptedResume(re)
|
|
3214
|
+
if (recovery === 'sent') process.stderr.write(`\n ◆ auto-resumed interrupted Codex turn (${rec.id.slice(0, 8)}) — sent continue.\n`)
|
|
3207
3215
|
}
|
|
3208
3216
|
// Context-carry (2026-07-08): the SAME mid-turn case but the SDK context CANNOT
|
|
3209
3217
|
// resume (canResume false — a stale/expired session). A bare "continue" would land
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Runtime-specific bridge-restart recovery.
|
|
2
|
+
//
|
|
3
|
+
// Claude owns a long-lived input stream: its continue must wait for the init system
|
|
4
|
+
// event proving that stream is consumed. Codex is one process per turn and has no idle
|
|
5
|
+
// init event: sending the prompt IS what launches the resumed process.
|
|
6
|
+
|
|
7
|
+
export function sendInterruptedContinue(entry) {
|
|
8
|
+
if (!entry) return false
|
|
9
|
+
entry.pendingAutoResume = false
|
|
10
|
+
try { return entry.session?.sendTurn?.('continue') !== false }
|
|
11
|
+
catch { return false }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function armInterruptedResume(entry) {
|
|
15
|
+
if (!entry?.session) return 'unavailable'
|
|
16
|
+
if (entry.runtime === 'codex') return sendInterruptedContinue(entry) ? 'sent' : 'unavailable'
|
|
17
|
+
entry.pendingAutoResume = true
|
|
18
|
+
return 'armed'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A real person speaking always wins the race. Otherwise a human turn that starts a
|
|
22
|
+
// cold Claude/Codex runtime can trigger its init event and enqueue a stale second
|
|
23
|
+
// `continue` behind the person's actual request.
|
|
24
|
+
export function supersedeInterruptedResume(entry) {
|
|
25
|
+
if (!entry?.pendingAutoResume) return false
|
|
26
|
+
entry.pendingAutoResume = false
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.203",
|
|
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": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"transcript-sanitize.mjs",
|
|
28
28
|
"session-store.mjs",
|
|
29
29
|
"cross-terminal.mjs",
|
|
30
|
+
"interrupted-resume.mjs",
|
|
30
31
|
"flow-conductor.mjs",
|
|
31
32
|
"flow-worktree.mjs",
|
|
32
33
|
"flow-task-graph.mjs",
|