thinkpool-pair 0.7.301 → 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/account.mjs +22 -3
- package/bridge.mjs +31 -1
- package/package.json +1 -1
- package/serve-consent.mjs +22 -2
- package/side-lane.mjs +21 -0
package/account.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { saveAuth, loadAuth, loadDirs, bindDir, loadServed, rememberServed, load
|
|
|
17
17
|
import { isSafeToRestart } from './update-gate.mjs'
|
|
18
18
|
import { makeThrottledTrack, presenceSelfEchoVerdict } from './presence.mjs'
|
|
19
19
|
import { publicKeyB64, announceProviders, listProviders, addProvider, addProviderModel, removeProvider, unseal } from './providers.mjs'
|
|
20
|
-
import { supervisorServes } from './serve-consent.mjs'
|
|
20
|
+
import { supervisorServes, supervisorRoomsToStop } from './serve-consent.mjs'
|
|
21
21
|
import { resolveServeDir } from './serve-dir.mjs'
|
|
22
22
|
import { pairKeyFor, pairTopic, CROSSROOM_BUS } from './cross-terminal.mjs'
|
|
23
23
|
import { installedAgentCommands } from './agent-detect.mjs'
|
|
@@ -798,6 +798,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
798
798
|
return
|
|
799
799
|
}
|
|
800
800
|
let rooms = []
|
|
801
|
+
let discoverySucceeded = false
|
|
801
802
|
const me = session.user.id
|
|
802
803
|
try {
|
|
803
804
|
// Discovery (Contract C-CODE-2, owner-only since 2026-07-06): a host bridges ONLY
|
|
@@ -805,20 +806,38 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
805
806
|
// keeps rooms you merely JOINED as a guest out of auto-serve — your machine never
|
|
806
807
|
// spawns into another owner's session (cross-person serving removed; pairing is
|
|
807
808
|
// read-only peer peek, not hosting). The web watches tpacct:<session-owner>.
|
|
808
|
-
const { data } = await sb.from('code_sessions')
|
|
809
|
+
const { data, error } = await sb.from('code_sessions')
|
|
810
|
+
.select('code,name,last_active_at,owner_id,participants,closed_at')
|
|
811
|
+
.eq('owner_id', me)
|
|
812
|
+
.is('closed_at', null)
|
|
813
|
+
.order('last_active_at', { ascending: false })
|
|
814
|
+
.limit(20)
|
|
815
|
+
if (error) throw error
|
|
809
816
|
rooms = data || []
|
|
817
|
+
discoverySucceeded = true
|
|
810
818
|
// Prune refused entries for rooms we can no longer serve — owned OR granted (only
|
|
811
819
|
// on a SUCCESSFUL fetch, so a transient query error never wipes the set).
|
|
812
820
|
const servableCodes = new Set(rooms.map((r) => r.code))
|
|
813
821
|
for (const c of [...refused.keys()]) if (!servableCodes.has(c)) refused.delete(c)
|
|
814
822
|
} catch { /* transient — keep existing children, retry next tick */ }
|
|
823
|
+
// The realtime shutdown broadcast is an acceleration, never the lifecycle
|
|
824
|
+
// authority. Reconcile every successful discovery read so a bridge that missed
|
|
825
|
+
// `session-deleted` still stops after its room is closed, deleted, or transferred.
|
|
826
|
+
if (discoverySucceeded) {
|
|
827
|
+
for (const room of supervisorRoomsToStop({ rooms, childRooms: children.keys(), myUid: me })) {
|
|
828
|
+
const child = children.get(room)
|
|
829
|
+
if (!child) continue
|
|
830
|
+
console.log(`child_retire sup=${SUP_ID} room=${room} pid=${child.pid || '?'} reason=not-active-owned`)
|
|
831
|
+
try { child.kill('SIGTERM') } catch { /* child exit reconciliation retries next tick */ }
|
|
832
|
+
}
|
|
833
|
+
}
|
|
815
834
|
const dirs = loadDirs()
|
|
816
835
|
const served = loadServed()
|
|
817
836
|
for (const r of rooms) {
|
|
818
837
|
const room = r.code
|
|
819
838
|
if (!room || children.has(room)) { if (room) refused.delete(room); continue } // served → not refused
|
|
820
839
|
// Defense-in-depth behind the owner-only query above: serve iff we OWN the room.
|
|
821
|
-
if (!supervisorServes({ ownerId: r.owner_id, myUid: me })) continue
|
|
840
|
+
if (!supervisorServes({ ownerId: r.owner_id, myUid: me, closedAt: r.closed_at })) continue
|
|
822
841
|
// Resolve the serve dir: explicit bind > last-served memory > launch dir. REFUSE
|
|
823
842
|
// to auto-serve an unbound room from $HOME — that strips the agent's resume context
|
|
824
843
|
// (cwd-keyed SDK sessions) and shows your home folder as the repo (FVIHV1DE).
|
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
package/serve-consent.mjs
CHANGED
|
@@ -38,9 +38,29 @@ export function serveDecision ({ ownerId, myUid }) {
|
|
|
38
38
|
// defense-in-depth per-row check behind the supervisor's owner-only `.eq(owner_id, me)`
|
|
39
39
|
// query — a broad RLS row (owned OR merely participant) must still resolve to OWNED
|
|
40
40
|
// before a child is spawned. Contract C-CODE-2 (owner-only).
|
|
41
|
-
export function supervisorServes ({ ownerId, myUid }) {
|
|
41
|
+
export function supervisorServes ({ ownerId, myUid, closedAt = null }) {
|
|
42
42
|
if (!myUid) return false
|
|
43
|
-
return ownerId === myUid
|
|
43
|
+
return ownerId === myUid && !closedAt
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// supervisorRoomsToStop — reconcile the long-lived child map after a SUCCESSFUL
|
|
47
|
+
// discovery read. Closing, deleting, or transferring a room removes it from the
|
|
48
|
+
// active-owned result set; any existing child for that room must be retired even
|
|
49
|
+
// when the best-effort `session-deleted` realtime broadcast was missed.
|
|
50
|
+
//
|
|
51
|
+
// This stays pure so a transient discovery failure can be handled by the caller:
|
|
52
|
+
// account.mjs invokes it only after Supabase returned without an error. On a failed
|
|
53
|
+
// fetch the existing children stay alive and the next tick retries.
|
|
54
|
+
export function supervisorRoomsToStop ({ rooms, childRooms, myUid }) {
|
|
55
|
+
const activeOwned = new Set((rooms || [])
|
|
56
|
+
.filter((room) => supervisorServes({
|
|
57
|
+
ownerId: room?.owner_id,
|
|
58
|
+
myUid,
|
|
59
|
+
closedAt: room?.closed_at,
|
|
60
|
+
}))
|
|
61
|
+
.map((room) => room.code)
|
|
62
|
+
.filter(Boolean))
|
|
63
|
+
return [...(childRooms || [])].filter((code) => !activeOwned.has(code))
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
// fetchServeRow — read the room's owner with whatever identity the bridge holds
|
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
|
+
}
|