thinkpool-pair 0.7.223 → 0.7.224

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 CHANGED
@@ -50,7 +50,7 @@ import { validateProviderSwitch, providerSwitchPlan, BUILTIN_PROVIDER } from './
50
50
  import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
51
51
  import { z } from 'zod'
52
52
  import { startClaudeSession } from './claude-session.mjs'
53
- import { startCodexSession, readCodexDefaultModel, readCodexModels, codexConfigForMode, defaultModeForRuntime } from './codex-session.mjs'
53
+ import { startCodexSession, readCodexDefaultModel, readCodexModels, codexConfigForMode, codexThreadCanResume, defaultModeForRuntime } from './codex-session.mjs'
54
54
  import { canonicalRoomFilePath, waitForNativeImages } from './codex-images.mjs'
55
55
  import { createManagedLaneWorktree, removeManagedLaneWorktree } from './lane-worktree.mjs'
56
56
  import { commandOnPath } from './agent-detect.mjs'
@@ -103,7 +103,7 @@ const flowRedispatch = new Map()
103
103
  // broadcasts; without persistent state the cap can never bite.
104
104
  const flowBudgets = new Map()
105
105
  import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, laneStatusOf } from './cross-terminal.mjs'
106
- import { dispatchPendingRecap, recoverInterruptedTurn, recoverMissingResumeOnce, armInterruptedResume, sendInterruptedContinue, supersedeInterruptedResume } from './interrupted-resume.mjs'
106
+ import { dispatchPendingRecap, recoverInterruptedTurn, recoverMissingResumeOnce, sendInterruptedContinue, supersedeInterruptedResume } from './interrupted-resume.mjs'
107
107
  import { turnInFlight } from './update-gate.mjs'
108
108
  import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage } from './session-store.mjs'
109
109
  import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks, usageReportLine, codexUsageReportLine, buildRecapFromLog, RECAP_CAP, trimmedBeforeSeq, firstSeq } from './event-id.mjs'
@@ -1698,7 +1698,6 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
1698
1698
  flowReviewRound: Number.isInteger(flowReviewRound) && flowReviewRound >= 0 ? flowReviewRound : 0,
1699
1699
  dispatchBaseSha: dispatchBaseSha || null,
1700
1700
  revertTarget: revertTarget || null,
1701
- cwd: cwd || null, managedWorktree: managedWorktree || null,
1702
1701
  // Stable creation order — persisted so a bridge restart restores tabs in the SAME
1703
1702
  // order (not readdir/filesystem order). Legacy recs (no openedAt) derive it from the
1704
1703
  // first transcript event ts, so even the first post-fix restart is ordered right.
@@ -3627,7 +3626,13 @@ channel
3627
3626
  all.sort((a, b) => okey(a) - okey(b))
3628
3627
  if (all.length) for (const rec of all) {
3629
3628
  const wasInterrupted = restoredTurnOpen(rec.log || [])
3629
+ // Codex can persist a thread id before its rollout receives the
3630
+ // first metadata row. A stop followed immediately by a restart
3631
+ // leaves that file at zero bytes; both App Server and exec reject
3632
+ // it. Treat only that provably-empty case as non-resumable so the
3633
+ // existing fresh-thread recap recovery handles it.
3630
3634
  const resumable = canResume(rec)
3635
+ && (rec.runtime !== 'codex' || codexThreadCanResume(rec.sessionId))
3631
3636
  const recoveryRecap = wasInterrupted ? buildRecapFromLog(rec.log || [], RECAP_CAP) : ''
3632
3637
  // FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
3633
3638
  // bridge restart: the conductor keeps its subagent-block + plan interception, and
package/codex-session.mjs CHANGED
@@ -123,6 +123,22 @@ function codexRolloutPath(sessionId, { home, fsImpl }) {
123
123
  } catch { return null }
124
124
  }
125
125
 
126
+ // A stopped App Server turn can publish its thread id before Codex has written
127
+ // even the session_meta row. If the bridge restarts in that window, both
128
+ // thread/resume and `codex exec resume` reject the zero-byte rollout. Missing
129
+ // files remain "unknown" (Codex may resolve them through another store), but a
130
+ // rollout we can prove is empty is never resumable.
131
+ export function codexThreadCanResume(sessionId, {
132
+ home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'),
133
+ fsImpl = fs,
134
+ } = {}) {
135
+ if (!sessionId) return false
136
+ const file = codexRolloutPath(sessionId, { home, fsImpl })
137
+ if (!file) return true
138
+ try { return Number(fsImpl.statSync(file).size) > 0 }
139
+ catch { return true }
140
+ }
141
+
126
142
  // `codex exec --json` exposes lifetime thread totals on `turn.completed`. Those
127
143
  // totals are useful for billing, but they are NOT context occupancy: an agentic
128
144
  // turn can make hundreds of cached model calls and process many context windows.
@@ -365,8 +381,12 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
365
381
  sandbox = normalizeCodexSandbox(sandbox || modeConfig.sandbox)
366
382
  const publishAccess = codexPublishAccess({ cwd, env: { ...process.env, ...(env || {}) } })
367
383
  const childEnv = publishAccess.env
368
- let sessionId = resume || null
369
- let turnNo = resume ? 1 : 0
384
+ const requestedResume = resume || null
385
+ const resumeUsable = !requestedResume || codexThreadCanResume(requestedResume, {
386
+ home: childEnv.CODEX_HOME || path.join(os.homedir(), '.codex'),
387
+ })
388
+ let sessionId = resumeUsable ? requestedResume : null
389
+ let turnNo = sessionId ? 1 : 0
370
390
  let child = null
371
391
  let aborted = false
372
392
  let ended = false
@@ -398,6 +418,9 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
398
418
  })
399
419
 
400
420
  const note = (text) => { try { onEvent?.({ kind: 'note', text }) } catch { /* noop */ } }
421
+ if (requestedResume && !resumeUsable) {
422
+ queueMicrotask(() => note('The stopped Codex turn had no resumable context; continuing in a fresh thread.'))
423
+ }
401
424
  if (!['0', 'false', 'off'].includes(String(childEnv.TP_CODEX_APP_SERVER || '').trim().toLowerCase()) && !appServerGateResult.enabled) {
402
425
  queueMicrotask(() => note(`Codex App Server disabled: ${appServerGateResult.reason}`))
403
426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.223",
3
+ "version": "0.7.224",
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": {