thinkpool-pair 0.7.261 → 0.7.262

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
@@ -39,7 +39,7 @@ import { randomUUID } from 'node:crypto'
39
39
  import { createClient } from '@supabase/supabase-js'
40
40
  import { serveDecision, fetchServeRow, refusalMessage, gateFailAction } from './serve-consent.mjs'
41
41
  import { reapTerminalRow as _reapTerminalRow } from './reap-terminal.mjs'
42
- import { cancelDurableDispatchPermissions } from './dispatch-permission-cleanup.mjs'
42
+ import { cancelDurableDispatchPermissions, cancelDurablePermissions } from './dispatch-permission-cleanup.mjs'
43
43
  // Slice 3 — the pure decision layer for agent push events (turn-done / needs-input).
44
44
  // Unit-tested in bridge/agent-notify.test.mjs; everything with I/O stays here.
45
45
  import { createPermNotifier, shouldNotifyTurnDone, permissionSummary, clipSummary, isUserFacingLane, DEFAULT_MIN_TURN_MS } from './agent-notify.mjs'
@@ -3091,29 +3091,29 @@ function pendingResolution(pending, payload = {}) {
3091
3091
  // The bridge is the only component that knows when the SDK promise it created
3092
3092
  // has died. Persist that lifecycle boundary so a card cannot outlive its
3093
3093
  // executable intent after a steer, stop, close, or restart.
3094
- function cancelPendingDispatchPermissions(s, permissionIds = null, reason = 'dispatch_superseded') {
3094
+ function cancelPendingDurablePermissions(s, permissionIds = null, reason = 'permission_aborted', dispatchOnly = false) {
3095
3095
  const ids = permissionIds || [...(s?.pending?.entries?.() || [])]
3096
- .filter(([, pending]) => pending?.payload?.answerFormat === 'dispatch')
3097
3096
  .map(([id]) => id)
3098
3097
  if (!ids.length || !s?.id) return
3099
- void cancelDurableDispatchPermissions({
3098
+ const cancel = dispatchOnly ? cancelDurableDispatchPermissions : cancelDurablePermissions
3099
+ void cancel({
3100
3100
  supabaseUrl: SUPABASE_URL, anonKey: SUPABASE_ANON, token: codeAuthToken, roomCode: room,
3101
3101
  bridgeId: BRIDGE_ID, terminalId: s.id, permissionIds: ids, reason,
3102
3102
  }).then((result) => {
3103
- if (!result.ok && process.env.TP_DEBUG) process.stderr.write(`\n ◇ durable dispatch cleanup incomplete (${result.code})\n`)
3103
+ if (!result.ok && process.env.TP_DEBUG) process.stderr.write(`\n ◇ durable permission cleanup incomplete (${result.code})\n`)
3104
3104
  })
3105
3105
  }
3106
3106
 
3107
- async function cancelRestoredDispatchPermissions(terminalIds) {
3107
+ async function cancelRestoredDurablePermissions(terminalIds) {
3108
3108
  const terminals = [...new Set((terminalIds || []).filter(Boolean))]
3109
3109
  await Promise.all(terminals.map(async (terminalId) => {
3110
- const result = await cancelDurableDispatchPermissions({
3110
+ const result = await cancelDurablePermissions({
3111
3111
  supabaseUrl: SUPABASE_URL, anonKey: SUPABASE_ANON, token: codeAuthToken, roomCode: room,
3112
3112
  // New bridge process, old durable bridge authority: filter by the
3113
3113
  // restored terminal and let the cleanup read each historical binding.
3114
3114
  terminalId, reason: 'bridge_restarted',
3115
3115
  })
3116
- if (!result.ok && process.env.TP_DEBUG) process.stderr.write(`\n ◇ restored dispatch cleanup incomplete (${result.code})\n`)
3116
+ if (!result.ok && process.env.TP_DEBUG) process.stderr.write(`\n ◇ restored permission cleanup incomplete (${result.code})\n`)
3117
3117
  }))
3118
3118
  }
3119
3119
 
@@ -3123,7 +3123,7 @@ function drainPending(s) {
3123
3123
  // was already approved but is still re-reading durable authority must also
3124
3124
  // fail closed when this terminal stops or its turn settles.
3125
3125
  supersedeDispatchLease(s)
3126
- cancelPendingDispatchPermissions(s, null, 'dispatch_aborted')
3126
+ cancelPendingDurablePermissions(s, null, 'permission_aborted')
3127
3127
  for (const [, p] of s.pending) {
3128
3128
  if (p?.timer) clearTimeout(p.timer)
3129
3129
  try { p.resolve(pendingResolution(p, { decision: 'deny' })) } catch { /* noop */ }
@@ -3137,7 +3137,7 @@ function supersedePendingDispatch(s) {
3137
3137
  const lease = supersedeDispatchLease(s)
3138
3138
  if (!lease) return null
3139
3139
  const pending = s.pending?.get(lease.permissionId)
3140
- cancelPendingDispatchPermissions(s, [lease.permissionId], 'dispatch_superseded')
3140
+ cancelPendingDurablePermissions(s, [lease.permissionId], 'dispatch_superseded', true)
3141
3141
  if (pending?.timer) clearTimeout(pending.timer)
3142
3142
  s.pending?.delete(lease.permissionId)
3143
3143
  s.permNotifier?.resolve(lease.permissionId)
@@ -4059,7 +4059,7 @@ channel
4059
4059
  // Cancel their durable dispatch rows BEFORE opening a restored SDK
4060
4060
  // session, so a new permission raised during resume can never be
4061
4061
  // mistaken for a pre-restart card from the same terminal.
4062
- await cancelRestoredDispatchPermissions(all.map((rec) => rec.id))
4062
+ await cancelRestoredDurablePermissions(all.map((rec) => rec.id))
4063
4063
  if (all.length) for (const rec of all) {
4064
4064
  const wasInterrupted = restoredTurnOpen(rec.log || [])
4065
4065
  // Codex can persist a thread id before its rollout receives the
@@ -1,25 +1,30 @@
1
- // Durable cleanup for dispatch approvals that no longer have a live bridge
2
- // promise behind them. A dispatch row is intentionally human-resolvable only
3
- // while its originating bridge intent is live. When that intent is replaced
4
- // or a bridge restarts, cancel the exact pending row through the normal
5
- // authenticated RPC rather than leaving a misleading, non-executable card.
1
+ // Durable cleanup for approvals that no longer have a live bridge promise behind
2
+ // them. A row is human-resolvable only while its originating local intent is live.
3
+ // When that intent is replaced, aborted, or restarted, cancel the exact pending
4
+ // row through the normal authenticated RPC rather than leaving a non-executable
5
+ // card/lifecycle blocker.
6
6
 
7
7
  const PENDING = new Set(['pending'])
8
8
  const SETTLED = new Set(['denied', 'idempotent_replay', 'already_resolved', 'stale_or_already_resolved', 'item_expired'])
9
9
 
10
10
  const safeString = (value) => typeof value === 'string' && value.length > 0
11
11
 
12
- export function pendingDispatchItems (items, { bridgeId = null, terminalId, permissionIds = null } = {}) {
12
+ export function pendingDurablePermissionItems (items, { bridgeId = null, terminalId, permissionIds = null, actionKinds = ['approval', 'dispatch'] } = {}) {
13
13
  if ((bridgeId != null && !safeString(bridgeId)) || !safeString(terminalId)) return []
14
14
  const requested = permissionIds == null ? null : new Set(permissionIds.filter(safeString))
15
+ const actions = new Set(actionKinds)
15
16
  return (Array.isArray(items) ? items : []).filter((item) => {
16
17
  const permissionId = item?.request_context?.permission_id
17
- return PENDING.has(item?.status) && item?.item_kind === 'approval' && item?.action_kind === 'dispatch' &&
18
+ return PENDING.has(item?.status) && item?.item_kind === 'approval' && actions.has(item?.action_kind) &&
18
19
  (!bridgeId || item?.bridge_authority_id === bridgeId) && item?.local_authority_id === terminalId &&
19
20
  safeString(permissionId) && (!requested || requested.has(permissionId))
20
21
  })
21
22
  }
22
23
 
24
+ export function pendingDispatchItems (items, options = {}) {
25
+ return pendingDurablePermissionItems(items, { ...options, actionKinds: ['dispatch'] })
26
+ }
27
+
23
28
  const rpc = async ({ fetchImpl, supabaseUrl, headers, name, body }) => {
24
29
  const response = await fetchImpl(`${String(supabaseUrl).replace(/\/$/, '')}/rest/v1/rpc/${name}`, {
25
30
  method: 'POST', headers, body: JSON.stringify(body),
@@ -30,9 +35,9 @@ const rpc = async ({ fetchImpl, supabaseUrl, headers, name, body }) => {
30
35
 
31
36
  // Fetch through list_code_pair_controls so RLS applies to the bridge's current
32
37
  // room-member identity. Never accept an id supplied by a public broadcast.
33
- export async function cancelDurableDispatchPermissions ({
38
+ export async function cancelDurablePermissions ({
34
39
  fetchImpl = globalThis.fetch, supabaseUrl, anonKey, token, roomCode, bridgeId,
35
- terminalId, permissionIds = null, reason = 'dispatch_superseded',
40
+ terminalId, permissionIds = null, reason = 'permission_aborted', actionKinds = ['approval', 'dispatch'],
36
41
  } = {}) {
37
42
  if (typeof fetchImpl !== 'function' || !safeString(supabaseUrl) || !safeString(anonKey) ||
38
43
  !safeString(token) || !safeString(roomCode) || (bridgeId != null && !safeString(bridgeId)) || !safeString(terminalId)) {
@@ -46,7 +51,7 @@ export async function cancelDurableDispatchPermissions ({
46
51
  const listed = await rpc({ fetchImpl, supabaseUrl, headers, name: 'list_code_pair_controls', body: {
47
52
  p_session_code: String(roomCode).toUpperCase(), p_status: null, p_limit: 100,
48
53
  } })
49
- const candidates = pendingDispatchItems(listed, { bridgeId, terminalId, permissionIds })
54
+ const candidates = pendingDurablePermissionItems(listed, { bridgeId, terminalId, permissionIds, actionKinds })
50
55
  const outcomes = await Promise.all(candidates.map(async (item) => {
51
56
  const result = await rpc({ fetchImpl, supabaseUrl, headers, name: 'resolve_code_pair_control', body: {
52
57
  p_item_id: item.id,
@@ -59,7 +64,7 @@ export async function cancelDurableDispatchPermissions ({
59
64
  // value after the owner-authenticated RLS list—not a guessed current ID.
60
65
  p_bridge_authority_id: item.bridge_authority_id,
61
66
  p_local_authority_id: terminalId,
62
- p_idempotency_key: `cancel-dispatch:${item.id}:${reason}`.slice(0, 160),
67
+ p_idempotency_key: `cancel-permission:${item.id}:${reason}`.slice(0, 160),
63
68
  } })
64
69
  return { id: item.id, code: result?.code || 'invalid_response', ok: Boolean(result?.ok) && SETTLED.has(result.code) }
65
70
  }))
@@ -69,3 +74,7 @@ export async function cancelDurableDispatchPermissions ({
69
74
  return Object.freeze({ ok: false, code: 'cleanup_failed', canceled: 0 })
70
75
  }
71
76
  }
77
+
78
+ export function cancelDurableDispatchPermissions (options = {}) {
79
+ return cancelDurablePermissions({ ...options, actionKinds: ['dispatch'] })
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.261",
3
+ "version": "0.7.262",
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": {