@north-light/cli 0.3.532 → 0.3.533

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 (193) hide show
  1. package/dist/api/client.d.ts +6 -3
  2. package/dist/api/client.js +1 -1
  3. package/dist/api/contract.d.ts +1 -1
  4. package/dist/api/contract.js +1 -1
  5. package/dist/api/dto/canvas.d.ts +2 -0
  6. package/dist/api/dto/crons.d.ts +2 -0
  7. package/dist/builtin-memory/crouter-sdk/release-notes.md +8 -0
  8. package/dist/builtin-memory/northlight-apps/providers/README.md +2 -0
  9. package/dist/builtin-memory/northlight-apps/providers/sms.md +2 -0
  10. package/dist/clients/attach/chrome/inbox-strip.d.ts +6 -1
  11. package/dist/clients/attach/chrome/inbox-strip.js +1 -1
  12. package/dist/clients/attach/chrome/roster.js +1 -1
  13. package/dist/clients/attach/session/chrome-refresh.d.ts +12 -3
  14. package/dist/clients/attach/session/chrome-refresh.js +1 -1
  15. package/dist/clients/attach/session/document-links.js +1 -1
  16. package/dist/clients/attach/viewer.js +586 -587
  17. package/dist/commands/api-client.js +3 -3
  18. package/dist/commands/node/inspect.js +6 -6
  19. package/dist/commands/sys/context/expose.js +2 -2
  20. package/dist/commands/sys/db.d.ts +1 -0
  21. package/dist/commands/sys/db.js +1 -0
  22. package/dist/commands/sys.js +1 -1
  23. package/dist/core/bash-guard.js +9 -3
  24. package/dist/core/canvas/canvas.d.ts +12 -2
  25. package/dist/core/canvas/canvas.js +23 -20
  26. package/dist/core/canvas/daemon-actions.d.ts +0 -3
  27. package/dist/core/canvas/daemon-actions.js +7 -24
  28. package/dist/core/canvas/daemon-owner.d.ts +3 -0
  29. package/dist/core/canvas/daemon-owner.js +1 -6
  30. package/dist/core/canvas/db.d.ts +28 -20
  31. package/dist/core/canvas/db.js +4 -3
  32. package/dist/core/canvas/deliveries.d.ts +5 -0
  33. package/dist/core/canvas/deliveries.js +25 -30
  34. package/dist/core/canvas/export-sqlite.d.ts +14 -0
  35. package/dist/core/canvas/export-sqlite.js +3 -0
  36. package/dist/core/canvas/import-sqlite.d.ts +44 -0
  37. package/dist/core/canvas/import-sqlite.js +7 -0
  38. package/dist/core/canvas/node-tokens.js +3 -3
  39. package/dist/core/canvas/outcome-actions.js +4 -4
  40. package/dist/core/canvas/recovery.js +1 -1
  41. package/dist/core/canvas/schema-pg.sql +1022 -0
  42. package/dist/core/canvas/schema-version.d.ts +4 -0
  43. package/dist/core/canvas/schema-version.js +1 -0
  44. package/dist/core/canvas/sessions.js +3 -3
  45. package/dist/core/canvas/structured-requests.js +2 -2
  46. package/dist/core/canvas/tree.js +6 -6
  47. package/dist/core/canvas/waits.js +1 -1
  48. package/dist/core/feed/inbox.js +1 -1
  49. package/dist/core/feed/messages.js +5 -5
  50. package/dist/core/feed/reports.js +2 -2
  51. package/dist/core/grants/mirror.js +2 -2
  52. package/dist/core/graph/access.js +7 -7
  53. package/dist/core/graph/custom.js +10 -10
  54. package/dist/core/graph/deletion.js +6 -6
  55. package/dist/core/graph/documents.d.ts +1 -1
  56. package/dist/core/graph/documents.js +9 -9
  57. package/dist/core/graph/edges.d.ts +3 -3
  58. package/dist/core/graph/edges.js +3 -1
  59. package/dist/core/graph/events.js +7 -7
  60. package/dist/core/graph/exposures.d.ts +8 -8
  61. package/dist/core/graph/jobs.d.ts +1 -1
  62. package/dist/core/graph/jobs.js +2 -2
  63. package/dist/core/graph/kinds/custom.js +1 -1
  64. package/dist/core/graph/kinds/document.js +4 -4
  65. package/dist/core/graph/kinds/dto.js +1 -1
  66. package/dist/core/graph/kinds/job.js +2 -2
  67. package/dist/core/graph/kinds/kind.d.ts +2 -2
  68. package/dist/core/graph/kinds/kind.js +1 -1
  69. package/dist/core/graph/kinds/live-read.d.ts +5 -0
  70. package/dist/core/graph/kinds/live-read.js +1 -0
  71. package/dist/core/graph/kinds/node.js +5 -5
  72. package/dist/core/graph/kinds/owner.js +2 -2
  73. package/dist/core/graph/names.js +3 -3
  74. package/dist/core/graph/objects.d.ts +5 -5
  75. package/dist/core/graph/objects.js +10 -10
  76. package/dist/core/graph/package-docs.js +6 -5
  77. package/dist/core/graph/reader.d.ts +1 -1
  78. package/dist/core/graph/reader.js +2 -2
  79. package/dist/core/graph/receivers.js +4 -5
  80. package/dist/core/graph/repo-sync/exchange.js +2 -2
  81. package/dist/core/graph/repo-sync/repos.js +3 -3
  82. package/dist/core/graph/repo-sync/sync.js +1 -1
  83. package/dist/core/graph/rules.d.ts +3 -4
  84. package/dist/core/graph/rules.js +3 -3
  85. package/dist/core/graph/spaces/app.js +1 -1
  86. package/dist/core/graph/spaces/index.d.ts +1 -1
  87. package/dist/core/graph/spaces/index.js +1 -1
  88. package/dist/core/graph/spaces/own-memory.js +1 -1
  89. package/dist/core/graph/spaces/person.js +3 -3
  90. package/dist/core/graph/spaces/repo.d.ts +1 -1
  91. package/dist/core/graph/spaces/repo.js +1 -1
  92. package/dist/core/graph/types.d.ts +4 -3
  93. package/dist/core/graph/watches.d.ts +2 -2
  94. package/dist/core/graph/watches.js +17 -17
  95. package/dist/core/human/page-schema.js +5 -4
  96. package/dist/core/human/requests.d.ts +1 -1
  97. package/dist/core/human/requests.js +20 -23
  98. package/dist/core/peer/ingest.js +1 -1
  99. package/dist/core/peer/receiver.js +1 -1
  100. package/dist/core/peer/stubs.js +1 -1
  101. package/dist/core/providers/receipts.js +3 -2
  102. package/dist/core/review/comments.js +5 -4
  103. package/dist/core/runs/activity.js +4 -5
  104. package/dist/core/runs/confinement.d.ts +1 -1
  105. package/dist/core/runs/confinement.js +2 -2
  106. package/dist/core/runs/events.d.ts +1 -1
  107. package/dist/core/runs/events.js +5 -5
  108. package/dist/core/runs/operations.js +6 -4
  109. package/dist/core/runtime/close.d.ts +3 -0
  110. package/dist/core/runtime/close.js +1 -1
  111. package/dist/core/runtime/host.d.ts +5 -1
  112. package/dist/core/runtime/host.js +3 -3
  113. package/dist/core/runtime/launch.js +2 -2
  114. package/dist/core/runtime/ledger.js +3 -3
  115. package/dist/core/runtime/lifecycle.d.ts +9 -0
  116. package/dist/core/runtime/lifecycle.js +2 -2
  117. package/dist/core/runtime/nodes.d.ts +1 -1
  118. package/dist/core/runtime/nodes.js +1 -1
  119. package/dist/core/runtime/roadmap.js +1 -1
  120. package/dist/core/spaces/stop.d.ts +1 -0
  121. package/dist/core/spaces/stop.js +2 -2
  122. package/dist/core/statements.d.ts +17 -0
  123. package/dist/core/statements.js +2 -0
  124. package/dist/core/substrate/delivery/corpus.js +3 -3
  125. package/dist/core/substrate/delivery/deliver.js +5 -5
  126. package/dist/core/substrate/delivery/listings.js +1 -1
  127. package/dist/core/substrate/delivery/match.d.ts +2 -2
  128. package/dist/core/substrate/delivery/match.js +1 -1
  129. package/dist/core/substrate/delivery/plan.js +3 -3
  130. package/dist/core/substrate/delivery/render-boot.js +5 -5
  131. package/dist/core/substrate/delivery/render-event.js +6 -6
  132. package/dist/core/usage/ledger.js +8 -8
  133. package/dist/daemon/api/bridge.js +4 -4
  134. package/dist/daemon/api/document-listener.js +1 -1
  135. package/dist/daemon/api/handlers/broker-ops.js +1 -1
  136. package/dist/daemon/api/handlers/canvas.js +1 -1
  137. package/dist/daemon/api/handlers/crons.js +1 -1
  138. package/dist/daemon/api/handlers/custom-objects.js +3 -3
  139. package/dist/daemon/api/handlers/docs.js +1 -1
  140. package/dist/daemon/api/handlers/inbox.js +1 -1
  141. package/dist/daemon/api/handlers/memory.js +7 -7
  142. package/dist/daemon/api/handlers/messages.js +1 -1
  143. package/dist/daemon/api/handlers/nodes.js +1 -1
  144. package/dist/daemon/api/handlers/objects.js +3 -3
  145. package/dist/daemon/api/handlers/reviews.js +1 -1
  146. package/dist/daemon/api/handlers/structured-requests.js +1 -1
  147. package/dist/daemon/api/reader.js +1 -1
  148. package/dist/daemon/broker-signals/bus.d.ts +14 -6
  149. package/dist/daemon/broker-signals/bus.js +1 -10
  150. package/dist/daemon/crtrd-cli.js +1 -1
  151. package/dist/daemon/crtrd.js +12 -11
  152. package/dist/daemon/due.js +1 -1
  153. package/dist/daemon/export-sqlite-run.d.ts +1 -0
  154. package/dist/daemon/export-sqlite-run.js +4 -0
  155. package/dist/daemon/fleet.d.ts +1 -0
  156. package/dist/daemon/fleet.js +3 -3
  157. package/dist/daemon/human/settle.js +1 -1
  158. package/dist/daemon/manage.d.ts +10 -0
  159. package/dist/daemon/manage.js +3 -2
  160. package/dist/daemon/postgres/binary.d.ts +17 -0
  161. package/dist/daemon/postgres/binary.js +2 -0
  162. package/dist/daemon/postgres/cluster.d.ts +37 -0
  163. package/dist/daemon/postgres/cluster.js +6 -0
  164. package/dist/daemon/postgres/index.d.ts +2 -0
  165. package/dist/daemon/postgres/index.js +1 -0
  166. package/dist/daemon/postgres/pins.d.ts +7 -0
  167. package/dist/daemon/postgres/pins.js +1 -0
  168. package/dist/daemon/postgres/supervisor.d.ts +95 -0
  169. package/dist/daemon/postgres/supervisor.js +2 -0
  170. package/dist/daemon/reconcilers/bash-deadline.js +2 -2
  171. package/dist/daemon/reconcilers/delivery-lane.js +2 -2
  172. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +7 -3
  173. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +1 -1
  174. package/dist/daemon/reconcilers/node-lifecycle/tick.js +1 -1
  175. package/dist/daemon/reconcilers/storage-maintenance.d.ts +0 -6
  176. package/dist/daemon/reconcilers/storage-maintenance.js +1 -8
  177. package/dist/daemon/runs/observer.js +5 -4
  178. package/dist/migrations/004-canvas-documents/index.js +1 -1
  179. package/dist/pi-extensions/canvas-doc-substrate.d.ts +2 -0
  180. package/dist/pi-extensions/canvas-doc-substrate.js +6 -6
  181. package/dist/pi-extensions/envelope-merge.d.ts +3 -2
  182. package/dist/pi-extensions/envelope-merge.js +9 -8
  183. package/dist/shared/env.d.ts +3 -0
  184. package/dist/shared/env.js +2 -2
  185. package/docs/apps/providers/README.md +2 -0
  186. package/docs/apps/providers/sms.md +2 -0
  187. package/docs/public-api.md +11 -2
  188. package/docs/sdk/release-notes.md +8 -0
  189. package/package.json +6 -6
  190. package/packages/crouter-identity/package.json +1 -1
  191. package/runtime.lock.json +13 -26
  192. package/scripts/install-runtime.mjs +53 -3
  193. package/scripts/lib/isolation-env.mjs +3 -1
@@ -1,28 +1,11 @@
1
- var u=Object.defineProperty;var a=(t,e)=>u(t,"name",{value:e,configurable:!0});import{randomBytes as N}from"node:crypto";import{withCanvasWrite as l,openDb as c}from"./db.js";import{normalizedSettlement as f}from"./deliveries.js";import{withRetryBudget as T}from"./delivery-contract.js";import{emitEvent as y}from"../events/emit.js";async function O(t){await t.exec(`CREATE TABLE IF NOT EXISTS daemon_actions (
2
- action_id TEXT PRIMARY KEY,
3
- adapter_json TEXT NOT NULL,
4
- body_json TEXT,
5
- idempotency_key TEXT NOT NULL UNIQUE,
6
- state TEXT NOT NULL CHECK (state IN ('pending', 'handed_off', 'acknowledged', 'permanent_failed')),
7
- attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
8
- next_attempt_at INTEGER,
9
- claimed_at INTEGER,
10
- claim_owner TEXT,
11
- acknowledged_at INTEGER,
12
- failed_at INTEGER,
13
- last_failure_json TEXT,
14
- canceled_at INTEGER,
15
- created_at INTEGER NOT NULL,
16
- updated_at INTEGER NOT NULL
17
- );
18
- CREATE INDEX IF NOT EXISTS daemon_actions_due ON daemon_actions(state, next_attempt_at, created_at, action_id) WHERE canceled_at IS NULL;`)}a(O,"createDaemonActionsTable");function s(t){return{actionId:t.action_id,adapter:JSON.parse(t.adapter_json),body:t.body_json===null?null:JSON.parse(t.body_json),idempotencyKey:t.idempotency_key,state:t.state,attempts:t.attempts,nextAttemptAt:t.next_attempt_at,claimedAt:t.claimed_at,claimOwner:t.claim_owner,acknowledgedAt:t.acknowledged_at,failedAt:t.failed_at,lastFailure:t.last_failure_json===null?null:JSON.parse(t.last_failure_json),canceledAt:t.canceled_at,createdAt:t.created_at,updatedAt:t.updated_at}}a(s,"actionFrom");function A(){return N(16).toString("hex")}a(A,"newId");async function I(t){return await l(async e=>{const o=await e.prepare("SELECT action_id FROM daemon_actions WHERE idempotency_key = ?").get(t.idempotencyKey);if(o!==void 0)return o.action_id;const d=Date.now(),n=A(),r=t.bodyJson===void 0?null:JSON.stringify(t.bodyJson);return await e.prepare(`INSERT INTO daemon_actions (action_id, adapter_json, body_json, idempotency_key, state, attempts, next_attempt_at, created_at, updated_at)
19
- VALUES (?, ?, ?, ?, 'pending', 0, ?, ?, ?)`).run(n,JSON.stringify(t.adapter),r,t.idempotencyKey,d,d,d),n})}a(I,"enqueueDaemonAction");async function U(t){const e=await(await c()).prepare("SELECT * FROM daemon_actions WHERE idempotency_key = ?").get(t);return e===void 0?null:s(e)}a(U,"readDaemonAction");async function w(t){const e=await(await c()).prepare("SELECT * FROM daemon_actions WHERE action_id = ?").get(t);return e===void 0?null:s(e)}a(w,"readDaemonActionById");async function h(t){return(await(await c()).prepare(`SELECT * FROM daemon_actions
1
+ var f=Object.defineProperty;var a=(t,e)=>f(t,"name",{value:e,configurable:!0});import{randomBytes as y}from"node:crypto";import{withCanvasWrite as l,openDb as r}from"./db.js";import{normalizedSettlement as E}from"./deliveries.js";import{withRetryBudget as A}from"./delivery-contract.js";import{emitEvent as N}from"../events/emit.js";function _(t){return{actionId:t.action_id,adapter:JSON.parse(t.adapter_json),body:t.body_json===null?null:JSON.parse(t.body_json),idempotencyKey:t.idempotency_key,state:t.state,attempts:t.attempts,nextAttemptAt:t.next_attempt_at,claimedAt:t.claimed_at,claimOwner:t.claim_owner,acknowledgedAt:t.acknowledged_at,failedAt:t.failed_at,lastFailure:t.last_failure_json===null?null:JSON.parse(t.last_failure_json),canceledAt:t.canceled_at,createdAt:t.created_at,updatedAt:t.updated_at}}a(_,"actionFrom");function g(){return y(16).toString("hex")}a(g,"newId");async function T(t){return await l(async e=>{const o=await e.prepare("SELECT action_id FROM daemon_actions WHERE idempotency_key = ?").get(t.idempotencyKey);if(o!==void 0)return o.action_id;const d=Date.now(),n=g(),c=t.bodyJson===void 0?null:JSON.stringify(t.bodyJson);return await e.prepare(`INSERT INTO daemon_actions (action_id, adapter_json, body_json, idempotency_key, state, attempts, next_attempt_at, created_at, updated_at)
2
+ VALUES (?, ?, ?, ?, 'pending', 0, ?, ?, ?)`).run(n,JSON.stringify(t.adapter),c,t.idempotencyKey,d,d,d),n})}a(T,"enqueueDaemonAction");async function w(t){const e=await(await r()).prepare("SELECT * FROM daemon_actions WHERE idempotency_key = ?").get(t);return e===void 0?null:_(e)}a(w,"readDaemonAction");async function U(t){const e=await(await r()).prepare("SELECT * FROM daemon_actions WHERE action_id = ?").get(t);return e===void 0?null:_(e)}a(U,"readDaemonActionById");async function h(t){return(await(await r()).prepare(`SELECT * FROM daemon_actions
20
3
  WHERE state = 'pending' AND canceled_at IS NULL AND next_attempt_at <= ?
21
- ORDER BY next_attempt_at, created_at, action_id`).all(t)).map(s)}a(h,"dueDaemonActions");async function b(t,e,o){return await l(async d=>{const n=await d.prepare(`UPDATE daemon_actions SET state = 'handed_off', attempts = attempts + 1, claimed_at = ?, claim_owner = ?, updated_at = ?
22
- WHERE action_id = ? AND state = 'pending' AND canceled_at IS NULL AND next_attempt_at <= ?`).run(e,o,e,t,e);return Number(n.changes)===0?null:s(await d.prepare("SELECT * FROM daemon_actions WHERE action_id = ?").get(t))})}a(b,"claimDaemonAction");async function H(t,e,o,d,n=Date.now()){const r=T(f(d),e),i=r.settlement,p=i.state==="pending"||i.state==="permanent_failed"?JSON.stringify(i.failure):null,_=i.state==="released"?"pending":i.state,E=i.state==="pending"?i.nextAttemptAt:_==="pending"?n:null,m=Number((await(await c()).prepare(`UPDATE daemon_actions
4
+ ORDER BY next_attempt_at, created_at, action_id`).all(t)).map(_)}a(h,"dueDaemonActions");async function b(t,e,o){return await l(async d=>{const n=await d.prepare(`UPDATE daemon_actions SET state = 'handed_off', attempts = attempts + 1, claimed_at = ?, claim_owner = ?, updated_at = ?
5
+ WHERE action_id = ? AND state = 'pending' AND canceled_at IS NULL AND next_attempt_at <= ?`).run(e,o,e,t,e);return Number(n.changes)===0?null:_(await d.prepare("SELECT * FROM daemon_actions WHERE action_id = ?").get(t))})}a(b,"claimDaemonAction");async function H(t,e,o,d,n=Date.now()){const c=A(E(d),e),i=c.settlement,m=i.state==="pending"||i.state==="permanent_failed"?JSON.stringify(i.failure):null,s=i.state==="released"?"pending":i.state,u=i.state==="pending"?i.nextAttemptAt:s==="pending"?n:null,p=Number((await(await r()).prepare(`UPDATE daemon_actions
23
6
  SET state = ?, next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL,
24
7
  acknowledged_at = ?, failed_at = ?, last_failure_json = ?, updated_at = ?
25
- WHERE action_id = ? AND state = 'handed_off' AND attempts = ? AND claim_owner = ?`).run(_,E,_==="acknowledged"?n:null,_==="permanent_failed"?n:null,p,n,t,e,o)).changes)===1;return m&&r.exhausted&&i.state==="permanent_failed"&&y({level:"error",event:"daemon_action.exhausted",outcome:"failed",attempt:e,fields:{action_id:t,failure:i.failure,retry:`crtr sys retry-delivery ${t}`}}),m}a(H,"settleDaemonAction");async function j(t,e=Date.now()){return Number((await(await c()).prepare(`UPDATE daemon_actions
8
+ WHERE action_id = ? AND state = 'handed_off' AND attempts = ? AND claim_owner = ?`).run(s,u,s==="acknowledged"?n:null,s==="permanent_failed"?n:null,m,n,t,e,o)).changes)===1;return p&&c.exhausted&&i.state==="permanent_failed"&&N({level:"error",event:"daemon_action.exhausted",outcome:"failed",attempt:e,fields:{action_id:t,failure:i.failure,retry:`crtr sys retry-delivery ${t}`}}),p}a(H,"settleDaemonAction");async function W(t,e=Date.now()){return Number((await(await r()).prepare(`UPDATE daemon_actions
26
9
  SET state = 'pending', attempts = 0, next_attempt_at = ?, failed_at = NULL, updated_at = ?
27
- WHERE action_id = ? AND state = 'permanent_failed' AND canceled_at IS NULL AND last_failure_json IS NOT NULL`).run(e,e,t)).changes)===1}a(j,"retryFailedDaemonAction");async function C(t){return Number((await(await c()).prepare(`UPDATE daemon_actions SET state = 'pending', next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL, updated_at = ?
28
- WHERE state = 'handed_off'`).run(t,t)).changes)}a(C,"resetHandedOffDaemonActions");export{b as claimDaemonAction,O as createDaemonActionsTable,h as dueDaemonActions,I as enqueueDaemonAction,U as readDaemonAction,w as readDaemonActionById,C as resetHandedOffDaemonActions,j as retryFailedDaemonAction,H as settleDaemonAction};
10
+ WHERE action_id = ? AND state = 'permanent_failed' AND canceled_at IS NULL AND last_failure_json IS NOT NULL`).run(e,e,t)).changes)===1}a(W,"retryFailedDaemonAction");async function j(t){return Number((await(await r()).prepare(`UPDATE daemon_actions SET state = 'pending', next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL, updated_at = ?
11
+ WHERE state = 'handed_off'`).run(t,t)).changes)}a(j,"resetHandedOffDaemonActions");export{b as claimDaemonAction,h as dueDaemonActions,T as enqueueDaemonAction,w as readDaemonAction,U as readDaemonActionById,j as resetHandedOffDaemonActions,W as retryFailedDaemonAction,H as settleDaemonAction};
@@ -65,3 +65,6 @@ export declare function claimDaemonOwnership(token: string): Promise<DaemonOwner
65
65
  export declare function releaseDaemonOwnership(claim: DaemonOwnerClaim): Promise<{
66
66
  readonly released: boolean;
67
67
  }>;
68
+ /** Whether any daemon currently holds the ownership claim. A stopping daemon asks this (after releasing its own claim)
69
+ * before stopping the shared Postgres, so it never stops a database a successor has already claimed. */
70
+ export declare function daemonOwnershipClaimed(): Promise<boolean>;
@@ -1,6 +1 @@
1
- var p=Object.defineProperty;var i=(e,n)=>p(e,"name",{value:n,configurable:!0});import{openDb as s}from"./db.js";import{capturePidIdentities as E,recordedPidLiveness as m}from"./pid.js";const c="daemon_owner";function l(e){return JSON.stringify({pid:e.pid,pid_identity:e.pid_identity,token:e.token})}i(l,"serializeClaim");function f(e){let n;try{n=JSON.parse(e)}catch{return null}if(typeof n!="object"||n===null)return null;const{pid:a,pid_identity:t,token:r}=n;return!Number.isInteger(a)||a<=0||typeof t!="string"||t.length===0||typeof r!="string"||r.length===0?null:{pid:a,pid_identity:t,token:r}}i(f,"parseClaim");async function y(){await(await s()).exec(`
2
- CREATE TABLE IF NOT EXISTS canvas_meta (
3
- key TEXT PRIMARY KEY,
4
- value TEXT NOT NULL
5
- )
6
- `)}i(y,"ensureOwnerTable");async function w(){return(await(await s()).prepare("SELECT value FROM canvas_meta WHERE key = ?").get(c))?.value}i(w,"readSnapshot");async function O(e){const n=E([process.pid]),a=n?.get(process.pid);if(n===null||a===void 0)return{claimed:!1,reason:"own_identity_indeterminate"};const t={pid:process.pid,pid_identity:a,token:e},r=l(t);for(await y();;){const d=await w();if(d===void 0){if((await(await s()).prepare("INSERT INTO canvas_meta (key, value) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM canvas_meta WHERE key = ?)").run(c,r,c)).changes===1)return{claimed:!0,claim:t};continue}const o=f(d);if(o===null)return{claimed:!1,reason:"incumbent_malformed"};const u=m(o.pid,o.pid_identity);if(u==="alive"||u==="indeterminate")return{claimed:!1,reason:u==="alive"?"incumbent_alive":"incumbent_indeterminate",incumbentPid:o.pid,verdict:u};if((await(await s()).prepare("UPDATE canvas_meta SET value = ? WHERE key = ? AND value = ?").run(r,c,d)).changes===1)return{claimed:!0,claim:t}}}i(O,"claimDaemonOwnership");async function N(e){return{released:(await(await s()).prepare("DELETE FROM canvas_meta WHERE key = ? AND value = ?").run(c,l(e))).changes===1}}i(N,"releaseDaemonOwnership");export{c as DAEMON_OWNER_META_KEY,O as claimDaemonOwnership,N as releaseDaemonOwnership};
1
+ var m=Object.defineProperty;var i=(e,n)=>m(e,"name",{value:n,configurable:!0});import{openDb as d}from"./db.js";import{capturePidIdentities as f,recordedPidLiveness as E}from"./pid.js";const s="daemon_owner";function l(e){return JSON.stringify({pid:e.pid,pid_identity:e.pid_identity,token:e.token})}i(l,"serializeClaim");function y(e){let n;try{n=JSON.parse(e)}catch{return null}if(typeof n!="object"||n===null)return null;const{pid:a,pid_identity:t,token:r}=n;return!Number.isInteger(a)||a<=0||typeof t!="string"||t.length===0||typeof r!="string"||r.length===0?null:{pid:a,pid_identity:t,token:r}}i(y,"parseClaim");async function p(){return(await(await d()).prepare("SELECT value FROM canvas_meta WHERE key = ?").get(s))?.value}i(p,"readSnapshot");async function O(e){const n=f([process.pid]),a=n?.get(process.pid);if(n===null||a===void 0)return{claimed:!1,reason:"own_identity_indeterminate"};const t={pid:process.pid,pid_identity:a,token:e},r=l(t);for(;;){const u=await p();if(u===void 0){if((await(await d()).prepare("INSERT INTO canvas_meta (key, value) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM canvas_meta WHERE key = ?) ON CONFLICT (key) DO NOTHING").run(s,r,s)).changes===1)return{claimed:!0,claim:t};continue}const c=y(u);if(c===null)return{claimed:!1,reason:"incumbent_malformed"};const o=E(c.pid,c.pid_identity);if(o==="alive"||o==="indeterminate")return{claimed:!1,reason:o==="alive"?"incumbent_alive":"incumbent_indeterminate",incumbentPid:c.pid,verdict:o};if((await(await d()).prepare("UPDATE canvas_meta SET value = ? WHERE key = ? AND value = ?").run(r,s,u)).changes===1)return{claimed:!0,claim:t}}}i(O,"claimDaemonOwnership");async function N(e){return{released:(await(await d()).prepare("DELETE FROM canvas_meta WHERE key = ? AND value = ?").run(s,l(e))).changes===1}}i(N,"releaseDaemonOwnership");async function T(){return await p()!==void 0}i(T,"daemonOwnershipClaimed");export{s as DAEMON_OWNER_META_KEY,O as claimDaemonOwnership,T as daemonOwnershipClaimed,N as releaseDaemonOwnership};
@@ -1,17 +1,16 @@
1
- import { DatabaseSync, type SQLInputValue } from 'node:sqlite';
2
- export { MIGRATIONS } from './migrations.js';
3
- /** A schema change invalidates statements compiled against the old schema. */
4
- export declare function migrate(db: DatabaseSync): void;
5
- /** Only the daemon (and its API-server test host) registers this process-local hook.
6
- * Register after migration: TEMP triggers need the real tables to exist. */
7
- export declare function bindDaemonCanvasOpenHook(hook: (db: DatabaseSync) => void): void;
8
- export declare function clearDaemonCanvasOpenHook(): void;
9
- /** Close and forget the handle for the current home. Mainly for tests. */
10
- export declare function closeDb(): void;
11
- export type CanvasParams = [Record<string, SQLInputValue>, ...SQLInputValue[]] | SQLInputValue[];
1
+ import type { SQLInputValue } from 'node:sqlite';
2
+ /** Thrown for a unique-constraint violation (SQLSTATE 23505). `constraint` is the violated constraint or unique index. */
3
+ export declare class CanvasConstraintError extends Error {
4
+ readonly constraint: string | undefined;
5
+ readonly code = "23505";
6
+ constructor(message: string, constraint: string | undefined, options?: {
7
+ cause?: unknown;
8
+ });
9
+ }
10
+ export type CanvasValue = SQLInputValue;
11
+ export type CanvasParams = CanvasValue[];
12
12
  export interface CanvasRunResult {
13
- changes: number | bigint;
14
- lastInsertRowid: number | bigint;
13
+ changes: number;
15
14
  }
16
15
  export interface CanvasStatement {
17
16
  get<T = Record<string, unknown>>(...params: CanvasParams): Promise<T | undefined>;
@@ -19,18 +18,24 @@ export interface CanvasStatement {
19
18
  run(...params: CanvasParams): Promise<CanvasRunResult>;
20
19
  }
21
20
  export interface CanvasDb {
22
- /** A statement compiled once per connection; for fixed SQL only. */
21
+ /** A named prepared statement, translated once per connection; for fixed SQL only. */
23
22
  prepare(sql: string): CanvasStatement;
24
- /** A statement compiled per call; for generated SQL (IN lists, schema-dependent text). */
23
+ /** An unnamed statement per call; for generated SQL (IN lists, schema-dependent text). */
25
24
  prepareUncached(sql: string): CanvasStatement;
25
+ /** Run one or more statements with no parameters (no `?` translation). */
26
26
  exec(sql: string): Promise<void>;
27
27
  /** True when the calling async context is inside a `withCanvasWrite` body (its own open transaction). */
28
28
  readonly inTransaction: boolean;
29
- /** The SQLite connection, for the synchronous migration chain only. Deleted with the Postgres swap. */
30
- readonly raw: DatabaseSync;
31
29
  }
32
- /** Open (or reuse) the canvas db handle for the current `CRTR_HOME`. */
30
+ /** The URL of the database for the current `CRTR_HOME`. In a test it creates the home's database on first use. */
31
+ export declare function canvasDatabaseUrl(): Promise<string>;
32
+ /** Open (or reuse) the canvas db handle for the current `CRTR_HOME`. Keyed by database URL, so tests with distinct
33
+ * homes get independent handles. The first open connects and applies the schema. */
33
34
  export declare function openDb(): Promise<CanvasDb>;
35
+ /** Close and forget the connection for the current home. Mainly for tests. */
36
+ export declare function closeDb(): Promise<void>;
37
+ /** Close every connection this process holds (daemon shutdown). */
38
+ export declare function closeAllDbs(): Promise<void>;
34
39
  /** Run `hook` once the calling context's outermost transaction has committed (and released the write lock); drop it if
35
40
  * the transaction or the savepoint holding it rolls back. Outside a transaction it runs in the next microtask. Use it for anything
36
41
  * that tells another context about a write, so that context never reads uncommitted rows or hears of a rolled-back one. */
@@ -41,8 +46,11 @@ export interface CanvasWriteOptions {
41
46
  /** Always undo the body's writes, even when it returns (a dry run). Nested, it runs in a SAVEPOINT rolled back at the end. */
42
47
  rollback?: boolean;
43
48
  }
44
- /** Run `body` in one `BEGIN IMMEDIATE` transaction, the only transaction entry point for canvas.db.
49
+ /** Run `body` in one transaction, the only transaction entry point for the canvas database.
45
50
  * A call from inside the body's own async context joins it and only the outermost call commits; a call
46
51
  * from any other context waits its turn. A throwing body rolls back (with `savepoint`, a nested body
47
- * rolls back only its own writes). With `rollback` the body's writes are always undone. */
52
+ * rolls back only its own writes). With `rollback` the body's writes are always undone.
53
+ *
54
+ * Postgres aborts the whole transaction when a statement fails, so a body that catches a failed statement and
55
+ * carries on must run that statement inside a nested `withCanvasWrite(…, { savepoint: true })`. */
48
56
  export declare function withCanvasWrite<T>(body: (db: CanvasDb) => Promise<T> | T, options?: CanvasWriteOptions): Promise<T>;
@@ -1,3 +1,4 @@
1
- var C=Object.defineProperty;var o=(e,t)=>C(e,"name",{value:t,configurable:!0});import{DatabaseSync as v}from"node:sqlite";import{AsyncLocalStorage as R}from"node:async_hooks";import{forgetPrepared as y,prepared as $}from"../sqlite-statements.js";import{appendFileSync as D}from"node:fs";import{canvasDbPath as k,ensureHome as T}from"./paths.js";import{initializeEmptyLayout as _}from"../layout.js";import{migrate as I}from"./migrations.js";import{MIGRATIONS as re}from"./migrations.js";function J(e){try{I(e)}finally{y(e)}}o(J,"migrate");const l=new Map;let m;function Q(e){const t=x();e(t),m=e}o(Q,"bindDaemonCanvasOpenHook");function X(){m=void 0}o(X,"clearDaemonCanvasOpenHook");function P(){const e=process.env.CRTR_DB_OPEN_LEDGER;if(e)try{const t=`${new Date().toISOString()} ${process.pid} ${process.argv[1]??"<unknown>"}
2
- `;D(e,t)}catch{}}o(P,"recordOpenLedger");function x(){const e=k(),t=l.get(e);if(t)return t;_(),T(),P();const n=new v(e);n.exec("PRAGMA busy_timeout = 5000;"),n.exec("PRAGMA journal_mode = WAL;"),n.exec("PRAGMA foreign_keys = ON;");try{m?.(n)}catch(r){throw n.close(),r}return l.set(e,n),n}o(x,"openConnection");function Y(){const e=k(),t=l.get(e);t&&(y(t),t.close(),l.delete(e))}o(Y,"closeDb");const b=1e3,i=new R;class B{static{o(this,"FifoLock")}tail=Promise.resolve();pending=0;get busy(){return this.pending>0}async acquire(){this.pending++;const t=this.tail;let n;return this.tail=new Promise(r=>{n=r}),await t,()=>{this.pending--,n()}}}const A=new WeakMap,M=new WeakMap;function S(e){let t=A.get(e);return t||A.set(e,t=new B),t}o(S,"lockFor");function w(e){const t=i.getStore();return t!==void 0&&t.open&&t.db===e}o(w,"ownsTransaction");async function p(e,t){const n=S(e);if(w(e)){const f=i.getStore(),u=performance.now();try{return t()}finally{f.statementMs+=performance.now()-u}}if(!n.busy)return t();const r=await n.acquire();try{return t()}finally{r()}}o(p,"gated");function E(e,t){const n=o(r=>r,"args");return{get:o((...r)=>p(e,()=>t().get(...n(r))),"get"),all:o((...r)=>p(e,()=>t().all(...n(r))),"all"),run:o((...r)=>p(e,()=>t().run(...n(r))),"run")}}o(E,"wrapStatement");function N(e){let t=M.get(e);return t||(t={prepare:o(n=>E(e,()=>$(e,n)),"prepare"),prepareUncached:o(n=>E(e,()=>e.prepare(n)),"prepareUncached"),exec:o(n=>p(e,()=>{e.exec(n)}),"exec"),get inTransaction(){return w(e)},raw:e},M.set(e,t)),t}o(N,"canvasDbFor");async function G(){return N(x())}o(G,"openDb");function Z(e){const t=i.getStore();if(t===void 0||!t.open){queueMicrotask(e);return}t.afterCommit.push(e)}o(Z,"afterCommit");let q=0;async function ee(e,t={}){const n=await G(),r=n.raw;if(w(r)){if(!t.savepoint&&!t.rollback)return e(n);const a=`canvas_write_${++q}`;r.exec(`SAVEPOINT ${a}`);const s=i.getStore()?.afterCommit,h=s?.length??0;let d;try{d=await e(n)}catch(O){throw r.exec(`ROLLBACK TO ${a}; RELEASE ${a}`),s?.splice(h),O}return r.exec(t.rollback?`ROLLBACK TO ${a}; RELEASE ${a}`:`RELEASE ${a}`),t.rollback&&s?.splice(h),d}const f=await S(r).acquire(),u=performance.now(),L=new Error("canvas write transaction entered here"),c={db:r,open:!0,afterCommit:[],statementMs:0};let g=!1;try{r.exec("BEGIN IMMEDIATE");try{const a=await i.run(c,()=>e(n));return r.exec(t.rollback?"ROLLBACK":"COMMIT"),g=!t.rollback,a}catch(a){try{r.exec("ROLLBACK")}catch{}throw a}}finally{c.open=!1;const a=performance.now()-u;if(a-c.statementMs>b&&console.warn(`[canvas db] write transaction held ${Math.round(a)}ms (${Math.round(c.statementMs)}ms in its statements); its body did non-db work while every other db caller waited
3
- ${L.stack}`),f(),g)for(const s of c.afterCommit)queueMicrotask(s)}}o(ee,"withCanvasWrite");export{re as MIGRATIONS,Z as afterCommit,Q as bindDaemonCanvasOpenHook,X as clearDaemonCanvasOpenHook,Y as closeDb,J as migrate,G as openDb,ee as withCanvasWrite};
1
+ var O=Object.defineProperty;var a=(e,t)=>O(e,"name",{value:t,configurable:!0});import{AsyncLocalStorage as R}from"node:async_hooks";import{randomBytes as L}from"node:crypto";import{appendFileSync as x,mkdirSync as k,readFileSync as _}from"node:fs";import{dirname as M,join as C}from"node:path";import{fileURLToPath as N}from"node:url";import y from"pg";import{resolveDatabaseUrl as P}from"../../daemon/postgres/supervisor.js";import{publishFileExclusive as D}from"../fs-utils.js";import{initializeEmptyLayout as q,runtimeRoot as l}from"../layout.js";import{translatePlaceholders as B,translateOnce as I}from"../statements.js";import{ensureHome as U}from"./paths.js";import{CANVAS_SCHEMA_VERSION as m}from"./schema-version.js";const H="crtr_test_template";class F extends Error{static{a(this,"CanvasConstraintError")}constraint;code="23505";constructor(t,n,r){super(t,r),this.name="CanvasConstraintError",this.constraint=n}}function K(e){const t=process.env.CRTR_DB_OPEN_LEDGER;if(t)try{const n=`${new Date().toISOString()} ${process.pid} ${process.argv[1]??"<unknown>"} ${e.replace(/\/\/([^:@/]*):[^@/]*@/,"//$1@")}
2
+ `;x(t,n)}catch{}}a(K,"recordOpenLedger");const V=1e3,d=new R;class G{static{a(this,"FifoLock")}tail=Promise.resolve();pending=0;get busy(){return this.pending>0}async acquire(){this.pending++;const t=this.tail;let n;return this.tail=new Promise(r=>{n=r}),await t,()=>{this.pending--,n()}}}const W=new Set([114,3802]),J={getTypeParser:a(((e,t)=>W.has(e)?n=>n:e===20||e===1700?n=>{const r=Number(n);if(e===20&&!Number.isSafeInteger(r))throw new Error(`canvas db: bigint ${n} does not fit a JS number`);return r}:y.types.getTypeParser(e,t)),"getTypeParser")};class Y{static{a(this,"Handle")}url;lock=new G;db;client=null;connecting=null;schemaChecked=!1;active=0;constructor(t){this.url=t;const n=this;this.db={prepare:a(r=>this.statement(r,!0),"prepare"),prepareUncached:a(r=>this.statement(r,!1),"prepareUncached"),exec:a(r=>this.gated(async()=>{await this.send({text:r})}),"exec"),get inTransaction(){return E(n)}}}async connected(){return this.client!==null?this.client:(this.connecting??=this.connect().finally(()=>{this.connecting=null}),this.connecting)}async connect(){K(this.url);const t=new y.Client({connectionString:this.url,types:J,application_name:`crtr-${process.pid}`,connectionTimeoutMillis:1e4});t.on("error",n=>{console.warn(`[canvas db] connection error: ${n.message}`),this.drop(t)}),t.on("end",()=>this.drop(t)),await t.connect();try{this.schemaChecked||(await Q(t),this.schemaChecked=!0)}catch(n){throw await t.end().catch(()=>{}),n}return t.connection.stream.unref(),this.client=t,t}drop(t){this.client===t&&(this.client=null)}async send(t){const n=await this.connected(),r=d.getStore();if(r!==void 0&&r.open&&r.handle===this&&r.client!==n)throw new Error("canvas db: the connection was lost during this transaction; it was rolled back");return this.sendOn(n,t)}async sendOn(t,n){const r=t.connection.stream;this.active++===0&&r.ref();try{return await t.query(n)}catch(o){throw this.mapError(o,t)}finally{--this.active===0&&r.unref()}}mapError(t,n){const r=t;return r.code==="23505"?new F(r.message??"unique constraint violation",r.constraint,{cause:t}):((r.code==="57P01"||r.code==="ECONNRESET"||r.code==="EPIPE"||/Connection terminated|connection error|Client was closed/i.test(r.message??""))&&(this.drop(n),n.end().catch(()=>{})),t)}async gated(t){if(E(this)){const r=d.getStore(),o=performance.now();try{return await t()}finally{r.statementMs+=performance.now()-o}}if(!this.lock.busy)return t();const n=await this.lock.acquire();try{return await t()}finally{n()}}statement(t,n){const r=n?(()=>{const s=B(t);return{text:s.text,name:s.name}})():{text:I(t).text},o=a(s=>this.gated(()=>this.send({...r,values:s.map(j)})),"query");return{get:a(async(...s)=>(await o(s)).rows[0],"get"),all:a(async(...s)=>(await o(s)).rows,"all"),run:a(async(...s)=>({changes:(await o(s)).rowCount??0}),"run")}}async close(){const t=this.client;this.client=null,t!==null&&(t.connection.stream.ref(),await t.end().catch(()=>{}))}}function j(e){return ArrayBuffer.isView(e)&&!Buffer.isBuffer(e)?Buffer.from(e.buffer,e.byteOffset,e.byteLength):e}a(j,"bindable");function E(e){const t=d.getStore();return t!==void 0&&t.open&&t.handle===e}a(E,"ownsTransaction");const z=M(N(import.meta.url)),X=C(z,"schema-pg.sql"),T=7427001;async function v(e){if((await e.query("SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = 'canvas_meta'")).rowCount===0)return;const n=await e.query("SELECT value FROM canvas_meta WHERE key = 'schema_version'");return n.rows[0]===void 0?void 0:Number(n.rows[0].value)}a(v,"storedSchemaVersion");async function Q(e){await e.query("SELECT pg_advisory_lock($1)",[T]);try{let t=await v(e);if(t===void 0&&(await e.query(_(X,"utf8")),t=await v(e)),t!==m)throw new Error(t!==void 0&&t>m?`unsupported canvas schema version ${t} (this build supports ${m}); this build is older than the database. Select a newer runtime generation.`:`canvas schema version ${String(t)} does not match this build's ${m}`)}finally{await e.query("SELECT pg_advisory_unlock($1)",[T]).catch(()=>{})}}a(Q,"ensureSchema");function Z(){return process.env.NODE_TEST_CONTEXT!==void 0||process.execArgv.includes("--test")}a(Z,"runningUnderNodeTest");function w(e,t){const n=/^(postgres(?:ql)?:\/\/[^/?#]*)(\/[^?#]*)?(.*)$/s.exec(e);if(n===null)throw new Error(`not a postgres URL: ${e.replace(/\/\/[^@/]*@/,"//")}`);return`${n[1]}/${t}${n[3]}`}a(w,"withDatabase");async function tt(e){const t=C(l(),"test-db"),n=a(()=>{try{return _(t,"utf8").trim()||void 0}catch(h){if(h.code==="ENOENT")return;throw h}},"recorded"),r=n();if(r!==void 0)return w(e,r);k(l(),{recursive:!0});const o=`t_${L(8).toString("hex")}`,s=new y.Client({connectionString:w(e,"postgres")});await s.connect();try{if(await s.query(`CREATE DATABASE ${o} TEMPLATE ${H} STRATEGY file_copy`),!D(t,`${o}
3
+ `,384))return await s.query(`DROP DATABASE ${o}`),w(e,n())}finally{await s.end()}return w(e,o)}a(tt,"testDatabaseUrl");async function et(){const e=process.env.CRTR_TEST_PG_URL;if(e)return tt(e);if(Z())throw new Error("CRTR_TEST_PG_URL is not set: a test must reach its own Postgres (run the suite through the lane runner); refusing to open a real canvas database");return P(l())}a(et,"canvasDatabaseUrl");const f=new Map,p=new Map;async function A(){q(),U();const e=await et();p.set(l(),e);let t=f.get(e);return t===void 0&&f.set(e,t=new Y(e)),await t.connected(),t}a(A,"openHandle");async function pt(){return(await A()).db}a(pt,"openDb");async function yt(){const e=p.get(l());if(e===void 0)return;p.delete(l());const t=f.get(e);t!==void 0&&(f.delete(e),await t.close())}a(yt,"closeDb");async function Et(){const e=[...f.values()];f.clear(),p.clear(),await Promise.all(e.map(t=>t.close()))}a(Et,"closeAllDbs");function gt(e){const t=d.getStore();if(t===void 0||!t.open){queueMicrotask(e);return}t.afterCommit.push(e)}a(gt,"afterCommit");let nt=0;async function St(e,t={}){const n=await A(),r=n.db;if(E(n)){if(!t.savepoint&&!t.rollback)return e(r);const i=`canvas_write_${++nt}`;await n.send({text:`SAVEPOINT ${i}`});const c=d.getStore()?.afterCommit,S=c?.length??0;let b;try{b=await e(r)}catch($){throw await n.send({text:`ROLLBACK TO ${i}; RELEASE ${i}`}),c?.splice(S),$}return await n.send({text:t.rollback?`ROLLBACK TO ${i}; RELEASE ${i}`:`RELEASE ${i}`}),t.rollback&&c?.splice(S),b}const o=await n.lock.acquire(),s=performance.now(),h=new Error("canvas write transaction entered here");let g=!1;const u={handle:n,client:void 0,open:!0,afterCommit:[],statementMs:0};try{const i=await n.connected();await n.sendOn(i,{text:"BEGIN"}),u.client=i;try{const c=await d.run(u,()=>e(r));return await n.sendOn(i,{text:t.rollback?"ROLLBACK":"COMMIT"}),g=!t.rollback,c}catch(c){try{await n.sendOn(i,{text:"ROLLBACK"})}catch{}throw c}}finally{u.open=!1;const i=performance.now()-s;if(i-u.statementMs>V&&console.warn(`[canvas db] write transaction held ${Math.round(i)}ms (${Math.round(u.statementMs)}ms in its statements); its body did non-db work while every other db caller waited
4
+ ${h.stack}`),o(),g)for(const c of u.afterCommit)queueMicrotask(c)}}a(St,"withCanvasWrite");export{F as CanvasConstraintError,gt as afterCommit,et as canvasDatabaseUrl,Et as closeAllDbs,yt as closeDb,pt as openDb,St as withCanvasWrite};
@@ -112,6 +112,11 @@ export declare function pushDeliveryClaimIsCurrent(delivery: DeliveryRecord, own
112
112
  /** Reset mail held by an execution that is no longer this node's one live execution.
113
113
  * Push receiver deliveries have a due timestamp and are claimed by the daemon lane. */
114
114
  export declare function resetStaleExecutionMail(now: number): Promise<number>;
115
+ /** The one definition of wake mail that can still move a node, as a condition on a `deliveries` row aliased `d`;
116
+ * `states` is the delivery states that count. */
117
+ export declare const WAKE_MAIL_SQL: (d: string, states: string) => string;
118
+ /** Unseen wake mail: not yet acknowledged by an execution of the node. */
119
+ export declare const UNSEEN_WAKE_MAIL_SQL: (d: string) => string;
115
120
  export declare function hasUnseenWakeMail(nodeId: string): Promise<boolean>;
116
121
  export declare function hasWakeMailSince(nodeId: string, since: number): Promise<boolean>;
117
122
  export declare function acknowledgedNodeMailCount(nodeId: string): Promise<number>;
@@ -1,51 +1,46 @@
1
- var x=Object.defineProperty;var _=(e,a)=>x(e,"name",{value:a,configurable:!0});import{prepared as m}from"../sqlite-statements.js";import{existsSync as O,readFileSync as D}from"node:fs";import{randomBytes as C}from"node:crypto";import{withCanvasWrite as T,openDb as N}from"./db.js";import{inboxPath as A,passivePath as v}from"./paths.js";import{withRetryBudget as H}from"./delivery-contract.js";import{emitEvent as X}from"../events/emit.js";import{mapAsync as F}from"../async-array.js";function I(e){return{deliveryId:e.delivery_id,messageId:e.message_id,destinationId:e.destination_id,urgency:e.urgency,wake:e.wake===1?1:0,state:e.state,attempts:e.attempts,nextAttemptAt:e.next_attempt_at,claimedAt:e.claimed_at,claimOwner:e.claim_owner,acknowledgedAt:e.acknowledged_at,failedAt:e.failed_at,lastFailure:e.last_failure_json===null?null:JSON.parse(e.last_failure_json),idempotencyKey:e.idempotency_key,canceledAt:e.canceled_at,createdAt:e.created_at,updatedAt:e.updated_at}}_(I,"deliveryFrom");async function L(e,a,r,d){return await e.prepare(`SELECT 1 FROM nodes n JOIN executions e ON e.node_id = n.node_id
2
- WHERE n.node_id = ? AND e.execution_id = ? AND e.state != 'exited'${d?" AND n.intent IS NOT 'refresh'":""}`).get(a,r)!==void 0}_(L,"executionIsLive");async function w(e,a,r,d,s,E=!1){return(await e.prepare(`SELECT d.*, m.message_id AS m_message_id, m.seq, m.operation_id, m.created_at AS message_created_at,
3
- m.sender_node_id, m.sender_name, m.kind, m.label, m.body_ref, m.body_json, m.disposition,
4
- CASE WHEN d.state = 'handed_off' THEN 'inherited' ELSE 'fresh' END AS handoff
1
+ var x=Object.defineProperty;var r=(e,a)=>x(e,"name",{value:a,configurable:!0});import{prepared as m}from"../sqlite-statements.js";import{existsSync as I,readFileSync as O}from"node:fs";import{randomBytes as C}from"node:crypto";import{withCanvasWrite as T,openDb as N}from"./db.js";import{inboxPath as w,passivePath as D}from"./paths.js";import{withRetryBudget as X}from"./delivery-contract.js";import{emitEvent as F}from"../events/emit.js";import{mapAsync as H}from"../async-array.js";function R(e){return{deliveryId:e.delivery_id,messageId:e.message_id,destinationId:e.destination_id,urgency:e.urgency,wake:e.wake===1?1:0,state:e.state,attempts:e.attempts,nextAttemptAt:e.next_attempt_at,claimedAt:e.claimed_at,claimOwner:e.claim_owner,acknowledgedAt:e.acknowledged_at,failedAt:e.failed_at,lastFailure:e.last_failure_json===null?null:JSON.parse(e.last_failure_json),idempotencyKey:e.idempotency_key,canceledAt:e.canceled_at,createdAt:e.created_at,updatedAt:e.updated_at}}r(R,"deliveryFrom");async function L(e,a,o,d){return await e.prepare(`SELECT 1 FROM nodes n JOIN executions e ON e.node_id = n.node_id
2
+ WHERE n.node_id = ? AND e.execution_id = ? AND e.state != 'exited'${d?" AND n.intent IS DISTINCT FROM 'refresh'":""}`).get(a,o)!==void 0}r(L,"executionIsLive");async function A(e,a,o,d,s,E=!1){return(await e.prepare(`SELECT d.*, m.message_id AS m_message_id, m.seq, m.operation_id, m.created_at AS message_created_at,
3
+ m.sender_node_id, m.sender_name, m.kind, m.label, m.body_ref, m.body_json, m.disposition
5
4
  FROM deliveries d JOIN messages m ON m.message_id = d.message_id
6
5
  WHERE d.destination_id = ? AND d.wake = ?
7
- ${E?"AND m.kind = 'message' AND json_extract(m.body_json, '$.start_turn') = 0":d===0?"AND NOT (m.kind = 'message' AND coalesce(json_extract(m.body_json, '$.start_turn'), 1) = 0)":""}
6
+ ${E?"AND m.kind = 'message' AND m.body_json ->> 'start_turn' = 'false'":d===0?"AND NOT (m.kind = 'message' AND coalesce(m.body_json ->> 'start_turn', 'true') = 'false')":""}
8
7
  AND d.canceled_at IS NULL
9
- AND (d.state = 'pending' OR (d.state = 'handed_off' AND d.claim_owner = ?))
10
- ORDER BY m.seq`).all(a,d,r)).map(i=>({...I(i),message:{messageId:i.m_message_id,seq:Number(i.seq),operationId:i.operation_id,createdAt:i.message_created_at,senderNodeId:i.sender_node_id,senderName:i.sender_name,kind:i.kind,label:i.label,bodyRef:i.body_ref,bodyJson:i.body_json===null?null:JSON.parse(i.body_json),disposition:i.disposition},handoff:s?.has(i.delivery_id)===!0?"fresh":i.handoff}))}_(w,"nodeMailRows");const R=`SELECT d.delivery_id FROM deliveries d JOIN messages m ON m.message_id = d.message_id
8
+ AND d.state = 'handed_off' AND d.claim_owner = ?
9
+ ORDER BY m.seq`).all(a,d,o)).map(i=>({...R(i),message:{messageId:i.m_message_id,seq:Number(i.seq),operationId:i.operation_id,createdAt:i.message_created_at,senderNodeId:i.sender_node_id,senderName:i.sender_name,kind:i.kind,label:i.label,bodyRef:i.body_ref,bodyJson:i.body_json===null?null:JSON.parse(i.body_json),disposition:i.disposition},handoff:s?.has(i.delivery_id)===!0?"fresh":"inherited"}))}r(A,"nodeMailRows");const v=`SELECT d.delivery_id FROM deliveries d JOIN messages m ON m.message_id = d.message_id
11
10
  WHERE d.destination_id = ? AND d.wake = ? AND d.state = 'pending' AND d.canceled_at IS NULL
12
- AND NOT (d.wake = 0 AND m.kind = 'message' AND coalesce(json_extract(m.body_json, '$.start_turn'), 1) = 0)`,j=`UPDATE deliveries SET state = 'handed_off', attempts = attempts + 1, claim_owner = ?, claimed_at = ?, updated_at = ?
13
- WHERE delivery_id IN (${R})`;async function V(e,a,r,d=Date.now()){const s=r==="wake"?1:0;if(r==="deferred_run")return await T(async c=>{if(!await L(c,e,a,!0))return{action:"hold",deliveries:[]};const i=await c.prepare(`SELECT d.delivery_id FROM deliveries d JOIN messages m ON m.message_id = d.message_id
11
+ AND NOT (d.wake = 0 AND m.kind = 'message' AND coalesce(m.body_json ->> 'start_turn', 'true') = 'false')`,M=`UPDATE deliveries SET state = 'handed_off', attempts = attempts + 1, claim_owner = ?, claimed_at = ?, updated_at = ?
12
+ WHERE delivery_id IN (${v}) RETURNING delivery_id`;async function Z(e,a,o,d=Date.now()){const s=o==="wake"?1:0;if(o==="deferred_run")return await T(async c=>{if(!await L(c,e,a,!0))return{action:"hold",deliveries:[]};const i=await c.prepare(`SELECT d.delivery_id FROM deliveries d JOIN messages m ON m.message_id = d.message_id
14
13
  WHERE d.destination_id = ? AND d.wake = 0 AND d.canceled_at IS NULL
15
- AND m.kind = 'message' AND json_extract(m.body_json, '$.start_turn') = 0
16
- AND (d.state = 'pending' OR (d.state = 'handed_off' AND d.claim_owner = ?))`).all(e,a),t=new Set((await c.prepare(`SELECT d.delivery_id FROM deliveries d JOIN messages m ON m.message_id = d.message_id
17
- WHERE d.destination_id = ? AND d.wake = 0 AND d.state = 'pending' AND d.canceled_at IS NULL
18
- AND m.kind = 'message' AND json_extract(m.body_json, '$.start_turn') = 0`).all(e)).map(n=>n.delivery_id));return i.length&&await c.prepare(`UPDATE deliveries SET state = 'handed_off', attempts = attempts + 1, claim_owner = ?, claimed_at = ?, updated_at = ?
19
- WHERE delivery_id IN (${i.map(()=>"?").join(",")}) AND state = 'pending'`).run(a,d,d,...i.map(n=>n.delivery_id)),{action:"claimed",deliveries:await w(c,e,a,0,t,!0)}});const E=await N();return!E.inTransaction&&await E.prepare(R).get(e,s)===void 0?await L(E,e,a,!0)?{action:"claimed",deliveries:await w(E,e,a,s)}:{action:"hold",deliveries:[]}:await T(async c=>{if(!await L(c,e,a,!0))return{action:"hold",deliveries:[]};const i=new Set((await c.prepare(R).all(e,s)).map(t=>t.delivery_id));return await c.prepare(j).run(a,d,d,e,s),{action:"claimed",deliveries:await w(c,e,a,s,i)}})}_(V,"claimNodeMail");async function z(e,a,r=Date.now()){return await T(async d=>{if(!await L(d,e,a,!1))throw new Error(`execution ${a} is not live for node ${e}`);const s=new Set((await d.prepare(`SELECT delivery_id FROM deliveries
20
- WHERE destination_id = ? AND wake = 1 AND state = 'pending' AND canceled_at IS NULL`).all(e)).map(E=>E.delivery_id));return await d.prepare(`UPDATE deliveries SET state = 'handed_off', attempts = attempts + 1, claim_owner = ?, claimed_at = ?, updated_at = ?
21
- WHERE destination_id = ? AND wake = 1 AND state = 'pending' AND canceled_at IS NULL`).run(a,r,r,e),await w(d,e,a,1,s)})}_(z,"claimKickoffMail");function M(e){return e.state==="accepted"?{state:"acknowledged"}:e}_(M,"normalizedSettlement");async function W(e,a,r,d,s=Date.now()){const E=[],c=await T(async i=>{const n=(await i.prepare("SELECT canceled_at FROM deliveries WHERE delivery_id = ? AND next_attempt_at IS NOT NULL").get(e))?.canceled_at!=null?{state:"acknowledged"}:M(d),o=H(n,a),l=o.settlement,S=l.state==="pending"||l.state==="permanent_failed"?JSON.stringify(l.failure):null,g=l.state==="released"?"pending":l.state,p=l.state==="pending"?l.nextAttemptAt:g==="pending"?s:null,f=Number((await i.prepare(`UPDATE deliveries
14
+ AND m.kind = 'message' AND m.body_json ->> 'start_turn' = 'false'
15
+ AND (d.state = 'pending' OR (d.state = 'handed_off' AND d.claim_owner = ?))`).all(e,a),t=new Set(i.length===0?[]:(await c.prepare(`UPDATE deliveries SET state = 'handed_off', attempts = attempts + 1, claim_owner = ?, claimed_at = ?, updated_at = ?
16
+ WHERE delivery_id IN (${i.map(()=>"?").join(",")}) AND state = 'pending' RETURNING delivery_id`).all(a,d,d,...i.map(n=>n.delivery_id))).map(n=>n.delivery_id));return{action:"claimed",deliveries:await A(c,e,a,0,t,!0)}});const E=await N();return!E.inTransaction&&await E.prepare(v).get(e,s)===void 0?await L(E,e,a,!0)?{action:"claimed",deliveries:await A(E,e,a,s)}:{action:"hold",deliveries:[]}:await T(async c=>{if(!await L(c,e,a,!0))return{action:"hold",deliveries:[]};const i=new Set((await c.prepare(M).all(a,d,d,e,s)).map(t=>t.delivery_id));return{action:"claimed",deliveries:await A(c,e,a,s,i)}})}r(Z,"claimNodeMail");async function ee(e,a,o=Date.now()){return await T(async d=>{if(!await L(d,e,a,!1))throw new Error(`execution ${a} is not live for node ${e}`);const s=new Set((await d.prepare(`UPDATE deliveries SET state = 'handed_off', attempts = attempts + 1, claim_owner = ?, claimed_at = ?, updated_at = ?
17
+ WHERE destination_id = ? AND wake = 1 AND state = 'pending' AND canceled_at IS NULL RETURNING delivery_id`).all(a,o,o,e)).map(E=>E.delivery_id));return await A(d,e,a,1,s)})}r(ee,"claimKickoffMail");function W(e){return e.state==="accepted"?{state:"acknowledged"}:e}r(W,"normalizedSettlement");async function j(e,a,o,d,s=Date.now()){const E=[],c=await T(async i=>{const n=(await i.prepare("SELECT canceled_at FROM deliveries WHERE delivery_id = ? AND next_attempt_at IS NOT NULL").get(e))?.canceled_at!=null?{state:"acknowledged"}:W(d),l=X(n,a),_=l.settlement,S=_.state==="pending"||_.state==="permanent_failed"?JSON.stringify(_.failure):null,g=_.state==="released"?"pending":_.state,p=_.state==="pending"?_.nextAttemptAt:g==="pending"?s:null,f=Number((await i.prepare(`UPDATE deliveries
22
18
  SET state = ?, next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL,
23
19
  acknowledged_at = ?, failed_at = ?, last_failure_json = ?, updated_at = ?
24
- WHERE delivery_id = ? AND state = 'handed_off' AND attempts = ? AND claim_owner = ?`).run(g,p,g==="acknowledged"?s:null,g==="permanent_failed"?s:null,S,s,e,a,r)).changes)===1;if(f&&o.exhausted&&l.state==="permanent_failed"){const y=await i.prepare("SELECT destination_id, message_id FROM deliveries WHERE delivery_id = ?").get(e);E.push({failure:l.failure,destinationId:y.destination_id,messageId:y.message_id})}return f});for(const i of E)X({level:"error",event:"delivery.exhausted",outcome:"failed",attempt:a,fields:{delivery_id:e,destination_id:i.destinationId,message_id:i.messageId,failure:i.failure,retry:`crtr sys retry-delivery ${e}`}});return c}_(W,"settleDelivery");async function Z(e,a=Date.now()){return Number((await(await N()).prepare(`UPDATE deliveries
20
+ WHERE delivery_id = ? AND state = 'handed_off' AND attempts = ? AND claim_owner = ?`).run(g,p,g==="acknowledged"?s:null,g==="permanent_failed"?s:null,S,s,e,a,o)).changes)===1;if(f&&l.exhausted&&_.state==="permanent_failed"){const y=await i.prepare("SELECT destination_id, message_id FROM deliveries WHERE delivery_id = ?").get(e);E.push({failure:_.failure,destinationId:y.destination_id,messageId:y.message_id})}return f});for(const i of E)F({level:"error",event:"delivery.exhausted",outcome:"failed",attempt:a,fields:{delivery_id:e,destination_id:i.destinationId,message_id:i.messageId,failure:i.failure,retry:`crtr sys retry-delivery ${e}`}});return c}r(j,"settleDelivery");async function te(e,a=Date.now()){return Number((await(await N()).prepare(`UPDATE deliveries
25
21
  SET state = 'pending', attempts = 0, next_attempt_at = ?, failed_at = NULL, updated_at = ?
26
- WHERE delivery_id = ? AND state = 'permanent_failed' AND canceled_at IS NULL AND last_failure_json IS NOT NULL`).run(a,a,e)).changes)===1}_(Z,"retryFailedDelivery");async function ee(e,a,r,d=Date.now()){return await T(async s=>await L(s,e,a,!1)?{action:"acknowledged",results:await F(r,async c=>{const i=await s.prepare("SELECT 1 FROM deliveries WHERE delivery_id = ? AND destination_id = ?").get(c.deliveryId,e)!==void 0;return{deliveryId:c.deliveryId,outcome:i&&await W(c.deliveryId,c.attempts,a,{state:c.disposition},d)?c.disposition:"stale"}})}:{action:"hold",results:[]})}_(ee,"acknowledgeNodeMail");async function te(e){const a=await(await N()).prepare(`SELECT m.body_json FROM deliveries d JOIN messages m ON m.message_id = d.message_id
27
- WHERE d.delivery_id = ?`).get(e);return a===void 0||a.body_json===null?null:JSON.parse(a.body_json)}_(te,"messageBodyForDelivery");async function ae(e){return(await(await N()).prepare(`SELECT * FROM deliveries
22
+ WHERE delivery_id = ? AND state = 'permanent_failed' AND canceled_at IS NULL AND last_failure_json IS NOT NULL`).run(a,a,e)).changes)===1}r(te,"retryFailedDelivery");async function ae(e,a,o,d=Date.now()){return await T(async s=>await L(s,e,a,!1)?{action:"acknowledged",results:await H(o,async c=>{const i=await s.prepare("SELECT 1 FROM deliveries WHERE delivery_id = ? AND destination_id = ?").get(c.deliveryId,e)!==void 0;return{deliveryId:c.deliveryId,outcome:i&&await j(c.deliveryId,c.attempts,a,{state:c.disposition},d)?c.disposition:"stale"}})}:{action:"hold",results:[]})}r(ae,"acknowledgeNodeMail");async function ne(e){const a=await(await N()).prepare(`SELECT m.body_json FROM deliveries d JOIN messages m ON m.message_id = d.message_id
23
+ WHERE d.delivery_id = ?`).get(e);return a===void 0||a.body_json===null?null:JSON.parse(a.body_json)}r(ne,"messageBodyForDelivery");async function ie(e){return(await(await N()).prepare(`SELECT * FROM deliveries
28
24
  WHERE state = 'pending' AND canceled_at IS NULL AND next_attempt_at <= ?
29
- ORDER BY next_attempt_at, created_at, delivery_id`).all(e)).map(I)}_(ae,"duePushDeliveries");async function ne(e,a,r){return await T(async d=>{const s=await d.prepare(`UPDATE deliveries AS d SET state = 'handed_off', attempts = attempts + 1, claimed_at = ?, claim_owner = ?, updated_at = ?
25
+ ORDER BY next_attempt_at, created_at, delivery_id`).all(e)).map(R)}r(ie,"duePushDeliveries");async function de(e,a,o){return await T(async d=>{const s=await d.prepare(`UPDATE deliveries AS d SET state = 'handed_off', attempts = attempts + 1, claimed_at = ?, claim_owner = ?, updated_at = ?
30
26
  WHERE d.delivery_id = ? AND d.state = 'pending' AND d.canceled_at IS NULL AND d.next_attempt_at <= ?
31
27
  AND NOT EXISTS (SELECT 1 FROM deliveries old JOIN messages prior ON prior.message_id = old.message_id
32
28
  JOIN messages next ON next.message_id = d.message_id
33
29
  WHERE old.destination_id = d.destination_id AND old.delivery_id != d.delivery_id
34
30
  AND old.state = 'handed_off' AND old.canceled_at IS NOT NULL
35
- AND prior.event = 'outcome' AND next.event = 'outcome' AND prior.object_id = next.object_id)`).run(a,r,a,e,a);return Number(s.changes)===0?null:I(await d.prepare("SELECT * FROM deliveries WHERE delivery_id = ?").get(e))})}_(ne,"claimPushDelivery");async function ie(e){return await T(async a=>{const r=await a.prepare(`UPDATE deliveries SET state = 'permanent_failed', next_attempt_at = NULL,
31
+ AND prior.event = 'outcome' AND next.event = 'outcome' AND prior.object_id = next.object_id)`).run(a,o,a,e,a);return Number(s.changes)===0?null:R(await d.prepare("SELECT * FROM deliveries WHERE delivery_id = ?").get(e))})}r(de,"claimPushDelivery");async function se(e){return await T(async a=>{const o=await a.prepare(`UPDATE deliveries SET state = 'permanent_failed', next_attempt_at = NULL,
36
32
  claimed_at = NULL, claim_owner = NULL, failed_at = ?, updated_at = ?
37
- WHERE state = 'handed_off' AND canceled_at IS NOT NULL AND next_attempt_at IS NOT NULL`).run(e,e),d=await a.prepare(`UPDATE deliveries SET state = 'pending', next_attempt_at = CASE WHEN next_attempt_at IS NOT NULL THEN ? ELSE NULL END,
33
+ WHERE state = 'handed_off' AND canceled_at IS NOT NULL AND next_attempt_at IS NOT NULL`).run(e,e),d=await a.prepare(`UPDATE deliveries SET state = 'pending', next_attempt_at = CASE WHEN next_attempt_at IS NOT NULL THEN ?::bigint ELSE NULL END,
38
34
  claimed_at = NULL, claim_owner = NULL, updated_at = ?
39
- WHERE state = 'handed_off' AND (canceled_at IS NULL OR next_attempt_at IS NULL)`).run(e,e);return Number(r.changes)+Number(d.changes)})}_(ie,"resetHandedOffDeliveries");async function de(e,a){return await(await N()).prepare(`SELECT 1 FROM deliveries WHERE delivery_id = ? AND state = 'handed_off'
40
- AND attempts = ? AND claim_owner = ? AND canceled_at IS NULL`).get(e.deliveryId,e.attempts,a)!==void 0}_(de,"pushDeliveryClaimIsCurrent");async function se(e){return Number((await(await N()).prepare(`UPDATE deliveries AS d SET state = 'pending', claimed_at = NULL, claim_owner = NULL, updated_at = ?
35
+ WHERE state = 'handed_off' AND (canceled_at IS NULL OR next_attempt_at IS NULL)`).run(e,e);return Number(o.changes)+Number(d.changes)})}r(se,"resetHandedOffDeliveries");async function re(e,a){return await(await N()).prepare(`SELECT 1 FROM deliveries WHERE delivery_id = ? AND state = 'handed_off'
36
+ AND attempts = ? AND claim_owner = ? AND canceled_at IS NULL`).get(e.deliveryId,e.attempts,a)!==void 0}r(re,"pushDeliveryClaimIsCurrent");async function oe(e){return Number((await(await N()).prepare(`UPDATE deliveries AS d SET state = 'pending', claimed_at = NULL, claim_owner = NULL, updated_at = ?
41
37
  WHERE d.state = 'handed_off' AND d.next_attempt_at IS NULL
42
38
  AND NOT EXISTS (
43
39
  SELECT 1 FROM executions e WHERE e.node_id = d.destination_id AND e.execution_id = d.claim_owner AND e.state != 'exited'
44
- )`).run(e)).changes)}_(se,"resetStaleExecutionMail");async function re(e){return await(await N()).prepare(`SELECT 1 FROM deliveries WHERE destination_id = ? AND wake = 1
45
- AND canceled_at IS NULL AND urgency != 'deferred' AND state IN ('pending', 'handed_off') LIMIT 1`).get(e)!==void 0}_(re,"hasUnseenWakeMail");async function oe(e,a){return await(await N()).prepare(`SELECT 1 FROM deliveries WHERE destination_id = ? AND wake = 1
46
- AND urgency != 'deferred' AND created_at >= ? LIMIT 1`).get(e,a)!==void 0}_(oe,"hasWakeMailSince");async function _e(e){return Number((await(await N()).prepare("SELECT COUNT(*) AS count FROM deliveries WHERE destination_id = ? AND acknowledged_at IS NOT NULL").get(e)).count)}_(_e,"acknowledgedNodeMailCount");function u(){let e;do e=C(16).toString("hex");while(e==="00000000000000000000000000000000");return e}_(u,"newId");function k(e){if(!O(e))return[];const a=D(e,"utf8");return a.trim()===""?[]:a.split(`
47
- `).filter(r=>r.trim()!=="").map((r,d)=>{let s;try{s=JSON.parse(r)}catch(E){throw new Error(`invalid mailbox JSON at ${e}:${d+1}: ${E.message}`)}if(!/^(?!0{32}$)[0-9a-f]{32}$/.test(s.entry_id)||!/^(?!0{32}$)[0-9a-f]{32}$/.test(s.operation_id))throw new Error(`invalid mailbox identity at ${e}:${d+1}`);if(typeof s.ts!="string"||typeof s.label!="string")throw new Error(`incomplete mailbox entry at ${e}:${d+1}`);return s})}_(k,"readJsonLines");function $(e){return O(e)?new Set(D(e,"utf8").split(`
48
- `).map(a=>a.trim()).filter(Boolean)):new Set}_($,"readCanceled");function le(e){e.exec(`
40
+ )`).run(e)).changes)}r(oe,"resetStaleExecutionMail");const $=r((e,a)=>`${e}.wake = 1 AND ${e}.canceled_at IS NULL AND ${e}.urgency != 'deferred' AND ${e}.state IN (${a})`,"WAKE_MAIL_SQL"),b=r(e=>$(e,"'pending', 'handed_off'"),"UNSEEN_WAKE_MAIL_SQL");async function le(e){return await(await N()).prepare(`SELECT 1 FROM deliveries d WHERE d.destination_id = ? AND ${b("d")} LIMIT 1`).get(e)!==void 0}r(le,"hasUnseenWakeMail");async function _e(e,a){return await(await N()).prepare(`SELECT 1 FROM deliveries WHERE destination_id = ? AND wake = 1
41
+ AND urgency != 'deferred' AND created_at >= ? LIMIT 1`).get(e,a)!==void 0}r(_e,"hasWakeMailSince");async function ce(e){return Number((await(await N()).prepare("SELECT COUNT(*) AS count FROM deliveries WHERE destination_id = ? AND acknowledged_at IS NOT NULL").get(e)).count)}r(ce,"acknowledgedNodeMailCount");function u(){let e;do e=C(16).toString("hex");while(e==="00000000000000000000000000000000");return e}r(u,"newId");function U(e){if(!I(e))return[];const a=O(e,"utf8");return a.trim()===""?[]:a.split(`
42
+ `).filter(o=>o.trim()!=="").map((o,d)=>{let s;try{s=JSON.parse(o)}catch(E){throw new Error(`invalid mailbox JSON at ${e}:${d+1}: ${E.message}`)}if(!/^(?!0{32}$)[0-9a-f]{32}$/.test(s.entry_id)||!/^(?!0{32}$)[0-9a-f]{32}$/.test(s.operation_id))throw new Error(`invalid mailbox identity at ${e}:${d+1}`);if(typeof s.ts!="string"||typeof s.label!="string")throw new Error(`incomplete mailbox entry at ${e}:${d+1}`);return s})}r(U,"readJsonLines");function q(e){return I(e)?new Set(O(e,"utf8").split(`
43
+ `).map(a=>a.trim()).filter(Boolean)):new Set}r(q,"readCanceled");function Ee(e){e.exec(`
49
44
  CREATE TABLE IF NOT EXISTS messages (
50
45
  message_id TEXT PRIMARY KEY,
51
46
  seq INTEGER NOT NULL UNIQUE,
@@ -99,4 +94,4 @@ CREATE TABLE IF NOT EXISTS waits (
99
94
  );
100
95
  CREATE UNIQUE INDEX IF NOT EXISTS waits_live ON waits(node_id) WHERE satisfied_at IS NULL;
101
96
  CREATE INDEX IF NOT EXISTS waits_node ON waits(node_id);
102
- `);const a=[];for(const t of m(e,"SELECT node_id FROM nodes").all()){const n=t.node_id,o=k(A(n)),l=O(`${A(n)}.cursor`)?D(`${A(n)}.cursor`,"utf8").trim():"",S=o.findIndex(p=>p.entry_id===l),g=$(`${A(n)}.canceled`);o.forEach((p,f)=>a.push({nodeId:n,wake:1,source:"inbox",fileRank:0,physicalIndex:f,entry:p,acknowledged:S>=0&&f<=S,canceled:g.has(p.entry_id),seq:0}));for(const[p,f]of[[0,`${v(n)}.draining`],[1,v(n)]])k(f).forEach((y,h)=>a.push({nodeId:n,wake:0,source:"passive",fileRank:p,physicalIndex:h,entry:y,acknowledged:!1,canceled:!1,seq:0}))}a.sort((t,n)=>t.entry.ts.localeCompare(n.entry.ts)||t.nodeId.localeCompare(n.nodeId)||t.fileRank-n.fileRank||t.physicalIndex-n.physicalIndex),a.forEach((t,n)=>{t.seq=n+1});const r=new Map;for(const t of a){const n=`${t.nodeId}\0${t.source}`,o=r.get(n)??[];o.push(t),r.set(n,o)}for(const t of r.values()){const n=t.map(o=>o.seq).sort((o,l)=>o-l);t.sort((o,l)=>o.fileRank-l.fileRank||o.physicalIndex-l.physicalIndex).forEach((o,l)=>{o.seq=n[l]})}const d=m(e,"INSERT INTO messages (message_id, seq, operation_id, created_at, sender_node_id, sender_name, kind, label, body_ref, body_json, disposition) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),s=m(e,"INSERT INTO deliveries (delivery_id, message_id, destination_kind, destination_id, urgency, wake, state, attempts, acknowledged_at, canceled_at, created_at, updated_at) VALUES (?, ?, 'node_inbox', ?, ?, ?, ?, 0, ?, ?, ?, ?)");for(const t of a){const n=t.entry;d.run(n.entry_id,t.seq,n.operation_id,n.ts,n.from,n.from_name??null,n.kind,n.label,n.ref??null,n.data===void 0?null:JSON.stringify(n.data),n.disposition??null);const o=Date.parse(n.ts),l=Number.isFinite(o)?o:0;s.run(u(),n.entry_id,t.nodeId,n.tier,t.wake,t.acknowledged?"acknowledged":"pending",t.acknowledged?l:null,t.canceled?l:null,l,l)}const E=m(e,"SELECT * FROM human_action_deliveries").all();for(const t of E){const n=u();d.run(n,U(e),u(),new Date(t.created_at).toISOString(),null,null,"completed",t.request_id,null,t.completion_json,null);const o=t.state==="running"?"handed_off":t.state==="accepted"?"acknowledged":t.state;m(e,"INSERT INTO deliveries (delivery_id, message_id, destination_kind, destination_id, urgency, wake, state, attempts, next_attempt_at, claimed_at, claim_owner, acknowledged_at, failed_at, last_failure_json, adapter_json, idempotency_key, created_at, updated_at) VALUES (?, ?, 'external_action', 'human_action', 'normal', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(u(),n,o,t.attempt,t.next_attempt_at,t.claimed_at,t.claim_owner,t.accepted_at,t.permanent_failed_at,t.last_failure_json,JSON.stringify({adapter:"subprocess",argv:JSON.parse(t.argv_json),cwd:t.cwd}),`human-action:${t.request_id}`,t.created_at,t.updated_at)}const c=m(e,"SELECT * FROM node_outcome_deliveries").all();for(const t of c){const n=t.document_json===null?null:u();n!==null&&d.run(n,U(e),u(),new Date(t.created_at).toISOString(),null,null,"completed",t.node_id,null,t.document_json,null);const o=t.document_json===null?"armed":t.state==="running"?"handed_off":t.state==="accepted"?"acknowledged":t.state;m(e,"INSERT INTO deliveries (delivery_id, message_id, destination_kind, destination_id, urgency, wake, state, attempts, next_attempt_at, claimed_at, claim_owner, acknowledged_at, failed_at, last_failure_json, adapter_json, idempotency_key, created_at, updated_at) VALUES (?, ?, 'external_action', ?, 'normal', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(u(),n,t.action_name,o,t.attempt,t.next_attempt_at,t.claimed_at,t.claim_owner,t.accepted_at,t.permanent_failed_at,t.last_failure_json,JSON.stringify({adapter:"subprocess",argv:JSON.parse(t.argv_json),cwd:t.cwd,config_origin:t.config_origin,payload:t.payload_json===null?null:JSON.parse(t.payload_json)}),`node-outcome:${t.node_id}`,t.created_at,t.updated_at)}const i=m(e,"SELECT node_id, waiting_for, waiting_for_generation FROM nodes WHERE waiting_for IS NOT NULL").all();for(const t of i)m(e,"INSERT INTO waits (wait_id, node_id, kind, target_id, generation, created_at, satisfied_at) VALUES (?, ?, 'controller', ?, ?, ?, NULL)").run(u(),t.node_id,t.waiting_for,t.waiting_for_generation,Date.now())}_(le,"migrateMessagingState");function U(e){return Number(m(e,"SELECT COALESCE(MAX(seq), 0) AS seq FROM messages").get().seq)+1}_(U,"nextSeq");export{ee as acknowledgeNodeMail,_e as acknowledgedNodeMailCount,z as claimKickoffMail,V as claimNodeMail,ne as claimPushDelivery,I as deliveryFrom,ae as duePushDeliveries,re as hasUnseenWakeMail,oe as hasWakeMailSince,te as messageBodyForDelivery,le as migrateMessagingState,M as normalizedSettlement,de as pushDeliveryClaimIsCurrent,ie as resetHandedOffDeliveries,se as resetStaleExecutionMail,Z as retryFailedDelivery,W as settleDelivery};
97
+ `);const a=[];for(const t of m(e,"SELECT node_id FROM nodes").all()){const n=t.node_id,l=U(w(n)),_=I(`${w(n)}.cursor`)?O(`${w(n)}.cursor`,"utf8").trim():"",S=l.findIndex(p=>p.entry_id===_),g=q(`${w(n)}.canceled`);l.forEach((p,f)=>a.push({nodeId:n,wake:1,source:"inbox",fileRank:0,physicalIndex:f,entry:p,acknowledged:S>=0&&f<=S,canceled:g.has(p.entry_id),seq:0}));for(const[p,f]of[[0,`${D(n)}.draining`],[1,D(n)]])U(f).forEach((y,h)=>a.push({nodeId:n,wake:0,source:"passive",fileRank:p,physicalIndex:h,entry:y,acknowledged:!1,canceled:!1,seq:0}))}a.sort((t,n)=>t.entry.ts.localeCompare(n.entry.ts)||t.nodeId.localeCompare(n.nodeId)||t.fileRank-n.fileRank||t.physicalIndex-n.physicalIndex),a.forEach((t,n)=>{t.seq=n+1});const o=new Map;for(const t of a){const n=`${t.nodeId}\0${t.source}`,l=o.get(n)??[];l.push(t),o.set(n,l)}for(const t of o.values()){const n=t.map(l=>l.seq).sort((l,_)=>l-_);t.sort((l,_)=>l.fileRank-_.fileRank||l.physicalIndex-_.physicalIndex).forEach((l,_)=>{l.seq=n[_]})}const d=m(e,"INSERT INTO messages (message_id, seq, operation_id, created_at, sender_node_id, sender_name, kind, label, body_ref, body_json, disposition) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),s=m(e,"INSERT INTO deliveries (delivery_id, message_id, destination_kind, destination_id, urgency, wake, state, attempts, acknowledged_at, canceled_at, created_at, updated_at) VALUES (?, ?, 'node_inbox', ?, ?, ?, ?, 0, ?, ?, ?, ?)");for(const t of a){const n=t.entry;d.run(n.entry_id,t.seq,n.operation_id,n.ts,n.from,n.from_name??null,n.kind,n.label,n.ref??null,n.data===void 0?null:JSON.stringify(n.data),n.disposition??null);const l=Date.parse(n.ts),_=Number.isFinite(l)?l:0;s.run(u(),n.entry_id,t.nodeId,n.tier,t.wake,t.acknowledged?"acknowledged":"pending",t.acknowledged?_:null,t.canceled?_:null,_,_)}const E=m(e,"SELECT * FROM human_action_deliveries").all();for(const t of E){const n=u();d.run(n,k(e),u(),new Date(t.created_at).toISOString(),null,null,"completed",t.request_id,null,t.completion_json,null);const l=t.state==="running"?"handed_off":t.state==="accepted"?"acknowledged":t.state;m(e,"INSERT INTO deliveries (delivery_id, message_id, destination_kind, destination_id, urgency, wake, state, attempts, next_attempt_at, claimed_at, claim_owner, acknowledged_at, failed_at, last_failure_json, adapter_json, idempotency_key, created_at, updated_at) VALUES (?, ?, 'external_action', 'human_action', 'normal', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(u(),n,l,t.attempt,t.next_attempt_at,t.claimed_at,t.claim_owner,t.accepted_at,t.permanent_failed_at,t.last_failure_json,JSON.stringify({adapter:"subprocess",argv:JSON.parse(t.argv_json),cwd:t.cwd}),`human-action:${t.request_id}`,t.created_at,t.updated_at)}const c=m(e,"SELECT * FROM node_outcome_deliveries").all();for(const t of c){const n=t.document_json===null?null:u();n!==null&&d.run(n,k(e),u(),new Date(t.created_at).toISOString(),null,null,"completed",t.node_id,null,t.document_json,null);const l=t.document_json===null?"armed":t.state==="running"?"handed_off":t.state==="accepted"?"acknowledged":t.state;m(e,"INSERT INTO deliveries (delivery_id, message_id, destination_kind, destination_id, urgency, wake, state, attempts, next_attempt_at, claimed_at, claim_owner, acknowledged_at, failed_at, last_failure_json, adapter_json, idempotency_key, created_at, updated_at) VALUES (?, ?, 'external_action', ?, 'normal', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(u(),n,t.action_name,l,t.attempt,t.next_attempt_at,t.claimed_at,t.claim_owner,t.accepted_at,t.permanent_failed_at,t.last_failure_json,JSON.stringify({adapter:"subprocess",argv:JSON.parse(t.argv_json),cwd:t.cwd,config_origin:t.config_origin,payload:t.payload_json===null?null:JSON.parse(t.payload_json)}),`node-outcome:${t.node_id}`,t.created_at,t.updated_at)}const i=m(e,"SELECT node_id, waiting_for, waiting_for_generation FROM nodes WHERE waiting_for IS NOT NULL").all();for(const t of i)m(e,"INSERT INTO waits (wait_id, node_id, kind, target_id, generation, created_at, satisfied_at) VALUES (?, ?, 'controller', ?, ?, ?, NULL)").run(u(),t.node_id,t.waiting_for,t.waiting_for_generation,Date.now())}r(Ee,"migrateMessagingState");function k(e){return Number(m(e,"SELECT COALESCE(MAX(seq), 0) AS seq FROM messages").get().seq)+1}r(k,"nextSeq");export{b as UNSEEN_WAKE_MAIL_SQL,$ as WAKE_MAIL_SQL,ae as acknowledgeNodeMail,ce as acknowledgedNodeMailCount,ee as claimKickoffMail,Z as claimNodeMail,de as claimPushDelivery,R as deliveryFrom,ie as duePushDeliveries,le as hasUnseenWakeMail,_e as hasWakeMailSince,ne as messageBodyForDelivery,Ee as migrateMessagingState,W as normalizedSettlement,re as pushDeliveryClaimIsCurrent,se as resetHandedOffDeliveries,oe as resetStaleExecutionMail,te as retryFailedDelivery,j as settleDelivery};
@@ -0,0 +1,14 @@
1
+ export interface ExportResult {
2
+ path: string;
3
+ rows: number;
4
+ ms: number;
5
+ userVersion: number;
6
+ }
7
+ type Log = (message: string) => void;
8
+ /**
9
+ * Copy the Postgres database at `url` into a new canvas.db next to the runtime records and rename it into place.
10
+ * Refuses when a canvas.db already exists. On any failure the temp file is removed; the only thing that can remain is the
11
+ * export mark in Postgres, which is written just before the rename.
12
+ */
13
+ export declare function exportPostgresToSqlite(url: string, log?: Log): Promise<ExportResult>;
14
+ export {};
@@ -0,0 +1,3 @@
1
+ var V=Object.defineProperty;var f=(i,a)=>V(i,"name",{value:a,configurable:!0});import{DatabaseSync as W}from"node:sqlite";import{existsSync as Y,mkdirSync as k,mkdtempSync as J,renameSync as X,rmSync as P}from"node:fs";import{tmpdir as Q}from"node:os";import{join as A}from"node:path";import j from"pg";import{migrate as z,MIGRATIONS as I}from"./migrations.js";import{canvasDbPath as Z}from"./paths.js";import{EXPORT_MARK_KEY as F,IMPORT_MARK_KEY as ee,OBJECT_COLUMNS as te,pgColumns as ne,quote as c,TABLE_ORDER as C}from"./import-sqlite.js";const re=5e3,se=new Set,oe={exposures:"node, object, channel",rules:"object, position",edges:"type, src, dst",unresolved_links:"src, name",revision_parents:"child, parent",canvas_meta:"key",runtime_state:"key",message_causes:"message_id",peer_ingested:"key",peer_ingested_destinations:"key",message_sequence:"id",messages:"seq",review_comment_events:"seq",audit_rows:"id",peer_outbox:"id",app_users:"uid"},ae={objects:"id",revisions:"rev_id",watches:"watch_id"},H=new Set(["daemon_owner","schema_version",ee,F]);function ie(i){const a=J(A(Q(),"crtr-export-")),r=process.env.CRTR_HOME;process.env.CRTR_HOME=a,k(A(a,"runtime","nodes"),{recursive:!0});try{const s=new W(i);return s.exec("PRAGMA journal_mode = DELETE;"),s.exec("PRAGMA foreign_keys = ON;"),z(s),s.exec("DELETE FROM canvas_meta; DELETE FROM message_sequence;"),s}finally{r===void 0?delete process.env.CRTR_HOME:process.env.CRTR_HOME=r,P(a,{recursive:!0,force:!0})}}f(ie,"createEmptyDatabase");async function he(i,a=()=>{}){const r=Z();if(Y(r))throw new Error(`${r} already exists; refusing to overwrite it`);k(A(r,".."),{recursive:!0,mode:448});const s=`${r}.exporting-${process.pid}`,u=Date.now(),t=new j.Client({connectionString:i,application_name:`crtr-export-${process.pid}`,types:{getTypeParser:f(((l,o)=>l===20?d=>Number(d):j.types.getTypeParser(l,o)),"getTypeParser")}});await t.connect();let p;try{if((await t.query("SELECT value FROM canvas_meta WHERE key = 'schema_version'").catch(()=>({rows:[]}))).rows.length===0)throw new Error("the Postgres database has no canvas schema; nothing to export");p=ie(s);const o=p;o.exec("PRAGMA foreign_keys = OFF;");const d=await ne(t),L=[...d.keys()].filter(e=>!se.has(e)&&!C.includes(e));if(L.length>0)throw new Error(`export: Postgres tables with no export rule: ${L.join(", ")}`);await t.query("BEGIN READ ONLY ISOLATION LEVEL REPEATABLE READ");const _={};for(const[e,O]of Object.entries(ae)){const h=new Map;for(const E of(await t.query(`SELECT ${O} AS id, seq FROM ${e}`)).rows)h.set(E.id,Number(E.seq));_[e]=h}const B=_.objects;let M=0;o.exec("BEGIN");for(const e of C){const O=Date.now(),h=new Set(o.prepare(`PRAGMA table_info(${c(e)})`).all().map(n=>n.name)),E=d.get(e);if(E===void 0)throw new Error(`export: Postgres has no table ${e}`);const G=E.some(n=>n.name==="seq"),$=E.filter(n=>h.has(n.name)),K=new Set(te[e]??[]),R=e==="objects"||e==="revisions"||e==="watches"||e==="watch_subscriptions",v=$.map(n=>n.type==="jsonb"?`${c(n.name)}::text AS ${c(n.name)}`:c(n.name));e==="watches"&&v.push("watch_id AS _watch_id"),R&&v.push("seq AS _seq");const y=$.map(n=>c(n.name)),U=o.prepare(`INSERT INTO ${c(e)} (${R?["rowid",...y]:y}) VALUES (${(R?["?",...y]:y).map(()=>"?").join(", ")})`),q=G?"seq":oe[e],T=`export_${e}`;await t.query(`DECLARE ${T} NO SCROLL CURSOR FOR SELECT ${v.join(", ")} FROM ${c(e)}${e==="canvas_meta"?` WHERE key NOT IN (${[...H].map(n=>`'${n}'`).join(", ")})`:""}${q===void 0?"":` ORDER BY ${q}`}`);let b=0;for(;;){const n=await t.query(`FETCH ${re} FROM ${T}`);if(n.rows.length===0)break;for(const N of n.rows){const D=R?[Number(N._seq)]:[];for(const m of $){let w=N[m.name];e==="revision_parents"&&(m.name==="child"||m.name==="parent")?w=x(_.revisions,"revision",e,m.name,w):e==="watch_subscriptions"&&m.name==="watch"?w=x(_.watches,"watch",e,m.name,w):K.has(m.name)&&(w=x(B,"object",e,m.name,w)),D.push(w??null)}U.run(...D)}b+=n.rows.length}await t.query(`CLOSE ${T}`),M+=b,a(`export ${e}: ${b} rows in ${Date.now()-O} ms`)}o.exec("COMMIT"),await t.query("COMMIT");const g=o.prepare("PRAGMA foreign_key_check").all();if(g.length>0)throw new Error(`export: ${g.length} foreign key violation(s) in the exported canvas.db, first: ${JSON.stringify(g[0])}`);await ce(t,o);const S=o.prepare("PRAGMA user_version").get().user_version;if(S!==I.length)throw new Error(`export: exported canvas.db is at user_version ${S}, expected ${I.length}`);return o.close(),p=void 0,await t.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",[F,JSON.stringify({at:new Date().toISOString(),path:r})]),X(s,r),{path:r,rows:M,ms:Date.now()-u,userVersion:S}}catch(l){await t.query("ROLLBACK").catch(()=>{});try{p?.close()}catch{}for(const o of["","-wal","-shm","-journal"])P(`${s}${o}`,{force:!0});throw l}finally{await t.end().catch(()=>{})}}f(he,"exportPostgresToSqlite");function x(i,a,r,s,u){if(u==null)return null;const t=i.get(u);if(t===void 0)throw new Error(`export: ${r}.${s} references ${a} ${String(u)}, which was not exported (`);return t}f(x,"mapId");async function ce(i,a){const r=[];for(const s of C){const u=Number((await i.query(`SELECT count(*) AS n FROM ${c(s)}`)).rows[0].n);let t=Number(a.prepare(`SELECT count(*) AS n FROM ${c(s)}`).get().n),p=u;s==="canvas_meta"&&(p=Number((await i.query("SELECT count(*) AS n FROM canvas_meta WHERE key <> ALL($1)",[[...H]])).rows[0].n),t=Number(a.prepare("SELECT count(*) AS n FROM canvas_meta").get().n)),p!==t&&r.push(`${s}: Postgres ${p} rows, SQLite ${t}`)}if(r.length>0)throw new Error(`Postgres \u2192 SQLite export verification failed:
2
+ ${r.join(`
3
+ `)}`)}f(ce,"verify");export{he as exportPostgresToSqlite};
@@ -0,0 +1,44 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import pg from 'pg';
3
+ export declare const IMPORT_MARK_KEY = "imported_from_sqlite";
4
+ /** Written by `crtr sys db export-sqlite`; the Postgres data is stale against the exported canvas.db from then on. */
5
+ export declare const EXPORT_MARK_KEY = "exported_to_sqlite";
6
+ /** Every canvas table, in dependency order (a table follows every table its foreign keys name). The import and the
7
+ * rollback export both copy exactly this list. */
8
+ export declare const TABLE_ORDER: readonly ["objects", "documents", "revisions", "revision_parents", "jobs", "identities", "custom_objects", "context_sessions", "peer_stubs", "object_kinds", "watches", "watch_subscriptions", "exposures", "rules", "edges", "unresolved_links", "nodes", "focuses", "canvas_meta", "runtime_state", "warm_pool", "sessions", "executions", "worktrees", "node_faults", "node_tokens", "node_outcome_deliveries", "deleted_isolated_nodes", "create_requests", "waits", "messages", "message_sequence", "message_causes", "deliveries", "structured_requests", "daemon_actions", "human_requests", "human_action_deliveries", "review_comments", "review_comment_events", "run_events", "idempotency_keys", "grants", "memory_reads", "app_users", "uploads", "upload_uses", "shares", "audit_rows", "grantee_usage", "provider_receipts", "provider_approvals", "usage", "crons", "cron_runs", "peer_outbox", "peer_locations", "peer_ingested", "peer_ingested_destinations"];
9
+ /** Columns holding `objects.rowid` in SQLite and `objects.id` in Postgres. */
10
+ export declare const OBJECT_COLUMNS: Record<string, readonly string[]>;
11
+ export interface PgColumn {
12
+ name: string;
13
+ type: string;
14
+ identity: boolean;
15
+ }
16
+ export interface ImportTableStat {
17
+ table: string;
18
+ rows: number;
19
+ ms: number;
20
+ }
21
+ export interface ImportResult {
22
+ tables: ImportTableStat[];
23
+ verifyMs: number;
24
+ totalMs: number;
25
+ /** Per `table.column`, the NUL characters replaced with U+FFFD so jsonb would accept the value. */
26
+ nulReplaced: Record<string, number>;
27
+ }
28
+ type Log = (message: string) => void;
29
+ export declare function quote(identifier: string): string;
30
+ export declare function pgColumns(client: pg.Client): Promise<Map<string, PgColumn[]>>;
31
+ /** Copy an already-migrated SQLite database into the connected Postgres database, in one transaction. */
32
+ export declare function copySqliteToPostgres(sqlite: DatabaseSync, client: pg.Client, options?: {
33
+ log?: Log;
34
+ }): Promise<ImportResult>;
35
+ /** canvas.db → canvas.db.imported, with its -wal/-shm. An existing .imported (an earlier import, then a rollback export) is set aside, never overwritten. */
36
+ export declare function renameImportedDatabase(path?: string): void;
37
+ /**
38
+ * Daemon boot: import `canvas.db` into the database at `url` if there is one. Holds the on-disk migration lock for
39
+ * the whole import so `manage.ts` does not reap a daemon mid-copy. Re-entry: with the mark present and canvas.db
40
+ * still there (a crash between commit and rename) the rename is finished and nothing is copied.
41
+ * Returns null when there was nothing to do.
42
+ */
43
+ export declare function importSqliteCanvasIfPresent(url: string, log?: Log): Promise<ImportResult | null>;
44
+ export {};
@@ -0,0 +1,7 @@
1
+ var J=Object.defineProperty;var p=(t,r)=>J(t,"name",{value:r,configurable:!0});import{DatabaseSync as Q}from"node:sqlite";import{createHash as X,randomBytes as z}from"node:crypto";import{existsSync as T,mkdirSync as Z,renameSync as P}from"node:fs";import{dirname as ee}from"node:path";import te from"pg";import{withExclusiveDirectoryLockAsync as re}from"../exclusive-lock.js";import{DAEMON_STARTUP_WINDOW_MS as oe}from"../../daemon/startup-policy.js";import{migrate as se}from"./migrations.js";import{canvasDbPath as F,onDiskMigrationLockPath as ne}from"./paths.js";const q="imported_from_sqlite",B="exported_to_sqlite",C=1e4,L=["objects","documents","revisions","revision_parents","jobs","identities","custom_objects","context_sessions","peer_stubs","object_kinds","watches","watch_subscriptions","exposures","rules","edges","unresolved_links","nodes","focuses","canvas_meta","runtime_state","warm_pool","sessions","executions","worktrees","node_faults","node_tokens","node_outcome_deliveries","deleted_isolated_nodes","create_requests","waits","messages","message_sequence","message_causes","deliveries","structured_requests","daemon_actions","human_requests","human_action_deliveries","review_comments","review_comment_events","run_events","idempotency_keys","grants","memory_reads","app_users","uploads","upload_uses","shares","audit_rows","grantee_usage","provider_receipts","provider_approvals","usage","crons","cron_runs","peer_outbox","peer_locations","peer_ingested","peer_ingested_destinations"],ae=new Set(["sqlite_sequence","sqlite_stat1","sqlite_stat4","reviews"]),ie={objects:["owner"],documents:["object"],revisions:["document","author"],jobs:["object"],identities:["object"],custom_objects:["object"],context_sessions:["node"],peer_stubs:["object"],object_kinds:["owner"],watches:["watcher","object","link_app"],exposures:["node","object"],rules:["object","owner"],edges:["src","dst"],unresolved_links:["src"]},U=new Set(["daemon_owner","schema_version"]),ce={canvas_meta:"key",message_sequence:"id"};function W(){return z(16).toString("hex")}p(W,"newId");function m(t){return`"${t.replace(/"/g,'""')}"`}p(m,"quote");function ue(t){switch(t){case"bigint":return"bigint[]";case"double precision":return"float8[]";case"bytea":return"bytea[]";case"text":case"jsonb":return"text[]";default:throw new Error(`import: no array type for Postgres column type ${t}`)}}p(ue,"arrayType");async function de(t){const r=await t.query(`SELECT c.relname AS tbl, a.attname AS col, format_type(a.atttypid, a.atttypmod) AS type, a.attidentity <> '' AS identity
2
+ FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid
3
+ WHERE c.relnamespace = current_schema()::regnamespace AND c.relkind = 'r' AND a.attnum > 0 AND NOT a.attisdropped
4
+ ORDER BY c.relname, a.attnum`),e=new Map;for(const o of r.rows){const a=e.get(o.tbl)??[];a.push({name:o.col,type:o.type,identity:o.identity}),e.set(o.tbl,a)}return e}p(de,"pgColumns");const pe="\uFFFD";function me(t){if(!t.includes("\\u0000"))return{text:t,replaced:0};let r;try{r=JSON.parse(t)}catch{return{text:t,replaced:0}}let e=0;const o=p(u=>{const i=u.split("\0");return e+=i.length-1,i.join(pe)},"clean"),a=p(u=>typeof u=="string"?o(u):Array.isArray(u)?u.map(a):u!==null&&typeof u=="object"?Object.fromEntries(Object.entries(u).map(([i,_])=>[o(i),a(_)])):u,"walk"),w=a(r);return e===0?{text:t,replaced:e}:{text:JSON.stringify(w),replaced:e}}p(me,"scrubNul");async function fe(t,r,e,o,a,w,u){for(let i=0;i<o.length;i++)for(const _ of e){const s=a[w.indexOf(_)][i];if(s!=null){await t.query("SAVEPOINT import_probe");try{await t.query("SELECT $1::jsonb",[s]),await t.query("RELEASE SAVEPOINT import_probe")}catch(c){return await t.query("ROLLBACK TO SAVEPOINT import_probe"),new Error(`import: ${r}.${_.name} at ${u(o[i])} is not a value jsonb accepts: ${c.message}`,{cause:c})}}}}p(fe,"describeJsonbFailure");function j(t,r,e,o,a){if(a==null)return null;const w=t.get(Number(a));if(w===void 0)throw new Error(`import: ${e}.${o} references ${r} rowid ${String(a)}, which does not exist`);return w}p(j,"lookup");async function we(t,r,e,o,a,w){const u=t.prepare(`PRAGMA table_info(${m(e)})`).all(),i=new Set(u.map(n=>n.name)),s=!t.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(e).sql.toUpperCase().includes("WITHOUT ROWID"),c=new Set(ie[e]??[]),l=[];for(const n of o)if(n.name==="seq"&&!i.has("seq")){if(!s)throw new Error(`import: ${e} needs seq from rowid but has none`);l.push("rowid AS seq")}else if(n.name==="watch_id"||n.name==="subscription_id")l.push("NULL AS "+m(n.name));else if(i.has(n.name))l.push(m(n.name));else throw new Error(`import: SQLite table ${e} has no column ${n.name}`);s&&l.push("rowid AS _rowid");const $=e==="canvas_meta"?` WHERE key NOT IN (${[...U].map(n=>`'${n}'`).join(",")})`:"",S=t.prepare(`SELECT ${l.join(", ")} FROM ${m(e)}${$}${s?" ORDER BY rowid":""}`),y=o.map(n=>m(n.name)),h=o.map((n,d)=>`$${d+1}::${ue(n.type)}`),g=o.map((n,d)=>`c${d}`),A=o.map((n,d)=>n.type==="jsonb"?`c${d}::jsonb`:`c${d}`);let R=`INSERT INTO ${m(e)} (${y.join(", ")}) SELECT ${A.join(", ")} FROM unnest(${h.join(", ")}) AS u(${g.join(", ")})`;const v=ce[e];if(v!==void 0){const n=o.filter(d=>d.name!==v).map(d=>`${m(d.name)} = EXCLUDED.${m(d.name)}`);R+=` ON CONFLICT (${m(v)}) DO UPDATE SET ${n.join(", ")}`}const M=e==="objects",I=o.filter(n=>n.type==="jsonb"),D=s?"_rowid":o[0].name,K=p(n=>`${s?"rowid":D} ${String(n[D])}`,"key");let k=0,b=[];const x=p(async()=>{if(b.length===0)return;const n=o.map(()=>new Array(b.length));if(b.forEach((d,O)=>{o.forEach((f,V)=>{let E=d[f.name];if(f.name==="watch_id"||f.name==="subscription_id"?E=W():e==="revision_parents"&&(f.name==="child"||f.name==="parent")?E=j(a.revisions,"revision",e,f.name,E):e==="watch_subscriptions"&&f.name==="watch"?E=j(a.watches,"watch",e,f.name,E):c.has(f.name)&&(E=M?null:j(a.objects,"object",e,f.name,E)),f.type==="bytea"&&E instanceof Uint8Array&&!Buffer.isBuffer(E)&&(E=Buffer.from(E.buffer,E.byteOffset,E.byteLength)),f.type==="jsonb"&&typeof E=="string"){const N=me(E);N.replaced>0&&(E=N.text,w.set(`${e}.${f.name}`,(w.get(`${e}.${f.name}`)??0)+N.replaced))}n[V][O]=E??null}),e==="watches"&&a.watches.set(Number(d._rowid),n[o.findIndex(f=>f.name==="watch_id")][O])}),I.length===0)await r.query(R,n);else{await r.query("SAVEPOINT import_batch");try{await r.query(R,n)}catch(d){throw await r.query("ROLLBACK TO SAVEPOINT import_batch"),await fe(r,e,I,b,n,o,K)??d}await r.query("RELEASE SAVEPOINT import_batch")}k+=b.length,b=[]},"flush");for(const n of S.iterate())e==="objects"&&a.objects.set(Number(n._rowid),n.id),e==="revisions"&&a.revisions.set(Number(n._rowid),n.rev_id),b.push(n),b.length>=C&&await x();if(await x(),M){const n=t.prepare("SELECT id, owner FROM objects WHERE owner IS NOT NULL").all();for(let d=0;d<n.length;d+=C){const O=n.slice(d,d+C);await r.query("UPDATE objects o SET owner = v.owner FROM unnest($1::text[], $2::text[]) AS v(id, owner) WHERE o.id = v.id",[O.map(f=>f.id),O.map(f=>j(a.objects,"object","objects","owner",f.owner))])}}return k}p(we,"copyTable");async function Ee(t,r){for(const[e,o]of r)for(const a of o)a.identity&&await t.query(`SELECT setval(pg_get_serial_sequence($1, $2), COALESCE((SELECT max(${m(a.name)}) FROM ${m(e)}), 1), (SELECT max(${m(a.name)}) IS NOT NULL FROM ${m(e)}))`,[m(e),a.name])}p(Ee,"advanceIdentities");function H(t,r,e=""){return Number(t.prepare(`SELECT count(*) AS n FROM ${m(r)}${e}`).get().n)}p(H,"sqliteCount");async function le(t,r,e){const o=[];for(const s of L){if(s==="canvas_meta")continue;const c=H(t,s),l=Number((await r.query(`SELECT count(*) AS n FROM ${m(s)}`)).rows[0].n);c!==l&&o.push(`${s}: SQLite ${c} rows, Postgres ${l}`)}for(const s of t.prepare(`SELECT key, value FROM canvas_meta WHERE key NOT IN (${[...U].map(c=>`'${c}'`).join(",")})`).all())(await r.query("SELECT value FROM canvas_meta WHERE key = $1",[s.key])).rows[0]?.value!==s.value&&o.push(`canvas_meta ${s.key}: value differs`);const a=p(s=>{const c=X("sha256");for(const l of s)c.update(l).update(`
5
+ `);return c.digest("hex")},"hash"),w=t.prepare("SELECT id FROM objects ORDER BY rowid").all().map(s=>s.id),u=(await r.query("SELECT id FROM objects ORDER BY seq")).rows.map(s=>s.id);a(w)!==a(u)&&o.push("objects: ids in insertion order differ");const i=t.prepare("SELECT o.id AS id, p.id AS owner FROM objects o JOIN objects p ON p.rowid = o.owner ORDER BY o.rowid").all(),_=(await r.query("SELECT id, owner FROM objects WHERE owner IS NOT NULL ORDER BY seq")).rows;a(i.map(s=>`${s.id}>${s.owner}`))!==a(_.map(s=>`${s.id}>${s.owner}`))&&o.push("objects: owner links differ");for(const[s,c]of[["run_events","body_json"],["messages","body_json"],["deliveries","last_failure_json"]]){const l=Number(t.prepare(`SELECT COALESCE(sum(length(CAST(${m(c)} AS TEXT))), 0) AS n FROM ${m(s)}`).get().n),$=e.get(s).find(h=>h.name===c).type,S=$==="jsonb"?`${m(c)}::text`:m(c);if($==="jsonb")continue;const y=Number((await r.query(`SELECT COALESCE(sum(length(${S})), 0)::bigint AS n FROM ${m(s)}`)).rows[0].n);l!==y&&o.push(`${s}.${c}: total length SQLite ${l}, Postgres ${y}`)}if(o.length>0)throw new Error(`SQLite \u2192 Postgres import verification failed (rolled back; canvas.db untouched):
6
+ ${o.join(`
7
+ `)}`)}p(le,"verify");async function ye(t,r,e={}){const o=e.log??(()=>{}),a=Date.now(),w=await de(r),u=t.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map(y=>y.name),i=u.filter(y=>!ae.has(y)&&!L.includes(y));if(i.length>0)throw new Error(`import: SQLite tables with no copy rule: ${i.join(", ")}`);const _=L.filter(y=>!w.has(y));if(_.length>0)throw new Error(`import: Postgres tables missing from the schema: ${_.join(", ")}`);const s=u.includes("reviews")?H(t,"reviews"):0;if(s>0)throw new Error(`import: the legacy reviews table still holds ${s} rows after the SQLite migration chain`);const c={objects:new Map,revisions:new Map,watches:new Map},l=[],$=new Map;let S=0;await r.query("BEGIN");try{await r.query("ALTER TABLE nodes DISABLE TRIGGER USER"),await r.query("ALTER TABLE deliveries DISABLE TRIGGER USER");for(const h of L){const g=Date.now(),A=await we(t,r,h,w.get(h),c,$),R=Date.now()-g;l.push({table:h,rows:A,ms:R}),o(`import ${h}: ${A} rows in ${R} ms`)}await r.query("ALTER TABLE nodes ENABLE TRIGGER USER"),await r.query("ALTER TABLE deliveries ENABLE TRIGGER USER"),await Ee(r,w);const y=Date.now();await le(t,r,w),S=Date.now()-y,o(`import verify: ${S} ms`),await r.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2)",[q,JSON.stringify({at:new Date().toISOString(),rows:l.reduce((h,g)=>h+g.rows,0)})]),await r.query("COMMIT")}catch(y){throw await r.query("ROLLBACK").catch(()=>{}),y}return $.size>0&&o(`import: replaced NUL (\\u0000) with U+FFFD in jsonb values: ${[...$].map(([y,h])=>`${y} ${h}`).join(", ")}`),{tables:l,verifyMs:S,totalMs:Date.now()-a,nulReplaced:Object.fromEntries($)}}p(ye,"copySqliteToPostgres");const G=["","-wal","-shm"];function Y(t=F()){if(T(`${t}.imported`)){const r=`${t}.imported.${Date.now()}`;for(const e of G)T(`${t}.imported${e}`)&&P(`${t}.imported${e}`,`${r}${e}`)}for(const r of G)T(`${t}${r}`)&&P(`${t}${r}`,`${t}.imported${r}`)}p(Y,"renameImportedDatabase");async function Le(t,r=()=>{}){const e=F();if(!T(e))return null;const o=ne();return Z(ee(o),{recursive:!0,mode:448}),re(o,async()=>{if(!T(e))return null;const a=new te.Client({connectionString:t,application_name:`crtr-import-${process.pid}`});await a.connect();try{const w=await a.query("SELECT key FROM canvas_meta WHERE key = ANY($1)",[[q,B]]),u=new Set(w.rows.map(s=>s.key));if(u.has(q)){if(u.has(B))throw new Error(`${e} was written by \`crtr sys db export-sqlite\` and the Postgres database predates it; importing would drop the exported writes. Stop the daemon and delete the Postgres cluster (runtime/pg/data) to import ${e} afresh, or move ${e} aside to keep using Postgres.`);return Y(e),r(`import: finished the rename of ${e} (already imported)`),null}const i=new Q(e);let _;try{i.exec("PRAGMA busy_timeout = 5000;"),i.exec("PRAGMA foreign_keys = ON;"),se(i),_=await ye(i,a,{log:r})}finally{i.close()}return Y(e),r(`import: ${_.tables.reduce((s,c)=>s+c.rows,0)} rows in ${_.totalMs} ms; canvas.db renamed to canvas.db.imported`),_}finally{await a.end().catch(()=>{})}},{timeoutMs:oe,timeoutError:p(()=>new Error(`timed out waiting for the canvas import lock: ${o}`),"timeoutError")})}p(Le,"importSqliteCanvasIfPresent");export{B as EXPORT_MARK_KEY,q as IMPORT_MARK_KEY,ie as OBJECT_COLUMNS,L as TABLE_ORDER,ye as copySqliteToPostgres,Le as importSqliteCanvasIfPresent,de as pgColumns,m as quote,Y as renameImportedDatabase};
@@ -1,5 +1,5 @@
1
- var g=Object.defineProperty;var r=(e,n)=>g(e,"name",{value:n,configurable:!0});import{createHash as k,randomBytes as m}from"node:crypto";import{openDb as u}from"./db.js";import{terminalGrant as w}from"../grants/mirror.js";import{readConfig as _}from"../config.js";import{uidForApp as N}from"../spaces/users.js";const a=new Map,l=r(e=>k("sha256").update(e).digest("hex"),"hash");async function x(e,n,o,i){const t=await e.prepare("SELECT grantee, sub, grant_id, client_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(o);if(!t)throw new Error(`cannot mint node token: missing node ${o}`);const s=t.run_id&&t.run_id!==o?await e.prepare("SELECT sub, grant_id, client_id FROM nodes WHERE node_id = ?").get(t.run_id):void 0,d=t.grantee==="app:terminal"?await w():null,p=d?.sub??s?.sub??t.sub,E=d?.id??s?.grant_id??t.grant_id??null;if(t.grantee==="app:terminal"&&_("user").runtime?.issuer&&!d)throw new Error("cannot mint node token: terminal grant is not active");if(!p&&t.grantee!=="user:local"&&!(t.grantee==="app:terminal"&&!_("user").runtime?.issuer))throw new Error(`cannot mint node token: no subject for ${t.grantee}`);const c=m(32).toString("base64url");await e.prepare("INSERT INTO node_tokens (broker_process_id, node_id, token_hash, grantee, uid, sub, grant, client_id, scopes_json, issued_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)").run(n,o,l(c),t.grantee==="user:local"?"app:terminal":t.grantee,await N(t.grantee==="user:local"?"app:terminal":t.grantee),p??"user:local",E,s?.client_id??t.client_id,t.scopes_json,i),a.set(n,c)}r(x,"mintNodeToken");function A(e){const n=a.get(e);if(a.delete(e),!n)throw new Error(`missing node token for execution ${e}`);return n}r(A,"takeNodeToken");function T(e){a.delete(e)}r(T,"forgetNodeToken");async function D(e){if(!/^[A-Za-z0-9_-]{43}$/.test(e))return null;const n=await(await u()).prepare(`SELECT t.node_id, t.grantee, t.uid, t.sub, t.grant, t.client_id, t.scopes_json, n.run_id
1
+ var g=Object.defineProperty;var r=(e,n)=>g(e,"name",{value:n,configurable:!0});import{createHash as k,randomBytes as m}from"node:crypto";import{openDb as u}from"./db.js";import{terminalGrant as w}from"../grants/mirror.js";import{readConfig as _}from"../config.js";import{uidForApp as N}from"../spaces/users.js";const a=new Map,l=r(e=>k("sha256").update(e).digest("hex"),"hash");async function O(e,n,o,i){const t=await e.prepare("SELECT grantee, sub, grant_id, client_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(o);if(!t)throw new Error(`cannot mint node token: missing node ${o}`);const s=t.run_id&&t.run_id!==o?await e.prepare("SELECT sub, grant_id, client_id FROM nodes WHERE node_id = ?").get(t.run_id):void 0,d=t.grantee==="app:terminal"?await w():null,p=d?.sub??s?.sub??t.sub,E=d?.id??s?.grant_id??t.grant_id??null;if(t.grantee==="app:terminal"&&_("user").runtime?.issuer&&!d)throw new Error("cannot mint node token: terminal grant is not active");if(!p&&t.grantee!=="user:local"&&!(t.grantee==="app:terminal"&&!_("user").runtime?.issuer))throw new Error(`cannot mint node token: no subject for ${t.grantee}`);const c=m(32).toString("base64url");await e.prepare('INSERT INTO node_tokens (broker_process_id, node_id, token_hash, grantee, uid, sub, "grant", client_id, scopes_json, issued_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)').run(n,o,l(c),t.grantee==="user:local"?"app:terminal":t.grantee,await N(t.grantee==="user:local"?"app:terminal":t.grantee),p??"user:local",E,s?.client_id??t.client_id,t.scopes_json,i),a.set(n,c)}r(O,"mintNodeToken");function x(e){const n=a.get(e);if(a.delete(e),!n)throw new Error(`missing node token for execution ${e}`);return n}r(x,"takeNodeToken");function T(e){a.delete(e)}r(T,"forgetNodeToken");async function A(e){if(!/^[A-Za-z0-9_-]{43}$/.test(e))return null;const n=await(await u()).prepare(`SELECT t.node_id, t.grantee, t.uid, t.sub, t."grant", t.client_id, t.scopes_json, n.run_id
2
2
  FROM node_tokens t JOIN executions e ON e.execution_id = t.broker_process_id
3
3
  JOIN nodes n ON n.node_id = t.node_id
4
- WHERE t.token_hash = ? AND t.revoked_at IS NULL AND e.state != 'exited'`).get(l(e));return n?{app:n.grantee,node:n.node_id,run:n.run_id,uid:n.uid,sub:n.sub,grant:n.grant,client_id:n.client_id,scopes:n.scopes_json===null?null:JSON.parse(n.scopes_json)}:null}r(D,"resolveNodeToken");async function U(e){T(e),await f(await u(),"broker_process_id",e,new Date().toISOString())}r(U,"revokeExitedNodeToken");async function v(e){await(await u()).prepare(`UPDATE node_tokens SET revoked_at = ? WHERE revoked_at IS NULL
5
- AND broker_process_id IN (SELECT execution_id FROM executions WHERE daemon_epoch IS NOT ?)`).run(new Date().toISOString(),e)}r(v,"revokePriorEpochNodeTokens");async function H(e,n,o){return Number((await e.prepare("UPDATE node_tokens SET sub = ? WHERE sub = 'user:local'").run(n)).changes)+Number((await e.prepare("UPDATE node_tokens SET grantee = 'app:terminal' WHERE grantee = 'user:local'").run()).changes)+Number((await e.prepare('UPDATE node_tokens SET "grant" = NULL WHERE "grant" = ?').run(o)).changes)}r(H,"rewriteLocalNodeTokenIdentity");async function f(e,n,o,i){await e.prepareUncached(`UPDATE node_tokens SET revoked_at = ? WHERE ${n} = ? AND revoked_at IS NULL`).run(i,o)}r(f,"revokeNodeTokens");export{T as forgetNodeToken,x as mintNodeToken,D as resolveNodeToken,U as revokeExitedNodeToken,f as revokeNodeTokens,v as revokePriorEpochNodeTokens,H as rewriteLocalNodeTokenIdentity,A as takeNodeToken};
4
+ WHERE t.token_hash = ? AND t.revoked_at IS NULL AND e.state != 'exited'`).get(l(e));return n?{app:n.grantee,node:n.node_id,run:n.run_id,uid:n.uid,sub:n.sub,grant:n.grant,client_id:n.client_id,scopes:n.scopes_json===null?null:JSON.parse(n.scopes_json)}:null}r(A,"resolveNodeToken");async function U(e){T(e),await f(await u(),"broker_process_id",e,new Date().toISOString())}r(U,"revokeExitedNodeToken");async function v(e){await(await u()).prepare(`UPDATE node_tokens SET revoked_at = ? WHERE revoked_at IS NULL
5
+ AND broker_process_id IN (SELECT execution_id FROM executions WHERE daemon_epoch IS DISTINCT FROM ?)`).run(new Date().toISOString(),e)}r(v,"revokePriorEpochNodeTokens");async function H(e,n,o){return Number((await e.prepare("UPDATE node_tokens SET sub = ? WHERE sub = 'user:local'").run(n)).changes)+Number((await e.prepare("UPDATE node_tokens SET grantee = 'app:terminal' WHERE grantee = 'user:local'").run()).changes)+Number((await e.prepare('UPDATE node_tokens SET "grant" = NULL WHERE "grant" = ?').run(o)).changes)}r(H,"rewriteLocalNodeTokenIdentity");async function f(e,n,o,i){await e.prepareUncached(`UPDATE node_tokens SET revoked_at = ? WHERE ${n} = ? AND revoked_at IS NULL`).run(i,o)}r(f,"revokeNodeTokens");export{T as forgetNodeToken,O as mintNodeToken,A as resolveNodeToken,U as revokeExitedNodeToken,f as revokeNodeTokens,v as revokePriorEpochNodeTokens,H as rewriteLocalNodeTokenIdentity,x as takeNodeToken};
@@ -1,6 +1,6 @@
1
- var p=Object.defineProperty;var i=(t,a)=>p(t,"name",{value:a,configurable:!0});import{createObject as u,nodeObject as l}from"../graph/objects.js";import{recordWatch as d}from"../graph/watches.js";import{argvReceiver as m}from"../graph/receivers.js";import{finalizeNodeMail as w,insertMessage as v}from"../feed/messages.js";import{deliveryFrom as E}from"./deliveries.js";async function s(t,a){const e=await t.prepare(`SELECT a.*, c.receiver_json, c.payload_json FROM objects a
1
+ var p=Object.defineProperty;var i=(t,a)=>p(t,"name",{value:a,configurable:!0});import{createObject as u,nodeObject as l}from"../graph/objects.js";import{recordWatch as d}from"../graph/watches.js";import{argvReceiver as m}from"../graph/receivers.js";import{finalizeNodeMail as v,insertMessage as w}from"../feed/messages.js";import{deliveryFrom as E}from"./deliveries.js";async function s(t,a){const e=await t.prepare(`SELECT a.*, c.receiver_json, c.payload_json FROM objects a
2
2
  JOIN objects n ON n.id = ? AND n.type = 'node'
3
- JOIN custom_objects c ON c.object = a.rowid
3
+ JOIN custom_objects c ON c.object = a.id
4
4
  WHERE a.type = 'action' AND a.name = ? AND a.owner = n.owner AND a.deleted_at IS NULL
5
- ORDER BY a.rowid DESC LIMIT 1`).get(a,`outcome/${a}`);if(!e)return null;const r=JSON.parse(e.receiver_json),o=await t.prepare("SELECT * FROM deliveries WHERE destination_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1").get(e.id);return{object:e,actionName:r.name,argv:r.argv,cwd:r.cwd,configOrigin:r.config_origin,payload:e.payload_json===null?null:JSON.parse(e.payload_json),delivery:o?E(o):null,updatedAt:r.updated_at??Date.parse(e.created_at)}}i(s,"readOutcomeAction");async function g(t,a,e){const r=await l(t,a),o=await s(t,a);if(o?.delivery)throw new Error(`outcome action ${a} already has a delivery`);const n=o?.object??await u(t,{type:"action",kind:null,owner:r.owner,privacy:"public",name:`outcome/${a}`,preview:e.actionName}),c=JSON.stringify({argv:e.argv,cwd:e.cwd,config_origin:e.configOrigin,name:e.actionName,updated_at:o?Math.max(Date.now(),o.updatedAt+1):Date.parse(n.created_at)});await t.prepare(`INSERT INTO custom_objects (object, receiver_json, payload_json) VALUES (?, ?, ?)
6
- ON CONFLICT(object) DO UPDATE SET receiver_json = excluded.receiver_json, payload_json = excluded.payload_json`).run(n.rowid,c,JSON.stringify(e.payload??null)),o?await t.prepare("UPDATE objects SET preview = ? WHERE rowid = ?").run(e.actionName,n.rowid):(await t.prepare("UPDATE objects SET reception = 2 WHERE rowid = ?").run(n.rowid),await d(t,n,r,"spawn",{patterns:["outcome"],config:[{threshold:1,delivery:"after-turn"}]}))}i(g,"registerOutcomeAction");async function D(t,a){const e=await s(t,a);return e?e.delivery?!1:(await t.prepare("DELETE FROM objects WHERE rowid = ?").run(e.object.rowid),!0):!0}i(D,"unregisterOutcomeAction");async function S(t,a,e){const r=await s(t,a);if(!r||r.delivery)throw new Error(`outcome action ${a} cannot be delivered again`);const o=await l(t,a),n=w({senderNodeId:a,senderName:o.name,kind:"completed",label:`outcome node ${o.name}`,bodyJson:e,urgency:"normal"},null),c=await v(t,{...n,senderNodeId:a,senderName:o.name,kind:"completed",label:`outcome node ${o.name}`,disposition:null,event:{event:"outcome",objectId:a,level:1,actorId:a}});await m.enqueue(t,{messageId:c,event:"outcome",level:1,senderNodeId:a,elevatesTier:!1},[{id:r.object.id,reception:2,delivery:"after-turn"}])}i(S,"enqueueLateOutcomeAction");export{S as enqueueLateOutcomeAction,s as readOutcomeAction,g as registerOutcomeAction,D as unregisterOutcomeAction};
5
+ ORDER BY a.seq DESC LIMIT 1`).get(a,`outcome/${a}`);if(!e)return null;const r=JSON.parse(e.receiver_json),o=await t.prepare("SELECT * FROM deliveries WHERE destination_id = ? ORDER BY created_at DESC, seq DESC LIMIT 1").get(e.id);return{object:e,actionName:r.name,argv:r.argv,cwd:r.cwd,configOrigin:r.config_origin,payload:e.payload_json===null?null:JSON.parse(e.payload_json),delivery:o?E(o):null,updatedAt:r.updated_at??Date.parse(e.created_at)}}i(s,"readOutcomeAction");async function g(t,a,e){const r=await l(t,a),o=await s(t,a);if(o?.delivery)throw new Error(`outcome action ${a} already has a delivery`);const n=o?.object??await u(t,{type:"action",kind:null,owner:r.owner,privacy:"public",name:`outcome/${a}`,preview:e.actionName}),c=JSON.stringify({argv:e.argv,cwd:e.cwd,config_origin:e.configOrigin,name:e.actionName,updated_at:o?Math.max(Date.now(),o.updatedAt+1):Date.parse(n.created_at)});await t.prepare(`INSERT INTO custom_objects (object, receiver_json, payload_json) VALUES (?, ?, ?)
6
+ ON CONFLICT(object) DO UPDATE SET receiver_json = excluded.receiver_json, payload_json = excluded.payload_json`).run(n.id,c,JSON.stringify(e.payload??null)),o?await t.prepare("UPDATE objects SET preview = ? WHERE id = ?").run(e.actionName,n.id):(await t.prepare("UPDATE objects SET reception = 2 WHERE id = ?").run(n.id),await d(t,n,r,"spawn",{patterns:["outcome"],config:[{threshold:1,delivery:"after-turn"}]}))}i(g,"registerOutcomeAction");async function D(t,a){const e=await s(t,a);return e?e.delivery?!1:(await t.prepare("DELETE FROM objects WHERE id = ?").run(e.object.id),!0):!0}i(D,"unregisterOutcomeAction");async function S(t,a,e){const r=await s(t,a);if(!r||r.delivery)throw new Error(`outcome action ${a} cannot be delivered again`);const o=await l(t,a),n=v({senderNodeId:a,senderName:o.name,kind:"completed",label:`outcome node ${o.name}`,bodyJson:e,urgency:"normal"},null),c=await w(t,{...n,senderNodeId:a,senderName:o.name,kind:"completed",label:`outcome node ${o.name}`,disposition:null,event:{event:"outcome",objectId:a,level:1,actorId:a}});await m.enqueue(t,{messageId:c,event:"outcome",level:1,senderNodeId:a,elevatesTier:!1},[{id:r.object.id,reception:2,delivery:"after-turn"}])}i(S,"enqueueLateOutcomeAction");export{S as enqueueLateOutcomeAction,s as readOutcomeAction,g as registerOutcomeAction,D as unregisterOutcomeAction};