thinkpool-pair 0.7.364 → 0.7.366
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/LICENSE +18 -0
- package/README.md +20 -0
- package/abort-turn-barrier.mjs +1 -23
- package/account.mjs +1 -1437
- package/acp-client.mjs +1 -140
- package/agent-detect.mjs +1 -28
- package/agent-notify.mjs +1 -142
- package/agent-visibility.mjs +1 -67
- package/auth-store.mjs +1 -120
- package/bridge.mjs +1 -6266
- package/byok-detect.mjs +1 -126
- package/claude-command-catalog.mjs +1 -91
- package/claude-session.mjs +1 -1519
- package/code-event-contract.mjs +1 -118
- package/codex-app-server.mjs +1 -340
- package/codex-commands.mjs +1 -83
- package/codex-event-mapper.mjs +1 -232
- package/codex-images.mjs +1 -69
- package/codex-mcp-http.mjs +1 -131
- package/codex-session.mjs +1 -1295
- package/command-catalog.mjs +1 -118
- package/command-guidance.mjs +1 -8
- package/context-contract.mjs +1 -95
- package/context-windows.mjs +1 -107
- package/cross-terminal.mjs +1 -789
- package/cumulative-event-relay.mjs +1 -53
- package/design-edit.mjs +1 -424
- package/design-source-contract.mjs +1 -4
- package/direct-pair-room.mjs +1 -57
- package/dispatch-lease.mjs +1 -37
- package/dispatch-permission-cleanup.mjs +1 -86
- package/edit-diff.mjs +1 -136
- package/error-recovery.mjs +1 -50
- package/event-bounds.mjs +1 -121
- package/event-delivery-queue.mjs +1 -60
- package/event-id.mjs +1 -549
- package/evidence-citations.mjs +1 -50
- package/evidence-compact.mjs +1 -11
- package/flow-assembly.mjs +1 -196
- package/flow-budget.mjs +1 -84
- package/flow-conductor.mjs +1 -259
- package/flow-context-store.mjs +1 -387
- package/flow-host-revert.mjs +1 -42
- package/flow-models.mjs +1 -139
- package/flow-preview.mjs +1 -148
- package/flow-receipt.mjs +1 -122
- package/flow-redispatch.mjs +1 -72
- package/flow-review-gate.mjs +1 -402
- package/flow-review-reflect.mjs +1 -115
- package/flow-review.mjs +1 -152
- package/flow-scope-evidence.mjs +1 -117
- package/flow-skill-registry.mjs +1 -140
- package/flow-task-graph.mjs +1 -562
- package/flow-worktree.mjs +1 -71
- package/git-diff-report.mjs +1 -121
- package/hermes-delegation-guard.mjs +1 -14
- package/hermes-event-mapper.mjs +1 -194
- package/hermes-isolation.mjs +1 -61
- package/hermes-model-cache.mjs +1 -54
- package/hermes-policy.mjs +1 -92
- package/hermes-probe.mjs +1 -57
- package/hermes-session.mjs +1 -683
- package/hermes-setup.mjs +1 -167
- package/host-memory.mjs +1 -116
- package/interrupted-resume.mjs +1 -95
- package/keep-awake.mjs +1 -148
- package/key-shape.mjs +1 -49
- package/lane-continuation.mjs +1 -83
- package/lane-lifecycle.mjs +1 -189
- package/lane-worktree.mjs +1 -77
- package/launcher.mjs +1 -354
- package/mcp-flight-recorder.mjs +1 -79
- package/mockup-delivery.mjs +1 -57
- package/model-prices.mjs +1 -113
- package/package.json +13 -4
- package/pair-bus.mjs +1 -98
- package/pair-control-authority.mjs +1 -89
- package/past-work-search.mjs +1 -105
- package/plan-meters.mjs +1 -144
- package/presence.mjs +1 -191
- package/privacy-report.mjs +1 -108
- package/provider-resilience.mjs +1 -356
- package/provider.mjs +1 -133
- package/providers.mjs +1 -491
- package/publish-guard.mjs +2 -0
- package/publish-manifest.json +129 -0
- package/question-response.mjs +1 -58
- package/reap-terminal.mjs +1 -68
- package/recap.mjs +1 -297
- package/replay-transport.mjs +1 -64
- package/repo-search.mjs +1 -190
- package/review-check.mjs +1 -182
- package/runtime-contract.mjs +1 -93
- package/runtime-registry.mjs +1 -64
- package/runtime-session.mjs +1 -20
- package/scheduled-run-admission.mjs +1 -364
- package/scheduled-runs.mjs +1 -268
- package/sdk-admission.mjs +1 -9
- package/sdk-smoke.mjs +1 -61
- package/serve-consent.mjs +1 -118
- package/serve-dir.mjs +1 -40
- package/service.mjs +1 -877
- package/session-store.mjs +1 -484
- package/side-lane.mjs +1 -63
- package/supabase-key.mjs +1 -176
- package/supervisor-ready.mjs +1 -57
- package/switch-provider.mjs +1 -84
- package/terminal-name.mjs +1 -359
- package/terminal-row-reconcile.mjs +1 -70
- package/thinkpool-prompt-contracts.mjs +1 -85
- package/thinkpool-room-prompt.mjs +1 -187
- package/transcript-sanitize.mjs +1 -332
- package/turn-stall.mjs +1 -61
- package/update-gate.mjs +1 -53
- package/viewport.mjs +1 -810
- package/worker-completion.mjs +1 -57
|
@@ -1,86 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
7
|
-
const PENDING = new Set(['pending'])
|
|
8
|
-
const SETTLED = new Set(['denied', 'idempotent_replay', 'already_resolved', 'stale_or_already_resolved', 'item_expired'])
|
|
9
|
-
|
|
10
|
-
const safeString = (value) => typeof value === 'string' && value.length > 0
|
|
11
|
-
|
|
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 []
|
|
15
|
-
const requested = permissionIds == null ? null : new Set(permissionIds.filter(safeString))
|
|
16
|
-
const actions = new Set(actionKinds)
|
|
17
|
-
return (Array.isArray(items) ? items : []).filter((item) => {
|
|
18
|
-
const permissionId = item?.request_context?.permission_id
|
|
19
|
-
return PENDING.has(item?.status) && item?.item_kind === 'approval' && actions.has(item?.action_kind) &&
|
|
20
|
-
(!bridgeId || item?.bridge_authority_id === bridgeId) &&
|
|
21
|
-
(!currentBridgeId || item?.bridge_authority_id !== currentBridgeId) &&
|
|
22
|
-
(!terminalId || item?.local_authority_id === terminalId) &&
|
|
23
|
-
safeString(permissionId) && (!requested || requested.has(permissionId))
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function pendingDispatchItems (items, options = {}) {
|
|
28
|
-
return pendingDurablePermissionItems(items, { ...options, actionKinds: ['dispatch'] })
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const rpc = async ({ fetchImpl, supabaseUrl, headers, name, body }) => {
|
|
32
|
-
const response = await fetchImpl(`${String(supabaseUrl).replace(/\/$/, '')}/rest/v1/rpc/${name}`, {
|
|
33
|
-
method: 'POST', headers, body: JSON.stringify(body),
|
|
34
|
-
})
|
|
35
|
-
if (!response?.ok) throw new Error(`${name}_failed`)
|
|
36
|
-
return response.json()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Fetch through list_code_pair_controls so RLS applies to the bridge's current
|
|
40
|
-
// room-member identity. Never accept an id supplied by a public broadcast.
|
|
41
|
-
export async function cancelDurablePermissions ({
|
|
42
|
-
fetchImpl = globalThis.fetch, supabaseUrl, anonKey, token, roomCode, bridgeId,
|
|
43
|
-
currentBridgeId = null, terminalId = null, permissionIds = null,
|
|
44
|
-
reason = 'permission_aborted', actionKinds = ['approval', 'dispatch'],
|
|
45
|
-
} = {}) {
|
|
46
|
-
if (typeof fetchImpl !== 'function' || !safeString(supabaseUrl) || !safeString(anonKey) ||
|
|
47
|
-
!safeString(token) || !safeString(roomCode) || (bridgeId != null && !safeString(bridgeId)) ||
|
|
48
|
-
(currentBridgeId != null && !safeString(currentBridgeId)) ||
|
|
49
|
-
(terminalId != null && !safeString(terminalId)) || (terminalId == null && currentBridgeId == null)) {
|
|
50
|
-
return Object.freeze({ ok: false, code: 'cleanup_unavailable', canceled: 0 })
|
|
51
|
-
}
|
|
52
|
-
if (permissionIds != null && (!Array.isArray(permissionIds) || !permissionIds.length)) {
|
|
53
|
-
return Object.freeze({ ok: true, code: 'nothing_to_cancel', canceled: 0 })
|
|
54
|
-
}
|
|
55
|
-
const headers = { apikey: anonKey, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
|
|
56
|
-
try {
|
|
57
|
-
const listed = await rpc({ fetchImpl, supabaseUrl, headers, name: 'list_code_pair_controls', body: {
|
|
58
|
-
p_session_code: String(roomCode).toUpperCase(), p_status: null, p_limit: 100,
|
|
59
|
-
} })
|
|
60
|
-
const candidates = pendingDurablePermissionItems(listed, { bridgeId, currentBridgeId, terminalId, permissionIds, actionKinds })
|
|
61
|
-
const outcomes = await Promise.all(candidates.map(async (item) => {
|
|
62
|
-
const result = await rpc({ fetchImpl, supabaseUrl, headers, name: 'resolve_code_pair_control', body: {
|
|
63
|
-
p_item_id: item.id,
|
|
64
|
-
p_expected_version: item.version,
|
|
65
|
-
p_decision: 'deny',
|
|
66
|
-
p_resolution_code: 'canceled',
|
|
67
|
-
p_resolution_payload: { decision: 'deny', reason },
|
|
68
|
-
// A restart receives a fresh bridge ID. The old row remains protected
|
|
69
|
-
// by its immutable authority binding, so resolve it with that exact
|
|
70
|
-
// value after the owner-authenticated RLS list—not a guessed current ID.
|
|
71
|
-
p_bridge_authority_id: item.bridge_authority_id,
|
|
72
|
-
p_local_authority_id: item.local_authority_id,
|
|
73
|
-
p_idempotency_key: `cancel-permission:${item.id}:${reason}`.slice(0, 160),
|
|
74
|
-
} })
|
|
75
|
-
return { id: item.id, code: result?.code || 'invalid_response', ok: Boolean(result?.ok) && SETTLED.has(result.code) }
|
|
76
|
-
}))
|
|
77
|
-
const canceled = outcomes.filter((outcome) => outcome.ok).length
|
|
78
|
-
return Object.freeze({ ok: canceled === outcomes.length, code: canceled === outcomes.length ? 'canceled' : 'cleanup_partial', canceled, outcomes: Object.freeze(outcomes) })
|
|
79
|
-
} catch {
|
|
80
|
-
return Object.freeze({ ok: false, code: 'cleanup_failed', canceled: 0 })
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function cancelDurableDispatchPermissions (options = {}) {
|
|
85
|
-
return cancelDurablePermissions({ ...options, actionKinds: ['dispatch'] })
|
|
86
|
-
}
|
|
1
|
+
const e=new Set(["pending"]),n=new Set(["denied","idempotent_replay","already_resolved","stale_or_already_resolved","item_expired"]),i=e=>"string"==typeof e&&e.length>0;export function pendingDurablePermissionItems(n,{bridgeId:r=null,currentBridgeId:t=null,terminalId:a=null,permissionIds:o=null,actionKinds:l=["approval","dispatch"]}={}){if(null!=r&&!i(r)||null!=t&&!i(t)||null!=a&&!i(a)||null==a&&null==t)return[];const s=null==o?null:new Set(o.filter(i)),d=new Set(l);return(Array.isArray(n)?n:[]).filter(n=>{const o=n?.request_context?.permission_id;return e.has(n?.status)&&"approval"===n?.item_kind&&d.has(n?.action_kind)&&(!r||n?.bridge_authority_id===r)&&(!t||n?.bridge_authority_id!==t)&&(!a||n?.local_authority_id===a)&&i(o)&&(!s||s.has(o))})}export function pendingDispatchItems(e,n={}){return pendingDurablePermissionItems(e,{...n,actionKinds:["dispatch"]})}const r=async({fetchImpl:e,supabaseUrl:n,headers:i,name:r,body:t})=>{const a=await e(`${String(n).replace(/\/$/,"")}/rest/v1/rpc/${r}`,{method:"POST",headers:i,body:JSON.stringify(t)});if(!a?.ok)throw new Error(`${r}_failed`);return a.json()};export async function cancelDurablePermissions({fetchImpl:e=globalThis.fetch,supabaseUrl:t,anonKey:a,token:o,roomCode:l,bridgeId:s,currentBridgeId:d=null,terminalId:c=null,permissionIds:p=null,reason:u="permission_aborted",actionKinds:_=["approval","dispatch"]}={}){if("function"!=typeof e||!i(t)||!i(a)||!i(o)||!i(l)||null!=s&&!i(s)||null!=d&&!i(d)||null!=c&&!i(c)||null==c&&null==d)return Object.freeze({ok:!1,code:"cleanup_unavailable",canceled:0});if(!(null==p||Array.isArray(p)&&p.length))return Object.freeze({ok:!0,code:"nothing_to_cancel",canceled:0});const h={apikey:a,Authorization:`Bearer ${o}`,"Content-Type":"application/json"};try{const i=pendingDurablePermissionItems(await r({fetchImpl:e,supabaseUrl:t,headers:h,name:"list_code_pair_controls",body:{p_session_code:String(l).toUpperCase(),p_status:null,p_limit:100}}),{bridgeId:s,currentBridgeId:d,terminalId:c,permissionIds:p,actionKinds:_}),a=await Promise.all(i.map(async i=>{const a=await r({fetchImpl:e,supabaseUrl:t,headers:h,name:"resolve_code_pair_control",body:{p_item_id:i.id,p_expected_version:i.version,p_decision:"deny",p_resolution_code:"canceled",p_resolution_payload:{decision:"deny",reason:u},p_bridge_authority_id:i.bridge_authority_id,p_local_authority_id:i.local_authority_id,p_idempotency_key:`cancel-permission:${i.id}:${u}`.slice(0,160)}});return{id:i.id,code:a?.code||"invalid_response",ok:Boolean(a?.ok)&&n.has(a.code)}})),o=a.filter(e=>e.ok).length;return Object.freeze({ok:o===a.length,code:o===a.length?"canceled":"cleanup_partial",canceled:o,outcomes:Object.freeze(a)})}catch{return Object.freeze({ok:!1,code:"cleanup_failed",canceled:0})}}export function cancelDurableDispatchPermissions(e={}){return cancelDurablePermissions({...e,actionKinds:["dispatch"]})}
|
package/edit-diff.mjs
CHANGED
|
@@ -1,136 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// package cannot import the web app's ../src tree.
|
|
3
|
-
export const normalizeEditKind = (value) => {
|
|
4
|
-
const kind = typeof value === 'object' ? value?.type : value
|
|
5
|
-
if (kind === 'add' || kind === 'delete' || kind === 'rename') return kind
|
|
6
|
-
return 'update'
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const splitLines = (value) => {
|
|
10
|
-
if (!value) return []
|
|
11
|
-
const lines = String(value).replace(/\r\n?/g, '\n').split('\n')
|
|
12
|
-
if (lines.at(-1) === '') lines.pop()
|
|
13
|
-
return lines
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const fallbackDiff = (before, after) => {
|
|
17
|
-
let start = 0
|
|
18
|
-
while (start < before.length && start < after.length && before[start] === after[start]) start++
|
|
19
|
-
let oldEnd = before.length
|
|
20
|
-
let newEnd = after.length
|
|
21
|
-
while (oldEnd > start && newEnd > start && before[oldEnd - 1] === after[newEnd - 1]) {
|
|
22
|
-
oldEnd--
|
|
23
|
-
newEnd--
|
|
24
|
-
}
|
|
25
|
-
return [
|
|
26
|
-
...before.slice(0, start).map((text) => ({ type: 'context', text })),
|
|
27
|
-
...before.slice(start, oldEnd).map((text) => ({ type: 'remove', text })),
|
|
28
|
-
...after.slice(start, newEnd).map((text) => ({ type: 'add', text })),
|
|
29
|
-
...before.slice(oldEnd).map((text) => ({ type: 'context', text })),
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function lineDiff(before, after, maxDistance = 600) {
|
|
34
|
-
if (!before.length) return after.map((text) => ({ type: 'add', text }))
|
|
35
|
-
if (!after.length) return before.map((text) => ({ type: 'remove', text }))
|
|
36
|
-
const max = Math.min(before.length + after.length, maxDistance)
|
|
37
|
-
const frontier = new Map([[1, 0]])
|
|
38
|
-
const trace = []
|
|
39
|
-
for (let distance = 0; distance <= max; distance++) {
|
|
40
|
-
trace.push(new Map(frontier))
|
|
41
|
-
for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
|
|
42
|
-
const down = frontier.get(diagonal + 1) ?? -1
|
|
43
|
-
const right = frontier.get(diagonal - 1) ?? -1
|
|
44
|
-
let oldIndex = diagonal === -distance || (diagonal !== distance && right < down)
|
|
45
|
-
? Math.max(0, down)
|
|
46
|
-
: Math.max(0, right + 1)
|
|
47
|
-
let newIndex = oldIndex - diagonal
|
|
48
|
-
while (oldIndex < before.length && newIndex < after.length && before[oldIndex] === after[newIndex]) {
|
|
49
|
-
oldIndex++
|
|
50
|
-
newIndex++
|
|
51
|
-
}
|
|
52
|
-
frontier.set(diagonal, oldIndex)
|
|
53
|
-
if (oldIndex < before.length || newIndex < after.length) continue
|
|
54
|
-
|
|
55
|
-
const operations = []
|
|
56
|
-
let x = before.length
|
|
57
|
-
let y = after.length
|
|
58
|
-
for (let d = distance; d >= 0; d--) {
|
|
59
|
-
const snapshot = trace[d]
|
|
60
|
-
const k = x - y
|
|
61
|
-
const snapshotDown = snapshot.get(k + 1) ?? -1
|
|
62
|
-
const snapshotRight = snapshot.get(k - 1) ?? -1
|
|
63
|
-
const previousK = k === -d || (k !== d && snapshotRight < snapshotDown) ? k + 1 : k - 1
|
|
64
|
-
const previousX = Math.max(0, snapshot.get(previousK) ?? 0)
|
|
65
|
-
const previousY = previousX - previousK
|
|
66
|
-
while (x > previousX && y > previousY) {
|
|
67
|
-
operations.push({ type: 'context', text: before[x - 1] })
|
|
68
|
-
x--
|
|
69
|
-
y--
|
|
70
|
-
}
|
|
71
|
-
if (d === 0) break
|
|
72
|
-
if (x === previousX) {
|
|
73
|
-
operations.push({ type: 'add', text: after[y - 1] })
|
|
74
|
-
y--
|
|
75
|
-
} else {
|
|
76
|
-
operations.push({ type: 'remove', text: before[x - 1] })
|
|
77
|
-
x--
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return operations.reverse()
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return fallbackDiff(before, after)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const previewLine = (type, text = '') => {
|
|
87
|
-
if (type === 'add') return `+${text}`
|
|
88
|
-
if (type === 'remove') return `-${text}`
|
|
89
|
-
if (type === 'context') return ` ${text}`
|
|
90
|
-
return `…${text}`
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function compactOperations(operations, context = 2, maxLines = 80) {
|
|
94
|
-
const changed = []
|
|
95
|
-
operations.forEach((operation, index) => {
|
|
96
|
-
if (operation.type !== 'context') changed.push(index)
|
|
97
|
-
})
|
|
98
|
-
if (!changed.length) return []
|
|
99
|
-
|
|
100
|
-
const ranges = []
|
|
101
|
-
for (const index of changed) {
|
|
102
|
-
const start = Math.max(0, index - context)
|
|
103
|
-
const end = Math.min(operations.length, index + context + 1)
|
|
104
|
-
const previous = ranges.at(-1)
|
|
105
|
-
if (previous && start <= previous.end) previous.end = Math.max(previous.end, end)
|
|
106
|
-
else ranges.push({ start, end })
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const compacted = []
|
|
110
|
-
ranges.forEach((range, index) => {
|
|
111
|
-
if (index > 0) compacted.push({ type: 'omit', text: `${range.start - ranges[index - 1].end} unchanged lines` })
|
|
112
|
-
compacted.push(...operations.slice(range.start, range.end))
|
|
113
|
-
})
|
|
114
|
-
if (compacted.length <= maxLines) return compacted
|
|
115
|
-
|
|
116
|
-
const head = Math.floor((maxLines - 1) / 2)
|
|
117
|
-
const tail = maxLines - head - 1
|
|
118
|
-
return [
|
|
119
|
-
...compacted.slice(0, head),
|
|
120
|
-
{ type: 'omit', text: `${compacted.length - head - tail} preview lines hidden` },
|
|
121
|
-
...compacted.slice(-tail),
|
|
122
|
-
]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function summarizeTextDiff(oldText, newText, options = {}) {
|
|
126
|
-
const operations = lineDiff(splitLines(oldText), splitLines(newText), options.maxDistance)
|
|
127
|
-
const added = operations.reduce((total, operation) => total + (operation.type === 'add' ? 1 : 0), 0)
|
|
128
|
-
const removed = operations.reduce((total, operation) => total + (operation.type === 'remove' ? 1 : 0), 0)
|
|
129
|
-
const lines = compactOperations(operations, options.context ?? 2, options.maxLines ?? 80)
|
|
130
|
-
return {
|
|
131
|
-
added,
|
|
132
|
-
removed,
|
|
133
|
-
lines,
|
|
134
|
-
preview: lines.map((line) => previewLine(line.type, line.text)).join('\n'),
|
|
135
|
-
}
|
|
136
|
-
}
|
|
1
|
+
export const normalizeEditKind=t=>{const e="object"==typeof t?t?.type:t;return"add"===e||"delete"===e||"rename"===e?e:"update"};const t=t=>{if(!t)return[];const e=String(t).replace(/\r\n?/g,"\n").split("\n");return""===e.at(-1)&&e.pop(),e};export function summarizeTextDiff(e,n,r={}){const o=function(t,e,n=600){if(!t.length)return e.map(t=>({type:"add",text:t}));if(!e.length)return t.map(t=>({type:"remove",text:t}));const r=Math.min(t.length+e.length,n),o=new Map([[1,0]]),a=[];for(let n=0;n<=r;n++){a.push(new Map(o));for(let r=-n;r<=n;r+=2){const p=o.get(r+1)??-1,s=o.get(r-1)??-1;let c=r===-n||r!==n&&s<p?Math.max(0,p):Math.max(0,s+1),l=c-r;for(;c<t.length&&l<e.length&&t[c]===e[l];)c++,l++;if(o.set(r,c),c<t.length||l<e.length)continue;const h=[];let i=t.length,d=e.length;for(let r=n;r>=0;r--){const n=a[r],o=i-d,p=n.get(o+1)??-1,s=n.get(o-1)??-1,c=o===-r||o!==r&&s<p?o+1:o-1,l=Math.max(0,n.get(c)??0),u=l-c;for(;i>l&&d>u;)h.push({type:"context",text:t[i-1]}),i--,d--;if(0===r)break;i===l?(h.push({type:"add",text:e[d-1]}),d--):(h.push({type:"remove",text:t[i-1]}),i--)}return h.reverse()}}return((t,e)=>{let n=0;for(;n<t.length&&n<e.length&&t[n]===e[n];)n++;let r=t.length,o=e.length;for(;r>n&&o>n&&t[r-1]===e[o-1];)r--,o--;return[...t.slice(0,n).map(t=>({type:"context",text:t})),...t.slice(n,r).map(t=>({type:"remove",text:t})),...e.slice(n,o).map(t=>({type:"add",text:t})),...t.slice(r).map(t=>({type:"context",text:t}))]})(t,e)}(t(e),t(n),r.maxDistance),a=o.reduce((t,e)=>t+("add"===e.type?1:0),0),p=o.reduce((t,e)=>t+("remove"===e.type?1:0),0),s=function(t,e=2,n=80){const r=[];if(t.forEach((t,e)=>{"context"!==t.type&&r.push(e)}),!r.length)return[];const o=[];for(const n of r){const r=Math.max(0,n-e),a=Math.min(t.length,n+e+1),p=o.at(-1);p&&r<=p.end?p.end=Math.max(p.end,a):o.push({start:r,end:a})}const a=[];if(o.forEach((e,n)=>{n>0&&a.push({type:"omit",text:e.start-o[n-1].end+" unchanged lines"}),a.push(...t.slice(e.start,e.end))}),a.length<=n)return a;const p=Math.floor((n-1)/2),s=n-p-1;return[...a.slice(0,p),{type:"omit",text:a.length-p-s+" preview lines hidden"},...a.slice(-s)]}(o,r.context??2,r.maxLines??80);return{added:a,removed:p,lines:s,preview:s.map(t=>((t,e="")=>"add"===t?`+${e}`:"remove"===t?`-${e}`:"context"===t?` ${e}`:`…${e}`)(t.type,t.text)).join("\n")}}
|
package/error-recovery.mjs
CHANGED
|
@@ -1,50 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// retry and therefore cannot alter native adapter transport behavior.
|
|
3
|
-
|
|
4
|
-
import { isSecretFreeRoomPayload, runtimeSupportsCapability } from './runtime-contract.mjs'
|
|
5
|
-
|
|
6
|
-
export const ERROR_RECOVERY_VERSION = 1
|
|
7
|
-
export const MAX_RECOVERY_ATTEMPTS = 3
|
|
8
|
-
const CODE = /^[a-z][a-z0-9_-]{0,79}$/
|
|
9
|
-
const SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/
|
|
10
|
-
|
|
11
|
-
const policyFor = ({ runtime, capabilityId }) => runtimeSupportsCapability(runtime, capabilityId)
|
|
12
|
-
? Object.freeze({ maxAttempts: MAX_RECOVERY_ATTEMPTS })
|
|
13
|
-
: Object.freeze({ maxAttempts: 1 })
|
|
14
|
-
|
|
15
|
-
export function normalizeErrorRecovery({ runtime, capabilityId, code = 'runtime_error', retryable = false, attempt = 1, evidenceRefs = [] } = {}) {
|
|
16
|
-
const policy = policyFor({ runtime, capabilityId })
|
|
17
|
-
const safeAttempt = Math.max(1, Math.min(policy.maxAttempts, Number.isSafeInteger(attempt) ? attempt : 1))
|
|
18
|
-
const safeRefs = (Array.isArray(evidenceRefs) ? evidenceRefs : []).slice(0, 16)
|
|
19
|
-
.filter((ref) => ref && SAFE_REF.test(String(ref.type || '')) && SAFE_REF.test(String(ref.id || '')))
|
|
20
|
-
.map((ref) => ({ type: String(ref.type), id: String(ref.id) }))
|
|
21
|
-
const admitted = runtimeSupportsCapability(runtime, capabilityId) && CODE.test(String(code))
|
|
22
|
-
const canRetry = admitted && retryable === true && safeAttempt < policy.maxAttempts
|
|
23
|
-
const envelope = {
|
|
24
|
-
version: ERROR_RECOVERY_VERSION,
|
|
25
|
-
capabilityId: admitted ? capabilityId : 'unknown',
|
|
26
|
-
code: admitted ? String(code) : 'invalid_recovery_input',
|
|
27
|
-
retryable: canRetry,
|
|
28
|
-
attempt: safeAttempt,
|
|
29
|
-
maxAttempts: policy.maxAttempts,
|
|
30
|
-
next: canRetry ? 'retry' : (admitted && retryable === true ? 'ask_human' : 'stop'),
|
|
31
|
-
evidenceRefs: safeRefs,
|
|
32
|
-
}
|
|
33
|
-
return Object.freeze(isSecretFreeRoomPayload(envelope, 4096) ? envelope : {
|
|
34
|
-
version: ERROR_RECOVERY_VERSION, capabilityId: 'unknown', code: 'invalid_recovery_input', retryable: false,
|
|
35
|
-
attempt: 1, maxAttempts: 1, next: 'stop', evidenceRefs: [],
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function recoveryCodeEvent(input) {
|
|
40
|
-
const recovery = normalizeErrorRecovery(input)
|
|
41
|
-
return Object.freeze({
|
|
42
|
-
kind: recovery.next === 'ask_human' ? 'needs-input' : 'error',
|
|
43
|
-
subtype: 'bounded_recovery',
|
|
44
|
-
message: recovery.next === 'ask_human'
|
|
45
|
-
? `Recovery attempt ${recovery.attempt}/${recovery.maxAttempts} is exhausted; human input is required.`
|
|
46
|
-
: `Recovery stopped safely (${recovery.code}).`,
|
|
47
|
-
recovery,
|
|
48
|
-
verified: false,
|
|
49
|
-
})
|
|
50
|
-
}
|
|
1
|
+
import{isSecretFreeRoomPayload as e,runtimeSupportsCapability as t}from"./runtime-contract.mjs";export const ERROR_RECOVERY_VERSION=1;export const MAX_RECOVERY_ATTEMPTS=3;const r=/^[a-z][a-z0-9_-]{0,79}$/,n=/^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/;export function normalizeErrorRecovery({runtime:i,capabilityId:a,code:o="runtime_error",retryable:m=!1,attempt:s=1,evidenceRefs:p=[]}={}){const c=(({runtime:e,capabilityId:r})=>t(e,r)?Object.freeze({maxAttempts:3}):Object.freeze({maxAttempts:1}))({runtime:i,capabilityId:a}),d=Math.max(1,Math.min(c.maxAttempts,Number.isSafeInteger(s)?s:1)),y=(Array.isArray(p)?p:[]).slice(0,16).filter(e=>e&&n.test(String(e.type||""))&&n.test(String(e.id||""))).map(e=>({type:String(e.type),id:String(e.id)})),u=t(i,a)&&r.test(String(o)),v=u&&!0===m&&d<c.maxAttempts,x={version:1,capabilityId:u?a:"unknown",code:u?String(o):"invalid_recovery_input",retryable:v,attempt:d,maxAttempts:c.maxAttempts,next:v?"retry":u&&!0===m?"ask_human":"stop",evidenceRefs:y};return Object.freeze(e(x,4096)?x:{version:1,capabilityId:"unknown",code:"invalid_recovery_input",retryable:!1,attempt:1,maxAttempts:1,next:"stop",evidenceRefs:[]})}export function recoveryCodeEvent(e){const t=normalizeErrorRecovery(e);return Object.freeze({kind:"ask_human"===t.next?"needs-input":"error",subtype:"bounded_recovery",message:"ask_human"===t.next?`Recovery attempt ${t.attempt}/${t.maxAttempts} is exhausted; human input is required.`:`Recovery stopped safely (${t.code}).`,recovery:t,verified:!1})}
|
package/event-bounds.mjs
CHANGED
|
@@ -1,121 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/* Browser-safe structured-event bounding shared by the bridge broadcast path and
|
|
4
|
-
the web client's persisted reload snapshot. Large Codex/Claude tool payloads
|
|
5
|
-
must remain recognisable (tool name + a useful preview), but must never turn a
|
|
6
|
-
replay frame or tx: seed row into a multi-megabyte hydration dependency. */
|
|
7
|
-
|
|
8
|
-
export const STRUCTURED_EVENT_CAP = 80000
|
|
9
|
-
// 14 × the 150KB replay-frame budget: the largest complete rolling window the
|
|
10
|
-
// transport can deliver without trimming. Older events remain in the JSONL archive.
|
|
11
|
-
export const STRUCTURED_LOG_BYTE_CAP = 2100000
|
|
12
|
-
|
|
13
|
-
const sizeOf = (value) => {
|
|
14
|
-
try { return JSON.stringify(value).length } catch { return Infinity }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const clipString = (value, limit) => {
|
|
18
|
-
if (value.length <= limit) return value
|
|
19
|
-
return `${value.slice(0, limit)}\n…[truncated ${value.length - limit} bytes]`
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function compactValue(value, budget, depth = 0) {
|
|
23
|
-
if (sizeOf(value) <= budget) return value
|
|
24
|
-
if (typeof value === 'string') return clipString(value, Math.max(80, budget - 80))
|
|
25
|
-
if (value == null || typeof value !== 'object') return value
|
|
26
|
-
if (depth >= 4) return '[nested value omitted — too large]'
|
|
27
|
-
|
|
28
|
-
if (Array.isArray(value)) {
|
|
29
|
-
const out = []
|
|
30
|
-
const limit = Math.min(value.length, 8)
|
|
31
|
-
const per = Math.max(240, Math.floor((budget - 180) / Math.max(1, limit)))
|
|
32
|
-
for (let i = 0; i < limit; i++) out.push(compactValue(value[i], per, depth + 1))
|
|
33
|
-
if (value.length > limit) out.push(`…[${value.length - limit} more items omitted]`)
|
|
34
|
-
return out
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const entries = Object.entries(value)
|
|
38
|
-
const out = {}
|
|
39
|
-
const limit = Math.min(entries.length, 16)
|
|
40
|
-
const per = Math.max(240, Math.floor((budget - 180) / Math.max(1, limit)))
|
|
41
|
-
for (let i = 0; i < limit; i++) {
|
|
42
|
-
const [key, nested] = entries[i]
|
|
43
|
-
out[key] = compactValue(nested, per, depth + 1)
|
|
44
|
-
}
|
|
45
|
-
if (entries.length > limit) out._omitted = `${entries.length - limit} more fields`
|
|
46
|
-
return out
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function compactBlock(block, budget) {
|
|
50
|
-
if (!block || typeof block !== 'object') return compactValue(block, budget)
|
|
51
|
-
if (sizeOf(block) <= budget) return block
|
|
52
|
-
if (block.type === 'image') {
|
|
53
|
-
return { type: 'text', text: '[image omitted from replay — open the live session to view]' }
|
|
54
|
-
}
|
|
55
|
-
const out = {}
|
|
56
|
-
for (const key of ['type', 'id', 'name', 'tool_use_id']) if (block[key] != null) out[key] = block[key]
|
|
57
|
-
// A replay needs the tool identity and a useful preview, not a second full copy
|
|
58
|
-
// of a giant patch/result that already lives in the durable archive.
|
|
59
|
-
const payloadBudget = Math.min(block.type === 'tool_use' ? 12000 : 20000, Math.max(500, budget - 600))
|
|
60
|
-
if (typeof block.text === 'string') out.text = clipString(block.text, payloadBudget)
|
|
61
|
-
if (block.input != null) out.input = compactValue(block.input, payloadBudget, 1)
|
|
62
|
-
if (block.content != null) out.content = compactValue(block.content, payloadBudget, 1)
|
|
63
|
-
if (!('text' in out) && !('input' in out) && !('content' in out)) out.text = `[${block.type || 'content'} omitted — too large]`
|
|
64
|
-
return out
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function boundStructuredEvent(event, cap = STRUCTURED_EVENT_CAP) {
|
|
68
|
-
if (!event || typeof event !== 'object' || sizeOf(event) <= cap) return event
|
|
69
|
-
|
|
70
|
-
const out = { ...event, _bounded: true }
|
|
71
|
-
if (out.evidence) out.evidence = compactEvidenceEnvelope(out.evidence, 0)
|
|
72
|
-
const stringBudget = Math.min(12000, Math.max(1000, Math.floor(cap / 4)))
|
|
73
|
-
for (const key of ['text', 'output', 'stdout', 'stderr', 'input']) {
|
|
74
|
-
if (typeof out[key] === 'string') out[key] = clipString(out[key], stringBudget)
|
|
75
|
-
}
|
|
76
|
-
if (typeof out.content === 'string') out.content = clipString(out.content, stringBudget)
|
|
77
|
-
else if (Array.isArray(out.content)) {
|
|
78
|
-
const per = Math.max(900, Math.floor((cap - 2000) / Math.max(1, Math.min(out.content.length, 8))))
|
|
79
|
-
out.content = out.content.slice(0, 8).map(block => compactBlock(block, per))
|
|
80
|
-
if (event.content.length > 8) out.content.push({ type: 'text', text: `…[${event.content.length - 8} more blocks omitted]` })
|
|
81
|
-
}
|
|
82
|
-
if (Array.isArray(out.blocks)) {
|
|
83
|
-
const per = Math.max(900, Math.floor((cap - 2000) / Math.max(1, Math.min(out.blocks.length, 8))))
|
|
84
|
-
out.blocks = out.blocks.slice(0, 8).map(block => compactBlock(block, per))
|
|
85
|
-
if (event.blocks.length > 8) out.blocks.push({ type: 'text', text: `…[${event.blocks.length - 8} more blocks omitted]` })
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (sizeOf(out) <= cap) return out
|
|
89
|
-
|
|
90
|
-
// Hard fallback: retain ordering/dedup identity and recognisable tool blocks.
|
|
91
|
-
const identity = {}
|
|
92
|
-
for (const key of ['kind', 'seq', 'cid', 'ts', 'term', 'toolUseId', 'isError', 'by', 'crosspost', 'relaySourceName']) {
|
|
93
|
-
if (out[key] != null) identity[key] = out[key]
|
|
94
|
-
}
|
|
95
|
-
if (out.evidence) identity.evidence = compactEvidenceEnvelope(out.evidence, 0)
|
|
96
|
-
if (Array.isArray(out.blocks)) identity.blocks = out.blocks.slice(0, 4).map(block => compactBlock(block, 6000))
|
|
97
|
-
else if (Array.isArray(out.content)) identity.content = out.content.slice(0, 4).map(block => compactBlock(block, 6000))
|
|
98
|
-
else if (typeof out.content === 'string') identity.content = clipString(out.content, 12000)
|
|
99
|
-
else if (typeof out.text === 'string') identity.text = clipString(out.text, 12000)
|
|
100
|
-
identity._bounded = true
|
|
101
|
-
identity._notice = 'Large tool payload truncated for fast transcript hydration'
|
|
102
|
-
return identity
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function boundStructuredLog(events, {
|
|
106
|
-
eventCap = STRUCTURED_EVENT_CAP,
|
|
107
|
-
byteCap = STRUCTURED_LOG_BYTE_CAP,
|
|
108
|
-
countCap = 2000,
|
|
109
|
-
} = {}) {
|
|
110
|
-
if (!Array.isArray(events) || !events.length) return []
|
|
111
|
-
const bounded = events.slice(-countCap).map((event) => boundStructuredEvent(event, eventCap))
|
|
112
|
-
let bytes = 0
|
|
113
|
-
let start = bounded.length
|
|
114
|
-
while (start > 0) {
|
|
115
|
-
const size = sizeOf(bounded[start - 1]) + 1
|
|
116
|
-
if (start < bounded.length && bytes + size > byteCap) break
|
|
117
|
-
bytes += size
|
|
118
|
-
start--
|
|
119
|
-
}
|
|
120
|
-
return start === 0 ? bounded : bounded.slice(start)
|
|
121
|
-
}
|
|
1
|
+
import{compactEvidenceEnvelope as t}from"./evidence-compact.mjs";export const STRUCTURED_EVENT_CAP=8e4;export const STRUCTURED_LOG_BYTE_CAP=21e5;const e=t=>{try{return JSON.stringify(t).length}catch{return 1/0}},n=(t,e)=>t.length<=e?t:`${t.slice(0,e)}\n…[truncated ${t.length-e} bytes]`;function o(t,r,c=0){if(e(t)<=r)return t;if("string"==typeof t)return n(t,Math.max(80,r-80));if(null==t||"object"!=typeof t)return t;if(c>=4)return"[nested value omitted — too large]";if(Array.isArray(t)){const e=[],n=Math.min(t.length,8),i=Math.max(240,Math.floor((r-180)/Math.max(1,n)));for(let r=0;r<n;r++)e.push(o(t[r],i,c+1));return t.length>n&&e.push(`…[${t.length-n} more items omitted]`),e}const i=Object.entries(t),s={},a=Math.min(i.length,16),l=Math.max(240,Math.floor((r-180)/Math.max(1,a)));for(let t=0;t<a;t++){const[e,n]=i[t];s[e]=o(n,l,c+1)}return i.length>a&&(s._omitted=i.length-a+" more fields"),s}function r(t,r){if(!t||"object"!=typeof t)return o(t,r);if(e(t)<=r)return t;if("image"===t.type)return{type:"text",text:"[image omitted from replay — open the live session to view]"};const c={};for(const e of["type","id","name","tool_use_id"])null!=t[e]&&(c[e]=t[e]);const i=Math.min("tool_use"===t.type?12e3:2e4,Math.max(500,r-600));return"string"==typeof t.text&&(c.text=n(t.text,i)),null!=t.input&&(c.input=o(t.input,i,1)),null!=t.content&&(c.content=o(t.content,i,1)),"text"in c||"input"in c||"content"in c||(c.text=`[${t.type||"content"} omitted — too large]`),c}export function boundStructuredEvent(o,c=8e4){if(!o||"object"!=typeof o||e(o)<=c)return o;const i={...o,_bounded:!0};i.evidence&&(i.evidence=t(i.evidence,0));const s=Math.min(12e3,Math.max(1e3,Math.floor(c/4)));for(const t of["text","output","stdout","stderr","input"])"string"==typeof i[t]&&(i[t]=n(i[t],s));if("string"==typeof i.content)i.content=n(i.content,s);else if(Array.isArray(i.content)){const t=Math.max(900,Math.floor((c-2e3)/Math.max(1,Math.min(i.content.length,8))));i.content=i.content.slice(0,8).map(e=>r(e,t)),o.content.length>8&&i.content.push({type:"text",text:`…[${o.content.length-8} more blocks omitted]`})}if(Array.isArray(i.blocks)){const t=Math.max(900,Math.floor((c-2e3)/Math.max(1,Math.min(i.blocks.length,8))));i.blocks=i.blocks.slice(0,8).map(e=>r(e,t)),o.blocks.length>8&&i.blocks.push({type:"text",text:`…[${o.blocks.length-8} more blocks omitted]`})}if(e(i)<=c)return i;const a={};for(const t of["kind","seq","cid","ts","term","toolUseId","isError","by","crosspost","relaySourceName"])null!=i[t]&&(a[t]=i[t]);return i.evidence&&(a.evidence=t(i.evidence,0)),Array.isArray(i.blocks)?a.blocks=i.blocks.slice(0,4).map(t=>r(t,6e3)):Array.isArray(i.content)?a.content=i.content.slice(0,4).map(t=>r(t,6e3)):"string"==typeof i.content?a.content=n(i.content,12e3):"string"==typeof i.text&&(a.text=n(i.text,12e3)),a._bounded=!0,a._notice="Large tool payload truncated for fast transcript hydration",a}export function boundStructuredLog(t,{eventCap:n=8e4,byteCap:o=21e5,countCap:r=2e3}={}){if(!Array.isArray(t)||!t.length)return[];const c=t.slice(-r).map(t=>boundStructuredEvent(t,n));let i=0,s=c.length;for(;s>0;){const t=e(c[s-1])+1;if(s<c.length&&i+t>o)break;i+=t,s--}return 0===s?c:c.slice(s)}
|
package/event-delivery-queue.mjs
CHANGED
|
@@ -1,60 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// The bridge's local JSONL archive is durable, but a live `code-event` still
|
|
4
|
-
// has to cross Realtime before either viewer can write its shared tx: snapshot.
|
|
5
|
-
// Never let a failed (or merely late) assistant frame allow its terminal result
|
|
6
|
-
// to overtake it. Keep the head queued until Realtime acknowledges it, then move
|
|
7
|
-
// on in order. A fresh browser replay remains the crash-recovery backstop.
|
|
8
|
-
|
|
9
|
-
const defaultWait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
10
|
-
|
|
11
|
-
export const acceptedRealtimeDelivery = (result) =>
|
|
12
|
-
result === 'ok' || result?.success === true
|
|
13
|
-
|
|
14
|
-
export function createEventDeliveryQueue ({ send, wait = defaultWait, retryDelay = (attempt) => Math.min(10_000, 250 * (2 ** Math.min(attempt, 6))) } = {}) {
|
|
15
|
-
if (typeof send !== 'function') throw new TypeError('event delivery queue requires send(frame)')
|
|
16
|
-
const frames = []
|
|
17
|
-
let running = false
|
|
18
|
-
let closed = false
|
|
19
|
-
let idle = Promise.resolve()
|
|
20
|
-
let settleIdle = null
|
|
21
|
-
|
|
22
|
-
const run = async () => {
|
|
23
|
-
if (running || closed) return
|
|
24
|
-
running = true
|
|
25
|
-
try {
|
|
26
|
-
while (!closed && frames.length) {
|
|
27
|
-
const head = frames[0]
|
|
28
|
-
let result = null
|
|
29
|
-
try { result = await send(head.frame) } catch { result = null }
|
|
30
|
-
if (acceptedRealtimeDelivery(result)) {
|
|
31
|
-
frames.shift()
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
34
|
-
head.attempt += 1
|
|
35
|
-
await wait(retryDelay(head.attempt))
|
|
36
|
-
}
|
|
37
|
-
} finally {
|
|
38
|
-
running = false
|
|
39
|
-
if (!frames.length && settleIdle) { settleIdle(); settleIdle = null }
|
|
40
|
-
if (!closed && frames.length) void run()
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
enqueue (frame) {
|
|
46
|
-
if (closed) return false
|
|
47
|
-
if (!frames.length && !running) idle = new Promise((resolve) => { settleIdle = resolve })
|
|
48
|
-
frames.push({ frame, attempt: 0 })
|
|
49
|
-
void run()
|
|
50
|
-
return true
|
|
51
|
-
},
|
|
52
|
-
pending: () => frames.length,
|
|
53
|
-
whenIdle: () => idle,
|
|
54
|
-
close () {
|
|
55
|
-
closed = true
|
|
56
|
-
frames.length = 0
|
|
57
|
-
if (settleIdle) { settleIdle(); settleIdle = null }
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
const e=e=>new Promise(t=>setTimeout(t,e));export const acceptedRealtimeDelivery=e=>"ok"===e||!0===e?.success;export function createEventDeliveryQueue({send:t,wait:n=e,retryDelay:l=e=>Math.min(1e4,250*2**Math.min(e,6))}={}){if("function"!=typeof t)throw new TypeError("event delivery queue requires send(frame)");const r=[];let i=!1,a=!1,o=Promise.resolve(),s=null;const c=async()=>{if(!i&&!a){i=!0;try{for(;!a&&r.length;){const e=r[0];let i=null;try{i=await t(e.frame)}catch{i=null}acceptedRealtimeDelivery(i)?r.shift():(e.attempt+=1,await n(l(e.attempt)))}}finally{i=!1,!r.length&&s&&(s(),s=null),!a&&r.length&&c()}}};return{enqueue:e=>!a&&(r.length||i||(o=new Promise(e=>{s=e})),r.push({frame:e,attempt:0}),c(),!0),pending:()=>r.length,whenIdle:()=>o,close(){a=!0,r.length=0,s&&(s(),s=null)}}}
|