thinkpool-pair 0.7.262 → 0.7.264

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 CHANGED
@@ -3104,17 +3104,20 @@ function cancelPendingDurablePermissions(s, permissionIds = null, reason = 'perm
3104
3104
  })
3105
3105
  }
3106
3106
 
3107
- async function cancelRestoredDurablePermissions(terminalIds) {
3108
- const terminals = [...new Set((terminalIds || []).filter(Boolean))]
3109
- await Promise.all(terminals.map(async (terminalId) => {
3110
- const result = await cancelDurablePermissions({
3111
- supabaseUrl: SUPABASE_URL, anonKey: SUPABASE_ANON, token: codeAuthToken, roomCode: room,
3112
- // New bridge process, old durable bridge authority: filter by the
3113
- // restored terminal and let the cleanup read each historical binding.
3114
- terminalId, reason: 'bridge_restarted',
3115
- })
3116
- if (!result.ok && process.env.TP_DEBUG) process.stderr.write(`\n ◇ restored permission cleanup incomplete (${result.code})\n`)
3117
- }))
3107
+ let restoredPermissionCleanupPending = false
3108
+
3109
+ async function cancelRestoredDurablePermissions() {
3110
+ restoredPermissionCleanupPending = true
3111
+ const result = await cancelDurablePermissions({
3112
+ supabaseUrl: SUPABASE_URL, anonKey: SUPABASE_ANON, token: codeAuthToken, roomCode: room,
3113
+ // A new process gets a fresh bridge id. Every approval bound to any older
3114
+ // id has lost its process-local resolver, including archived terminals that
3115
+ // no longer have a session file and therefore never enter loadAll().
3116
+ currentBridgeId: BRIDGE_ID, reason: 'bridge_restarted',
3117
+ })
3118
+ if (result.ok) restoredPermissionCleanupPending = false
3119
+ else if (process.env.TP_DEBUG) process.stderr.write(`\n ◇ restored permission cleanup incomplete (${result.code})\n`)
3120
+ return result
3118
3121
  }
3119
3122
 
3120
3123
  function drainPending(s) {
@@ -4059,7 +4062,7 @@ channel
4059
4062
  // Cancel their durable dispatch rows BEFORE opening a restored SDK
4060
4063
  // session, so a new permission raised during resume can never be
4061
4064
  // mistaken for a pre-restart card from the same terminal.
4062
- await cancelRestoredDurablePermissions(all.map((rec) => rec.id))
4065
+ await cancelRestoredDurablePermissions()
4063
4066
  if (all.length) for (const rec of all) {
4064
4067
  const wasInterrupted = restoredTurnOpen(rec.log || [])
4065
4068
  // Codex can persist a thread id before its rollout receives the
@@ -4601,6 +4604,10 @@ if (process.env.THINKPOOL_PAIR_AUTOUPDATE === '1' && VERSION) {
4601
4604
  if (!ok && codeAuthToken) process.stderr.write('\n ⚠ realtime auth refresh failed; private bridge features may need a reconnect.\n')
4602
4605
  })
4603
4606
  refreshOwnerPlan()
4607
+ // A restart sweep may have raced an expiring owner JWT. Keep failed
4608
+ // room sweep queued until the supervisor hands us its next fresh token,
4609
+ // then retry without leaving archived-terminal questions behind.
4610
+ if (restoredPermissionCleanupPending) void cancelRestoredDurablePermissions()
4604
4611
  }
4605
4612
  })
4606
4613
  setInterval(() => { try { process.send({ t: 'idle', idle: idleNow(), between: betweenTurns(), webPeer: webPeerPresent() }) } catch { /* parent gone */ } }, 5000).unref()
@@ -9,14 +9,17 @@ const SETTLED = new Set(['denied', 'idempotent_replay', 'already_resolved', 'sta
9
9
 
10
10
  const safeString = (value) => typeof value === 'string' && value.length > 0
11
11
 
12
- export function pendingDurablePermissionItems (items, { bridgeId = null, terminalId, permissionIds = null, actionKinds = ['approval', 'dispatch'] } = {}) {
13
- if ((bridgeId != null && !safeString(bridgeId)) || !safeString(terminalId)) return []
12
+ export function pendingDurablePermissionItems (items, { bridgeId = null, currentBridgeId = null, terminalId = null, permissionIds = null, actionKinds = ['approval', 'dispatch'] } = {}) {
13
+ if ((bridgeId != null && !safeString(bridgeId)) || (currentBridgeId != null && !safeString(currentBridgeId)) ||
14
+ (terminalId != null && !safeString(terminalId)) || (terminalId == null && currentBridgeId == null)) return []
14
15
  const requested = permissionIds == null ? null : new Set(permissionIds.filter(safeString))
15
16
  const actions = new Set(actionKinds)
16
17
  return (Array.isArray(items) ? items : []).filter((item) => {
17
18
  const permissionId = item?.request_context?.permission_id
18
19
  return PENDING.has(item?.status) && item?.item_kind === 'approval' && actions.has(item?.action_kind) &&
19
- (!bridgeId || item?.bridge_authority_id === bridgeId) && item?.local_authority_id === terminalId &&
20
+ (!bridgeId || item?.bridge_authority_id === bridgeId) &&
21
+ (!currentBridgeId || item?.bridge_authority_id !== currentBridgeId) &&
22
+ (!terminalId || item?.local_authority_id === terminalId) &&
20
23
  safeString(permissionId) && (!requested || requested.has(permissionId))
21
24
  })
22
25
  }
@@ -37,10 +40,13 @@ const rpc = async ({ fetchImpl, supabaseUrl, headers, name, body }) => {
37
40
  // room-member identity. Never accept an id supplied by a public broadcast.
38
41
  export async function cancelDurablePermissions ({
39
42
  fetchImpl = globalThis.fetch, supabaseUrl, anonKey, token, roomCode, bridgeId,
40
- terminalId, permissionIds = null, reason = 'permission_aborted', actionKinds = ['approval', 'dispatch'],
43
+ currentBridgeId = null, terminalId = null, permissionIds = null,
44
+ reason = 'permission_aborted', actionKinds = ['approval', 'dispatch'],
41
45
  } = {}) {
42
46
  if (typeof fetchImpl !== 'function' || !safeString(supabaseUrl) || !safeString(anonKey) ||
43
- !safeString(token) || !safeString(roomCode) || (bridgeId != null && !safeString(bridgeId)) || !safeString(terminalId)) {
47
+ !safeString(token) || !safeString(roomCode) || (bridgeId != null && !safeString(bridgeId)) ||
48
+ (currentBridgeId != null && !safeString(currentBridgeId)) ||
49
+ (terminalId != null && !safeString(terminalId)) || (terminalId == null && currentBridgeId == null)) {
44
50
  return Object.freeze({ ok: false, code: 'cleanup_unavailable', canceled: 0 })
45
51
  }
46
52
  if (permissionIds != null && (!Array.isArray(permissionIds) || !permissionIds.length)) {
@@ -51,7 +57,7 @@ export async function cancelDurablePermissions ({
51
57
  const listed = await rpc({ fetchImpl, supabaseUrl, headers, name: 'list_code_pair_controls', body: {
52
58
  p_session_code: String(roomCode).toUpperCase(), p_status: null, p_limit: 100,
53
59
  } })
54
- const candidates = pendingDurablePermissionItems(listed, { bridgeId, terminalId, permissionIds, actionKinds })
60
+ const candidates = pendingDurablePermissionItems(listed, { bridgeId, currentBridgeId, terminalId, permissionIds, actionKinds })
55
61
  const outcomes = await Promise.all(candidates.map(async (item) => {
56
62
  const result = await rpc({ fetchImpl, supabaseUrl, headers, name: 'resolve_code_pair_control', body: {
57
63
  p_item_id: item.id,
@@ -63,7 +69,7 @@ export async function cancelDurablePermissions ({
63
69
  // by its immutable authority binding, so resolve it with that exact
64
70
  // value after the owner-authenticated RLS list—not a guessed current ID.
65
71
  p_bridge_authority_id: item.bridge_authority_id,
66
- p_local_authority_id: terminalId,
72
+ p_local_authority_id: item.local_authority_id,
67
73
  p_idempotency_key: `cancel-permission:${item.id}:${reason}`.slice(0, 160),
68
74
  } })
69
75
  return { id: item.id, code: result?.code || 'invalid_response', ok: Boolean(result?.ok) && SETTLED.has(result.code) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.262",
3
+ "version": "0.7.264",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {