thinkpool-pair 0.7.287 → 0.7.289
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 +10 -3
- package/bridge.mjs +70 -3
- package/package.json +1 -1
package/account.mjs
CHANGED
|
@@ -437,6 +437,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
437
437
|
const POST_BUCKET = { max: 6, windowMs: 60_000 } // ≤6 inbound cross-room posts / partner / minute
|
|
438
438
|
let applyRequested = false // a user clicked "apply" in some room (Slice 3)
|
|
439
439
|
let pendingUpdate = null // newest published version once the poll sees it
|
|
440
|
+
let applyingUpdate = false
|
|
440
441
|
const warned = new Set()
|
|
441
442
|
const refused = new Map() // room -> reason ('home'|'none'|'dir-gone'): owned but NOT served, announced in presence so the web shows a precise "attach this session" card instead of spinning
|
|
442
443
|
|
|
@@ -1126,10 +1127,16 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
1126
1127
|
// Restart the account bridge to apply a pending update ONLY when it's safe: every
|
|
1127
1128
|
// child idle (between turns) AND either a user clicked apply OR nobody's watching
|
|
1128
1129
|
// (unattended fallback). The predicate is unit-tested (tests/update-gate.test.mjs).
|
|
1129
|
-
const applyIfIdle = () => {
|
|
1130
|
-
if (!isSafeToRestart({ pendingUpdate, stopping, childIdle, childBetween, childPeer, requested: applyRequested })) return
|
|
1130
|
+
const applyIfIdle = async () => {
|
|
1131
|
+
if (applyingUpdate || !isSafeToRestart({ pendingUpdate, stopping, childIdle, childBetween, childPeer, requested: applyRequested })) return
|
|
1132
|
+
applyingUpdate = true
|
|
1131
1133
|
process.stderr.write(`\n ◆ thinkpool-pair ${pendingUpdate} ready (running ${VERSION}) — restarting account bridge to update; sessions resume.\n`)
|
|
1132
|
-
|
|
1134
|
+
try {
|
|
1135
|
+
const svc = await import('./service.mjs')
|
|
1136
|
+
if (svc.serviceActive(null) && svc.updateService(null) !== false) return
|
|
1137
|
+
} catch { /* fall back to the legacy supervised restart below */ }
|
|
1138
|
+
applyingUpdate = false
|
|
1139
|
+
stop('SIGTERM')
|
|
1133
1140
|
}
|
|
1134
1141
|
|
|
1135
1142
|
// ── auto-update (account-service tier only) ───────────────────────────────
|
package/bridge.mjs
CHANGED
|
@@ -1009,6 +1009,7 @@ const markActivity = () => { lastActivity = Date.now() }
|
|
|
1009
1009
|
// ready" chip; the restart is gated to between turns (never mid-turn, Contract #1).
|
|
1010
1010
|
let pendingUpdate = null
|
|
1011
1011
|
let applyRequested = false // a user clicked "apply" in the room
|
|
1012
|
+
let managedUpdateRunning = false
|
|
1012
1013
|
// A web client (not another bridge) is present — checks presence metas for a
|
|
1013
1014
|
// non-bridge role. Gates the unattended auto-restart: when someone's watching we
|
|
1014
1015
|
// wait for their apply click instead of restarting under them.
|
|
@@ -3466,6 +3467,36 @@ function surfaceUpdate(version) {
|
|
|
3466
3467
|
announce()
|
|
3467
3468
|
}
|
|
3468
3469
|
|
|
3470
|
+
// A pinned managed room must stage the published runtime before it restarts.
|
|
3471
|
+
// Exiting and trusting launchd/systemd to "pick up latest" only reloads the same
|
|
3472
|
+
// immutable version. This is the room-scoped counterpart to account.mjs's
|
|
3473
|
+
// dashboard updater and is deliberately shared by the session and dashboard UI.
|
|
3474
|
+
const betweenUpdateTurns = () => {
|
|
3475
|
+
if (turnInFlight(sessions)) return false
|
|
3476
|
+
for (const t of terms.values()) if (t.buf) return false
|
|
3477
|
+
return true
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
async function applyStandaloneManagedUpdate() {
|
|
3481
|
+
if (!applyRequested || !pendingUpdate || managedUpdateRunning) return false
|
|
3482
|
+
if (process.send && process.env.THINKPOOL_PAIR_ACCOUNT_CHILD === '1') return false
|
|
3483
|
+
if (!betweenUpdateTurns()) return false
|
|
3484
|
+
managedUpdateRunning = true
|
|
3485
|
+
try {
|
|
3486
|
+
const svc = await import('./service.mjs')
|
|
3487
|
+
if (!svc.serviceActive(room)) return false
|
|
3488
|
+
const ok = svc.updateService(room) !== false
|
|
3489
|
+
if (!ok) managedUpdateRunning = false
|
|
3490
|
+
return ok
|
|
3491
|
+
} catch (error) {
|
|
3492
|
+
managedUpdateRunning = false
|
|
3493
|
+
process.stderr.write(`\n ⚠ bridge update failed: ${error?.message || error}\n`)
|
|
3494
|
+
return false
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
setInterval(() => { if (applyRequested) void applyStandaloneManagedUpdate() }, 5000).unref()
|
|
3499
|
+
|
|
3469
3500
|
// After the attached CLI exits, the host's stdin stops feeding a PTY —
|
|
3470
3501
|
// restore the cooked terminal so Ctrl-C reaches the bridge itself.
|
|
3471
3502
|
const detachLocal = () => {
|
|
@@ -3807,8 +3838,8 @@ channel
|
|
|
3807
3838
|
// inferred "working…" state (no turn → no result to clear it; the old bug).
|
|
3808
3839
|
// Instead announce a ◆ control line both readers see. Claude /compact + any
|
|
3809
3840
|
// other slash still flow through as a real, self-terminating turn.
|
|
3810
|
-
const ctlLine = (ctlText) => {
|
|
3811
|
-
const evt = { kind: 'control', text: ctlText, by: payload.by, cid: payload.cid }
|
|
3841
|
+
const ctlLine = (ctlText, details = {}) => {
|
|
3842
|
+
const evt = { kind: 'control', text: ctlText, by: payload.by, cid: payload.cid, ...details }
|
|
3812
3843
|
pushLog(s, evt)
|
|
3813
3844
|
bcast('code-event', { term: payload.term, evt })
|
|
3814
3845
|
}
|
|
@@ -3904,7 +3935,7 @@ channel
|
|
|
3904
3935
|
if (/^\/compact\s*$/.test(text)) {
|
|
3905
3936
|
if (s.runtime === 'codex') {
|
|
3906
3937
|
if (s.session.turnActive) {
|
|
3907
|
-
ctlLine('
|
|
3938
|
+
ctlLine('Compact after this turn ends.', { tone: 'caution' })
|
|
3908
3939
|
return
|
|
3909
3940
|
}
|
|
3910
3941
|
const recap = buildRecapFromLog(s.log, RECAP_CAP, { reason: 'compact' })
|
|
@@ -4116,6 +4147,42 @@ channel
|
|
|
4116
4147
|
try { process.send({ t: 'apply-update' }) } catch { /* parent gone */ }
|
|
4117
4148
|
}
|
|
4118
4149
|
})
|
|
4150
|
+
// Authenticated + acknowledged update contract used by both the room card and
|
|
4151
|
+
// the dashboard's room-scoped fallback. Unlike the legacy apply-update event,
|
|
4152
|
+
// this does not depend on the bridge having discovered npm first: the web names
|
|
4153
|
+
// the advertised target, the host updater independently resolves npm latest,
|
|
4154
|
+
// and only a managed service is allowed to accept the operation.
|
|
4155
|
+
.on('broadcast', { event: 'bridge-update' }, async ({ payload }) => {
|
|
4156
|
+
const nonce = payload?.nonce
|
|
4157
|
+
const reply = (ok, error, state) => channel.send({
|
|
4158
|
+
type: 'broadcast', event: 'bridge-update-res',
|
|
4159
|
+
payload: { nonce, ok, ...(error ? { error } : {}), ...(state ? { state } : {}) },
|
|
4160
|
+
})
|
|
4161
|
+
if (!nonce || !(await isRoomParticipant(payload?.jwt))) return reply(false, 'unauthorized')
|
|
4162
|
+
const target = typeof payload?.v === 'string' && /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(payload.v)
|
|
4163
|
+
? payload.v : null
|
|
4164
|
+
if (!target) return reply(false, 'invalid bridge version')
|
|
4165
|
+
|
|
4166
|
+
surfaceUpdate(target)
|
|
4167
|
+
applyRequested = true
|
|
4168
|
+
if (process.send && process.env.THINKPOOL_PAIR_ACCOUNT_CHILD === '1') {
|
|
4169
|
+
try { process.send({ t: 'apply-update' }) } catch { return reply(false, 'account supervisor is unavailable') }
|
|
4170
|
+
return reply(true, null, 'queued')
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
let managed = false
|
|
4174
|
+
try {
|
|
4175
|
+
const svc = await import('./service.mjs')
|
|
4176
|
+
managed = svc.serviceActive(room)
|
|
4177
|
+
} catch { /* reported as an explicit foreground refusal below */ }
|
|
4178
|
+
if (!managed) {
|
|
4179
|
+
applyRequested = false
|
|
4180
|
+
return reply(false, 'This bridge is running in a terminal. Restart it once with npx thinkpool-pair@latest, or install the background service to enable remote updates.')
|
|
4181
|
+
}
|
|
4182
|
+
if (!betweenUpdateTurns()) return reply(true, null, 'queued')
|
|
4183
|
+
await reply(true, null, 'applying')
|
|
4184
|
+
void applyStandaloneManagedUpdate()
|
|
4185
|
+
})
|
|
4119
4186
|
// Persist + re-announce terminal renames. The web also echoes term-rename to
|
|
4120
4187
|
// online peers directly; storing it here is what reaches a device that joins
|
|
4121
4188
|
// LATER (or a second machine) — those only ever see the announce.
|