thinkpool-pair 0.7.188 → 0.7.189
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 +11 -3
- package/claude-session.mjs +1 -1
- package/cross-terminal.mjs +23 -0
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -92,7 +92,7 @@ const flowRedispatch = new Map()
|
|
|
92
92
|
// wave BEFORE overrun. Lives bridge-side because waves dispatch across separate
|
|
93
93
|
// broadcasts; without persistent state the cap can never bite.
|
|
94
94
|
const flowBudgets = new Map()
|
|
95
|
-
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow } from './cross-terminal.mjs'
|
|
95
|
+
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint } from './cross-terminal.mjs'
|
|
96
96
|
import { turnInFlight } from './update-gate.mjs'
|
|
97
97
|
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage } from './session-store.mjs'
|
|
98
98
|
import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks, usageReportLine, buildRecapFromLog, RECAP_CAP, trimmedBeforeSeq, firstSeq } from './event-id.mjs'
|
|
@@ -1935,7 +1935,7 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1935
1935
|
// fork-bomb breaker, now one level deeper (2026-07-10 cascade-spawn-depth spec).
|
|
1936
1936
|
tool(
|
|
1937
1937
|
'spawn_terminal',
|
|
1938
|
-
'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,
|
|
1938
|
+
'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, then ALWAYS close_terminal it — a finished lane left open holds a slot against the room cap and blocks the next fan-out. 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.',
|
|
1939
1939
|
{
|
|
1940
1940
|
name: z.string().max(80).optional().describe('a short label for the new lane so the room (and you) can find it, e.g. "Research: topologies"'),
|
|
1941
1941
|
task: z.string().optional().describe('an initial task to hand the new lane immediately; omit to open it idle'),
|
|
@@ -1955,7 +1955,15 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1955
1955
|
plan: ownerPlan, // Free 3 / Plus 6 dispatch ceiling
|
|
1956
1956
|
disabled: process.env.TP_SPAWN_OFF === '1',
|
|
1957
1957
|
})
|
|
1958
|
-
|
|
1958
|
+
// A refusal that says "close one" without saying WHICH one sends the caller
|
|
1959
|
+
// hunting. Adapt the live roster to the pure helper: only lanes THIS caller
|
|
1960
|
+
// spawned and that aren't mid-turn — the exact set close_terminal accepts.
|
|
1961
|
+
if (!gate.ok) return okText(gate.reason + formatClosableHint(id, [...sessions.entries()].map(([sid, s]) => ({
|
|
1962
|
+
ref: String(sid).slice(0, 8),
|
|
1963
|
+
name: termNames[sid] || undefined,
|
|
1964
|
+
spawnedBy: s.spawnedBy,
|
|
1965
|
+
busy: s.session?.turnActive ?? false,
|
|
1966
|
+
}))))
|
|
1959
1967
|
// Record this spawn, pruning timestamps that have already left the window
|
|
1960
1968
|
// (identical predicate to recentSpawnCount) so the array can't grow unbounded.
|
|
1961
1969
|
entry.spawnTimes = [...(entry.spawnTimes || []).filter((t) => Number.isFinite(t) && t > now - SPAWN.windowMs), now]
|
package/claude-session.mjs
CHANGED
|
@@ -700,7 +700,7 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
700
700
|
'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.',
|
|
701
701
|
'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.',
|
|
702
702
|
'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.',
|
|
703
|
-
'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),
|
|
703
|
+
'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). CLOSING IS PART OF THE JOB, not housekeeping you get to skip: the moment you have read a lane\'s result and acted on it, close_terminal it. A finished lane left open holds a slot against the room\'s machine cap and against your dispatch ceiling, so the NEXT fan-out — yours, or a person\'s — gets refused; rooms silt up with dead lanes exactly this way. Before each new wave of spawns, list your open lanes (read_terminal with no argument) and close the finished ones first. If a spawn is refused for capacity, the refusal names your own idle lanes so you do not have to hunt. You may only close lanes you spawned yourself — never close a sibling\'s, and never close one that is mid-turn. 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.',
|
|
704
704
|
'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.',
|
|
705
705
|
'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.',
|
|
706
706
|
'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
|
@@ -284,6 +284,29 @@ export const spawnDecision = ({ hop = 0, spawnTimes = [], now = 0, spawnedLive =
|
|
|
284
284
|
return { ok: true }
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
// When a spawn is refused, "close one before opening another" is useless advice if the
|
|
288
|
+
// caller has to go hunting for WHICH one. Rooms fill up with finished lanes nobody
|
|
289
|
+
// closed (Max, 2026-07-10: "clean up the finished ensemble terminals there's a lot"),
|
|
290
|
+
// because the old preamble asked politely and nothing named the cost. So the refusal
|
|
291
|
+
// now names the caller's OWN idle spawned lanes — the exact ones close_terminal will
|
|
292
|
+
// accept — newest last, capped so a long list can't drown the reason above it.
|
|
293
|
+
//
|
|
294
|
+
// Pure: takes the roster, returns a string. `lanes` = [{ ref, name, spawnedBy, busy }].
|
|
295
|
+
// Only lanes this caller spawned are suggested: close_terminal refuses the others, and
|
|
296
|
+
// suggesting a sibling's lane is how you get an agent killing someone else's work.
|
|
297
|
+
export const CLOSABLE_HINT_MAX = 6
|
|
298
|
+
export const closableLanes = (callerId, lanes = []) =>
|
|
299
|
+
(Array.isArray(lanes) ? lanes : []).filter((l) => l && l.spawnedBy === callerId && !l.busy)
|
|
300
|
+
|
|
301
|
+
export const formatClosableHint = (callerId, lanes = [], max = CLOSABLE_HINT_MAX) => {
|
|
302
|
+
const idle = closableLanes(callerId, lanes)
|
|
303
|
+
if (!idle.length) return ''
|
|
304
|
+
const shown = idle.slice(0, max)
|
|
305
|
+
const list = shown.map((l) => `${l.ref}${l.name ? ` ("${l.name}")` : ''}`).join(', ')
|
|
306
|
+
const more = idle.length > shown.length ? ` (+${idle.length - shown.length} more)` : ''
|
|
307
|
+
return `\n\nYou have ${idle.length} finished lane${idle.length === 1 ? '' : 's'} of your own still open — collect ${idle.length === 1 ? 'it' : 'them'} with read_terminal, then close_terminal: ${list}${more}`
|
|
308
|
+
}
|
|
309
|
+
|
|
287
310
|
// ── Tier 1 cross-ROOM peek (read-only, same-machine) ─────────────────────────
|
|
288
311
|
// Thinkpool Ensemble expanded from cross-TERMINAL (sibling lanes in one room) to
|
|
289
312
|
// cross-SESSION (the account's OTHER rooms served by the same account supervisor).
|