thinkpool-pair 0.7.302 → 0.7.303

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
@@ -124,7 +124,7 @@ import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkRepl
124
124
  import { createLatestReplayPump, requestedReplayIds } from './replay-transport.mjs'
125
125
  import { classifyCodeEvent, unknownCodeEventNotice } from './code-event-contract.mjs'
126
126
  import { fetchPairControlDeliveryAuthority } from './pair-control-authority.mjs'
127
- import { SIDE_HANDOFF_PROMPT, SIDE_ROLE_PROMPT, appendSideContext, assistantTextSince, sideContextBlock, sideSnapshot } from './side-lane.mjs'
127
+ import { SIDE_HANDOFF_PROMPT, SIDE_ROLE_PROMPT, appendSideContext, assistantTextSince, dispatchSideContexts, sideContextBlock, sideSnapshot } from './side-lane.mjs'
128
128
  import { planMeterLine } from './plan-meters.mjs'
129
129
  import { priceForModel } from './model-prices.mjs'
130
130
  import { makeThrottledTrack } from './presence.mjs'
@@ -994,6 +994,32 @@ function advanceStructuredTurn(entry, now = Date.now()) {
994
994
  return true
995
995
  }
996
996
 
997
+ function dispatchPendingSideContexts(entry) {
998
+ if (!entry?.pendingSideContexts?.length || typeof entry.session?.sendTurn !== 'function') return false
999
+ const outcome = dispatchSideContexts({
1000
+ contexts: entry.pendingSideContexts,
1001
+ busy: entry.session?.turnActive === true,
1002
+ sendTurn: (prompt) => entry.session?.sendTurn(prompt),
1003
+ })
1004
+ entry.pendingSideContexts = outcome.pending
1005
+ if (!outcome.dispatched) return false
1006
+ beginStructuredTurn(entry)
1007
+ entry.flush?.()
1008
+ announce()
1009
+ return true
1010
+ }
1011
+
1012
+ function schedulePendingSideContexts(entry) {
1013
+ if (!entry?.pendingSideContexts?.length || entry._sideContextTimer) return
1014
+ entry._sideContextTimer = setTimeout(() => {
1015
+ entry._sideContextTimer = null
1016
+ if (dispatchPendingSideContexts(entry)) {
1017
+ process.stderr.write('\n ◆ started main turn from side-lane handoff.\n')
1018
+ }
1019
+ }, 0)
1020
+ entry._sideContextTimer.unref?.()
1021
+ }
1022
+
997
1023
  function stampStructuredTurn(entry, event) {
998
1024
  if (event && event.turnRev == null && Number(entry?._turnRev) > 0) event.turnRev = entry._turnRev
999
1025
  return event
@@ -3238,6 +3264,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
3238
3264
  const done = { kind: 'control', text: 'Brought to main.', by: request.by }
3239
3265
  stampEvent(done); pushLog(entry, done); bcast('code-event', { term: id, evt: done })
3240
3266
  parent.flush?.()
3267
+ schedulePendingSideContexts(parent)
3241
3268
  } else {
3242
3269
  const failed = { kind: 'control', text: parent ? 'Couldn’t prepare a handoff — try again.' : 'The main terminal is no longer available.', by: request.by }
3243
3270
  stampEvent(failed); pushLog(entry, failed); bcast('code-event', { term: id, evt: failed })
@@ -3252,6 +3279,9 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
3252
3279
  const stalePermissionIds = [...entry.pending.keys()]
3253
3280
  drainPending(entry)
3254
3281
  for (const pendingId of stalePermissionIds) bcast('code-perm', { term: id, id: pendingId, decision: 'deny', name: 'agent' })
3282
+ // Bring-to-main never interrupts an active parent turn. If a handoff arrived
3283
+ // while this lane was working, start it on the first idle tick after settle.
3284
+ schedulePendingSideContexts(entry)
3255
3285
  const settledAt = Date.now()
3256
3286
  if (shouldRecordTurnDone({ entry, subtype: evt.subtype, startedAt, now: settledAt })) {
3257
3287
  persistAgentEvent({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.302",
3
+ "version": "0.7.303",
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": {
package/side-lane.mjs CHANGED
@@ -8,6 +8,9 @@ Focus first on reading, searching, comparing, and answering the side task. You a
8
8
  export const SIDE_HANDOFF_PROMPT = `Prepare a compact handoff for the main terminal now.
9
9
  Return only the handoff. Include: conclusion, strongest evidence, files changed or artifacts produced, and any unresolved question or recommended next action. Do not continue the investigation and do not address the reader conversationally.`
10
10
 
11
+ export const SIDE_MAIN_TURN_PROMPT = `A room member chose Bring to main.
12
+ Read the side-lane handoff below, incorporate the relevant findings into the main lane's current work, and respond now. If the handoff recommends a next step that is already authorized and in scope, take it; otherwise explain the concrete impact on the current work.`
13
+
11
14
  export function sideSnapshot(log) {
12
15
  return buildRecapFromLog(Array.isArray(log) ? log : [], SIDE_RECAP_CAP)
13
16
  }
@@ -40,3 +43,21 @@ export function appendSideContext(contexts, context, cap = 4) {
40
43
  if (!context) return Array.isArray(contexts) ? contexts.slice(-cap) : []
41
44
  return [...(Array.isArray(contexts) ? contexts : []), context].slice(-cap)
42
45
  }
46
+
47
+ export function sideMainTurnPrompt(contexts) {
48
+ const handoffs = (Array.isArray(contexts) ? contexts : []).filter(Boolean)
49
+ if (!handoffs.length) return ''
50
+ return `${SIDE_MAIN_TURN_PROMPT}\n\n${handoffs.join('\n\n')}`
51
+ }
52
+
53
+ export function dispatchSideContexts({ contexts, busy, sendTurn }) {
54
+ const pending = (Array.isArray(contexts) ? contexts : []).filter(Boolean).slice(-4)
55
+ if (!pending.length || busy || typeof sendTurn !== 'function') return { dispatched: false, pending }
56
+ const prompt = sideMainTurnPrompt(pending)
57
+ try {
58
+ if (sendTurn(prompt) === false) return { dispatched: false, pending }
59
+ } catch {
60
+ return { dispatched: false, pending }
61
+ }
62
+ return { dispatched: true, pending: [] }
63
+ }