thinkpool-pair 0.7.181 → 0.7.183
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 +16 -53
- package/claude-session.mjs +2 -2
- package/cross-terminal.mjs +12 -3
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -744,18 +744,11 @@ let myServeUid = null // this bridge's authed uid (null = anon)
|
|
|
744
744
|
refreshOwnerPlan() // fire-and-forget on boot (codeAuthToken now set from env OR the stored login)
|
|
745
745
|
|
|
746
746
|
// ── Pre-flight budget caps — spec docs/specs/2026-07-01-budget-caps.md (nexos steal #1).
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
// work. TP_BUDGET_OFF disables the whole path. `roomCapped` caches whether a cap exists so a
|
|
752
|
-
// transient ledger error fails CLOSED only for a room we KNOW is capped, and open otherwise.
|
|
747
|
+
// TP_BUDGET_OFF disables the spend-ledger path entirely (record_spend). The per-room output-
|
|
748
|
+
// token CAP was removed (2026-07) — redundant for BYOK (the provider enforces spend limits),
|
|
749
|
+
// a per-turn check_budget round-trip on every authed turn, and a fail-closed hazard. The
|
|
750
|
+
// spend LEDGER stays: it powers the Session-info "Session tokens" stat.
|
|
753
751
|
const BUDGET_OFF = process.env.TP_BUDGET_OFF === '1'
|
|
754
|
-
let roomCapped = false
|
|
755
|
-
function monthResetLabel () {
|
|
756
|
-
const d = new Date()
|
|
757
|
-
return new Date(d.getFullYear(), d.getMonth() + 1, 1).toISOString().slice(0, 10)
|
|
758
|
-
}
|
|
759
752
|
async function budgetRpc (fn, args) {
|
|
760
753
|
const r = await fetch(`${SUPABASE_URL}/rest/v1/rpc/${fn}`, {
|
|
761
754
|
method: 'POST',
|
|
@@ -828,36 +821,6 @@ function recordCodeUsage (model, usage) {
|
|
|
828
821
|
budgetRpc('record_code_usage', { p_room: room, p_model: model, p_input: input, p_output: output, p_cached: cached })
|
|
829
822
|
.catch(() => { /* best-effort — usage accounting is not load-bearing for the turn */ })
|
|
830
823
|
}
|
|
831
|
-
// Pre-flight gate. Returns true if the turn must be REFUSED (room cap reached). Refusal is
|
|
832
|
-
// legible (a ◆ control line both readers + late-joiners see), never a silent stall.
|
|
833
|
-
async function budgetBlocked (s, term) {
|
|
834
|
-
if (BUDGET_OFF || !codeAuthToken) return false
|
|
835
|
-
const refuse = (text) => {
|
|
836
|
-
const evt = { kind: 'control', text }
|
|
837
|
-
try { pushLog(s, evt) } catch { /* no session log */ }
|
|
838
|
-
bcast('code-event', { term, evt })
|
|
839
|
-
return true
|
|
840
|
-
}
|
|
841
|
-
try {
|
|
842
|
-
const rows = await budgetRpc('check_budget', { p_room: room })
|
|
843
|
-
const r = Array.isArray(rows) ? rows[0] : rows
|
|
844
|
-
roomCapped = !!(r && r.remaining_tokens != null)
|
|
845
|
-
if (r && r.ok === false) {
|
|
846
|
-
process.stderr.write(`\n ${A.yel}◆ turn refused — ${r.tightest_scope} budget cap reached (room ${room}).${A.rst}\n`)
|
|
847
|
-
return refuse(`◆ Turn blocked — this room hit its ${r.tightest_scope || 'budget'} cap. Resets ${monthResetLabel()}, or raise the cap.`)
|
|
848
|
-
}
|
|
849
|
-
return false
|
|
850
|
-
} catch (e) {
|
|
851
|
-
// Fail-closed ONLY where we know a cap exists — a transient ledger/DNS blip must never
|
|
852
|
-
// brick an UNCAPPED room (that would be a worse outage than the cap it's guarding).
|
|
853
|
-
if (roomCapped) {
|
|
854
|
-
process.stderr.write(`\n ${A.yel}◆ turn refused — budget check failed, room is capped (fail-closed).${A.rst}\n`)
|
|
855
|
-
return refuse('◆ Turn blocked — budget check is unavailable and this room has a cap set. Try again in a moment.')
|
|
856
|
-
}
|
|
857
|
-
process.stderr.write(`\n ${A.dim}◆ budget check skipped (${e.message}) — room uncapped, proceeding.${A.rst}\n`)
|
|
858
|
-
return false
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
824
|
// Throttled presence track() (presence.mjs) — a reconnect storm re-fires SUBSCRIBED and
|
|
862
825
|
// the handler re-track()s; without throttle that spirals into the per-client presence
|
|
863
826
|
// rate limit (ClientPresenceRateLimitReached). Lazy so it binds `channel` (the chain
|
|
@@ -1937,9 +1900,10 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1937
1900
|
// This opens a brand-new structured session server-side (the same path the
|
|
1938
1901
|
// web's "+ New terminal" and the restore loop use), so announce() surfaces
|
|
1939
1902
|
// its tab in the open web UI live. Auto-allowed by the PreToolUse gate but
|
|
1940
|
-
// bounded by spawnDecision (hop
|
|
1941
|
-
// TP_SPAWN_OFF kill-switch). A
|
|
1942
|
-
//
|
|
1903
|
+
// bounded by spawnDecision (depth-2 hop bound, burst window, the live terminal
|
|
1904
|
+
// cap, TP_SPAWN_OFF kill-switch). A person's lane (hop 0) may spawn conductors
|
|
1905
|
+
// (hop 1) which may spawn workers (hop 2); hop 2 can never spawn onward — the
|
|
1906
|
+
// fork-bomb breaker, now one level deeper (2026-07-10 cascade-spawn-depth spec).
|
|
1943
1907
|
tool(
|
|
1944
1908
|
'spawn_terminal',
|
|
1945
1909
|
'Open a NEW agent terminal (a fresh Claude lane) in this ThinkPool Code room — YOUR lane to use, not a sibling that is already working. Use it to fan work out: give it an initial `task` and it runs in parallel in its own lane; collect its result later with read_terminal, and close it with close_terminal when done. By default the new lane INHERITS your current permission mode — so if you are in bypass, it runs autonomously with no per-action clicking; pass `mode` to override. ALWAYS prefer spawning a fresh lane over post_to_terminal into one that is already busy. Tier guidance: default worker lanes to opus; use sonnet for mechanical work (renames, boilerplate, doc sweeps); reserve your own model for planning/review. Bounded by the room terminal cap and a per-turn limit.',
|
|
@@ -1984,9 +1948,11 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1984
1948
|
ne.spawnedBy = id // ownership: only the spawner may close_terminal it
|
|
1985
1949
|
announce() // (defensive) ensure the web renders the new tab live
|
|
1986
1950
|
if (args?.task) {
|
|
1987
|
-
// One
|
|
1988
|
-
//
|
|
1989
|
-
|
|
1951
|
+
// One level deeper than the spawner: hop = parent.hop + 1, so the depth-2
|
|
1952
|
+
// bound in spawnDecision binds at every level (hop-0 person's lane → hop-1
|
|
1953
|
+
// conductor → hop-2 worker, which can never spawn onward). A person speaking
|
|
1954
|
+
// to any lane still resets it to hop 0 (code-turn). Mirrors Tier C injection.
|
|
1955
|
+
ne.hop = (entry.hop || 0) + 1; ne.peekCount = 0; ne.postCount = 0; ne.spawnTimes = []
|
|
1990
1956
|
const msg = `[Task from terminal ${fromRef}'s agent — relayed via ThinkPool cross-terminal; you are a fresh lane it opened for you]\n${args.task}`
|
|
1991
1957
|
const evt = { kind: 'you', text: msg, by: `terminal ${fromRef} (agent)`, crosspost: true }
|
|
1992
1958
|
stampEvent(evt); pushLog(ne, evt); bcast('code-event', { term: newId, evt })
|
|
@@ -2244,8 +2210,9 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
2244
2210
|
const b = flowBudgets.get(entry.flowSessionId)
|
|
2245
2211
|
if (b) flowBudgets.set(entry.flowSessionId, recordSpend(b, evt.usage.output_tokens || 0))
|
|
2246
2212
|
}
|
|
2247
|
-
//
|
|
2248
|
-
//
|
|
2213
|
+
// Fold EVERY completed turn's output tokens into the room's durable monthly ledger
|
|
2214
|
+
// (lanes + normal turns) — powers the Session-info "Session tokens" stat. (The cap that
|
|
2215
|
+
// consumed this ledger was removed; the accounting stays.)
|
|
2249
2216
|
if (evt.kind === 'result' && evt.usage) { recordRoomSpend(evt.usage.output_tokens || 0); recordCodeUsage(evt.model, evt.usage) }
|
|
2250
2217
|
// FL-B1 (lane) — a flow lane that ENDS ITS TURN is done: a bypass lane runs its slice
|
|
2251
2218
|
// to completion in one turn, then narrates "done" and stops. Models often skip the
|
|
@@ -2870,10 +2837,6 @@ channel
|
|
|
2870
2837
|
// control lines (like /model) so both members AND late-joiners see it. Scope is honest —
|
|
2871
2838
|
// the Agent SDK exposes tokens + cost, not plan/rate-limit meters, and the card says so.
|
|
2872
2839
|
if (/^\/usage\s*$/.test(text)) { ctlLine(usageReportLine(s.model, s.log)); return }
|
|
2873
|
-
// Pre-flight budget gate (room scope) — refuse the billable agent turn if the room hit
|
|
2874
|
-
// its cap. The slash controls above (/model, /clear, /compact) are intentionally exempt:
|
|
2875
|
-
// they let a user manage or REDUCE spend even when capped. Legible refusal, no silent stall.
|
|
2876
|
-
if (await budgetBlocked(s, payload.term)) return
|
|
2877
2840
|
// Context-carry (2026-07-08) point 4: a real human turn arrived BEFORE the post-switch/
|
|
2878
2841
|
// restore recap fired on init-`system`. Prepend the recap to THIS turn (one turn, not two
|
|
2879
2842
|
// — a separate recap turn would race + double-fire) and clear the flag so the init handler
|
package/claude-session.mjs
CHANGED
|
@@ -503,7 +503,7 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
503
503
|
// hasn't already opted into). If the spawner is already in bypass, children inherit
|
|
504
504
|
// bypass with NO prompt (you opted in once); a non-bypass child gates itself per
|
|
505
505
|
// action anyway, so no spawn prompt there either. Bridge-side spawnDecision still
|
|
506
|
-
// enforces
|
|
506
|
+
// enforces the depth-2 hop bound / burst window / machineMax / TP_SPAWN_OFF regardless.
|
|
507
507
|
if (toolName === 'mcp__thinkpool__spawn_terminal') {
|
|
508
508
|
const childMode = toolInput?.mode || mode // inherit this lane's mode by default
|
|
509
509
|
const escalating = childMode === 'bypassPermissions' && mode !== 'bypassPermissions'
|
|
@@ -690,7 +690,7 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
690
690
|
'RUN IT, DO NOT ASK: verify your own work before calling it done. Actually run or serve what you changed, observe that it behaves correctly, and show the evidence in the room — a screenshot of the running result, the passing test output, or the real response — rather than telling the user "this should work, go test it." If you could not verify something, say exactly what is unverified. This room follows verify-before-claiming: runtime evidence you produced, not assertion.',
|
|
691
691
|
'CROSS-TERMINAL AWARENESS: this room may have other terminals open alongside yours — other agents working, or shells the people are driving. You have a READ-ONLY tool, read_terminal: call it with no arguments to list the other open terminals, or with a terminal ref/id/command to read that terminal\'s recent activity. Reach for it when your work depends on what another terminal is doing (e.g. someone says "see what the other terminal hit", or you need to coordinate with a sibling agent before acting). It only ever reads — it never changes another terminal. Identify a terminal by its NAME or its ref/id from the roster, never by an on-screen number like "Terminal 2" — those positional labels renumber when a terminal is closed, so they do not reliably point at a lane.',
|
|
692
692
|
'CROSS-TERMINAL HAND-OFF: you also have post_to_terminal(terminal, text) to send a message or task to ANOTHER AGENT terminal in this room (not a plain shell). Use it sparingly and only when the people clearly want the lanes to coordinate — e.g. "tell the backend terminal the API is ready", or to hand a sibling agent a concrete task. Every post requires a person in the room to approve a card before it is delivered, and an agent that was itself reached via a cross-post cannot post onward — so do not rely on it for chit-chat or loops. Prefer read_terminal to understand a sibling before you ever post to it.',
|
|
693
|
-
'CROSS-TERMINAL SPAWN: when a person asks you to fan work out across multiple agents — research lanes, parallel sub-tasks, a swarm — open YOUR OWN fresh lanes with spawn_terminal(name?, task?, model?); do NOT dump the work into siblings that are already busy (a sibling is mid-task unless read_terminal shows it idle, and hijacking it derails their work). spawn_terminal opens a brand-new Claude lane and, if you pass `task`, hands it that task immediately; it appears as a new terminal in the room. A spawned lane INHERITS your current permission mode by default — so if you are in bypass, your lanes run autonomously with no per-action clicking; pass `mode` only to override. Raising a lane to bypass from a non-bypass lane asks the room to confirm once (everything else spawns without a prompt). Collect each lane\'s result later with read_terminal (a spawned lane cannot post back to you), and tidy up with close_terminal once you have what you need — you may only close lanes you spawned yourself. You are bounded: a
|
|
693
|
+
'CROSS-TERMINAL SPAWN: when a person asks you to fan work out across multiple agents — research lanes, parallel sub-tasks, a swarm — open YOUR OWN fresh lanes with spawn_terminal(name?, task?, model?); do NOT dump the work into siblings that are already busy (a sibling is mid-task unless read_terminal shows it idle, and hijacking it derails their work). spawn_terminal opens a brand-new Claude lane and, if you pass `task`, hands it that task immediately; it appears as a new terminal in the room. A spawned lane INHERITS your current permission mode by default — so if you are in bypass, your lanes run autonomously with no per-action clicking; pass `mode` only to override. Raising a lane to bypass from a non-bypass lane asks the room to confirm once (everything else spawns without a prompt). Collect each lane\'s result later with read_terminal (a spawned lane cannot post back to you), and tidy up with close_terminal once you have what you need — you may only close lanes you spawned yourself. You are bounded: a sliding spawn-burst window, a per-plan dispatch ceiling on concurrent spawned lanes (Free 3 / Plus 6) that is separate from the human terminals so it never blocks them, an overall machine cap on live lanes, and a DEPTH bound — a lane opened by a person may spawn workers, and those workers may spawn one further level (a conductor dispatching its crew), but that second level can never spawn onward (so no runaway). This is how you "open the terminals yourself" instead of asking a person to.',
|
|
694
694
|
'CROSS-SESSION AWARENESS: the Ensemble reaches across your SESSIONS, not just the terminals in this room. list_sessions() lists your OTHER ThinkPool Code rooms — both your own rooms running on this machine AND your partner\'s rooms in the same pair, reachable over the per-pair bus (a room on the partner\'s machine shows its host). read_session(session, terminal?) reads recent activity inside one (omit `terminal` to list that room\'s terminals, or pass a ref/name to read that lane). Both are READ-ONLY — they never change another session, and they reach ONLY your own rooms and rooms you share with your partner, never a stranger\'s. Reach for them when work spans rooms — "what\'s the other project up to", "pick up where the other session left off", or to check a long-running task elsewhere before you act here.',
|
|
695
695
|
'CROSS-SESSION HAND-OFF: post_to_session(session, text, terminal?) sends a task or message to an agent in ANOTHER of your rooms — your own, or your partner\'s over the pair bus. Use it sparingly and only when the people clearly want the rooms to coordinate — e.g. hand the API room\'s agent a concrete follow-up once the frontend is ready. It is dual-consent: a person in YOUR room approves sending, and a person in the TARGET room approves receiving, before anything is delivered — so never rely on it for chit-chat or loops, and an agent that was itself reached via a cross-room post cannot post onward to a third room. It spends real model tokens in the other room (maybe on the other person\'s machine), so prefer read_session to understand a room before you ever post into it, and only post one concrete hand-off at a time. All of this works only under the ThinkPool account bridge; a standalone room sees just its own terminals.',
|
|
696
696
|
'SUBAGENT POLICY: in this room, delegated work goes through the Ensemble. When you fan out research, parallel sub-tasks, reviews, or any substantive multi-step delegated work, open spawn_terminal lanes — they are visible in the room, the people can watch and steer them, and every other lane can peer at their work. Do NOT reach for the built-in Task/Agent subagents for that work: an in-process subagent is invisible to the room, cannot be peered at or steered, and its work is lost to the Ensemble. The only exception is a trivial, seconds-scale read-only lookup where a room lane would be pure overhead.',
|
package/cross-terminal.mjs
CHANGED
|
@@ -240,12 +240,21 @@ export const crossPostNeedsCard = ({ mode = 'default', spawnedByMe = false } = {
|
|
|
240
240
|
// this stays a PURE function (no Date.now inside; the clock is injected, like
|
|
241
241
|
// spawnCount was before).
|
|
242
242
|
export const SPAWN = {
|
|
243
|
-
maxHop:
|
|
244
|
-
|
|
243
|
+
maxHop: 2, // human turn = hop 0; its spawned lane (hop 1, a "conductor") MAY spawn
|
|
244
|
+
// workers (hop 2); hop ≥ 2 can never spawn onward. Depth-2 cascades,
|
|
245
|
+
// fork-bomb still impossible by construction — every level also passes
|
|
246
|
+
// machineMax + the plan ceiling + the burst window + the kill-switch.
|
|
247
|
+
// Was 1 until 2026-07-10 (docs/specs/2026-07-10-cascade-spawn-depth.md):
|
|
248
|
+
// conductors dispatched by a person's lane could not dispatch at all.
|
|
249
|
+
perWindowCap: 6, // spawn_terminal calls allowed per rolling window (burst breaker);
|
|
250
|
+
// 6 matches plusMaxSpawned so one wave can open a full crew (was 4)
|
|
245
251
|
windowMs: 10 * 60 * 1000, // sliding window over which perWindowCap applies (10 min)
|
|
246
252
|
freeMaxSpawned: 3, // Free plan dispatch ceiling (concurrent spawned lanes)
|
|
247
253
|
plusMaxSpawned: 6, // Plus plan dispatch ceiling
|
|
248
|
-
machineMax:
|
|
254
|
+
machineMax: 12, // hard cap on TOTAL live lanes (the laptop/runaway backstop). 10 → 12
|
|
255
|
+
// 2026-07-10: finished-but-open lanes were starving slots. NOT higher:
|
|
256
|
+
// at ~10 live lanes CPU contention corrupted Lighthouse runs that day —
|
|
257
|
+
// this cap protects the host and measurement quality, not just runaway.
|
|
249
258
|
}
|
|
250
259
|
|
|
251
260
|
// Plan → dispatch ceiling. Anything not 'plus' is treated as Free (the safe default).
|