thinkpool-pair 0.7.315 → 0.7.317
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 +95 -26
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -982,6 +982,8 @@ function syncStructuredTurn(entry, now = Date.now()) {
|
|
|
982
982
|
if (busy) {
|
|
983
983
|
entry._turnRev = (Number(entry._turnRev) || 0) + 1
|
|
984
984
|
entry._turnStart = now
|
|
985
|
+
entry._settledTurnRev = null
|
|
986
|
+
entry._turnSettledAt = null
|
|
985
987
|
}
|
|
986
988
|
entry._busyAnn = busy
|
|
987
989
|
return true
|
|
@@ -992,6 +994,8 @@ function beginStructuredTurn(entry, now = Date.now()) {
|
|
|
992
994
|
entry._turnRev = (Number(entry._turnRev) || 0) + 1
|
|
993
995
|
entry._turnStart = now
|
|
994
996
|
entry._busyAnn = true
|
|
997
|
+
entry._settledTurnRev = null
|
|
998
|
+
entry._turnSettledAt = null
|
|
995
999
|
return true
|
|
996
1000
|
}
|
|
997
1001
|
|
|
@@ -999,9 +1003,43 @@ function advanceStructuredTurn(entry, now = Date.now()) {
|
|
|
999
1003
|
entry._turnRev = (Number(entry._turnRev) || 0) + 1
|
|
1000
1004
|
entry._turnStart = now
|
|
1001
1005
|
entry._busyAnn = true
|
|
1006
|
+
entry._settledTurnRev = null
|
|
1007
|
+
entry._turnSettledAt = null
|
|
1002
1008
|
return true
|
|
1003
1009
|
}
|
|
1004
1010
|
|
|
1011
|
+
// Every bridge-authored task must cross the same lifecycle boundary as a
|
|
1012
|
+
// browser-authored code-turn. Runtime adapters claim `turnActive` synchronously,
|
|
1013
|
+
// but the roster deliberately trusts `_busyAnn`; calling sendTurn directly can
|
|
1014
|
+
// therefore leave a real turn painted idle until its first provider event.
|
|
1015
|
+
// Keep send + revision + visible echo + rejection boundary + announce atomic so
|
|
1016
|
+
// cross-posts, spawned tasks and other non-composer ingress cannot drift again.
|
|
1017
|
+
function dispatchStructuredTurn(entry, text, {
|
|
1018
|
+
options,
|
|
1019
|
+
visibleEvent,
|
|
1020
|
+
rejectionMessage = 'The agent did not accept this turn; retry after the lane is available.',
|
|
1021
|
+
} = {}) {
|
|
1022
|
+
if (!entry?.session) return { accepted: false, error: new Error('session unavailable') }
|
|
1023
|
+
syncStructuredTurn(entry)
|
|
1024
|
+
let accepted = false
|
|
1025
|
+
let error = null
|
|
1026
|
+
try { accepted = entry.session.sendTurn(text, options) !== false }
|
|
1027
|
+
catch (cause) { error = cause }
|
|
1028
|
+
if (accepted) beginStructuredTurn(entry)
|
|
1029
|
+
else syncStructuredTurn(entry)
|
|
1030
|
+
if (visibleEvent) {
|
|
1031
|
+
pushLog(entry, visibleEvent)
|
|
1032
|
+
bcast('code-event', { term: entry.id, evt: visibleEvent })
|
|
1033
|
+
}
|
|
1034
|
+
if (!accepted) {
|
|
1035
|
+
const failed = { kind: 'error', message: rejectionMessage, recoverable: true }
|
|
1036
|
+
pushLog(entry, failed)
|
|
1037
|
+
bcast('code-event', { term: entry.id, evt: failed })
|
|
1038
|
+
}
|
|
1039
|
+
announce()
|
|
1040
|
+
return { accepted, error }
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1005
1043
|
function dispatchPendingSideContexts(entry) {
|
|
1006
1044
|
if (!entry?.pendingSideContexts?.length || typeof entry.session?.sendTurn !== 'function') return false
|
|
1007
1045
|
const outcome = dispatchSideContexts({
|
|
@@ -1249,7 +1287,7 @@ const announce = () => {
|
|
|
1249
1287
|
// laneStatusOf: authoritative busy/idle + last-action timestamp/age +
|
|
1250
1288
|
// STUCK/BLOCKED alert. The bridge owns the turn and permission state, so
|
|
1251
1289
|
// every roster consumer reads one status instead of reconstructing it.
|
|
1252
|
-
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, ...laneStatusOf(s), turnRev: Number(s._turnRev) || 0, ...(laneBusyOf(s) && s._turnStart ? { turnStartedAt: s._turnStart } : {}), hasTranscript: s.log.length > 0, commands: s.commands, mode: s.mode || undefined, effort: s.effort || undefined, name: termNames[id] || undefined, model: s.model || undefined, capabilities: { ...structuredRuntimeMetadata(s.runtime || 'claude'), modes: structuredModesForLane(s.runtime || 'claude', s) }, ...(s.runtime === 'codex' ? { approvalPolicy: codexConfigForMode(s.mode).approvalPolicy, models: s.models || [], canSteer: s.session?.canSteer ?? false } : s.runtime === 'hermes' ? { models: s.models || [], canSteer: s.session?.canSteer ?? false } : {}), ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.sideParent ? { sideParent: s.sideParent, sideTask: s.sideTask || undefined, sideHandback: !!s.sideHandback } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
|
|
1290
|
+
...[...sessions.entries()].map(([id, s]) => ({ id, cmd: s.cmd, kind: 'structured', runtime: s.runtime || 'claude', alive: true, ...laneStatusOf(s), turnRev: Number(s._turnRev) || 0, ...(laneBusyOf(s) && s._turnStart ? { turnStartedAt: s._turnStart } : {}), ...(!laneBusyOf(s) && Number(s._settledTurnRev) > 0 && Number(s._settledTurnRev) === Number(s._turnRev) ? { settledTurnRev: Number(s._settledTurnRev), turnSettledAt: Number(s._turnSettledAt) || undefined } : {}), hasTranscript: s.log.length > 0, commands: s.commands, mode: s.mode || undefined, effort: s.effort || undefined, name: termNames[id] || undefined, model: s.model || undefined, capabilities: { ...structuredRuntimeMetadata(s.runtime || 'claude'), modes: structuredModesForLane(s.runtime || 'claude', s) }, ...(s.runtime === 'codex' ? { approvalPolicy: codexConfigForMode(s.mode).approvalPolicy, models: s.models || [], canSteer: s.session?.canSteer ?? false } : s.runtime === 'hermes' ? { models: s.models || [], canSteer: s.session?.canSteer ?? false } : {}), ...(s.archiveOldestSeq != null ? { oldestSeq: s.archiveOldestSeq } : {}), ...(s.spawnedBy ? { spawned: true, spawnedBy: s.spawnedBy } : {}), ...(s.sideParent ? { sideParent: s.sideParent, sideTask: s.sideTask || undefined, sideHandback: !!s.sideHandback } : {}), ...(s.flowSessionId ? { flowId: s.flowSessionId, flowRole: s.flowTaskKey ? 'lane' : 'conductor' } : {}),
|
|
1253
1291
|
// provider: the registered LLM-provider this lane runs on, NAME-ONLY {id,name}
|
|
1254
1292
|
// (NEVER the key or baseUrl). Additive; older clients ignore it. Omitted for the
|
|
1255
1293
|
// built-in/default Claude path (no badge). Makes the lane's provider badge +
|
|
@@ -1436,8 +1474,13 @@ async function receiveCrossRoomPost({ fromRoom, fromHost, fromTerminalName, text
|
|
|
1436
1474
|
const msg = `[From: room ${fromLabel} — relayed via the ThinkPool cross-room Ensemble, approved by a person in this room]\n${body}`
|
|
1437
1475
|
const evt = { kind: 'you', text: msg, by: `room ${fromRoom}`, crosspost: true, relaySourceName: String(fromTerminalName || '').trim().slice(0, 80) || undefined }
|
|
1438
1476
|
autoNameTerminal(targetId, body)
|
|
1439
|
-
|
|
1440
|
-
|
|
1477
|
+
const dispatched = dispatchStructuredTurn(te, msg, {
|
|
1478
|
+
visibleEvent: evt,
|
|
1479
|
+
rejectionMessage: 'The incoming cross-room task was not accepted; retry after the lane is available.',
|
|
1480
|
+
})
|
|
1481
|
+
if (!dispatched.accepted) return { error: dispatched.error
|
|
1482
|
+
? `Could not deliver to room ${room} — the lane may have just closed.`
|
|
1483
|
+
: `Could not deliver to room ${room} — the lane refused the turn (check its visible host-memory/runtime error).` }
|
|
1441
1484
|
return { ok: true, ref: String(targetId).slice(0, 8) }
|
|
1442
1485
|
}
|
|
1443
1486
|
|
|
@@ -2130,6 +2173,9 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2130
2173
|
// first transcript event ts, so even the first post-fix restart is ordered right.
|
|
2131
2174
|
openedAt: openedAt || (Array.isArray(log) ? (log.find((e) => e?.ts)?.ts || 0) : 0) || Date.now() }
|
|
2132
2175
|
entry._turnRev = entry.log.reduce((max, event) => Math.max(max, Number(event?.turnRev) || 0), 0)
|
|
2176
|
+
const restoredBoundary = [...entry.log].reverse().find((event) => event?.kind === 'result' && Number(event?.turnRev) > 0)
|
|
2177
|
+
entry._settledTurnRev = Number(restoredBoundary?.turnRev) || null
|
|
2178
|
+
entry._turnSettledAt = Number(restoredBoundary?.ts) || null
|
|
2133
2179
|
entry.interruptedRecap = restoredTurnOpen(entry.log) ? buildRecapFromLog(entry.log, RECAP_CAP) : null
|
|
2134
2180
|
// Slice 3 — a permission card left unanswered past the grace window pushes
|
|
2135
2181
|
// "<lane> — needs you: <what>"; answering it anywhere retracts the banner
|
|
@@ -2252,13 +2298,11 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2252
2298
|
// No revert, no verdict broadcast, no markFlowDone — the loop stays open.
|
|
2253
2299
|
if (!decision.stop) {
|
|
2254
2300
|
const next = round + 1
|
|
2255
|
-
|
|
2256
|
-
entry.
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
)
|
|
2261
|
-
} catch { /* lane may have closed mid-verdict */ }
|
|
2301
|
+
dispatchStructuredTurn(entry,
|
|
2302
|
+
entry.runtime === 'codex'
|
|
2303
|
+
? `[Flow review — round ${next}/${REVIEW_DEFAULTS.maxRounds}] The happy path held, but you have NOT reported your checks exhausted. Dig one more round, then call submit_flow_review again — pass:false with specific evidence if you break it, or pass:true AND exhausted:true only when nothing remains to check.`
|
|
2304
|
+
: `[Flow review — round ${next}/${REVIEW_DEFAULTS.maxRounds}] The happy path held, but you have NOT reported your checks exhausted and you are under both the round and budget ceilings. Dig one more round: hunt the edge cases, the reload, the second click, concurrent use, the error path — the places the builder didn't. Then re-Write FLOW_REVIEW.json — pass:false with a specific reason if you break it, or pass:true AND exhausted:true if you genuinely have nothing left to check.`,
|
|
2305
|
+
{ rejectionMessage: 'The next Flow review round was not accepted; retry after the lane is available.' })
|
|
2262
2306
|
process.stderr.write(`\n ${A.dim}◆ review round ${round} inconclusive — digging again (${next}/${REVIEW_DEFAULTS.maxRounds}) on ${target || entry.flowTaskKey}${A.rst}\n`)
|
|
2263
2307
|
return { ok: true, message: `Round ${round} recorded (pass, not yet exhausted). Keep hunting — asked you for round ${next}/${REVIEW_DEFAULTS.maxRounds}.` }
|
|
2264
2308
|
}
|
|
@@ -2673,10 +2717,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2673
2717
|
// arrived (rides the existing code-event 'you' path — no new topic).
|
|
2674
2718
|
const evt = { kind: 'you', text: msg, by: `terminal ${fromRef} (agent)`, crosspost: true, relaySourceName: termNames[id] || undefined }
|
|
2675
2719
|
autoNameTerminal(target.id, args.text)
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2720
|
+
const dispatched = dispatchStructuredTurn(te, msg, {
|
|
2721
|
+
visibleEvent: evt,
|
|
2722
|
+
rejectionMessage: 'The cross-terminal task was not accepted; retry after the lane is available.',
|
|
2723
|
+
})
|
|
2724
|
+
if (!dispatched.accepted) return okText(dispatched.error
|
|
2725
|
+
? `Could not deliver to terminal ${target.ref} — it may have just closed.`
|
|
2726
|
+
: `Could not deliver to terminal ${target.ref} — it refused the turn. Check its visible host-memory/runtime error.`)
|
|
2680
2727
|
return okText(`Delivered to terminal ${target.ref} (${target.cmd}). It will respond in its own lane; check back with read_terminal.`)
|
|
2681
2728
|
},
|
|
2682
2729
|
)] : []),
|
|
@@ -2727,9 +2774,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2727
2774
|
const msg = `[Task from main terminal ${fromRef}'s agent — opened as an independent MAIN CASCADE CONDUCTOR terminal, not an Ensemble child]\n${String(args.task).trim()}`
|
|
2728
2775
|
const evt = { kind: 'you', text: msg, by: `main terminal ${fromRef} (agent)`, crosspost: true, relaySourceName: termNames[id] || undefined }
|
|
2729
2776
|
autoNameTerminal(newId, args.task)
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2777
|
+
const dispatched = dispatchStructuredTurn(conductor, msg, {
|
|
2778
|
+
visibleEvent: evt,
|
|
2779
|
+
rejectionMessage: 'The initial conductor task was not accepted; retry after the lane is available.',
|
|
2780
|
+
})
|
|
2781
|
+
if (!dispatched.accepted) return okText(dispatched.error
|
|
2782
|
+
? `Opened main conductor ${newRef}${args?.name ? ` ("${args.name}")` : ''}, but it may still be starting — the initial task could not be delivered.`
|
|
2783
|
+
: `Opened main conductor ${newRef}, but host pressure prevented its runtime from starting. Existing lanes remain connected; free memory and retry the task.`)
|
|
2733
2784
|
return okText(`Opened independent main Cascade conductor ${newRef}${args?.name ? ` ("${args.name}")` : ''} and handed it the task. It is a main terminal, not an Ensemble lane.`)
|
|
2734
2785
|
},
|
|
2735
2786
|
)] : []),
|
|
@@ -2847,8 +2898,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2847
2898
|
const msg = `[Task from terminal ${fromRef}'s agent — relayed via ThinkPool Ensemble; you are its WORKER SUB-TERMINAL, never a main terminal or Cascade conductor]\n${args.task}${reviewTarget}`
|
|
2848
2899
|
const evt = { kind: 'you', text: msg, by: `terminal ${fromRef} (agent)`, crosspost: true, relaySourceName: termNames[id] || undefined }
|
|
2849
2900
|
autoNameTerminal(newId, args.task)
|
|
2850
|
-
|
|
2851
|
-
|
|
2901
|
+
const dispatched = dispatchStructuredTurn(ne, msg, {
|
|
2902
|
+
visibleEvent: evt,
|
|
2903
|
+
rejectionMessage: 'The initial worker task was not accepted; retry after the lane is available.',
|
|
2904
|
+
})
|
|
2905
|
+
if (!dispatched.accepted) return okText(dispatched.error
|
|
2906
|
+
? `Opened lane ${newRef}${args?.name ? ` ("${args.name}")` : ''}, but it may still be starting — could not hand off the task. Try post_to_terminal shortly.`
|
|
2907
|
+
: `Opened lane ${newRef}${args?.name ? ` ("${args.name}")` : ''}, but host pressure prevented its runtime from accepting the task. Existing lanes remain connected; free memory and retry.`)
|
|
2852
2908
|
return okText(`Opened agent lane ${newRef}${args?.name ? ` ("${args.name}")` : ''} and handed it the task. It runs in its own lane — check back with read_terminal, then close_terminal when done.`)
|
|
2853
2909
|
}
|
|
2854
2910
|
return okText(`Opened idle agent lane ${newRef}${args?.name ? ` ("${args.name}")` : ''}. Hand it work with post_to_terminal, or a person can type into it.`)
|
|
@@ -3197,6 +3253,10 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
3197
3253
|
const mockupDeliveryBoundary = isMockupDeliveryBoundary(evt, { turnActive: entry.session?.turnActive === true })
|
|
3198
3254
|
stampStructuredTurn(entry, evt)
|
|
3199
3255
|
stampEvent(evt)
|
|
3256
|
+
if (terminalBoundary) {
|
|
3257
|
+
entry._settledTurnRev = Number(evt.turnRev) || Number(entry._turnRev) || null
|
|
3258
|
+
entry._turnSettledAt = Number(evt.ts) || Date.now()
|
|
3259
|
+
}
|
|
3200
3260
|
const stalledChanged = evt.kind === 'stalled' ? !entry.stalled : !!entry.stalled
|
|
3201
3261
|
entry.stalled = evt.kind === 'stalled'
|
|
3202
3262
|
entry.lastEvent = evt
|
|
@@ -3897,12 +3957,15 @@ channel
|
|
|
3897
3957
|
}
|
|
3898
3958
|
child.hop = 0
|
|
3899
3959
|
const childEvent = { kind: 'you', text: task, by }
|
|
3900
|
-
stampEvent(childEvent); pushLog(child, childEvent); bcast('code-event', { term: payload.id, evt: childEvent })
|
|
3901
3960
|
const parentEvent = { kind: 'side-started', sideId: payload.id, sideName, task, by }
|
|
3902
|
-
|
|
3961
|
+
pushLog(parent, parentEvent); bcast('code-event', { term: sideParent, evt: parentEvent })
|
|
3903
3962
|
const snapshot = sideSnapshot(parent.log.filter((event) => event.cid !== parentEvent.cid))
|
|
3904
3963
|
const prompt = snapshot ? `${snapshot}\n\n--- side task ---\n${task}` : task
|
|
3905
|
-
|
|
3964
|
+
const dispatched = dispatchStructuredTurn(child, prompt, {
|
|
3965
|
+
visibleEvent: childEvent,
|
|
3966
|
+
rejectionMessage: 'The side-lane task was not accepted; retry after the lane is available.',
|
|
3967
|
+
})
|
|
3968
|
+
if (!dispatched.accepted) {
|
|
3906
3969
|
bcast('side-open-failed', { id: payload.id, parent: sideParent, message: 'The side agent could not start.' })
|
|
3907
3970
|
endStructured(payload.id)
|
|
3908
3971
|
return
|
|
@@ -4084,8 +4147,10 @@ channel
|
|
|
4084
4147
|
const preparing = { kind: 'control', text: 'Preparing a compact handoff for main…', by }
|
|
4085
4148
|
stampEvent(preparing); pushLog(side, preparing); bcast('code-event', { term: payload.term, evt: preparing })
|
|
4086
4149
|
side.flush?.(); announce()
|
|
4087
|
-
|
|
4088
|
-
|
|
4150
|
+
const dispatched = dispatchStructuredTurn(side, SIDE_HANDOFF_PROMPT, {
|
|
4151
|
+
rejectionMessage: 'The side-lane handoff was not accepted; retry after the lane is available.',
|
|
4152
|
+
})
|
|
4153
|
+
if (!dispatched.accepted) {
|
|
4089
4154
|
side.sideHandback = null
|
|
4090
4155
|
const failed = { kind: 'control', text: 'Couldn’t start the handoff — try again.', by }
|
|
4091
4156
|
stampEvent(failed); pushLog(side, failed); bcast('code-event', { term: payload.term, evt: failed })
|
|
@@ -4806,7 +4871,9 @@ flowChannel
|
|
|
4806
4871
|
// to pin a cheaper/smarter conductor. Lanes get tiered below via laneModelFor.
|
|
4807
4872
|
openStructured({ id: cid, runtime: flowRuntime, model: conductorModel, mode: flowRuntime === 'codex' ? 'plan' : 'default', rolePrompt: flowRuntime === 'claude' ? FLOW_CONDUCTOR_PROMPT : FLOW_CODEX_CONDUCTOR_PROMPT, flowSessionId: payload.flowId, flowRole: 'conductor', spawnedBy: `flow:${payload.flowId}` })
|
|
4808
4873
|
const ce = sessions.get(cid)
|
|
4809
|
-
if (ce?.session)
|
|
4874
|
+
if (ce?.session) dispatchStructuredTurn(ce, payload.prompt || '', {
|
|
4875
|
+
rejectionMessage: 'The Flow conductor task was not accepted; retry after the lane is available.',
|
|
4876
|
+
})
|
|
4810
4877
|
process.stderr.write(`\n ${A.mag}◆ flow conductor launched — flow ${String(payload.flowId).slice(0, 8)} (plan mode).${A.rst}\n`)
|
|
4811
4878
|
announce()
|
|
4812
4879
|
})
|
|
@@ -4953,7 +5020,9 @@ flowChannel
|
|
|
4953
5020
|
`\nProject (context): ${payload.flowPrompt || ''}\n\n` +
|
|
4954
5021
|
`Build your slice. Own only your files. Done = it runs + meets acceptance. Commit when done.` +
|
|
4955
5022
|
(flowRuntime === 'codex' || flowRuntime === 'hermes' ? ` Then call the ThinkPool mark_flow_done MCP tool; do not write FLOW_DONE.` : '')
|
|
4956
|
-
|
|
5023
|
+
dispatchStructuredTurn(le, spec, {
|
|
5024
|
+
rejectionMessage: `Flow slice ${t.task_key} was not accepted; retry after the lane is available.`,
|
|
5025
|
+
})
|
|
4957
5026
|
}
|
|
4958
5027
|
}
|
|
4959
5028
|
assignments.push({ task_key: t.task_key, laneId })
|