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.
Files changed (116) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6266
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -60
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -92
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -79
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +13 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-prompt-contracts.mjs +1 -85
  111. package/thinkpool-room-prompt.mjs +1 -187
  112. package/transcript-sanitize.mjs +1 -332
  113. package/turn-stall.mjs +1 -61
  114. package/update-gate.mjs +1 -53
  115. package/viewport.mjs +1 -810
  116. package/worker-completion.mjs +1 -57
@@ -1,86 +1 @@
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
-
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
- // Bridge-local diff primitives. Keep this module self-contained: the npm
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")}}
@@ -1,50 +1 @@
1
- // A safe recovery envelope. It records a bounded decision; it never performs a
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 { compactEvidenceEnvelope } from './evidence-compact.mjs'
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)}
@@ -1,60 +1 @@
1
- // Ordered, acknowledged delivery for bridge-produced transcript events.
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)}}}