thinkpool-pair 0.7.242 → 0.7.243

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
@@ -105,7 +105,7 @@ const flowRedispatch = new Map()
105
105
  // wave BEFORE overrun. Lives bridge-side because waves dispatch across separate
106
106
  // broadcasts; without persistent state the cap can never bite.
107
107
  const flowBudgets = new Map()
108
- import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, laneStatusOf, nativeClaudeFallbackHint } from './cross-terminal.mjs'
108
+ import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, closeLaneDecision, laneStatusOf, nativeClaudeFallbackHint } from './cross-terminal.mjs'
109
109
  import { dispatchPendingRecap, recoverInterruptedTurn, recoverMissingResumeOnce, sendInterruptedContinue, shouldAttemptNativeResume, supersedeInterruptedResume } from './interrupted-resume.mjs'
110
110
  import { turnInFlight } from './update-gate.mjs'
111
111
  import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage } from './session-store.mjs'
@@ -2296,7 +2296,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2296
2296
  ref: String(sid).slice(0, 8),
2297
2297
  name: termNames[sid] || undefined,
2298
2298
  spawnedBy: s.spawnedBy,
2299
- busy: s.session?.turnActive ?? false,
2299
+ busy: (s.session?.turnActive ?? false) || (s.pending?.size ?? 0) > 0,
2300
2300
  }))))
2301
2301
  // Record this spawn, pruning timestamps that have already left the window
2302
2302
  // (identical predicate to recentSpawnCount) so the array can't grow unbounded.
@@ -2376,7 +2376,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2376
2376
  // the web's code-close), which no-ops if the id isn't a live session.
2377
2377
  ...(!entry.flowRole ? [tool(
2378
2378
  'close_terminal',
2379
- 'Close an agent lane that YOU opened with spawn_terminal (identified by ref/id/name). You can only close lanes you spawned yourself — never a sibling someone else is working in, and never the main terminal. Use it to clean up after a fan-out once you have collected the results with read_terminal.',
2379
+ 'Close an IDLE agent lane that YOU opened with spawn_terminal (identified by ref/id/name). Active turns and permission waits are protected: collect the completed result with read_terminal before closing. You can only close lanes you spawned yourself — never a sibling someone else is working in, and never the main terminal.',
2380
2380
  {
2381
2381
  terminal: z.string().describe('ref, id, or name of the lane you spawned (from read_terminal)'),
2382
2382
  },
@@ -2387,7 +2387,15 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
2387
2387
  if (!target) return okText(`No terminal matches "${args?.terminal}". Call read_terminal with no argument to list the open terminals.`)
2388
2388
  const te = sessions.get(target.id)
2389
2389
  if (!te) return okText(`Terminal ${target.ref} is not an agent lane (or already closed) — only spawned agent lanes can be closed this way.`)
2390
- if (te.spawnedBy !== id) return okText(`Terminal ${target.ref} was not opened by you — you can only close lanes you spawned yourself. Leave it to the person running it.`)
2390
+ const close = closeLaneDecision({
2391
+ callerId: id,
2392
+ spawnedBy: te.spawnedBy,
2393
+ busy: (te.session?.turnActive ?? false) || (te.pending?.size ?? 0) > 0,
2394
+ })
2395
+ if (!close.ok && close.reason === 'not-owner') return okText(`Terminal ${target.ref} was not opened by you — you can only close lanes you spawned yourself. Leave it to the person running it.`)
2396
+ if (!close.ok && close.reason === 'busy') {
2397
+ return okText(`Terminal ${target.ref} is still working or waiting for a permission decision. close_terminal will not kill an active turn. Wait until read_terminal reports it idle/completed, collect its result, then close it. If it is genuinely stuck, ask a person to press Stop.`)
2398
+ }
2391
2399
  endStructured(target.id)
2392
2400
  return okText(`Closed agent lane ${target.ref}${target.name ? ` ("${target.name}")` : ''}.`)
2393
2401
  },
@@ -220,7 +220,7 @@ export const formatPeek = ({ selfId, sessions, terms, names = {}, terminal, line
220
220
  } else {
221
221
  body = cleanPtyTail(hit.buf, n).join('\n') || '(no output yet)'
222
222
  }
223
- return `Terminal ${label(hit)} (${hit.kind} · ${hit.cmd}) — last ${n} lines:\n${body}`
223
+ return `Terminal ${label(hit)} (${hit.kind} · ${hit.cmd}) · ${laneStatusText(hit)} — last ${n} lines:\n${body}`
224
224
  }
225
225
 
226
226
  // A native Claude Code account failure is provider-wide, not model-specific. Retrying
@@ -417,8 +417,18 @@ export const spawnDecision = ({ hop = 0, spawnTimes = [], now = 0, spawnedLive =
417
417
  // Only lanes this caller spawned are suggested: close_terminal refuses the others, and
418
418
  // suggesting a sibling's lane is how you get an agent killing someone else's work.
419
419
  export const CLOSABLE_HINT_MAX = 6
420
+ export const closeLaneDecision = ({ callerId, spawnedBy, busy } = {}) => {
421
+ if (!callerId || spawnedBy !== callerId) return { ok: false, reason: 'not-owner' }
422
+ if (busy) return { ok: false, reason: 'busy' }
423
+ return { ok: true }
424
+ }
425
+
420
426
  export const closableLanes = (callerId, lanes = []) =>
421
- (Array.isArray(lanes) ? lanes : []).filter((l) => l && l.spawnedBy === callerId && !l.busy)
427
+ (Array.isArray(lanes) ? lanes : []).filter((l) => l && closeLaneDecision({
428
+ callerId,
429
+ spawnedBy: l.spawnedBy,
430
+ busy: l.busy,
431
+ }).ok)
422
432
 
423
433
  export const formatClosableHint = (callerId, lanes = [], max = CLOSABLE_HINT_MAX) => {
424
434
  const idle = closableLanes(callerId, lanes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.242",
3
+ "version": "0.7.243",
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": {