@north-light/cli 0.3.551 → 0.3.553

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 (110) hide show
  1. package/dist/api/client.d.ts +11 -0
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/contract.d.ts +1 -1
  4. package/dist/api/contract.js +1 -1
  5. package/dist/api/dto/charges.d.ts +122 -0
  6. package/dist/api/dto/charges.js +0 -0
  7. package/dist/api/dto/usage.d.ts +3 -3
  8. package/dist/api/index.d.ts +1 -0
  9. package/dist/api/index.js +1 -1
  10. package/dist/api/routes.d.ts +3 -0
  11. package/dist/api/routes.js +1 -1
  12. package/dist/builtin-memory/crouter-sdk/release-notes.md +10 -0
  13. package/dist/clients/attach/viewer.js +50 -50
  14. package/dist/commands/sys/spend.d.ts +1 -0
  15. package/dist/commands/sys/spend.js +3 -0
  16. package/dist/commands/sys.js +1 -1
  17. package/dist/core/canvas/canvas.js +4 -4
  18. package/dist/core/canvas/db.js +4 -4
  19. package/dist/core/canvas/export-sqlite.js +2 -2
  20. package/dist/core/canvas/import-sqlite.d.ts +2 -0
  21. package/dist/core/canvas/import-sqlite.js +4 -4
  22. package/dist/core/canvas/person.js +7 -16
  23. package/dist/core/canvas/schema-chain.d.ts +34 -0
  24. package/dist/core/canvas/schema-chain.js +1 -0
  25. package/dist/core/canvas/schema-pg/.gitkeep +0 -0
  26. package/dist/core/canvas/schema-pg.sql +61 -0
  27. package/dist/core/canvas/schema-version.d.ts +29 -3
  28. package/dist/core/canvas/schema-version.js +1 -1
  29. package/dist/core/canvas/types.d.ts +4 -0
  30. package/dist/core/config.js +1 -1
  31. package/dist/core/grants/mirror.d.ts +2 -1
  32. package/dist/core/grants/mirror.js +2 -2
  33. package/dist/core/ledger/backfill.d.ts +12 -0
  34. package/dist/core/ledger/backfill.js +22 -0
  35. package/dist/core/ledger/charges.d.ts +18 -0
  36. package/dist/core/ledger/charges.js +1 -0
  37. package/dist/core/ledger/check.d.ts +44 -0
  38. package/dist/core/ledger/check.js +7 -0
  39. package/dist/core/ledger/index.d.ts +9 -0
  40. package/dist/core/ledger/index.js +1 -0
  41. package/dist/core/ledger/limits.d.ts +15 -0
  42. package/dist/core/ledger/limits.js +3 -0
  43. package/dist/core/ledger/months.d.ts +6 -0
  44. package/dist/core/ledger/months.js +1 -0
  45. package/dist/core/ledger/payer.d.ts +34 -0
  46. package/dist/core/ledger/payer.js +1 -0
  47. package/dist/core/ledger/read.d.ts +44 -0
  48. package/dist/core/ledger/read.js +3 -0
  49. package/dist/core/ledger/totals.d.ts +19 -0
  50. package/dist/core/ledger/totals.js +4 -0
  51. package/dist/core/ledger/types.d.ts +66 -0
  52. package/dist/core/ledger/types.js +0 -0
  53. package/dist/core/llm/run.d.ts +17 -5
  54. package/dist/core/llm/run.js +2 -2
  55. package/dist/core/providers/call.d.ts +7 -1
  56. package/dist/core/providers/call.js +1 -1
  57. package/dist/core/providers/errors.js +1 -1
  58. package/dist/core/providers/receipts.d.ts +7 -1
  59. package/dist/core/providers/receipts.js +1 -3
  60. package/dist/core/runs/operations.d.ts +1 -0
  61. package/dist/core/runs/operations.js +7 -7
  62. package/dist/core/runtime/broker/cli-relay.js +1 -1
  63. package/dist/core/runtime/broker/model-adapter.js +2 -2
  64. package/dist/core/runtime/broker/turn-cause.d.ts +7 -0
  65. package/dist/core/runtime/broker/turn-cause.js +1 -0
  66. package/dist/core/runtime/broker.js +1 -1
  67. package/dist/core/runtime/nodes.d.ts +2 -0
  68. package/dist/core/runtime/nodes.js +1 -1
  69. package/dist/core/runtime/recycle.js +1 -1
  70. package/dist/core/runtime/reset.js +1 -1
  71. package/dist/core/runtime/revive.js +2 -2
  72. package/dist/core/runtime/spawn.d.ts +2 -0
  73. package/dist/core/runtime/spawn.js +2 -2
  74. package/dist/core/runtime/start-check.d.ts +15 -0
  75. package/dist/core/runtime/start-check.js +1 -0
  76. package/dist/core/runtime/warm-pool.js +1 -1
  77. package/dist/core/user-settings.d.ts +4 -0
  78. package/dist/core/user-settings.js +1 -1
  79. package/dist/daemon/api/app-listener.js +13 -13
  80. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  81. package/dist/daemon/api/handlers/charges.d.ts +3 -0
  82. package/dist/daemon/api/handlers/charges.js +1 -0
  83. package/dist/daemon/api/handlers/llm.d.ts +9 -5
  84. package/dist/daemon/api/handlers/llm.js +3 -3
  85. package/dist/daemon/api/handlers/messages.js +2 -2
  86. package/dist/daemon/api/handlers/providers.d.ts +2 -0
  87. package/dist/daemon/api/handlers/providers.js +1 -1
  88. package/dist/daemon/api/handlers/run-routes.js +1 -1
  89. package/dist/daemon/api/handlers/runs.d.ts +1 -1
  90. package/dist/daemon/api/handlers/runs.js +1 -1
  91. package/dist/daemon/api/operations.js +1 -1
  92. package/dist/daemon/api/router.d.ts +8 -0
  93. package/dist/daemon/api/router.js +1 -1
  94. package/dist/daemon/api/server.js +1 -1
  95. package/dist/daemon/cron/authority.d.ts +1 -1
  96. package/dist/daemon/cron-run.js +2 -2
  97. package/dist/daemon/crtrd.js +10 -10
  98. package/dist/daemon/fleet.js +4 -4
  99. package/dist/daemon/reconcilers/action-lane.js +1 -1
  100. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +2 -0
  101. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +1 -1
  102. package/dist/types.d.ts +4 -0
  103. package/docs/sdk/release-notes.md +10 -0
  104. package/package.json +3 -2
  105. package/packages/crouter-identity/package.json +1 -1
  106. package/runtime.lock.json +13 -13
  107. package/scripts/install-runtime.mjs +6 -20
  108. package/scripts/lib/canvas-schema-gate.mjs +20 -0
  109. package/dist/core/usage/ledger.d.ts +0 -49
  110. package/dist/core/usage/ledger.js +0 -10
@@ -0,0 +1 @@
1
+ export declare const sysSpendBranch: import("../../core/command.js").BranchDef;
@@ -0,0 +1,3 @@
1
+ var y=Object.defineProperty;var t=(e,r)=>y(e,"name",{value:r,configurable:!0});import{defineBranch as w,defineLeaf as c}from"../../core/command.js";import{InputError as l}from"../../core/io.js";import{cliClient as m}from"../api-client.js";const d=t(e=>e===null?"unpriced":`$${e.toFixed(e<1?4:2)}`,"usd"),$=t(e=>[e.funding&&`funding ${e.funding}`,e.grantee&&`grantee ${e.grantee}`,e.provider&&`provider ${e.provider}`,e.sub&&`sub ${e.sub}`].filter(Boolean).join(", ")||"everything","filters"),p=t(e=>`- \`${e.id}\` ${e.payer} ${d(e.usd_per_month)}/month on ${$(e)} (${e.source})`,"limitLine"),b=c({name:"show",description:"show a month's spend by payer and funding, with the limits",whenToUse:"checking what a payer has spent this month and which limits apply",help:{name:"sys spend show",summary:"month totals by payer and by payer and funding from the ledger, then every limit row",params:[{kind:"flag",name:"month",type:"string",required:!1,constraint:"UTC month YYYY-MM. Default: the current month."}],output:[{name:"month",type:"string",required:!0,constraint:"The month shown, YYYY-MM."},{name:"totals",type:"ChargeTotalsDTO",required:!0,constraint:"Record: {month, by_payer, by_funding, limits}. Migrated user-paid model history has funding `legacy`."}],outputKind:"object",effects:["None. Read-only."]},run:t(async e=>{const r=await m().listCharges({limit:1,...e.month===void 0?{}:{month:String(e.month)}});if(r.totals===void 0)throw new l({error:"owner_only",message:"month totals and limits are read by the person; this caller sees only its own app's charges",next:"Run `crtr sys spend show` from the person's own shell, not from inside a node."});return{month:r.month,totals:r.totals}},"run"),render:t(e=>{const r=e.totals;return[`Spend for ${r.month}:`,...r.by_payer.length===0?["- nothing charged"]:r.by_payer.map(i=>`- ${i.payer} ${d(i.usd)}${r.by_funding.filter(s=>s.payer===i.payer).map(s=>` ${s.funding} ${d(s.usd)}`).join("")}`),"Limits:",...r.limits.length===0?["- none"]:r.limits.map(p)].join(`
2
+ `)},"render")});function v(e){const r=new Map;for(const n of e)r.set(n.provider??"(none)",(r.get(n.provider??"(none)")??0)+(n.usd??0));const i=new Set(e.map(n=>n.request)),s=t(n=>e.filter(o=>(o.caused_by??null)===n),"children"),a=[],u=new Set,h=t((n,o)=>{if(!u.has(n.id)&&(u.add(n.id),a.push(`${" ".repeat(o)}- ${n.at} ${n.kind} ${n.what} ${d(n.usd)} ${n.payer}/${n.funding}${n.provider?` \u2192 ${n.provider}`:""} vendor_ref ${n.vendor_ref??"none"}${n.refused?` REFUSED by ${n.refused.limit}`:""}`),n.request!==null))for(const g of s(`request:${n.request}`))h(g,o+1)},"print"),f=e.filter(n=>n.caused_by===null||!(n.caused_by.startsWith("request:")&&i.has(n.caused_by.slice(8))));for(const n of f)h(n,0);for(const n of e)h(n,0);return[...a,"Total per provider:",...[...r].map(([n,o])=>`- ${n} ${d(o)}`)].join(`
3
+ `)}t(v,"tree");const q=c({name:"chain",description:"list the charge rows under a run, message, request or charge on this runtime",whenToUse:"finding what one run, message or request cost and matching its vendor_ref to a vendor's usage line",help:{name:"sys spend chain",summary:"the rows under a reference as a tree, with each row's vendor_ref and a total per provider",params:[{kind:"positional",name:"reference",type:"string",required:!0,constraint:"`run:<id>`, `message:<id>`, `request:<id>`, `cron_run:<id>` or a charge id. Reads this runtime only."}],output:[{name:"reference",type:"string",required:!0,constraint:"The reference read."},{name:"charges",type:"ChargeDTO[]",required:!0,constraint:"Rows in time order; empty when this runtime holds none under the reference."}],outputKind:"object",effects:["None. Read-only."]},run:t(async e=>{const r=String(e.reference),i=[];for(let s;;){const a=await m().listCharges({under:r,...s===void 0?{}:{after:s}});if(i.push(...a.charges),a.next===null)break;s=a.next}return{reference:r,charges:i}},"run"),render:t(e=>{const r=e.charges;return r.length===0?`No charges under ${String(e.reference)} on this runtime.`:v(r)},"render")}),_=c({name:"limit",description:"set or clear one of the person's dollar limits",whenToUse:"stopping a payer's spend (0 is an emergency stop) or capping it per month; clearing a limit you set",help:{name:"sys spend limit",summary:"write or remove the limit row `person:<name>`; the check refuses calls billed to the payer once the month reaches it",params:[{kind:"positional",name:"name",type:"string",required:!0,constraint:"A name for the row; the id is `person:<name>`."},{kind:"flag",name:"payer",type:"string",required:!1,constraint:"`user:<id>` (the person) or `app:<id>`. Required unless --clear."},{kind:"flag",name:"usd",type:"string",required:!1,constraint:"Dollars per month, 0 or more; 0 stops the payer. Required unless --clear."},{kind:"flag",name:"funding",type:"enum",choices:["bill","subscription","key","endpoint","take","legacy"],required:!1,constraint:"Limit only this funding. Default: any."},{kind:"flag",name:"grantee",type:"string",required:!1,constraint:"Limit only charges attributed to this app, such as `app:terminal`. Default: any."},{kind:"flag",name:"provider",type:"string",required:!1,constraint:"Limit only money going to this vendor or provider app, such as `anthropic`. Default: any."},{kind:"flag",name:"clear",type:"bool",required:!1,constraint:"Remove the row instead of writing it."}],output:[{name:"limit",type:"LimitDTO",required:!1,constraint:"The row written; absent with --clear."},{name:"removed",type:"string",required:!1,constraint:"The id removed with --clear."}],outputKind:"object",effects:["Writes or deletes one `limits` row with source `person`. Takes effect on the next call."]},run:t(async e=>{const r=`person:${String(e.name)}`;if(e.clear===!0)return{removed:(await m().removeSpendLimit(r)).id};const i=e.usd===void 0?Number.NaN:Number(e.usd);if(typeof e.payer!="string"||!Number.isFinite(i)||i<0)throw new l({error:"nothing_to_set",message:"a limit needs --payer and --usd (0 or more), or --clear",next:"Pass --payer user:<id> --usd 20, or --clear to remove the row."});return{limit:await m().setSpendLimit(r,{payer:e.payer,usd_per_month:i,...e.funding===void 0?{}:{funding:e.funding},...e.grantee===void 0?{}:{grantee:String(e.grantee)},...e.provider===void 0?{}:{provider:String(e.provider)}})}},"run"),render:t(e=>e.removed!==void 0?`Removed ${String(e.removed)}.`:p(e.limit),"render")}),D=w({name:"spend",description:"the money ledger: spend by payer, the chain under a run, and dollar limits",whenToUse:"reading what was spent and why, or stopping or capping a payer's spend",help:{name:"sys spend",summary:"read this runtime's charges and set the person's dollar limits",model:"Every model call and priced tool call is one charge row naming its payer, funding and cause. A limit row bounds a payer's month; the check refuses calls billed to that payer once it is reached. `sys limits` is the unrelated per-app memory and disk cap."},children:[b,q,_]});export{D as sysSpendBranch};
@@ -1 +1 @@
1
- var s=Object.defineProperty;var r=(o,m)=>s(o,"name",{value:m,configurable:!0});import{defineSysBranch as t}from"./sys/branch.js";import{configBranch as f}from"./sys/config.js";import{sysEnvBranch as e}from"./sys/env.js";import{daemonBranch as i}from"./sys/daemon.js";import{sysDoctorLeaf as a}from"./sys/doctor.js";import{sysFeedbackLeaf as p}from"./sys/feedback.js";import{sysLogsLeaf as n}from"./sys/logs.js";import{sysRetryDeliveryLeaf as y}from"./sys/retry-delivery.js";import{sysWorktreesLeaf as c}from"./sys/worktrees.js";import{sysSpacesLeaf as L}from"./sys/spaces.js";import{sysLimitsBranch as h}from"./sys/limits.js";import{sysSettingsLeaf as B}from"./sys/settings.js";import{sysConnectLeaf as S}from"./sys/connect.js";import{sysSetupLeaf as g}from"./sys/setup.js";import{tutorialBranch as u}from"./sys/tutorial/branch.js";import{supportBranch as d}from"./sys/support.js";import{contextBranch as D}from"./sys/context.js";import{sysSyncBranch as b}from"./sys/sync.js";import{sysMigrateLeaf as k}from"./sys/migrate.js";import{sysJoinLeaf as l}from"./sys/join.js";import{sysDbBranch as v}from"./sys/db.js";import{sysUpdateLeaf as x,sysVersionLeaf as C}from"./sys/update.js";function Y(){return t([B,f,e,S,g,u,a,D,p,n,c,y,L,h,d,x,C,k,l,b,v,i])}r(Y,"registerSys");export{Y as registerSys};
1
+ var s=Object.defineProperty;var r=(o,m)=>s(o,"name",{value:m,configurable:!0});import{defineSysBranch as t}from"./sys/branch.js";import{configBranch as f}from"./sys/config.js";import{sysEnvBranch as e}from"./sys/env.js";import{daemonBranch as i}from"./sys/daemon.js";import{sysDoctorLeaf as a}from"./sys/doctor.js";import{sysFeedbackLeaf as p}from"./sys/feedback.js";import{sysLogsLeaf as n}from"./sys/logs.js";import{sysRetryDeliveryLeaf as y}from"./sys/retry-delivery.js";import{sysWorktreesLeaf as c}from"./sys/worktrees.js";import{sysSpacesLeaf as L}from"./sys/spaces.js";import{sysLimitsBranch as h}from"./sys/limits.js";import{sysSpendBranch as B}from"./sys/spend.js";import{sysSettingsLeaf as S}from"./sys/settings.js";import{sysConnectLeaf as d}from"./sys/connect.js";import{sysSetupLeaf as g}from"./sys/setup.js";import{tutorialBranch as u}from"./sys/tutorial/branch.js";import{supportBranch as D}from"./sys/support.js";import{contextBranch as b}from"./sys/context.js";import{sysSyncBranch as k}from"./sys/sync.js";import{sysMigrateLeaf as l}from"./sys/migrate.js";import{sysJoinLeaf as v}from"./sys/join.js";import{sysDbBranch as x}from"./sys/db.js";import{sysUpdateLeaf as C,sysVersionLeaf as E}from"./sys/update.js";function _(){return t([S,f,e,d,g,u,a,b,p,n,c,y,L,h,B,D,C,E,l,v,k,x,i])}r(_,"registerSys");export{_ as registerSys};
@@ -1,8 +1,8 @@
1
- var ie=Object.defineProperty;var i=(e,n)=>ie(e,"name",{value:n,configurable:!0});import{envNodeId as W}from"../../shared/env.js";import{lstatSync as oe,realpathSync as F,rmdirSync as re,rmSync as se,statSync as de}from"node:fs";import{basename as ue,dirname as b,isAbsolute as le,join as S,relative as ce,resolve as _e,sep as pe}from"node:path";import{removeTreeBeneath as C}from"../spaces/open-beneath.js";import{emitEvent as fe}from"../events/emit.js";import{withCanvasWrite as p,openDb as s}from"./db.js";import{conversationStore as Ee,ConversationNotFoundError as we}from"../conversation-store/index.js";import{hasUnseenWakeMail as ye,UNSEEN_WAKE_MAIL_SQL as me}from"./deliveries.js";import{isAppRun as H}from"./app-run.js";import{registerOutcomeAction as M}from"./outcome-actions.js";import{publish as Ne}from"../graph/events.js";import{nodeObject as Re}from"../graph/objects.js";import{findNodeBySessionFileSql as Se}from"./sessions.js";import{hydrateFault as he}from"./recovery.js";import{hasUnreconciledWorktree as Oe,openWorktree as Te,readWorktree as Le,worktreeFromRow as De}from"./worktrees.js";import{isPidAlive as ve,isPidPresent as P,recordedPidLiveness as ge}from"./pid.js";import{ensureHome as Ae,jobDir as ke,nodeDir as xe,nodeRunDir as $}from"./paths.js";import{appForNode as Ue,ensureNodeDirs as je,nodeAgentDir as Ie}from"./node-agent-paths.js";import{appSpace as T}from"../layout.js";import{createObject as We,getById as L,ownerForGrantee as Fe,setObjectKind as be,setObjectName as Ce}from"../graph/objects.js";import{profileObject as He}from"../graph/reader.js";import{writeActsFor as q}from"../permissions/facts.js";import{CrtrError as Me}from"../errors.js";import{deleteNodeWithOwned as Pe}from"../graph/deletion.js";import{SUBTREE_ALL_IDS_SQL as $e,SUBTREE_IDS_SQL as qe}from"./tree.js";import{filterAsync as m,mapAsync as D}from"../async-array.js";function E(e,n){if(e==null)return null;const t=JSON.stringify(e);if(t===void 0)throw new TypeError(`${n} must be JSON-serializable`);return t}i(E,"jsonValue");async function vn(e,n){if((await e.prepare("SELECT deleting_at FROM nodes WHERE node_id = ?").get(n))?.deleting_at)throw new Error("run is deleting")}i(vn,"assertRunNotDeleting");async function B(e,n,t){try{return await He(e,n,t)}catch(a){if(a instanceof Me&&a.code==="not_found")return null;throw a}}i(B,"profileObjectFor");async function Be(e,n){const t=await Fe(n,e.grantee??"app:terminal"),a=e.profile_id?await B(n,e.profile_id,t):null;await We(n,{id:e.node_id,type:"node",kind:e.kind,owner:(a??t).id,privacy:"public",name:e.name,preview:""}),a===null&&e.sub&&await q(n,e.node_id,e.sub),await n.prepare(`INSERT INTO nodes (node_id, description, title, icon, cycles, mode, lifecycle, cwd, host_kind, profile_id, parent, spawned_by, creator, fork_from, fork_source_file, created, passive_default, cycle_pending, model_override, persona_ack_json, review_binding_json, launch_config_json, scopes_json, isolated, status, intent, final_report, finalized_at, deadline_at, "window", tmux_session, pane, sub, grant_id, client_id, metadata_json, run_id, event_cursor, usage_json, deleting_at, app) VALUES (${Array(41).fill("?").join(", ")})`).run(e.node_id,e.description??null,e.title??null,e.icon??null,e.cycles??null,e.mode,e.lifecycle,e.cwd,e.host_kind??null,e.profile_id??null,e.parent??null,e.spawned_by??null,e.creator??null,e.fork_from??null,e.fork_source_file??null,e.created,e.passive_default===!0?1:0,e.cycle_pending===!0?1:0,e.model_override??null,E(e.persona_ack,"persona_ack"),E(e.review_binding,"review_binding"),E(e.launch,"launch"),E(e.scopes,"scopes"),e.isolated===!0?1:0,e.status??"active",e.intent??null,e.final_report??null,e.finalized_at??null,e.deadline_at??null,e.window??null,e.tmux_session??null,e.pane??null,e.sub??null,e.grant_id??null,e.client_id??null,e.metadata_json??null,e.run_id??null,e.event_cursor??null,e.usage_json??null,e.deleting_at??null,t.id)}i(Be,"seedRow");function z(e){if(e.fault_id==null)return null;const n=Object.entries(e).filter(([t])=>t.startsWith("fault_")).map(([t,a])=>[t==="fault_id"?t:t.slice(6),a]);return he(Object.fromEntries(n))}i(z,"faultFrom");function f(e){const n={node_id:e.node_id,name:e.name,description:e.description??void 0,cycles:e.cycles??void 0,kind:e.kind,mode:e.mode,lifecycle:e.lifecycle,status:e.status,cwd:e.cwd,host_kind:e.host_kind??null,profile_id:e.profile_id??null,scopes:e.scopes_json==null?null:JSON.parse(e.scopes_json),isolated:e.isolated===1,grantee:e.grantee,sub:e.sub??null,grant_id:e.grant_id??null,client_id:e.client_id??null,metadata_json:e.metadata_json??null,run_id:e.run_id??null,event_cursor:e.event_cursor??null,last_activity_at:e.last_activity_at??null,usage_json:e.usage_json??null,last_turn_error_json:e.last_turn_error_json??null,deleting_at:e.deleting_at??null,parent:e.parent??null,created:e.created,intent:e.intent??null,frozen_at:e.frozen_at??null,respawn_failures:e.respawn_failures??0,respawn_not_before:e.respawn_not_before??null,launched_at:e.launched_at??null,last_settled_at:e.last_settled_at??null,terminal_reason:e.terminal_reason??null,final_report:e.final_report??null,finalized_at:e.finalized_at??null,outcome_kind:e.outcome_kind??null,outcome_reason:e.outcome_reason??null,outcome_at:e.outcome_at??null,outcome_revision:e.outcome_revision??0,outcome_detail_json:e.outcome_detail_json??null,deadline_at:e.deadline_at??null,telemetry_tokens_in:e.telemetry_tokens_in??null,telemetry_context_tokens:e.telemetry_context_tokens??null,telemetry_last_activity:e.telemetry_last_activity??null,telemetry_updated_at:e.telemetry_updated_at??null,pi_pid:e.pi_pid??null,pi_pid_identity:e.pi_pid_identity??null,window:e.window??null,tmux_session:e.tmux_session??null,pane:e.pane??null,fault:null,streaming:!1},t=P(n.pi_pid),a=z(e);return n.fault=a?.link==="pi\u2192provider"&&a.retry.by==="daemon"&&a.retry.disposition==="auto"&&!t?null:a,n.streaming=e.turn_open_at!==null&&e.turn_closed_at===null&&t,n}i(f,"rowFrom");async function J(e,n){return n??=await Le(e.node_id),{...f(e),title:e.title??void 0,icon:e.icon??void 0,spawned_by:e.spawned_by??null,creator:e.creator??null,fork_from:e.fork_from??null,fork_source_file:e.fork_source_file??null,passive_default:e.passive_default===1,cycle_pending:e.cycle_pending===1,model_override:e.model_override??null,scopes:e.scopes_json==null?null:JSON.parse(e.scopes_json),persona_ack:e.persona_ack_json==null?void 0:JSON.parse(e.persona_ack_json),review_binding:e.review_binding_json==null?void 0:JSON.parse(e.review_binding_json),launch:e.launch_config_json==null?void 0:JSON.parse(e.launch_config_json),pi_session_id:e.pi_session_id??null,pi_session_file:e.pi_session_file??null,...n===null?{}:{managed_worktree:n}}}i(J,"metaFrom");async function gn(e,n={}){return Ae(),await p(async t=>{await Be(e,t),e.managed_worktree!==void 0&&e.managed_worktree!==null&&await Te(e.node_id,e.managed_worktree,t),n.outcomeDelivery!==void 0&&await M(t,e.node_id,n.outcomeDelivery),await n.withinCreate?.(t,e)}),await je(e.node_id),await h(e.node_id)}i(gn,"createNode");async function h(e){const n=await(await s()).prepare("SELECT * FROM node_rows WHERE node_id = ?").get(e);return n===void 0?null:await J(n)}i(h,"getNode");const ze=1440*60*1e3;function Y(e){return{requestKey:e.request_key,nodeId:e.node_id,state:e.state}}i(Y,"createRequestRecord");async function An(e){const n=await(await s()).prepareUncached("SELECT request_key, node_id, state FROM create_requests WHERE request_key = ?").get(e);return n===void 0?null:Y(n)}i(An,"findCreateRequest");async function kn(e){return await p(async n=>{const t=Date.now();await n.prepareUncached("DELETE FROM create_requests WHERE expires_at <= ?").run(t);const a=await n.prepareUncached("SELECT request_key, node_id, state FROM create_requests WHERE request_key = ?").get(e);return a!==void 0?Y(a):(await n.prepareUncached("INSERT INTO create_requests (request_key, node_id, expires_at, state) VALUES (?, NULL, ?, 'reserved')").run(e,t+ze),{requestKey:e,nodeId:null,state:"reserved"})})}i(kn,"reserveCreateRequest");async function xn(e=Date.now()){return Number((await(await s()).prepareUncached("DELETE FROM create_requests WHERE expires_at <= ?").run(e)).changes)}i(xn,"pruneExpiredCreateRequests");async function Un(e,n){await(await s()).prepareUncached("UPDATE create_requests SET node_id = ?, state = 'complete' WHERE request_key = ?").run(n,e)}i(Un,"recordCreateRequestNode");async function jn(e,n){await(await s()).prepareUncached("UPDATE create_requests SET node_id = ?, state = 'reserved' WHERE request_key = ?").run(n,e)}i(jn,"reserveCreateRequestNode");async function In(e,n){await(await s()).prepareUncached("UPDATE create_requests SET state = 'complete' WHERE request_key = ? AND node_id = ?").run(e,n)}i(In,"completeCreateRequest");async function Wn(e,n){await p(async t=>{if(n===null){await t.prepareUncached("DELETE FROM create_requests WHERE request_key = ? AND node_id IS NULL").run(e);return}await t.prepareUncached("DELETE FROM create_requests WHERE request_key = ? AND node_id = ?").run(e,n)})}i(Wn,"abandonCreateRequest");function Fn(e){return _e(e)}i(Fn,"canonicalSessionFile");async function bn(e){const n=await Se(e);return n===null?null:await h(n)}i(bn,"findNodeBySessionFile");async function Cn(e){const n=await(await s()).prepare("SELECT * FROM node_rows WHERE node_id = ?").get(e);return n?f(n):null}i(Cn,"getRow");async function Hn(e){const n=await(await s()).prepare("SELECT * FROM node_rows WHERE pane = ?").get(e);return n?f(n):null}i(Hn,"getRowByPane");async function Mn(e){return(await(await s()).prepare(`SELECT * FROM node_rows
1
+ var ie=Object.defineProperty;var i=(e,n)=>ie(e,"name",{value:n,configurable:!0});import{envNodeId as W}from"../../shared/env.js";import{lstatSync as oe,realpathSync as b,rmdirSync as re,rmSync as se,statSync as de}from"node:fs";import{basename as ue,dirname as F,isAbsolute as le,join as S,relative as ce,resolve as _e,sep as pe}from"node:path";import{removeTreeBeneath as C}from"../spaces/open-beneath.js";import{emitEvent as fe}from"../events/emit.js";import{withCanvasWrite as p,openDb as s}from"./db.js";import{conversationStore as Ee,ConversationNotFoundError as we}from"../conversation-store/index.js";import{hasUnseenWakeMail as ye,UNSEEN_WAKE_MAIL_SQL as me}from"./deliveries.js";import{isAppRun as H}from"./app-run.js";import{registerOutcomeAction as M}from"./outcome-actions.js";import{publish as Ne}from"../graph/events.js";import{nodeObject as Re}from"../graph/objects.js";import{findNodeBySessionFileSql as Se}from"./sessions.js";import{hydrateFault as he}from"./recovery.js";import{hasUnreconciledWorktree as Oe,openWorktree as Te,readWorktree as Le,worktreeFromRow as De}from"./worktrees.js";import{isPidAlive as ve,isPidPresent as P,recordedPidLiveness as ge}from"./pid.js";import{ensureHome as Ae,jobDir as ke,nodeDir as xe,nodeRunDir as $}from"./paths.js";import{appForNode as Ue,ensureNodeDirs as je,nodeAgentDir as Ie}from"./node-agent-paths.js";import{appSpace as T}from"../layout.js";import{createObject as We,getById as L,ownerForGrantee as be,setObjectKind as Fe,setObjectName as Ce}from"../graph/objects.js";import{profileObject as He}from"../graph/reader.js";import{writeActsFor as q}from"../permissions/facts.js";import{CrtrError as Me}from"../errors.js";import{deleteNodeWithOwned as Pe}from"../graph/deletion.js";import{SUBTREE_ALL_IDS_SQL as $e,SUBTREE_IDS_SQL as qe}from"./tree.js";import{filterAsync as m,mapAsync as D}from"../async-array.js";function E(e,n){if(e==null)return null;const t=JSON.stringify(e);if(t===void 0)throw new TypeError(`${n} must be JSON-serializable`);return t}i(E,"jsonValue");async function vn(e,n){if((await e.prepare("SELECT deleting_at FROM nodes WHERE node_id = ?").get(n))?.deleting_at)throw new Error("run is deleting")}i(vn,"assertRunNotDeleting");async function B(e,n,t){try{return await He(e,n,t)}catch(a){if(a instanceof Me&&a.code==="not_found")return null;throw a}}i(B,"profileObjectFor");async function Be(e,n){const t=await be(n,e.grantee??"app:terminal"),a=e.profile_id?await B(n,e.profile_id,t):null;await We(n,{id:e.node_id,type:"node",kind:e.kind,owner:(a??t).id,privacy:"public",name:e.name,preview:""}),a===null&&e.sub&&await q(n,e.node_id,e.sub),await n.prepare(`INSERT INTO nodes (node_id, description, title, icon, cycles, mode, lifecycle, cwd, host_kind, profile_id, parent, spawned_by, creator, fork_from, fork_source_file, created, passive_default, cycle_pending, model_override, persona_ack_json, review_binding_json, launch_config_json, scopes_json, isolated, status, intent, final_report, finalized_at, deadline_at, "window", tmux_session, pane, sub, grant_id, client_id, metadata_json, run_id, event_cursor, usage_json, deleting_at, caused_by, app) VALUES (${Array(42).fill("?").join(", ")})`).run(e.node_id,e.description??null,e.title??null,e.icon??null,e.cycles??null,e.mode,e.lifecycle,e.cwd,e.host_kind??null,e.profile_id??null,e.parent??null,e.spawned_by??null,e.creator??null,e.fork_from??null,e.fork_source_file??null,e.created,e.passive_default===!0?1:0,e.cycle_pending===!0?1:0,e.model_override??null,E(e.persona_ack,"persona_ack"),E(e.review_binding,"review_binding"),E(e.launch,"launch"),E(e.scopes,"scopes"),e.isolated===!0?1:0,e.status??"active",e.intent??null,e.final_report??null,e.finalized_at??null,e.deadline_at??null,e.window??null,e.tmux_session??null,e.pane??null,e.sub??null,e.grant_id??null,e.client_id??null,e.metadata_json??null,e.run_id??null,e.event_cursor??null,e.usage_json??null,e.deleting_at??null,e.caused_by??null,t.id)}i(Be,"seedRow");function z(e){if(e.fault_id==null)return null;const n=Object.entries(e).filter(([t])=>t.startsWith("fault_")).map(([t,a])=>[t==="fault_id"?t:t.slice(6),a]);return he(Object.fromEntries(n))}i(z,"faultFrom");function f(e){const n={node_id:e.node_id,name:e.name,description:e.description??void 0,cycles:e.cycles??void 0,kind:e.kind,mode:e.mode,lifecycle:e.lifecycle,status:e.status,cwd:e.cwd,host_kind:e.host_kind??null,profile_id:e.profile_id??null,scopes:e.scopes_json==null?null:JSON.parse(e.scopes_json),isolated:e.isolated===1,grantee:e.grantee,sub:e.sub??null,grant_id:e.grant_id??null,client_id:e.client_id??null,metadata_json:e.metadata_json??null,run_id:e.run_id??null,event_cursor:e.event_cursor??null,last_activity_at:e.last_activity_at??null,usage_json:e.usage_json??null,caused_by:e.caused_by??null,last_turn_error_json:e.last_turn_error_json??null,deleting_at:e.deleting_at??null,parent:e.parent??null,created:e.created,intent:e.intent??null,frozen_at:e.frozen_at??null,respawn_failures:e.respawn_failures??0,respawn_not_before:e.respawn_not_before??null,launched_at:e.launched_at??null,last_settled_at:e.last_settled_at??null,terminal_reason:e.terminal_reason??null,final_report:e.final_report??null,finalized_at:e.finalized_at??null,outcome_kind:e.outcome_kind??null,outcome_reason:e.outcome_reason??null,outcome_at:e.outcome_at??null,outcome_revision:e.outcome_revision??0,outcome_detail_json:e.outcome_detail_json??null,deadline_at:e.deadline_at??null,telemetry_tokens_in:e.telemetry_tokens_in??null,telemetry_context_tokens:e.telemetry_context_tokens??null,telemetry_last_activity:e.telemetry_last_activity??null,telemetry_updated_at:e.telemetry_updated_at??null,pi_pid:e.pi_pid??null,pi_pid_identity:e.pi_pid_identity??null,window:e.window??null,tmux_session:e.tmux_session??null,pane:e.pane??null,fault:null,streaming:!1},t=P(n.pi_pid),a=z(e);return n.fault=a?.link==="pi\u2192provider"&&a.retry.by==="daemon"&&a.retry.disposition==="auto"&&!t?null:a,n.streaming=e.turn_open_at!==null&&e.turn_closed_at===null&&t,n}i(f,"rowFrom");async function J(e,n){return n??=await Le(e.node_id),{...f(e),title:e.title??void 0,icon:e.icon??void 0,spawned_by:e.spawned_by??null,creator:e.creator??null,fork_from:e.fork_from??null,fork_source_file:e.fork_source_file??null,passive_default:e.passive_default===1,cycle_pending:e.cycle_pending===1,model_override:e.model_override??null,scopes:e.scopes_json==null?null:JSON.parse(e.scopes_json),persona_ack:e.persona_ack_json==null?void 0:JSON.parse(e.persona_ack_json),review_binding:e.review_binding_json==null?void 0:JSON.parse(e.review_binding_json),launch:e.launch_config_json==null?void 0:JSON.parse(e.launch_config_json),pi_session_id:e.pi_session_id??null,pi_session_file:e.pi_session_file??null,...n===null?{}:{managed_worktree:n}}}i(J,"metaFrom");async function gn(e,n={}){return Ae(),await p(async t=>{await Be(e,t),e.managed_worktree!==void 0&&e.managed_worktree!==null&&await Te(e.node_id,e.managed_worktree,t),n.outcomeDelivery!==void 0&&await M(t,e.node_id,n.outcomeDelivery),await n.withinCreate?.(t,e)}),await je(e.node_id),await h(e.node_id)}i(gn,"createNode");async function h(e){const n=await(await s()).prepare("SELECT * FROM node_rows WHERE node_id = ?").get(e);return n===void 0?null:await J(n)}i(h,"getNode");const ze=1440*60*1e3;function Y(e){return{requestKey:e.request_key,nodeId:e.node_id,state:e.state}}i(Y,"createRequestRecord");async function An(e){const n=await(await s()).prepareUncached("SELECT request_key, node_id, state FROM create_requests WHERE request_key = ?").get(e);return n===void 0?null:Y(n)}i(An,"findCreateRequest");async function kn(e){return await p(async n=>{const t=Date.now();await n.prepareUncached("DELETE FROM create_requests WHERE expires_at <= ?").run(t);const a=await n.prepareUncached("SELECT request_key, node_id, state FROM create_requests WHERE request_key = ?").get(e);return a!==void 0?Y(a):(await n.prepareUncached("INSERT INTO create_requests (request_key, node_id, expires_at, state) VALUES (?, NULL, ?, 'reserved')").run(e,t+ze),{requestKey:e,nodeId:null,state:"reserved"})})}i(kn,"reserveCreateRequest");async function xn(e=Date.now()){return Number((await(await s()).prepareUncached("DELETE FROM create_requests WHERE expires_at <= ?").run(e)).changes)}i(xn,"pruneExpiredCreateRequests");async function Un(e,n){await(await s()).prepareUncached("UPDATE create_requests SET node_id = ?, state = 'complete' WHERE request_key = ?").run(n,e)}i(Un,"recordCreateRequestNode");async function jn(e,n){await(await s()).prepareUncached("UPDATE create_requests SET node_id = ?, state = 'reserved' WHERE request_key = ?").run(n,e)}i(jn,"reserveCreateRequestNode");async function In(e,n){await(await s()).prepareUncached("UPDATE create_requests SET state = 'complete' WHERE request_key = ? AND node_id = ?").run(e,n)}i(In,"completeCreateRequest");async function Wn(e,n){await p(async t=>{if(n===null){await t.prepareUncached("DELETE FROM create_requests WHERE request_key = ? AND node_id IS NULL").run(e);return}await t.prepareUncached("DELETE FROM create_requests WHERE request_key = ? AND node_id = ?").run(e,n)})}i(Wn,"abandonCreateRequest");function bn(e){return _e(e)}i(bn,"canonicalSessionFile");async function Fn(e){const n=await Se(e);return n===null?null:await h(n)}i(Fn,"findNodeBySessionFile");async function Cn(e){const n=await(await s()).prepare("SELECT * FROM node_rows WHERE node_id = ?").get(e);return n?f(n):null}i(Cn,"getRow");async function Hn(e){const n=await(await s()).prepare("SELECT * FROM node_rows WHERE pane = ?").get(e);return n?f(n):null}i(Hn,"getRowByPane");async function Mn(e){return(await(await s()).prepare(`SELECT * FROM node_rows
2
2
  WHERE deadline_at IS NOT NULL AND outcome_at IS NULL
3
3
  AND deadline_at <= ? AND status IN ('active','idle') AND kind != 'human'
4
4
  ORDER BY deadline_at, created, node_id`).all(e)).map(f)}i(Mn,"dueNodeDeadlines");async function Pn(e,n){return Number((await(await s()).prepare("UPDATE nodes SET deadline_at = ? WHERE node_id = ?").run(n,e)).changes)===1}i(Pn,"setNodeDeadline");async function $n(e,n){return!Number.isFinite(n)||n<=0?0:await p(async t=>{const a=await t.prepare(`SELECT node_id, deadline_at FROM nodes
5
- WHERE profile_id = ? AND deadline_at IS NOT NULL AND status IN ('active', 'idle')`).all(e),o=t.prepare("UPDATE nodes SET deadline_at = ? WHERE node_id = ? AND deadline_at = ?");let r=0;for(const d of a){const l=Date.parse(d.deadline_at);Number.isNaN(l)||(r+=Number((await o.run(new Date(l+n).toISOString(),d.node_id,d.deadline_at)).changes))}return r})}i($n,"shiftProfileNodeDeadlines");function v(e,n){if(e==null)return null;if(typeof e=="string"||typeof e=="number"||typeof e=="bigint"||e instanceof Uint8Array)return e;throw new TypeError(`${n} must be a SQLite scalar`)}i(v,"sqlInput");async function qn(e,n){const t=[],a=i((r,d=r)=>{Object.prototype.hasOwnProperty.call(n,r)&&t.push([d,v(n[r],r)])},"add");if(a("description"),a("title"),a("icon"),a("cycles"),a("mode"),a("lifecycle"),a("cwd"),a("host_kind"),a("profile_id"),a("parent"),a("spawned_by"),a("creator"),a("fork_from"),a("fork_source_file"),a("model_override"),Object.prototype.hasOwnProperty.call(n,"passive_default")&&t.push(["passive_default",n.passive_default===!0?1:0]),Object.prototype.hasOwnProperty.call(n,"cycle_pending")&&t.push(["cycle_pending",n.cycle_pending===!0?1:0]),Object.prototype.hasOwnProperty.call(n,"persona_ack")&&t.push(["persona_ack_json",E(n.persona_ack,"persona_ack")]),Object.prototype.hasOwnProperty.call(n,"review_binding")&&t.push(["review_binding_json",E(n.review_binding,"review_binding")]),Object.prototype.hasOwnProperty.call(n,"isolated"))throw new TypeError("isolated cannot change after node creation");Object.prototype.hasOwnProperty.call(n,"scopes")&&t.push(["scopes_json",E(n.scopes,"scopes")]),Object.prototype.hasOwnProperty.call(n,"launch")&&t.push(["launch_config_json",E(n.launch,"launch")]);const o=i(r=>Object.prototype.hasOwnProperty.call(n,r),"has");return await p(async r=>{const d=await L(r,e);if(d===null||d.type!=="node")throw new Error(`unknown node: ${e}`);if(o("name")&&await Ce(r,d,v(n.name,"name")),o("kind")&&await be(r,d,v(n.kind,"kind")),t.length===0)return;if(o("profile_id")){const u=await r.prepare("SELECT app FROM nodes WHERE node_id = ?").get(e),_=u?.app?await L(r,u.app):null;if(_){const w=n.profile_id?await B(r,n.profile_id,_):null;await r.prepare("UPDATE objects SET owner = ? WHERE id = ?").run((w??_).id,e);const R=(await r.prepare("SELECT sub FROM nodes WHERE node_id = ?").get(e)).sub;w===null&&R&&await q(r,e,R)}}if((await r.prepareUncached(`UPDATE nodes SET ${t.map(([u])=>`${u} = ?`).join(", ")} WHERE node_id = ?`).run(...t.map(([,u])=>u),e)).changes!==1)throw new Error(`unknown node: ${e}`)}),await h(e)}i(qn,"updateNode");async function Bn(e,n){await(await s()).prepare("UPDATE nodes SET status = ? WHERE node_id = ?").run(n,e)}i(Bn,"setStatus");async function zn(e,n){await(await s()).prepare("UPDATE nodes SET intent = ? WHERE node_id = ?").run(n??null,e)}i(zn,"setIntent");async function Jn(e,n,t={}){if(t.outcome===void 0){const o=[],r=[];Object.prototype.hasOwnProperty.call(n,"status")&&(o.push("status = ?"),r.push(n.status??null)),Object.prototype.hasOwnProperty.call(n,"intent")&&(o.push("intent = ?"),r.push(n.intent??null)),Object.prototype.hasOwnProperty.call(n,"terminal_reason")&&(o.push("terminal_reason = ?"),r.push(n.terminal_reason??null));const d=["node_id = ?"];r.push(e),t.fromStatuses!==void 0&&(d.push(`status IN (${t.fromStatuses.map(()=>"?").join(", ")})`),r.push(...t.fromStatuses)),t.requireNoFinalReport===!0&&d.push("final_report IS NULL");const l=await(await s()).prepareUncached(`UPDATE nodes SET ${o.join(", ")} WHERE ${d.join(" AND ")}`).run(...r);return Number(l.changes)}const a=t.outcome;return await p(async o=>{const r=[],d=[];Object.prototype.hasOwnProperty.call(n,"status")&&(r.push("status = ?"),d.push(n.status??null)),Object.prototype.hasOwnProperty.call(n,"intent")&&(r.push("intent = ?"),d.push(n.intent??null)),Object.prototype.hasOwnProperty.call(n,"terminal_reason")&&(r.push("terminal_reason = ?"),d.push(n.terminal_reason??null));const l=i((R,y)=>a.supersede===!0?"?":`CASE WHEN outcome_at IS NULL THEN ? ELSE ${y} END`,"outcomeValue");r.push(`outcome_kind = ${l(a.kind,"outcome_kind")}`,`outcome_reason = ${l(a.reason,"outcome_reason")}`,`outcome_detail_json = ${l(a.detailJson,"outcome_detail_json")}`,`outcome_revision = ${a.supersede===!0?"outcome_revision + 1":"CASE WHEN outcome_at IS NULL THEN outcome_revision + 1 ELSE outcome_revision END"}`,`outcome_at = ${l(a.at,"outcome_at")}`),d.push(a.kind,a.reason,a.detailJson,a.at);const u=["node_id = ?"];d.push(e),t.fromStatuses!==void 0&&(u.push(`status IN (${t.fromStatuses.map(()=>"?").join(", ")})`),d.push(...t.fromStatuses)),t.requireNoFinalReport===!0&&u.push("final_report IS NULL");const _=await o.prepareUncached(`UPDATE nodes SET ${r.join(", ")} WHERE ${u.join(" AND ")}`).run(...d);if(Number(_.changes)===0)return 0;const w=await o.prepare("SELECT outcome_at FROM nodes WHERE node_id = ?").get(e);return(a.supersede===!0||w.outcome_at===a.at)&&a.documentJson!==null&&a.documentJson!==void 0&&await Ne(o,{event:"outcome",object:await Re(o,e),actor:null,attrs:{revision:Number((await o.prepare("SELECT outcome_revision FROM nodes WHERE node_id = ?").get(e)).outcome_revision)},messageBody:JSON.parse(a.documentJson)}),Number(_.changes)})}i(Jn,"transitionStatusIntent");async function Yn(e){return(await(await s()).prepare("SELECT final_report FROM nodes WHERE node_id = ?").get(e))?.final_report??null}i(Yn,"getFinalReport");async function Qn(e,n){return(await(await s()).prepare("UPDATE nodes SET final_report = NULL, finalized_at = NULL WHERE node_id = ? AND final_report = ?").run(e,n)).changes>0}i(Qn,"clearFinalReportIfMatches");async function Vn(e,n){await(await s()).prepare('UPDATE nodes SET tmux_session = ?, "window" = ?, pane = ? WHERE node_id = ?').run(n.tmux_session??null,n.window??null,n.pane??null,e)}i(Vn,"setPresence");async function Kn(e,n){await(await s()).prepare("UPDATE nodes SET launched_at = ?, frozen_at = NULL, respawn_not_before = NULL WHERE node_id = ?").run(n,e)}i(Kn,"recordLaunch");async function Gn(e,n){await(await s()).prepare("UPDATE nodes SET last_settled_at = ? WHERE node_id = ?").run(n,e)}i(Gn,"recordSettledTurn");async function Xn(e,n){await(await s()).prepare("UPDATE nodes SET frozen_at = ? WHERE node_id = ?").run(n,e)}i(Xn,"setFrozen");async function Zn(e){await(await s()).prepare("UPDATE nodes SET frozen_at = NULL WHERE node_id = ?").run(e)}i(Zn,"clearFrozen");async function et(e,n){await(await s()).prepare("UPDATE nodes SET respawn_failures = ?, respawn_not_before = ? WHERE node_id = ?").run(n.failures,n.notBefore,e)}i(et,"recordRespawnAttempt");const N="node_id NOT IN (SELECT node_id FROM warm_pool)",Q=["state","cleanup_state","path","branch","repo_root","base_ref","base_sha","created_at","closed_at","next_attempt_at","attempts","quarantined","sweep_reason","sweep_detail","sweep_last_at","sweep_observed_json","abandoned_json","land_intent_json","disposal_json"];function Je(e){return e.worktree_state===null?null:De(Object.fromEntries(Q.map(n=>[n,e[`worktree_${n}`]])))}i(Je,"worktreeFromProjection");async function nt(e){const n=Q.map(a=>`w.${a} AS worktree_${a}`).join(", "),t=await(await s()).prepareUncached(`SELECT n.*, ${n} FROM node_rows n LEFT JOIN worktrees w ON w.node_id = n.node_id WHERE n.profile_id = ? ORDER BY n.created`).all(e);return await D(t,async a=>await J(a,Je(a)))}i(nt,"listNodesByProfile");async function tt(){return(await(await s()).prepare(`SELECT
5
+ WHERE profile_id = ? AND deadline_at IS NOT NULL AND status IN ('active', 'idle')`).all(e),o=t.prepare("UPDATE nodes SET deadline_at = ? WHERE node_id = ? AND deadline_at = ?");let r=0;for(const d of a){const l=Date.parse(d.deadline_at);Number.isNaN(l)||(r+=Number((await o.run(new Date(l+n).toISOString(),d.node_id,d.deadline_at)).changes))}return r})}i($n,"shiftProfileNodeDeadlines");function v(e,n){if(e==null)return null;if(typeof e=="string"||typeof e=="number"||typeof e=="bigint"||e instanceof Uint8Array)return e;throw new TypeError(`${n} must be a SQLite scalar`)}i(v,"sqlInput");async function qn(e,n){const t=[],a=i((r,d=r)=>{Object.prototype.hasOwnProperty.call(n,r)&&t.push([d,v(n[r],r)])},"add");if(a("description"),a("title"),a("icon"),a("cycles"),a("mode"),a("lifecycle"),a("cwd"),a("host_kind"),a("profile_id"),a("parent"),a("spawned_by"),a("creator"),a("fork_from"),a("fork_source_file"),a("model_override"),Object.prototype.hasOwnProperty.call(n,"passive_default")&&t.push(["passive_default",n.passive_default===!0?1:0]),Object.prototype.hasOwnProperty.call(n,"cycle_pending")&&t.push(["cycle_pending",n.cycle_pending===!0?1:0]),Object.prototype.hasOwnProperty.call(n,"persona_ack")&&t.push(["persona_ack_json",E(n.persona_ack,"persona_ack")]),Object.prototype.hasOwnProperty.call(n,"review_binding")&&t.push(["review_binding_json",E(n.review_binding,"review_binding")]),Object.prototype.hasOwnProperty.call(n,"isolated"))throw new TypeError("isolated cannot change after node creation");Object.prototype.hasOwnProperty.call(n,"scopes")&&t.push(["scopes_json",E(n.scopes,"scopes")]),Object.prototype.hasOwnProperty.call(n,"launch")&&t.push(["launch_config_json",E(n.launch,"launch")]);const o=i(r=>Object.prototype.hasOwnProperty.call(n,r),"has");return await p(async r=>{const d=await L(r,e);if(d===null||d.type!=="node")throw new Error(`unknown node: ${e}`);if(o("name")&&await Ce(r,d,v(n.name,"name")),o("kind")&&await Fe(r,d,v(n.kind,"kind")),t.length===0)return;if(o("profile_id")){const u=await r.prepare("SELECT app FROM nodes WHERE node_id = ?").get(e),_=u?.app?await L(r,u.app):null;if(_){const w=n.profile_id?await B(r,n.profile_id,_):null;await r.prepare("UPDATE objects SET owner = ? WHERE id = ?").run((w??_).id,e);const R=(await r.prepare("SELECT sub FROM nodes WHERE node_id = ?").get(e)).sub;w===null&&R&&await q(r,e,R)}}if((await r.prepareUncached(`UPDATE nodes SET ${t.map(([u])=>`${u} = ?`).join(", ")} WHERE node_id = ?`).run(...t.map(([,u])=>u),e)).changes!==1)throw new Error(`unknown node: ${e}`)}),await h(e)}i(qn,"updateNode");async function Bn(e,n){await(await s()).prepare("UPDATE nodes SET status = ? WHERE node_id = ?").run(n,e)}i(Bn,"setStatus");async function zn(e,n){await(await s()).prepare("UPDATE nodes SET intent = ? WHERE node_id = ?").run(n??null,e)}i(zn,"setIntent");async function Jn(e,n,t={}){if(t.outcome===void 0){const o=[],r=[];Object.prototype.hasOwnProperty.call(n,"status")&&(o.push("status = ?"),r.push(n.status??null)),Object.prototype.hasOwnProperty.call(n,"intent")&&(o.push("intent = ?"),r.push(n.intent??null)),Object.prototype.hasOwnProperty.call(n,"terminal_reason")&&(o.push("terminal_reason = ?"),r.push(n.terminal_reason??null));const d=["node_id = ?"];r.push(e),t.fromStatuses!==void 0&&(d.push(`status IN (${t.fromStatuses.map(()=>"?").join(", ")})`),r.push(...t.fromStatuses)),t.requireNoFinalReport===!0&&d.push("final_report IS NULL");const l=await(await s()).prepareUncached(`UPDATE nodes SET ${o.join(", ")} WHERE ${d.join(" AND ")}`).run(...r);return Number(l.changes)}const a=t.outcome;return await p(async o=>{const r=[],d=[];Object.prototype.hasOwnProperty.call(n,"status")&&(r.push("status = ?"),d.push(n.status??null)),Object.prototype.hasOwnProperty.call(n,"intent")&&(r.push("intent = ?"),d.push(n.intent??null)),Object.prototype.hasOwnProperty.call(n,"terminal_reason")&&(r.push("terminal_reason = ?"),d.push(n.terminal_reason??null));const l=i((R,y)=>a.supersede===!0?"?":`CASE WHEN outcome_at IS NULL THEN ? ELSE ${y} END`,"outcomeValue");r.push(`outcome_kind = ${l(a.kind,"outcome_kind")}`,`outcome_reason = ${l(a.reason,"outcome_reason")}`,`outcome_detail_json = ${l(a.detailJson,"outcome_detail_json")}`,`outcome_revision = ${a.supersede===!0?"outcome_revision + 1":"CASE WHEN outcome_at IS NULL THEN outcome_revision + 1 ELSE outcome_revision END"}`,`outcome_at = ${l(a.at,"outcome_at")}`),d.push(a.kind,a.reason,a.detailJson,a.at);const u=["node_id = ?"];d.push(e),t.fromStatuses!==void 0&&(u.push(`status IN (${t.fromStatuses.map(()=>"?").join(", ")})`),d.push(...t.fromStatuses)),t.requireNoFinalReport===!0&&u.push("final_report IS NULL");const _=await o.prepareUncached(`UPDATE nodes SET ${r.join(", ")} WHERE ${u.join(" AND ")}`).run(...d);if(Number(_.changes)===0)return 0;const w=await o.prepare("SELECT outcome_at FROM nodes WHERE node_id = ?").get(e);return(a.supersede===!0||w.outcome_at===a.at)&&a.documentJson!==null&&a.documentJson!==void 0&&await Ne(o,{event:"outcome",object:await Re(o,e),actor:null,attrs:{revision:Number((await o.prepare("SELECT outcome_revision FROM nodes WHERE node_id = ?").get(e)).outcome_revision)},messageBody:JSON.parse(a.documentJson)}),Number(_.changes)})}i(Jn,"transitionStatusIntent");async function Yn(e){return(await(await s()).prepare("SELECT final_report FROM nodes WHERE node_id = ?").get(e))?.final_report??null}i(Yn,"getFinalReport");async function Qn(e,n){return(await(await s()).prepare("UPDATE nodes SET final_report = NULL, finalized_at = NULL WHERE node_id = ? AND final_report = ?").run(e,n)).changes>0}i(Qn,"clearFinalReportIfMatches");async function Vn(e,n){await(await s()).prepare('UPDATE nodes SET tmux_session = ?, "window" = ?, pane = ? WHERE node_id = ?').run(n.tmux_session??null,n.window??null,n.pane??null,e)}i(Vn,"setPresence");async function Kn(e,n){await(await s()).prepare("UPDATE nodes SET launched_at = ?, frozen_at = NULL, respawn_not_before = NULL WHERE node_id = ?").run(n,e)}i(Kn,"recordLaunch");async function Gn(e,n){await(await s()).prepare("UPDATE nodes SET last_settled_at = ? WHERE node_id = ?").run(n,e)}i(Gn,"recordSettledTurn");async function Xn(e,n){await(await s()).prepare("UPDATE nodes SET frozen_at = ? WHERE node_id = ?").run(n,e)}i(Xn,"setFrozen");async function Zn(e){await(await s()).prepare("UPDATE nodes SET frozen_at = NULL WHERE node_id = ?").run(e)}i(Zn,"clearFrozen");async function et(e,n){await(await s()).prepare("UPDATE nodes SET respawn_failures = ?, respawn_not_before = ? WHERE node_id = ?").run(n.failures,n.notBefore,e)}i(et,"recordRespawnAttempt");const N="node_id NOT IN (SELECT node_id FROM warm_pool)",Q=["state","cleanup_state","path","branch","repo_root","base_ref","base_sha","created_at","closed_at","next_attempt_at","attempts","quarantined","sweep_reason","sweep_detail","sweep_last_at","sweep_observed_json","abandoned_json","land_intent_json","disposal_json"];function Je(e){return e.worktree_state===null?null:De(Object.fromEntries(Q.map(n=>[n,e[`worktree_${n}`]])))}i(Je,"worktreeFromProjection");async function nt(e){const n=Q.map(a=>`w.${a} AS worktree_${a}`).join(", "),t=await(await s()).prepareUncached(`SELECT n.*, ${n} FROM node_rows n LEFT JOIN worktrees w ON w.node_id = n.node_id WHERE n.profile_id = ? ORDER BY n.created`).all(e);return await D(t,async a=>await J(a,Je(a)))}i(nt,"listNodesByProfile");async function tt(){return(await(await s()).prepare(`SELECT
6
6
  node_id, name, description, kind, mode, lifecycle, status, cwd, profile_id,
7
7
  grantee, parent, created, frozen_at, terminal_reason, final_report,
8
8
  telemetry_tokens_in, telemetry_updated_at, pi_pid, turn_open_at, turn_closed_at,
@@ -56,7 +56,7 @@ var ie=Object.defineProperty;var i=(e,n)=>ie(e,"name",{value:n,configurable:!0})
56
56
  AND status IN ('active','idle') AND cwd = ?
57
57
  AND ${N}
58
58
  ORDER BY created DESC`).all(e)).map(f)}i(yt,"listResumableRoots");export*from"./tree.js";async function mt(e,n,t){return await p(async a=>{const o=await a.prepare("SELECT status, final_report, frozen_at FROM nodes WHERE node_id = ?").get(e);return o===void 0?{kind:"missing"}:n(o)?{kind:"terminal",status:o.status,final_report:o.final_report}:{kind:"ran",result:await t()}})}i(mt,"withFreshTerminalGuard");async function O(e){return await Oe(e)}i(O,"hasUnreconciledManagedWorktree");async function G(e,n={}){return!["done","dead","canceled"].includes(e.status)||e.pi_pid!==null||e.frozen_at!==null||await O(e.node_id)||e.node_id===W()||H(e)?!1:n.pastTtl===!0||!["crashed","stranded",null].includes(e.terminal_reason??null)}i(G,"isCollectible");async function A(e){if(e.length===0)return[];const n=e.map(()=>"?").join(","),t=await(await s()).prepareUncached(`SELECT s.session_id, s.transcript_path, s.node_id, n.grantee FROM sessions s JOIN node_rows n ON n.node_id = s.node_id WHERE s.node_id IN (${n}) AND s.session_id IS NOT NULL AND s.transcript_path IS NOT NULL ORDER BY s.created_at, s.seq`).all(...e);return[...new Map(t.map(a=>[a.session_id,{id:a.session_id,path:a.transcript_path,dir:S(T(a.grantee).conversations,a.node_id)}])).values()]}i(A,"nodeConversationIds");async function k(e){const n=await s(),t=n.prepare("SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1"),a=n.prepare(`SELECT 1 FROM nodes n WHERE n.fork_source_file LIKE '%\\_' || ? || '.jsonl' ESCAPE '\\'
59
- AND NOT EXISTS (SELECT 1 FROM sessions s WHERE s.node_id = n.node_id) LIMIT 1`);for(const{id:o,path:r,dir:d}of e)if(!(await t.get(o)!==void 0||await a.get(o)!==void 0)&&oe(r,{throwIfNoEntry:!1})!==void 0){if(!Ge(d,r)){console.error(`[canvas] not deleting transcript outside its node's conversations directory: ${r}`);continue}try{await Ee().delete(o,r)}catch(l){if(!(l instanceof we))throw l}}}i(k,"deleteNodeConversations");function Ge(e,n){try{const t=ce(F(e),S(F(b(n)),ue(n)));return t!==""&&t!==".."&&!t.startsWith(`..${pe}`)&&!le(t)}catch(t){if(t.code==="ENOENT")return!1;throw t}}i(Ge,"isBeneath");async function x(e,n){const t=await L(e,n);return t===null||t.deleted_at!==null?!1:(await Pe(e,t),!0)}i(x,"deleteNodeObject");async function U(e){return{nodeId:e,app:await h(e)===null?null:await Ue(e)}}i(U,"nodeFiles");async function X({nodeId:e,app:n}){n!==null&&C(T(n).nodes,await Ie(e,n)),C(b($(e)),$(e))}i(X,"removeNodeAppTrees");function Z({nodeId:e}){return[xe(e)]}i(Z,"nodeOwnedDirs");function ee({nodeId:e,app:n}){if(n!==null)try{re(S(T(n).conversations,e))}catch(t){if(!["ENOENT","ENOTEMPTY"].includes(t.code??""))throw t}}i(ee,"removeNodeConversationDir");async function j(e){await X(e);for(const n of Z(e))se(n,{recursive:!0,force:!0});ee(e)}i(j,"removeNodeFiles");async function Nt(e){if(await O(e))return{deleted:!1};const n=await U(e),t=await A([e]),a=await p(async o=>(await x(o,e),{deleted:!0}));return await k(t),await j(n),a}i(Nt,"deleteNode");async function Rt(e){const n=[...new Set(e)];if(n.length===0)return[];const t=await m(n,O);if(t.length>0)throw new Error(`refusing to delete nodes with unreconciled managed worktrees: ${t.join(", ")}`);const a=await D(n,U),o=await A(n),r=n.map(()=>"?").join(","),d=(await(await s()).prepareUncached(`SELECT node_id FROM nodes WHERE node_id IN (${r}) ORDER BY created`).all(...n)).map(({node_id:u})=>u),l=await m(d,async u=>await p(async _=>await x(_,u)));await k(o);for(const u of l){const _=a[n.indexOf(u)];await I(_,async()=>await j(_))}return l}i(Rt,"deleteNodes");async function St(e){const n=e.dryRun??!1,t=e.includeStale??!1,a=Date.now()-e.ttlDays*864e5,o=new Date(a).toISOString(),r=W()??"",d=await s(),l=i(c=>ae(c)<a,"quiet"),u=i(c=>({node_id:c.node_id,status:c.status,created:c.created}),"toPruned"),_=await Xe(),w=(await m((await d.prepare(`SELECT * FROM node_rows
59
+ AND NOT EXISTS (SELECT 1 FROM sessions s WHERE s.node_id = n.node_id) LIMIT 1`);for(const{id:o,path:r,dir:d}of e)if(!(await t.get(o)!==void 0||await a.get(o)!==void 0)&&oe(r,{throwIfNoEntry:!1})!==void 0){if(!Ge(d,r)){console.error(`[canvas] not deleting transcript outside its node's conversations directory: ${r}`);continue}try{await Ee().delete(o,r)}catch(l){if(!(l instanceof we))throw l}}}i(k,"deleteNodeConversations");function Ge(e,n){try{const t=ce(b(e),S(b(F(n)),ue(n)));return t!==""&&t!==".."&&!t.startsWith(`..${pe}`)&&!le(t)}catch(t){if(t.code==="ENOENT")return!1;throw t}}i(Ge,"isBeneath");async function x(e,n){const t=await L(e,n);return t===null||t.deleted_at!==null?!1:(await Pe(e,t),!0)}i(x,"deleteNodeObject");async function U(e){return{nodeId:e,app:await h(e)===null?null:await Ue(e)}}i(U,"nodeFiles");async function X({nodeId:e,app:n}){n!==null&&C(T(n).nodes,await Ie(e,n)),C(F($(e)),$(e))}i(X,"removeNodeAppTrees");function Z({nodeId:e}){return[xe(e)]}i(Z,"nodeOwnedDirs");function ee({nodeId:e,app:n}){if(n!==null)try{re(S(T(n).conversations,e))}catch(t){if(!["ENOENT","ENOTEMPTY"].includes(t.code??""))throw t}}i(ee,"removeNodeConversationDir");async function j(e){await X(e);for(const n of Z(e))se(n,{recursive:!0,force:!0});ee(e)}i(j,"removeNodeFiles");async function Nt(e){if(await O(e))return{deleted:!1};const n=await U(e),t=await A([e]),a=await p(async o=>(await x(o,e),{deleted:!0}));return await k(t),await j(n),a}i(Nt,"deleteNode");async function Rt(e){const n=[...new Set(e)];if(n.length===0)return[];const t=await m(n,O);if(t.length>0)throw new Error(`refusing to delete nodes with unreconciled managed worktrees: ${t.join(", ")}`);const a=await D(n,U),o=await A(n),r=n.map(()=>"?").join(","),d=(await(await s()).prepareUncached(`SELECT node_id FROM nodes WHERE node_id IN (${r}) ORDER BY created`).all(...n)).map(({node_id:u})=>u),l=await m(d,async u=>await p(async _=>await x(_,u)));await k(o);for(const u of l){const _=a[n.indexOf(u)];await I(_,async()=>await j(_))}return l}i(Rt,"deleteNodes");async function St(e){const n=e.dryRun??!1,t=e.includeStale??!1,a=Date.now()-e.ttlDays*864e5,o=new Date(a).toISOString(),r=W()??"",d=await s(),l=i(c=>ae(c)<a,"quiet"),u=i(c=>({node_id:c.node_id,status:c.status,created:c.created}),"toPruned"),_=await Xe(),w=(await m((await d.prepare(`SELECT * FROM node_rows
60
60
  WHERE status IN ('dead', 'done', 'canceled') AND created < ?
61
61
  ORDER BY created`).all(o)).map(f),async c=>await G(c,{pastTtl:!0})&&l(c)&&!_.has(c.node_id)&&!await e.held(c.node_id))).map(u),R=t?(await m((await d.prepare(`SELECT * FROM node_rows
62
62
  WHERE status IN ('active', 'idle') AND created < ?
@@ -64,4 +64,4 @@ var ie=Object.defineProperty;var i=(e,n)=>ie(e,"name",{value:n,configurable:!0})
64
64
  JOIN objects d ON d.id = w.object AND d.type = 'document' AND d.deleted_at IS NULL
65
65
  JOIN objects o ON o.id = w.watcher AND o.deleted_at IS NULL
66
66
  LEFT JOIN nodes n ON n.node_id = w.watcher
67
- WHERE w.watcher != d.owner AND d.owner IS NOT NULL AND (n.node_id IS NULL OR n.status IN ('active', 'idle'))`).all();return new Set(e.map(n=>n.owner))}i(Xe,"ownersOfWatchedDocuments");async function ne(e){const t=(await s()).prepare("SELECT node_id FROM nodes WHERE run_id = ? AND node_id != ?"),a=new Set(e.map(o=>o.node_id));return await m(e,async o=>(await t.all(o.node_id,o.node_id)).every(r=>a.has(r.node_id)))}i(ne,"retainRunRootsWithMembers");async function te(e,n){if(e.length===0)return;const t=await D(e,async o=>await U(o.node_id)),a=await A(e.map(o=>o.node_id));for(const o of e)await p(async r=>await x(r,o.node_id));if(await k(a),n!==void 0){for(const o of t)await I(o,async()=>{await X(o),ee(o)});n(t.flatMap(Z));return}for(const o of t)await I(o,async()=>await j(o))}i(te,"deletePrunedNodes");async function I(e,n){try{n()}catch(t){fe({level:"error",event:"canvas.node_files.remove_failed",error:t,fields:{node_id:e.nodeId}})}}i(I,"eachNodeBestEffort");function ae(e){let n=0;for(const t of[e.created,e.launched_at,e.last_settled_at,e.finalized_at,e.outcome_at,e.last_activity_at]){if(t==null)continue;const a=Date.parse(t);Number.isFinite(a)&&a>n&&(n=a)}try{const t=de(S(ke(e.node_id),"telemetry.json"),{throwIfNoEntry:!1});t!==void 0&&t.mtimeMs>n&&(n=t.mtimeMs)}catch{}return n}i(ae,"lastActivityMs");async function ht(e){const n=e.dryRun??!1,t=await Ye(),a=await m(t,async u=>await G(u)&&!await e.held(u.node_id)),o=t.length-a.length,r=Math.max(0,e.maxNodes-o),d=a.map(u=>({row:u,recency:ae(u)})).sort((u,_)=>_.recency-u.recency).slice(r).map(({row:u})=>({node_id:u.node_id,status:u.status,created:u.created})),l=await ne(d);return n||l.length===0?{pruned:l,dryRun:n}:(await te(l),{pruned:l,dryRun:n})}i(ht,"pruneToLimit");export{V as PARKED_RESIDENT_SQL,Ke as RESUMABLE_NODE_SQL,Wn as abandonCreateRequest,vn as assertRunNotDeleting,tt as browseNodes,Fn as canonicalSessionFile,Et as childrenOf,pt as claimWarmSpare,Qn as clearFinalReportIfMatches,Zn as clearFrozen,In as completeCreateRequest,gn as createNode,Nt as deleteNode,Rt as deleteNodes,Mn as dueNodeDeadlines,An as findCreateRequest,bn as findNodeBySessionFile,Yn as getFinalReport,h as getNode,Cn as getRow,Hn as getRowByPane,it as graphNodes,G as isCollectible,_t as isWarmSpare,ae as lastActivityMs,Ye as listNodes,nt as listNodesByProfile,rt as listNodesForSupervision,dt as listParkedResidentsWithWakeMail,yt as listResumableRoots,ot as listTerminalPlacementResidue,ct as listWarmSpares,U as nodeFiles,Ve as nodesForSupervisionSql,xn as pruneExpiredCreateRequests,St as pruneNodes,ht as pruneToLimit,Un as recordCreateRequestNode,Kn as recordLaunch,et as recordRespawnAttempt,Gn as recordSettledTurn,ut as registerWarmSpare,j as removeNodeFiles,kn as reserveCreateRequest,jn as reserveCreateRequestNode,st as resumableNodeCwds,wt as rosterNodes,Xn as setFrozen,zn as setIntent,Pn as setNodeDeadline,Vn as setPresence,Bn as setStatus,$n as shiftProfileNodeDeadlines,Qe as terminalPlacementResidueSql,Jn as transitionStatusIntent,lt as unregisterWarmSpare,qn as updateNode,at as updateNodeTelemetry,ft as warmSpareCount,mt as withFreshTerminalGuard};
67
+ WHERE w.watcher != d.owner AND d.owner IS NOT NULL AND (n.node_id IS NULL OR n.status IN ('active', 'idle'))`).all();return new Set(e.map(n=>n.owner))}i(Xe,"ownersOfWatchedDocuments");async function ne(e){const t=(await s()).prepare("SELECT node_id FROM nodes WHERE run_id = ? AND node_id != ?"),a=new Set(e.map(o=>o.node_id));return await m(e,async o=>(await t.all(o.node_id,o.node_id)).every(r=>a.has(r.node_id)))}i(ne,"retainRunRootsWithMembers");async function te(e,n){if(e.length===0)return;const t=await D(e,async o=>await U(o.node_id)),a=await A(e.map(o=>o.node_id));for(const o of e)await p(async r=>await x(r,o.node_id));if(await k(a),n!==void 0){for(const o of t)await I(o,async()=>{await X(o),ee(o)});n(t.flatMap(Z));return}for(const o of t)await I(o,async()=>await j(o))}i(te,"deletePrunedNodes");async function I(e,n){try{n()}catch(t){fe({level:"error",event:"canvas.node_files.remove_failed",error:t,fields:{node_id:e.nodeId}})}}i(I,"eachNodeBestEffort");function ae(e){let n=0;for(const t of[e.created,e.launched_at,e.last_settled_at,e.finalized_at,e.outcome_at,e.last_activity_at]){if(t==null)continue;const a=Date.parse(t);Number.isFinite(a)&&a>n&&(n=a)}try{const t=de(S(ke(e.node_id),"telemetry.json"),{throwIfNoEntry:!1});t!==void 0&&t.mtimeMs>n&&(n=t.mtimeMs)}catch{}return n}i(ae,"lastActivityMs");async function ht(e){const n=e.dryRun??!1,t=await Ye(),a=await m(t,async u=>await G(u)&&!await e.held(u.node_id)),o=t.length-a.length,r=Math.max(0,e.maxNodes-o),d=a.map(u=>({row:u,recency:ae(u)})).sort((u,_)=>_.recency-u.recency).slice(r).map(({row:u})=>({node_id:u.node_id,status:u.status,created:u.created})),l=await ne(d);return n||l.length===0?{pruned:l,dryRun:n}:(await te(l),{pruned:l,dryRun:n})}i(ht,"pruneToLimit");export{V as PARKED_RESIDENT_SQL,Ke as RESUMABLE_NODE_SQL,Wn as abandonCreateRequest,vn as assertRunNotDeleting,tt as browseNodes,bn as canonicalSessionFile,Et as childrenOf,pt as claimWarmSpare,Qn as clearFinalReportIfMatches,Zn as clearFrozen,In as completeCreateRequest,gn as createNode,Nt as deleteNode,Rt as deleteNodes,Mn as dueNodeDeadlines,An as findCreateRequest,Fn as findNodeBySessionFile,Yn as getFinalReport,h as getNode,Cn as getRow,Hn as getRowByPane,it as graphNodes,G as isCollectible,_t as isWarmSpare,ae as lastActivityMs,Ye as listNodes,nt as listNodesByProfile,rt as listNodesForSupervision,dt as listParkedResidentsWithWakeMail,yt as listResumableRoots,ot as listTerminalPlacementResidue,ct as listWarmSpares,U as nodeFiles,Ve as nodesForSupervisionSql,xn as pruneExpiredCreateRequests,St as pruneNodes,ht as pruneToLimit,Un as recordCreateRequestNode,Kn as recordLaunch,et as recordRespawnAttempt,Gn as recordSettledTurn,ut as registerWarmSpare,j as removeNodeFiles,kn as reserveCreateRequest,jn as reserveCreateRequestNode,st as resumableNodeCwds,wt as rosterNodes,Xn as setFrozen,zn as setIntent,Pn as setNodeDeadline,Vn as setPresence,Bn as setStatus,$n as shiftProfileNodeDeadlines,Qe as terminalPlacementResidueSql,Jn as transitionStatusIntent,lt as unregisterWarmSpare,qn as updateNode,at as updateNodeTelemetry,ft as warmSpareCount,mt as withFreshTerminalGuard};
@@ -1,4 +1,4 @@
1
- var L=Object.defineProperty;var a=(e,t)=>L(e,"name",{value:t,configurable:!0});import{AsyncLocalStorage as x}from"node:async_hooks";import{createHash as N,randomBytes as P}from"node:crypto";import{appendFileSync as D,mkdirSync as M,readFileSync as b}from"node:fs";import{dirname as q,join as T}from"node:path";import{fileURLToPath as I}from"node:url";import p from"pg";import{resolveDatabaseUrl as B}from"../../daemon/postgres/supervisor.js";import{publishFileExclusive as F}from"../fs-utils.js";import{initializeEmptyLayout as U,runtimeRoot as d}from"../layout.js";import{translatePlaceholders as H,translateOnce as K}from"../statements.js";import{ensureHome as V}from"./paths.js";import{CANVAS_SCHEMA_VERSION as w}from"./schema-version.js";const G="crtr_test_template";class W extends Error{static{a(this,"CanvasConstraintError")}constraint;code="23505";constructor(t,n,r){super(t,r),this.name="CanvasConstraintError",this.constraint=n}}function v(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
- `;D(t,n)}catch{}}a(v,"recordOpenLedger");const Y=4,z=/\b(?:FOR\s+(?:NO\s+KEY\s+)?(?:UPDATE|SHARE)|FOR\s+KEY\s+SHARE|INTO|nextval|setval|set_config|pg_notify|pg_advisory\w*|pg_try_advisory\w*|lo_\w+)\b/i;function J(e){return/^\s*SELECT\b/i.test(e)&&!z.test(e)}a(J,"isReadStatement");const X=1e3,h=new x;class j{static{a(this,"FifoLock")}tail=Promise.resolve();pending=0;get busy(){return this.pending>0}drained(){return this.tail}async acquire(){this.pending++;const t=this.tail;let n;return this.tail=new Promise(r=>{n=r}),await t,()=>{this.pending--,n()}}}const Z=new Set([114,3802]),C={getTypeParser:a(((e,t)=>Z.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}:p.types.getTypeParser(e,t)),"getTypeParser")};class Q{static{a(this,"Handle")}url;lock=new j;db;client=null;pool=null;connecting=null;schemaChecked=!1;active=0;inFlight=new Set;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(){v(this.url);const t=new p.Client({connectionString:this.url,types:C,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 rt(t),this.schemaChecked=!0)}catch(n){throw await t.end().catch(()=>{}),n}return t.connection.stream.unref(),this.client=t,t}readPool(){if(this.pool!==null)return this.pool;v(this.url);const t=new p.Pool({connectionString:this.url,types:C,application_name:`crtr-${process.pid}`,connectionTimeoutMillis:1e4,max:Y,idleTimeoutMillis:1e4,allowExitOnIdle:!0});return t.on("error",n=>console.warn(`[canvas db] read connection error: ${n.message}`)),this.pool=t}async read(t){await this.lock.drained(),this.inFlight.size>0&&await Promise.all([...this.inFlight]);try{return await this.readPool().query(t)}catch(n){throw this.mapError(n,null)}}drop(t){this.client===t&&(this.client=null)}async send(t){const n=await this.connected(),r=h.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();const o=t.query(n),s=o.then(()=>{},()=>{});this.inFlight.add(s),s.then(()=>this.inFlight.delete(s));try{return await o}catch(i){throw this.mapError(i,t)}finally{--this.active===0&&r.unref()}}mapError(t,n){const r=t;return r.code==="23505"?new W(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??""))&&n!==null&&(this.drop(n),n.end().catch(()=>{})),t)}async gated(t){if(E(this)){const r=h.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 i=H(t);return{text:i.text,name:i.name}})():{text:K(t).text},o=J(r.text),s=a(i=>{const m={...r,values:i.map(tt)};return o&&!E(this)?this.read(m):this.gated(()=>this.send(m))},"query");return{get:a(async(...i)=>(await s(i)).rows[0],"get"),all:a(async(...i)=>(await s(i)).rows,"all"),run:a(async(...i)=>({changes:(await s(i)).rowCount??0}),"run")}}async close(){const t=this.pool;this.pool=null,await t?.end().catch(()=>{}),(await this.lock.acquire())();const n=this.client;this.client=null,n!==null&&(n.connection.stream.ref(),await n.end().catch(()=>{}))}}function tt(e){return ArrayBuffer.isView(e)&&!Buffer.isBuffer(e)?Buffer.from(e.buffer,e.byteOffset,e.byteLength):e}a(tt,"bindable");function E(e){const t=h.getStore();return t!==void 0&&t.open&&t.handle===e}a(E,"ownsTransaction");const et=q(I(import.meta.url)),nt=T(et,"schema-pg.sql"),A=7427001;async function O(e,t){await e.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",[t.key,t.hash])}a(O,"recordBlock");async function R(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(R,"storedSchemaVersion");async function rt(e){await e.query("SELECT pg_advisory_lock($1)",[A]);try{const t=b(nt,"utf8"),n=[...t.matchAll(/^-- (\w+):begin\n([\s\S]*?)^-- \1:end$/gm)].map(([,o,s])=>({key:`schema_block.${o}`,sql:s,hash:N("sha256").update(s).digest("hex")}));let r=await R(e);if(r===void 0){await e.query(t),r=await R(e);for(const o of n)await O(e,o)}else if(r===w){const o=new Map((await e.query("SELECT key, value FROM canvas_meta WHERE key LIKE 'schema\\_block.%'")).rows.map(s=>[s.key,s.value]));for(const s of n)o.get(s.key)!==s.hash&&(await e.query(s.sql),await O(e,s))}if(r!==w)throw new Error(r!==void 0&&r>w?`unsupported canvas schema version ${r} (this build supports ${w}); this build is older than the database. Select a newer runtime generation.`:`canvas schema version ${String(r)} does not match this build's ${w}`)}finally{await e.query("SELECT pg_advisory_unlock($1)",[A]).catch(()=>{})}}a(rt,"ensureSchema");function at(){return process.env.NODE_TEST_CONTEXT!==void 0||process.execArgv.includes("--test")}a(at,"runningUnderNodeTest");function y(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(y,"withDatabase");async function st(e){const t=T(d(),"test-db"),n=a(()=>{try{return b(t,"utf8").trim()||void 0}catch(i){if(i.code==="ENOENT")return;throw i}},"recorded"),r=n();if(r!==void 0)return y(e,r);M(d(),{recursive:!0});const o=`t_${P(8).toString("hex")}`,s=new p.Client({connectionString:y(e,"postgres")});await s.connect();try{if(await s.query(`CREATE DATABASE ${o} TEMPLATE ${G} STRATEGY file_copy`),!F(t,`${o}
3
- `,384))return await s.query(`DROP DATABASE ${o}`),y(e,n())}finally{await s.end()}return y(e,o)}a(st,"testDatabaseUrl");async function ot(){const e=process.env.CRTR_TEST_PG_URL;if(e)return st(e);if(at())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 B(d())}a(ot,"canvasDatabaseUrl");const f=new Map,g=new Map;async function k(){U(),V();const e=await ot();g.set(d(),e);let t=f.get(e);return t===void 0&&f.set(e,t=new Q(e)),await t.connected(),t}a(k,"openHandle");async function _t(){return(await k()).db}a(_t,"openDb");async function bt(){const e=g.get(d());if(e===void 0)return;g.delete(d());const t=f.get(e);t!==void 0&&(f.delete(e),await t.close())}a(bt,"closeDb");async function Tt(){const e=[...f.values()];f.clear(),g.clear(),await Promise.all(e.map(t=>t.close()))}a(Tt,"closeAllDbs");function vt(e){const t=h.getStore();if(t===void 0||!t.open){queueMicrotask(e);return}t.afterCommit.push(e)}a(vt,"afterCommit");let it=0;async function Ct(e,t={}){const n=await k(),r=n.db;if(E(n)){if(!t.savepoint&&!t.rollback)return e(r);const c=`canvas_write_${++it}`;await n.send({text:`SAVEPOINT ${c}`});const l=h.getStore()?.afterCommit,S=l?.length??0;let _;try{_=await e(r)}catch($){throw await n.send({text:`ROLLBACK TO ${c}; RELEASE ${c}`}),l?.splice(S),$}return await n.send({text:t.rollback?`ROLLBACK TO ${c}; RELEASE ${c}`:`RELEASE ${c}`}),t.rollback&&l?.splice(S),_}const o=await n.lock.acquire(),s=performance.now(),i=new Error("canvas write transaction entered here");let m=!1;const u={handle:n,client:void 0,open:!0,afterCommit:[],statementMs:0};try{const c=await n.connected();await n.sendOn(c,{text:"BEGIN"}),u.client=c;try{const l=await h.run(u,()=>e(r));return await n.sendOn(c,{text:t.rollback?"ROLLBACK":"COMMIT"}),m=!t.rollback,l}catch(l){try{await n.sendOn(c,{text:"ROLLBACK"})}catch{}throw l}}finally{u.open=!1;const c=performance.now()-s;if(c-u.statementMs>X&&console.warn(`[canvas db] write transaction held ${Math.round(c)}ms (${Math.round(u.statementMs)}ms in its statements); its body did non-db work while every other db caller waited
4
- ${i.stack}`),o(),m)for(const l of u.afterCommit)queueMicrotask(l)}}a(Ct,"withCanvasWrite");export{W as CanvasConstraintError,vt as afterCommit,ot as canvasDatabaseUrl,Tt as closeAllDbs,bt as closeDb,J as isReadStatement,_t as openDb,Ct as withCanvasWrite};
1
+ var C=Object.defineProperty;var a=(n,t)=>C(n,"name",{value:t,configurable:!0});import{AsyncLocalStorage as R}from"node:async_hooks";import{randomBytes as x}from"node:crypto";import{appendFileSync as $,mkdirSync as P,readFileSync as k}from"node:fs";import{join as L}from"node:path";import m from"pg";import{resolveDatabaseUrl as D}from"../../daemon/postgres/supervisor.js";import{publishFileExclusive as M}from"../fs-utils.js";import{initializeEmptyLayout as N,runtimeRoot as h}from"../layout.js";import{translatePlaceholders as B,translateOnce as q}from"../statements.js";import{ensureHome as U}from"./paths.js";import{emitEvent as F}from"../events/emit.js";import{ensureSchema as T}from"./schema-chain.js";import{canvasDatabaseSchema as I,canvasSchemaDatabaseUrl as K,canvasSearchPathOptions as b}from"./schema-version.js";const G="crtr_test_template";class H extends Error{static{a(this,"CanvasConstraintError")}constraint;code="23505";constructor(t,e,r){super(t,r),this.name="CanvasConstraintError",this.constraint=e}}function _(n){const t=process.env.CRTR_DB_OPEN_LEDGER;if(t)try{const e=`${new Date().toISOString()} ${process.pid} ${process.argv[1]??"<unknown>"} ${n.replace(/\/\/([^:@/]*):[^@/]*@/,"//$1@")}
2
+ `;$(t,e)}catch{}}a(_,"recordOpenLedger");const Y=4,z=/\b(?:FOR\s+(?:NO\s+KEY\s+)?(?:UPDATE|SHARE)|FOR\s+KEY\s+SHARE|INTO|nextval|setval|set_config|pg_notify|pg_advisory\w*|pg_try_advisory\w*|lo_\w+)\b/i;function J(n){return/^\s*SELECT\b/i.test(n)&&!z.test(n)}a(J,"isReadStatement");const V=1e3,f=new R;class W{static{a(this,"FifoLock")}tail=Promise.resolve();pending=0;get busy(){return this.pending>0}drained(){return this.tail}async acquire(){this.pending++;const t=this.tail;let e;return this.tail=new Promise(r=>{e=r}),await t,()=>{this.pending--,e()}}}const j=new Set([114,3802]),O={getTypeParser:a(((n,t)=>j.has(n)?e=>e:n===20||n===1700?e=>{const r=Number(e);if(n===20&&!Number.isSafeInteger(r))throw new Error(`canvas db: bigint ${e} does not fit a JS number`);return r}:m.types.getTypeParser(n,t)),"getTypeParser")};class X{static{a(this,"Handle")}url;lock=new W;db;client=null;pool=null;connecting=null;schemaChecked=!1;active=0;inFlight=new Set;constructor(t){this.url=t;const e=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 w(e)}}}async connected(){return this.client!==null?this.client:(this.connecting??=this.connect().finally(()=>{this.connecting=null}),this.connecting)}async connect(){_(this.url);const t=new m.Client({connectionString:this.url,types:O,application_name:`crtr-${process.pid}`,connectionTimeoutMillis:1e4,options:b()});t.on("error",e=>{console.warn(`[canvas db] connection error: ${e.message}`),this.drop(t)}),t.on("end",()=>this.drop(t)),await t.connect();try{this.schemaChecked||(await Q(t),this.schemaChecked=!0)}catch(e){throw await t.end().catch(()=>{}),e}return t.connection.stream.unref(),this.client=t,t}readPool(){if(this.pool!==null)return this.pool;_(this.url);const t=new m.Pool({connectionString:this.url,types:O,application_name:`crtr-${process.pid}`,connectionTimeoutMillis:1e4,options:b(),max:Y,idleTimeoutMillis:1e4,allowExitOnIdle:!0});return t.on("error",e=>console.warn(`[canvas db] read connection error: ${e.message}`)),this.pool=t}async read(t){await this.lock.drained(),this.inFlight.size>0&&await Promise.all([...this.inFlight]);try{return await this.readPool().query(t)}catch(e){throw this.mapError(e,null)}}drop(t){this.client===t&&(this.client=null)}async send(t){const e=await this.connected(),r=f.getStore();if(r!==void 0&&r.open&&r.handle===this&&r.client!==e)throw new Error("canvas db: the connection was lost during this transaction; it was rolled back");return this.sendOn(e,t)}async sendOn(t,e){const r=t.connection.stream;this.active++===0&&r.ref();const o=t.query(e),i=o.then(()=>{},()=>{});this.inFlight.add(i),i.then(()=>this.inFlight.delete(i));try{return await o}catch(s){throw this.mapError(s,t)}finally{--this.active===0&&r.unref()}}mapError(t,e){const r=t;return r.code==="23505"?new H(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??""))&&e!==null&&(this.drop(e),e.end().catch(()=>{})),t)}async gated(t){if(w(this)){const r=f.getStore(),o=performance.now();try{return await t()}finally{r.statementMs+=performance.now()-o}}if(!this.lock.busy)return t();const e=await this.lock.acquire();try{return await t()}finally{e()}}statement(t,e){const r=e?(()=>{const s=B(t);return{text:s.text,name:s.name}})():{text:q(t).text},o=J(r.text),i=a(s=>{const u={...r,values:s.map(Z)};return o&&!w(this)?this.read(u):this.gated(()=>this.send(u))},"query");return{get:a(async(...s)=>(await i(s)).rows[0],"get"),all:a(async(...s)=>(await i(s)).rows,"all"),run:a(async(...s)=>({changes:(await i(s)).rowCount??0}),"run")}}async close(){const t=this.pool;this.pool=null,await t?.end().catch(()=>{}),(await this.lock.acquire())();const e=this.client;this.client=null,e!==null&&(e.connection.stream.ref(),await e.end().catch(()=>{}))}}function Z(n){return ArrayBuffer.isView(n)&&!Buffer.isBuffer(n)?Buffer.from(n.buffer,n.byteOffset,n.byteLength):n}a(Z,"bindable");function w(n){const t=f.getStore();return t!==void 0&&t.open&&t.handle===n}a(w,"ownsTransaction");async function Q(n){const t=a((i,s,u)=>{F({level:i,event:s,fields:u})},"emit"),e=I(),r=K();if(r===void 0){await T(n,{schema:e,emit:t});return}const o=new m.Client({connectionString:r,application_name:`crtr-schema-${process.pid}`,connectionTimeoutMillis:1e4});await o.connect();try{await T(o,{schema:e,emit:t})}finally{await o.end().catch(()=>{})}}a(Q,"applySchema");function tt(){return process.env.NODE_TEST_CONTEXT!==void 0||process.execArgv.includes("--test")}a(tt,"runningUnderNodeTest");function y(n,t){const e=/^(postgres(?:ql)?:\/\/[^/?#]*)(\/[^?#]*)?(.*)$/s.exec(n);if(e===null)throw new Error(`not a postgres URL: ${n.replace(/\/\/[^@/]*@/,"//")}`);return`${e[1]}/${t}${e[3]}`}a(y,"withDatabase");async function et(n){const t=L(h(),"test-db"),e=a(()=>{try{return k(t,"utf8").trim()||void 0}catch(s){if(s.code==="ENOENT")return;throw s}},"recorded"),r=e();if(r!==void 0)return y(n,r);P(h(),{recursive:!0});const o=`t_${x(8).toString("hex")}`,i=new m.Client({connectionString:y(n,"postgres")});await i.connect();try{if(await i.query(`CREATE DATABASE ${o} TEMPLATE ${G} STRATEGY file_copy`),!M(t,`${o}
3
+ `,384))return await i.query(`DROP DATABASE ${o}`),y(n,e())}finally{await i.end()}return y(n,o)}a(et,"testDatabaseUrl");async function nt(){const n=process.env.CRTR_TEST_PG_URL;if(n)return et(n);if(tt())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 D(h())}a(nt,"canvasDatabaseUrl");const p=new Map,g=new Map;async function v(){N(),U();const n=await nt();g.set(h(),n);let t=p.get(n);return t===void 0&&p.set(n,t=new X(n)),await t.connected(),t}a(v,"openHandle");async function gt(){return(await v()).db}a(gt,"openDb");async function Et(){const n=g.get(h());if(n===void 0)return;g.delete(h());const t=p.get(n);t!==void 0&&(p.delete(n),await t.close())}a(Et,"closeDb");async function St(){const n=[...p.values()];p.clear(),g.clear(),await Promise.all(n.map(t=>t.close()))}a(St,"closeAllDbs");function Tt(n){const t=f.getStore();if(t===void 0||!t.open){queueMicrotask(n);return}t.afterCommit.push(n)}a(Tt,"afterCommit");let rt=0;async function bt(n,t={}){const e=await v(),r=e.db;if(w(e)){if(!t.savepoint&&!t.rollback)return n(r);const c=`canvas_write_${++rt}`;await e.send({text:`SAVEPOINT ${c}`});const l=f.getStore()?.afterCommit,E=l?.length??0;let S;try{S=await n(r)}catch(A){throw await e.send({text:`ROLLBACK TO ${c}; RELEASE ${c}`}),l?.splice(E),A}return await e.send({text:t.rollback?`ROLLBACK TO ${c}; RELEASE ${c}`:`RELEASE ${c}`}),t.rollback&&l?.splice(E),S}const o=await e.lock.acquire(),i=performance.now(),s=new Error("canvas write transaction entered here");let u=!1;const d={handle:e,client:void 0,open:!0,afterCommit:[],statementMs:0};try{const c=await e.connected();await e.sendOn(c,{text:"BEGIN"}),d.client=c;try{const l=await f.run(d,()=>n(r));return await e.sendOn(c,{text:t.rollback?"ROLLBACK":"COMMIT"}),u=!t.rollback,l}catch(l){try{await e.sendOn(c,{text:"ROLLBACK"})}catch{}throw l}}finally{d.open=!1;const c=performance.now()-i;if(c-d.statementMs>V&&console.warn(`[canvas db] write transaction held ${Math.round(c)}ms (${Math.round(d.statementMs)}ms in its statements); its body did non-db work while every other db caller waited
4
+ ${s.stack}`),o(),u)for(const l of d.afterCommit)queueMicrotask(l)}}a(bt,"withCanvasWrite");export{H as CanvasConstraintError,Tt as afterCommit,nt as canvasDatabaseUrl,St as closeAllDbs,Et as closeDb,J as isReadStatement,gt as openDb,bt as withCanvasWrite};
@@ -1,3 +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(["environments","relations","schemas"]),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 _e(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 m;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");m=ie(s);const o=m;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 h={};for(const[e,O]of Object.entries(ae)){const _=new Map;for(const E of(await t.query(`SELECT ${O} AS id, seq FROM ${e}`)).rows)_.set(E.id,Number(E.seq));h[e]=_}const B=h.objects;let M=0;o.exec("BEGIN");for(const e of C){const O=Date.now(),_=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=>_.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 p of $){let w=N[p.name];e==="revision_parents"&&(p.name==="child"||p.name==="parent")?w=x(h.revisions,"revision",e,p.name,w):e==="watch_subscriptions"&&p.name==="watch"?w=x(h.watches,"watch",e,p.name,w):K.has(p.name)&&(w=x(B,"object",e,p.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(),m=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{m?.close()}catch{}for(const o of["","-wal","-shm","-journal"])P(`${s}${o}`,{force:!0});throw l}finally{await t.end().catch(()=>{})}}f(_e,"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),m=u;s==="canvas_meta"&&(m=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)),m!==t&&r.push(`${s}: Postgres ${m} rows, SQLite ${t}`)}if(r.length>0)throw new Error(`Postgres \u2192 SQLite export verification failed:
1
+ var V=Object.defineProperty;var l=(i,a)=>V(i,"name",{value:a,configurable:!0});import{DatabaseSync as W}from"node:sqlite";import{existsSync as Y,mkdirSync as P,mkdtempSync as J,renameSync as X,rmSync as k}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{canvasSearchPathOptions as ee}from"./schema-version.js";import{EXPORT_MARK_KEY as F,IMPORT_MARK_KEY as te,OBJECT_COLUMNS as ne,pgColumns as re,quote as c,TABLE_ORDER as C}from"./import-sqlite.js";const se=5e3,oe=new Set(["environments","relations","schemas","charges","charge_totals","limits"]),ae={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"},ie={objects:"id",revisions:"rev_id",watches:"watch_id"},H=new Set(["daemon_owner","schema_version","schema_min_reader","ledger_migrated",te,F]);function ce(i){const a=J(A(Q(),"crtr-export-")),r=process.env.CRTR_HOME;process.env.CRTR_HOME=a,P(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,k(a,{recursive:!0,force:!0})}}l(ce,"createEmptyDatabase");async function ge(i,a=()=>{}){const r=Z();if(Y(r))throw new Error(`${r} already exists; refusing to overwrite it`);P(A(r,".."),{recursive:!0,mode:448});const s=`${r}.exporting-${process.pid}`,m=Date.now(),t=new j.Client({connectionString:i,application_name:`crtr-export-${process.pid}`,options:ee(),types:{getTypeParser:l(((f,o)=>f===20?d=>Number(d):j.types.getTypeParser(f,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=ce(s);const o=p;o.exec("PRAGMA foreign_keys = OFF;");const d=await re(t),L=[...d.keys()].filter(e=>!oe.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 h={};for(const[e,O]of Object.entries(ie)){const _=new Map;for(const E of(await t.query(`SELECT ${O} AS id, seq FROM ${e}`)).rows)_.set(E.id,Number(E.seq));h[e]=_}const B=h.objects;let M=0;o.exec("BEGIN");for(const e of C){const O=Date.now(),_=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=>_.has(n.name)),K=new Set(ne[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 g=$.map(n=>c(n.name)),U=o.prepare(`INSERT INTO ${c(e)} (${R?["rowid",...g]:g}) VALUES (${(R?["?",...g]:g).map(()=>"?").join(", ")})`),q=G?"seq":ae[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 ${se} FROM ${T}`);if(n.rows.length===0)break;for(const N of n.rows){const D=R?[Number(N._seq)]:[];for(const u of $){let w=N[u.name];e==="revision_parents"&&(u.name==="child"||u.name==="parent")?w=x(h.revisions,"revision",e,u.name,w):e==="watch_subscriptions"&&u.name==="watch"?w=x(h.watches,"watch",e,u.name,w):K.has(u.name)&&(w=x(B,"object",e,u.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 y=o.prepare("PRAGMA foreign_key_check").all();if(y.length>0)throw new Error(`export: ${y.length} foreign key violation(s) in the exported canvas.db, first: ${JSON.stringify(y[0])}`);await me(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()-m,userVersion:S}}catch(f){await t.query("ROLLBACK").catch(()=>{});try{p?.close()}catch{}for(const o of["","-wal","-shm","-journal"])k(`${s}${o}`,{force:!0});throw f}finally{await t.end().catch(()=>{})}}l(ge,"exportPostgresToSqlite");function x(i,a,r,s,m){if(m==null)return null;const t=i.get(m);if(t===void 0)throw new Error(`export: ${r}.${s} references ${a} ${String(m)}, which was not exported (`);return t}l(x,"mapId");async function me(i,a){const r=[];for(const s of C){const m=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=m;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
2
  ${r.join(`
3
- `)}`)}f(ce,"verify");export{_e as exportPostgresToSqlite};
3
+ `)}`)}l(me,"verify");export{ge as exportPostgresToSqlite};
@@ -8,10 +8,12 @@ export declare const EXPORT_MARK_KEY = "exported_to_sqlite";
8
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
9
  /** Columns holding `objects.rowid` in SQLite and `objects.id` in Postgres. */
10
10
  export declare const OBJECT_COLUMNS: Record<string, readonly string[]>;
11
+ /** `optional`: the column may be left out of an INSERT (nullable, or it has a default or is an identity). */
11
12
  export interface PgColumn {
12
13
  name: string;
13
14
  type: string;
14
15
  identity: boolean;
16
+ optional: boolean;
15
17
  }
16
18
  export interface ImportTableStat {
17
19
  table: string;
@@ -1,7 +1,7 @@
1
- var Q=Object.defineProperty;var m=(t,o)=>Q(t,"name",{value:o,configurable:!0});import{DatabaseSync as X}from"node:sqlite";import{createHash as z,randomBytes as Z}from"node:crypto";import{existsSync as O,mkdirSync as ee,renameSync as F}from"node:fs";import{dirname as te}from"node:path";import oe from"pg";import{withExclusiveDirectoryLockAsync as se}from"../exclusive-lock.js";import{DAEMON_STARTUP_WINDOW_MS as re}from"../../daemon/startup-policy.js";import{migrate as ne}from"./migrations.js";import{readLegacyPrivacy as ae}from"../graph/types.js";import{canvasDbPath as B,onDiskMigrationLockPath as ie}from"./paths.js";const q="imported_from_sqlite",U="exported_to_sqlite",M=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"],ce=new Set(["sqlite_sequence","sqlite_stat1","sqlite_stat4","reviews"]),ue={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"]},pe={objects:["schema_type"],nodes:["app"],revisions:["acting_app","acting_for"]},W=new Set(["daemon_owner","schema_version"]),de={canvas_meta:"key",message_sequence:"id"};function H(){return Z(16).toString("hex")}m(H,"newId");function f(t){return`"${t.replace(/"/g,'""')}"`}m(f,"quote");function me(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}`)}}m(me,"arrayType");async function fe(t){const o=await t.query(`SELECT c.relname AS tbl, a.attname AS col, format_type(a.atttypid, a.atttypmod) AS type, a.attidentity <> '' AS identity
1
+ var X=Object.defineProperty;var p=(t,o)=>X(t,"name",{value:o,configurable:!0});import{DatabaseSync as z}from"node:sqlite";import{createHash as Z,randomBytes as ee}from"node:crypto";import{existsSync as O,mkdirSync as te,renameSync as F}from"node:fs";import{dirname as oe}from"node:path";import se from"pg";import{withExclusiveDirectoryLockAsync as re}from"../exclusive-lock.js";import{DAEMON_STARTUP_WINDOW_MS as ne}from"../../daemon/startup-policy.js";import{migrate as ae}from"./migrations.js";import{readLegacyPrivacy as ie}from"../graph/types.js";import{canvasDbPath as B,onDiskMigrationLockPath as ce}from"./paths.js";import{canvasSearchPathOptions as ue}from"./schema-version.js";const C="imported_from_sqlite",U="exported_to_sqlite",M=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"],pe=new Set(["sqlite_sequence","sqlite_stat1","sqlite_stat4","reviews"]),de={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"]},W=new Set(["daemon_owner","schema_version","schema_min_reader","ledger_migrated"]),me={canvas_meta:"key",message_sequence:"id"};function H(){return ee(16).toString("hex")}p(H,"newId");function f(t){return`"${t.replace(/"/g,'""')}"`}p(f,"quote");function fe(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(fe,"arrayType");async function we(t){const o=await t.query(`SELECT c.relname AS tbl, a.attname AS col, format_type(a.atttypid, a.atttypmod) AS type, a.attidentity <> '' AS identity, (NOT a.attnotnull OR a.atthasdef OR a.attidentity <> '') AS optional
2
2
  FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid
3
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 a of o.rows){const r=e.get(a.tbl)??[];r.push({name:a.col,type:a.type,identity:a.identity}),e.set(a.tbl,r)}return e}m(fe,"pgColumns");const we="\uFFFD";function Ee(t){if(!t.includes("\\u0000"))return{text:t,replaced:0};let o;try{o=JSON.parse(t)}catch{return{text:t,replaced:0}}let e=0;const a=m(i=>{const u=i.split("\0");return e+=u.length-1,u.join(we)},"clean"),r=m(i=>typeof i=="string"?a(i):Array.isArray(i)?i.map(r):i!==null&&typeof i=="object"?Object.fromEntries(Object.entries(i).map(([u,l])=>[a(u),r(l)])):i,"walk"),E=r(o);return e===0?{text:t,replaced:e}:{text:JSON.stringify(E),replaced:e}}m(Ee,"scrubNul");async function le(t,o,e,a,r,E,i){for(let u=0;u<a.length;u++)for(const l of e){const n=r[E.indexOf(l)][u];if(n!=null){await t.query("SAVEPOINT import_probe");try{await t.query("SELECT $1::jsonb",[n]),await t.query("RELEASE SAVEPOINT import_probe")}catch(c){return await t.query("ROLLBACK TO SAVEPOINT import_probe"),new Error(`import: ${o}.${l.name} at ${i(a[u])} is not a value jsonb accepts: ${c.message}`,{cause:c})}}}}m(le,"describeJsonbFailure");function j(t,o,e,a,r){if(r==null)return null;const E=t.get(Number(r));if(E===void 0)throw new Error(`import: ${e}.${a} references ${o} rowid ${String(r)}, which does not exist`);return E}m(j,"lookup");async function ye(t,o,e,a,r,E){const i=a.filter(s=>!(pe[e]??[]).includes(s.name)),u=t.prepare(`PRAGMA table_info(${f(e)})`).all(),l=new Set(u.map(s=>s.name)),c=!t.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(e).sql.toUpperCase().includes("WITHOUT ROWID"),_=new Set(ue[e]??[]),h=[];for(const s of i)if(s.name==="seq"&&!l.has("seq")){if(!c)throw new Error(`import: ${e} needs seq from rowid but has none`);h.push("rowid AS seq")}else if(s.name==="watch_id"||s.name==="subscription_id")h.push("NULL AS "+f(s.name));else if(l.has(s.name))h.push(f(s.name));else throw new Error(`import: SQLite table ${e} has no column ${s.name}`);c&&h.push("rowid AS _rowid");const S=e==="canvas_meta"?` WHERE key NOT IN (${[...W].map(s=>`'${s}'`).join(",")})`:"",y=t.prepare(`SELECT ${h.join(", ")} FROM ${f(e)}${S}${c?" ORDER BY rowid":""}`),$=i.map(s=>f(s.name)),g=i.map((s,p)=>`$${p+1}::${me(s.type)}`),T=i.map((s,p)=>`c${p}`),A=i.map((s,p)=>s.type==="jsonb"?`c${p}::jsonb`:`c${p}`);let v=`INSERT INTO ${f(e)} (${$.join(", ")}) SELECT ${A.join(", ")} FROM unnest(${g.join(", ")}) AS u(${T.join(", ")})`;const N=de[e];if(N!==void 0){const s=i.filter(p=>p.name!==N).map(p=>`${f(p.name)} = EXCLUDED.${f(p.name)}`);v+=` ON CONFLICT (${f(N)}) DO UPDATE SET ${s.join(", ")}`}const I=e==="objects",D=i.filter(s=>s.type==="jsonb"),k=c?"_rowid":i[0].name,V=m(s=>`${c?"rowid":k} ${String(s[k])}`,"key");let x=0,b=[];const P=m(async()=>{if(b.length===0)return;const s=i.map(()=>new Array(b.length));if(b.forEach((p,R)=>{i.forEach((d,J)=>{let w=p[d.name];if(e==="objects"&&d.name==="privacy"?w=ae(w):d.name==="watch_id"||d.name==="subscription_id"?w=H():e==="revision_parents"&&(d.name==="child"||d.name==="parent")?w=j(r.revisions,"revision",e,d.name,w):e==="watch_subscriptions"&&d.name==="watch"?w=j(r.watches,"watch",e,d.name,w):_.has(d.name)&&(w=I?null:j(r.objects,"object",e,d.name,w)),d.type==="bytea"&&w instanceof Uint8Array&&!Buffer.isBuffer(w)&&(w=Buffer.from(w.buffer,w.byteOffset,w.byteLength)),d.type==="jsonb"&&typeof w=="string"){const C=Ee(w);C.replaced>0&&(w=C.text,E.set(`${e}.${d.name}`,(E.get(`${e}.${d.name}`)??0)+C.replaced))}s[J][R]=w??null}),e==="watches"&&r.watches.set(Number(p._rowid),s[i.findIndex(d=>d.name==="watch_id")][R])}),D.length===0)await o.query(v,s);else{await o.query("SAVEPOINT import_batch");try{await o.query(v,s)}catch(p){throw await o.query("ROLLBACK TO SAVEPOINT import_batch"),await le(o,e,D,b,s,i,V)??p}await o.query("RELEASE SAVEPOINT import_batch")}x+=b.length,b=[]},"flush");for(const s of y.iterate())e==="objects"&&r.objects.set(Number(s._rowid),s.id),e==="revisions"&&r.revisions.set(Number(s._rowid),s.rev_id),b.push(s),b.length>=M&&await P();if(await P(),I){const s=t.prepare("SELECT id, owner FROM objects WHERE owner IS NOT NULL").all();for(let p=0;p<s.length;p+=M){const R=s.slice(p,p+M);await o.query("UPDATE objects o SET owner = v.owner FROM unnest($1::text[], $2::text[]) AS v(id, owner) WHERE o.id = v.id",[R.map(d=>d.id),R.map(d=>j(r.objects,"object","objects","owner",d.owner))])}}return x}m(ye,"copyTable");async function _e(t,o){for(const[e,a]of o)for(const r of a)r.identity&&await t.query(`SELECT setval(pg_get_serial_sequence($1, $2), COALESCE((SELECT max(${f(r.name)}) FROM ${f(e)}), 1), (SELECT max(${f(r.name)}) IS NOT NULL FROM ${f(e)}))`,[f(e),r.name])}m(_e,"advanceIdentities");function G(t,o,e=""){return Number(t.prepare(`SELECT count(*) AS n FROM ${f(o)}${e}`).get().n)}m(G,"sqliteCount");async function he(t,o,e){const a=[];for(const n of L){if(n==="canvas_meta")continue;const c=G(t,n),_=Number((await o.query(`SELECT count(*) AS n FROM ${f(n)}`)).rows[0].n);c!==_&&a.push(`${n}: SQLite ${c} rows, Postgres ${_}`)}for(const n of t.prepare(`SELECT key, value FROM canvas_meta WHERE key NOT IN (${[...W].map(c=>`'${c}'`).join(",")})`).all())(await o.query("SELECT value FROM canvas_meta WHERE key = $1",[n.key])).rows[0]?.value!==n.value&&a.push(`canvas_meta ${n.key}: value differs`);const r=m(n=>{const c=z("sha256");for(const _ of n)c.update(_).update(`
5
- `);return c.digest("hex")},"hash"),E=t.prepare("SELECT id FROM objects ORDER BY rowid").all().map(n=>n.id),i=(await o.query("SELECT id FROM objects ORDER BY seq")).rows.map(n=>n.id);r(E)!==r(i)&&a.push("objects: ids in insertion order differ");const u=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(),l=(await o.query("SELECT id, owner FROM objects WHERE owner IS NOT NULL ORDER BY seq")).rows;r(u.map(n=>`${n.id}>${n.owner}`))!==r(l.map(n=>`${n.id}>${n.owner}`))&&a.push("objects: owner links differ");for(const[n,c]of[["run_events","body_json"],["messages","body_json"],["deliveries","last_failure_json"]]){const _=Number(t.prepare(`SELECT COALESCE(sum(length(CAST(${f(c)} AS TEXT))), 0) AS n FROM ${f(n)}`).get().n),h=e.get(n).find($=>$.name===c).type,S=h==="jsonb"?`${f(c)}::text`:f(c);if(h==="jsonb")continue;const y=Number((await o.query(`SELECT COALESCE(sum(length(${S})), 0)::bigint AS n FROM ${f(n)}`)).rows[0].n);_!==y&&a.push(`${n}.${c}: total length SQLite ${_}, Postgres ${y}`)}if(a.length>0)throw new Error(`SQLite \u2192 Postgres import verification failed (rolled back; canvas.db untouched):
4
+ ORDER BY c.relname, a.attnum`),e=new Map;for(const a of o.rows){const n=e.get(a.tbl)??[];n.push({name:a.col,type:a.type,identity:a.identity,optional:a.optional}),e.set(a.tbl,n)}return e}p(we,"pgColumns");const le="\uFFFD";function Ee(t){if(!t.includes("\\u0000"))return{text:t,replaced:0};let o;try{o=JSON.parse(t)}catch{return{text:t,replaced:0}}let e=0;const a=p(c=>{const i=c.split("\0");return e+=i.length-1,i.join(le)},"clean"),n=p(c=>typeof c=="string"?a(c):Array.isArray(c)?c.map(n):c!==null&&typeof c=="object"?Object.fromEntries(Object.entries(c).map(([i,_])=>[a(i),n(_)])):c,"walk"),l=n(o);return e===0?{text:t,replaced:e}:{text:JSON.stringify(l),replaced:e}}p(Ee,"scrubNul");async function ye(t,o,e,a,n,l,c){for(let i=0;i<a.length;i++)for(const _ of e){const s=n[l.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(u){return await t.query("ROLLBACK TO SAVEPOINT import_probe"),new Error(`import: ${o}.${_.name} at ${c(a[i])} is not a value jsonb accepts: ${u.message}`,{cause:u})}}}}p(ye,"describeJsonbFailure");function j(t,o,e,a,n){if(n==null)return null;const l=t.get(Number(n));if(l===void 0)throw new Error(`import: ${e}.${a} references ${o} rowid ${String(n)}, which does not exist`);return l}p(j,"lookup");async function _e(t,o,e,a,n,l){const c=t.prepare(`PRAGMA table_info(${f(e)})`).all(),i=new Set(c.map(r=>r.name)),_=p(r=>r==="seq"||r==="watch_id"||r==="subscription_id","minted"),s=a.filter(r=>i.has(r.name)||_(r.name)||!r.optional),E=!t.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(e).sql.toUpperCase().includes("WITHOUT ROWID"),b=new Set(de[e]??[]),$=[];for(const r of s)if(r.name==="seq"&&!i.has("seq")){if(!E)throw new Error(`import: ${e} needs seq from rowid but has none`);$.push("rowid AS seq")}else if(r.name==="watch_id"||r.name==="subscription_id")$.push("NULL AS "+f(r.name));else if(i.has(r.name))$.push(f(r.name));else throw new Error(`import: SQLite table ${e} has no column ${r.name}`);E&&$.push("rowid AS _rowid");const y=e==="canvas_meta"?` WHERE key NOT IN (${[...W].map(r=>`'${r}'`).join(",")})`:"",h=t.prepare(`SELECT ${$.join(", ")} FROM ${f(e)}${y}${E?" ORDER BY rowid":""}`),R=s.map(r=>f(r.name)),T=s.map((r,d)=>`$${d+1}::${fe(r.type)}`),A=s.map((r,d)=>`c${d}`),V=s.map((r,d)=>r.type==="jsonb"?`c${d}::jsonb`:`c${d}`);let v=`INSERT INTO ${f(e)} (${R.join(", ")}) SELECT ${V.join(", ")} FROM unnest(${T.join(", ")}) AS u(${A.join(", ")})`;const N=me[e];if(N!==void 0){const r=s.filter(d=>d.name!==N).map(d=>`${f(d.name)} = EXCLUDED.${f(d.name)}`);v+=` ON CONFLICT (${f(N)}) DO UPDATE SET ${r.join(", ")}`}const I=e==="objects",D=s.filter(r=>r.type==="jsonb"),k=E?"_rowid":s[0].name,J=p(r=>`${E?"rowid":k} ${String(r[k])}`,"key");let x=0,S=[];const P=p(async()=>{if(S.length===0)return;const r=s.map(()=>new Array(S.length));if(S.forEach((d,g)=>{s.forEach((m,Q)=>{let w=d[m.name];if(e==="objects"&&m.name==="privacy"?w=ie(w):m.name==="watch_id"||m.name==="subscription_id"?w=H():e==="revision_parents"&&(m.name==="child"||m.name==="parent")?w=j(n.revisions,"revision",e,m.name,w):e==="watch_subscriptions"&&m.name==="watch"?w=j(n.watches,"watch",e,m.name,w):b.has(m.name)&&(w=I?null:j(n.objects,"object",e,m.name,w)),m.type==="bytea"&&w instanceof Uint8Array&&!Buffer.isBuffer(w)&&(w=Buffer.from(w.buffer,w.byteOffset,w.byteLength)),m.type==="jsonb"&&typeof w=="string"){const q=Ee(w);q.replaced>0&&(w=q.text,l.set(`${e}.${m.name}`,(l.get(`${e}.${m.name}`)??0)+q.replaced))}r[Q][g]=w??null}),e==="watches"&&n.watches.set(Number(d._rowid),r[s.findIndex(m=>m.name==="watch_id")][g])}),D.length===0)await o.query(v,r);else{await o.query("SAVEPOINT import_batch");try{await o.query(v,r)}catch(d){throw await o.query("ROLLBACK TO SAVEPOINT import_batch"),await ye(o,e,D,S,r,s,J)??d}await o.query("RELEASE SAVEPOINT import_batch")}x+=S.length,S=[]},"flush");for(const r of h.iterate())e==="objects"&&n.objects.set(Number(r._rowid),r.id),e==="revisions"&&n.revisions.set(Number(r._rowid),r.rev_id),S.push(r),S.length>=M&&await P();if(await P(),I){const r=t.prepare("SELECT id, owner FROM objects WHERE owner IS NOT NULL").all();for(let d=0;d<r.length;d+=M){const g=r.slice(d,d+M);await o.query("UPDATE objects o SET owner = v.owner FROM unnest($1::text[], $2::text[]) AS v(id, owner) WHERE o.id = v.id",[g.map(m=>m.id),g.map(m=>j(n.objects,"object","objects","owner",m.owner))])}}return x}p(_e,"copyTable");async function he(t,o){for(const[e,a]of o)for(const n of a)n.identity&&await t.query(`SELECT setval(pg_get_serial_sequence($1, $2), COALESCE((SELECT max(${f(n.name)}) FROM ${f(e)}), 1), (SELECT max(${f(n.name)}) IS NOT NULL FROM ${f(e)}))`,[f(e),n.name])}p(he,"advanceIdentities");function G(t,o,e=""){return Number(t.prepare(`SELECT count(*) AS n FROM ${f(o)}${e}`).get().n)}p(G,"sqliteCount");async function $e(t,o,e){const a=[];for(const s of L){if(s==="canvas_meta")continue;const u=G(t,s),E=Number((await o.query(`SELECT count(*) AS n FROM ${f(s)}`)).rows[0].n);u!==E&&a.push(`${s}: SQLite ${u} rows, Postgres ${E}`)}for(const s of t.prepare(`SELECT key, value FROM canvas_meta WHERE key NOT IN (${[...W].map(u=>`'${u}'`).join(",")})`).all())(await o.query("SELECT value FROM canvas_meta WHERE key = $1",[s.key])).rows[0]?.value!==s.value&&a.push(`canvas_meta ${s.key}: value differs`);const n=p(s=>{const u=Z("sha256");for(const E of s)u.update(E).update(`
5
+ `);return u.digest("hex")},"hash"),l=t.prepare("SELECT id FROM objects ORDER BY rowid").all().map(s=>s.id),c=(await o.query("SELECT id FROM objects ORDER BY seq")).rows.map(s=>s.id);n(l)!==n(c)&&a.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 o.query("SELECT id, owner FROM objects WHERE owner IS NOT NULL ORDER BY seq")).rows;n(i.map(s=>`${s.id}>${s.owner}`))!==n(_.map(s=>`${s.id}>${s.owner}`))&&a.push("objects: owner links differ");for(const[s,u]of[["run_events","body_json"],["messages","body_json"],["deliveries","last_failure_json"]]){const E=Number(t.prepare(`SELECT COALESCE(sum(length(CAST(${f(u)} AS TEXT))), 0) AS n FROM ${f(s)}`).get().n),b=e.get(s).find(h=>h.name===u).type,$=b==="jsonb"?`${f(u)}::text`:f(u);if(b==="jsonb")continue;const y=Number((await o.query(`SELECT COALESCE(sum(length(${$})), 0)::bigint AS n FROM ${f(s)}`)).rows[0].n);E!==y&&a.push(`${s}.${u}: total length SQLite ${E}, Postgres ${y}`)}if(a.length>0)throw new Error(`SQLite \u2192 Postgres import verification failed (rolled back; canvas.db untouched):
6
6
  ${a.join(`
7
- `)}`)}m(he,"verify");async function $e(t,o,e={}){const a=e.log??(()=>{}),r=Date.now(),E=await fe(o),i=t.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map(y=>y.name),u=i.filter(y=>!ce.has(y)&&!L.includes(y));if(u.length>0)throw new Error(`import: SQLite tables with no copy rule: ${u.join(", ")}`);const l=L.filter(y=>!E.has(y));if(l.length>0)throw new Error(`import: Postgres tables missing from the schema: ${l.join(", ")}`);const n=i.includes("reviews")?G(t,"reviews"):0;if(n>0)throw new Error(`import: the legacy reviews table still holds ${n} rows after the SQLite migration chain`);const c={objects:new Map,revisions:new Map,watches:new Map},_=[],h=new Map;let S=0;await o.query("BEGIN");try{await o.query("ALTER TABLE nodes DISABLE TRIGGER USER"),await o.query("ALTER TABLE deliveries DISABLE TRIGGER USER");for(const $ of L){const g=Date.now(),T=await ye(t,o,$,E.get($),c,h),A=Date.now()-g;_.push({table:$,rows:T,ms:A}),a(`import ${$}: ${T} rows in ${A} ms`)}await o.query("ALTER TABLE nodes ENABLE TRIGGER USER"),await o.query("ALTER TABLE deliveries ENABLE TRIGGER USER"),await _e(o,E);const y=Date.now();await he(t,o,E),S=Date.now()-y,a(`import verify: ${S} ms`),await o.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2)",[q,JSON.stringify({at:new Date().toISOString(),rows:_.reduce(($,g)=>$+g.rows,0)})]),await o.query("COMMIT")}catch(y){throw await o.query("ROLLBACK").catch(()=>{}),y}return h.size>0&&a(`import: replaced NUL (\\u0000) with U+FFFD in jsonb values: ${[...h].map(([y,$])=>`${y} ${$}`).join(", ")}`),{tables:_,verifyMs:S,totalMs:Date.now()-r,nulReplaced:Object.fromEntries(h)}}m($e,"copySqliteToPostgres");const Y=["","-wal","-shm"];function K(t=B()){if(O(`${t}.imported`)){const o=`${t}.imported.${Date.now()}`;for(const e of Y)O(`${t}.imported${e}`)&&F(`${t}.imported${e}`,`${o}${e}`)}for(const o of Y)O(`${t}${o}`)&&F(`${t}${o}`,`${t}.imported${o}`)}m(K,"renameImportedDatabase");async function Ce(t,o=()=>{}){const e=B();if(!O(e))return null;const a=ie();return ee(te(a),{recursive:!0,mode:448}),se(a,async()=>{if(!O(e))return null;const r=new oe.Client({connectionString:t,application_name:`crtr-import-${process.pid}`});await r.connect();try{const E=await r.query("SELECT key FROM canvas_meta WHERE key = ANY($1)",[[q,U]]),i=new Set(E.rows.map(n=>n.key));if(i.has(q)){if(i.has(U))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 K(e),o(`import: finished the rename of ${e} (already imported)`),null}const u=new X(e);let l;try{u.exec("PRAGMA busy_timeout = 5000;"),u.exec("PRAGMA foreign_keys = ON;"),ne(u),l=await $e(u,r,{log:o})}finally{u.close()}return K(e),o(`import: ${l.tables.reduce((n,c)=>n+c.rows,0)} rows in ${l.totalMs} ms; canvas.db renamed to canvas.db.imported`),l}finally{await r.end().catch(()=>{})}},{timeoutMs:re,timeoutError:m(()=>new Error(`timed out waiting for the canvas import lock: ${a}`),"timeoutError")})}m(Ce,"importSqliteCanvasIfPresent");export{U as EXPORT_MARK_KEY,q as IMPORT_MARK_KEY,ue as OBJECT_COLUMNS,L as TABLE_ORDER,$e as copySqliteToPostgres,Ce as importSqliteCanvasIfPresent,fe as pgColumns,f as quote,K as renameImportedDatabase};
7
+ `)}`)}p($e,"verify");async function be(t,o,e={}){const a=e.log??(()=>{}),n=Date.now(),l=await we(o),c=t.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map(y=>y.name),i=c.filter(y=>!pe.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=>!l.has(y));if(_.length>0)throw new Error(`import: Postgres tables missing from the schema: ${_.join(", ")}`);const s=c.includes("reviews")?G(t,"reviews"):0;if(s>0)throw new Error(`import: the legacy reviews table still holds ${s} rows after the SQLite migration chain`);const u={objects:new Map,revisions:new Map,watches:new Map},E=[],b=new Map;let $=0;await o.query("BEGIN");try{await o.query("ALTER TABLE nodes DISABLE TRIGGER USER"),await o.query("ALTER TABLE deliveries DISABLE TRIGGER USER");for(const h of L){const R=Date.now(),T=await _e(t,o,h,l.get(h),u,b),A=Date.now()-R;E.push({table:h,rows:T,ms:A}),a(`import ${h}: ${T} rows in ${A} ms`)}await o.query("ALTER TABLE nodes ENABLE TRIGGER USER"),await o.query("ALTER TABLE deliveries ENABLE TRIGGER USER"),await he(o,l);const y=Date.now();await $e(t,o,l),$=Date.now()-y,a(`import verify: ${$} ms`),await o.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2)",[C,JSON.stringify({at:new Date().toISOString(),rows:E.reduce((h,R)=>h+R.rows,0)})]),await o.query("COMMIT")}catch(y){throw await o.query("ROLLBACK").catch(()=>{}),y}return b.size>0&&a(`import: replaced NUL (\\u0000) with U+FFFD in jsonb values: ${[...b].map(([y,h])=>`${y} ${h}`).join(", ")}`),{tables:E,verifyMs:$,totalMs:Date.now()-n,nulReplaced:Object.fromEntries(b)}}p(be,"copySqliteToPostgres");const Y=["","-wal","-shm"];function K(t=B()){if(O(`${t}.imported`)){const o=`${t}.imported.${Date.now()}`;for(const e of Y)O(`${t}.imported${e}`)&&F(`${t}.imported${e}`,`${o}${e}`)}for(const o of Y)O(`${t}${o}`)&&F(`${t}${o}`,`${t}.imported${o}`)}p(K,"renameImportedDatabase");async function Me(t,o=()=>{}){const e=B();if(!O(e))return null;const a=ce();return te(oe(a),{recursive:!0,mode:448}),re(a,async()=>{if(!O(e))return null;const n=new se.Client({connectionString:t,application_name:`crtr-import-${process.pid}`,options:ue()});await n.connect();try{const l=await n.query("SELECT key FROM canvas_meta WHERE key = ANY($1)",[[C,U]]),c=new Set(l.rows.map(s=>s.key));if(c.has(C)){if(c.has(U))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 K(e),o(`import: finished the rename of ${e} (already imported)`),null}const i=new z(e);let _;try{i.exec("PRAGMA busy_timeout = 5000;"),i.exec("PRAGMA foreign_keys = ON;"),ae(i),_=await be(i,n,{log:o})}finally{i.close()}return K(e),o(`import: ${_.tables.reduce((s,u)=>s+u.rows,0)} rows in ${_.totalMs} ms; canvas.db renamed to canvas.db.imported`),_}finally{await n.end().catch(()=>{})}},{timeoutMs:ne,timeoutError:p(()=>new Error(`timed out waiting for the canvas import lock: ${a}`),"timeoutError")})}p(Me,"importSqliteCanvasIfPresent");export{U as EXPORT_MARK_KEY,C as IMPORT_MARK_KEY,de as OBJECT_COLUMNS,L as TABLE_ORDER,be as copySqliteToPostgres,Me as importSqliteCanvasIfPresent,we as pgColumns,f as quote,K as renameImportedDatabase};
@@ -1,20 +1,11 @@
1
- var v=Object.defineProperty;var o=(e,n)=>v(e,"name",{value:n,configurable:!0});import{randomBytes as U}from"node:crypto";import{CrtrError as T}from"../errors.js";import{readConfig as D}from"../config.js";import{joinedOwner as f}from"../directory/join.js";import{installLocalTerminalGrant as P}from"../grants/local.js";import{dropLocalGrantMirror as k}from"../grants/mirror.js";import{ownerForGrantee as H,ensureOwner as W}from"../graph/objects.js";import{canvasDatabaseUrl as C,CanvasConstraintError as x,withCanvasWrite as S}from"./db.js";import{LEGACY_LOCAL_PERSON as p,LEGACY_LOCAL_TERMINAL_GRANT as b}from"./legacy-person.js";const E="person_id",h="local_person_retired",M=/^user:[A-Za-z0-9._-]+$/,O="abcdefghijklmnopqrstuvwxyz0123456789";class m extends Error{static{o(this,"PersonUnresolvedError")}code="person_unresolved";constructor(){super("the install's person is not resolved yet"),this.name="PersonUnresolvedError"}}let d;const j=new Map;function $(e){d=e}o($,"setPersonId");function ae(){if(d===void 0)throw new m;return d}o(ae,"personId");async function se(){if(d!==void 0)return d;const e=await C(),n=j.get(e);if(n!==void 0)return n;const t=await G();if(t===null)throw new m;return j.set(e,t),t}o(se,"personIdAsync");function F(){const e=U(12);let n="user:";for(let t=0;t<12;t++)n+=O[e[t]%O.length];return n}o(F,"mintPersonId");async function N(e,n){return(await e.prepare("SELECT value FROM canvas_meta WHERE key = ?").get(n))?.value??null}o(N,"readMeta");async function l(e,n,t){await e.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(n,t)}o(l,"writeMeta");async function q(){return await S(async e=>{const n=await N(e,E);if(n!==null)return{id:n,source:"stored"};const t=f();if(t!==null)return await l(e,E,t),{id:t,source:"joined"};if(D("user").runtime?.issuer){const a=await e.prepare("SELECT DISTINCT sub FROM grants").all();return a.length!==1?null:(await l(e,E,a[0].sub),{id:a[0].sub,source:"grants"})}const r=F();return await l(e,E,r),{id:r,source:"minted"}})}o(q,"resolvePerson");async function G(){return(await q())?.id??null}o(G,"resolvePersonId");const s=o(e=>Number(e.changes),"changes"),J=[["nodes","sub"],["node_tokens","sub"],["grants","sub"],["crons","sub"],["audit_rows","sub"],["provider_approvals","sub"],["audit_rows","grantee"],["run_events","grantee"],["node_tokens","grantee"],["uploads","grantee"],["provider_receipts","grantee"],["provider_approvals","grantee"]];function I(e,n){if(!M.test(n))throw new T("invalid_person",`${e} must be a user:<id> principal, got ${JSON.stringify(n)}`)}o(I,"assertPerson");async function L(e,n,t){if(I("from",n),I("to",t),n===t)throw new T("invalid_person","cannot substitute a person with itself");const r={};try{r.identities=s(await e.prepare("UPDATE identities SET scheme = 'user', external_id = ? WHERE scheme IN ('local-user', 'user') AND external_id = ?").run(t,n))}catch(i){throw i instanceof x?new T("person_conflict",`another person object already holds ${t}; merging two person objects is not supported (${i.message})`,void 0,{from:n,to:t}):i}r["daemon_actions.principal_sub"]=s(await e.prepare(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,sub}', to_jsonb(?::text))::text
1
+ var U=Object.defineProperty;var i=(e,n)=>U(e,"name",{value:n,configurable:!0});import{randomBytes as L}from"node:crypto";import{CrtrError as T}from"../errors.js";import{readConfig as j}from"../config.js";import{joinedOwner as D}from"../directory/join.js";import{installLocalTerminalGrant as b}from"../grants/local.js";import{dropLocalGrantMirror as v}from"../grants/mirror.js";import{ownerForGrantee as H,ensureOwner as W}from"../graph/objects.js";import{canvasDatabaseUrl as $,CanvasConstraintError as k,withCanvasWrite as R}from"./db.js";import{LEGACY_LOCAL_PERSON as u,LEGACY_LOCAL_TERMINAL_GRANT as C}from"./legacy-person.js";import{rebuildTotalsFor as x}from"../ledger/totals.js";const E="person_id",h="local_person_retired",M=/^user:[A-Za-z0-9._-]+$/,I="abcdefghijklmnopqrstuvwxyz0123456789";class f extends Error{static{i(this,"PersonUnresolvedError")}code="person_unresolved";constructor(){super("the install's person is not resolved yet"),this.name="PersonUnresolvedError"}}let l;const S=new Map;function F(e){l=e}i(F,"setPersonId");function ie(){if(l===void 0)throw new f;return l}i(ie,"personId");async function ce(){if(l!==void 0)return l;const e=await $(),n=S.get(e);if(n!==void 0)return n;const r=await Y();if(r===null)throw new f;return S.set(e,r),r}i(ce,"personIdAsync");function G(){const e=L(12);let n="user:";for(let r=0;r<12;r++)n+=I[e[r]%I.length];return n}i(G,"mintPersonId");async function N(e,n){return(await e.prepare("SELECT value FROM canvas_meta WHERE key = ?").get(n))?.value??null}i(N,"readMeta");async function d(e,n,r){await e.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(n,r)}i(d,"writeMeta");async function J(){return await R(async e=>{const n=await N(e,E);if(n!==null)return{id:n,source:"stored"};const r=D();if(r!==null)return await d(e,E,r),{id:r,source:"joined"};if(j("user").runtime?.issuer){const a=await e.prepare("SELECT DISTINCT sub FROM grants").all();return a.length!==1?null:(await d(e,E,a[0].sub),{id:a[0].sub,source:"grants"})}const t=G();return await d(e,E,t),{id:t,source:"minted"}})}i(J,"resolvePerson");async function Y(){return(await J())?.id??null}i(Y,"resolvePersonId");const s=i(e=>Number(e.changes),"changes"),z=[["nodes","sub"],["node_tokens","sub"],["grants","sub"],["crons","sub"],["audit_rows","sub"],["provider_approvals","sub"],["audit_rows","grantee"],["run_events","grantee"],["node_tokens","grantee"],["uploads","grantee"],["provider_approvals","grantee"]],B=[["charges","sub"],["charges","payer"],["charges","grantee"],["limits","payer"],["limits","sub"],["limits","grantee"]];function O(e,n){if(!M.test(n))throw new T("invalid_person",`${e} must be a user:<id> principal, got ${JSON.stringify(n)}`)}i(O,"assertPerson");async function P(e,n,r){if(O("from",n),O("to",r),n===r)throw new T("invalid_person","cannot substitute a person with itself");const t={};try{t.identities=s(await e.prepare("UPDATE identities SET scheme = 'user', external_id = ? WHERE scheme IN ('local-user', 'user') AND external_id = ?").run(r,n))}catch(o){throw o instanceof k?new T("person_conflict",`another person object already holds ${r}; merging two person objects is not supported (${o.message})`,void 0,{from:n,to:r}):o}t["daemon_actions.principal_sub"]=s(await e.prepare(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,sub}', to_jsonb(?::text))::text
2
2
  WHERE state IN ('pending', 'handed_off') AND adapter_json::jsonb ->> 'adapter' = 'cron_spawn'
3
- AND adapter_json::jsonb #>> '{principal,sub}' = ?`).run(t,n));for(const[i,c]of J)r[`${i}.${c}`]=s(await e.prepareUncached(`UPDATE ${i} SET ${c} = ? WHERE ${c} = ?`).run(t,n));r["idempotency_keys.grantee"]=s(await e.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = ?)").run(n,t))+s(await e.prepare("UPDATE idempotency_keys SET grantee = ? WHERE grantee = ?").run(t,n));const a=n.replace(/^user:/,""),u=t.replace(/^user:/,"");return r["relations.subject_id"]=s(await e.prepare(`DELETE FROM relations r WHERE r.subject_type = 'user' AND r.subject_id = ?
3
+ AND adapter_json::jsonb #>> '{principal,sub}' = ?`).run(r,n));for(const[o,c]of z)t[`${o}.${c}`]=s(await e.prepareUncached(`UPDATE ${o} SET ${c} = ? WHERE ${c} = ?`).run(r,n));t["idempotency_keys.grantee"]=s(await e.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = ?)").run(n,r))+s(await e.prepare("UPDATE idempotency_keys SET grantee = ? WHERE grantee = ?").run(r,n));const a=n.replace(/^user:/,""),p=r.replace(/^user:/,"");t["relations.subject_id"]=s(await e.prepare(`DELETE FROM relations r WHERE r.subject_type = 'user' AND r.subject_id = ?
4
4
  AND EXISTS (SELECT 1 FROM relations n WHERE n.subject_type = 'user' AND n.subject_id = ? AND n.resource_type = r.resource_type
5
- AND n.resource_id = r.resource_id AND n.relation = r.relation AND n.subject_relation = r.subject_relation)`).run(a,u))+s(await e.prepare("UPDATE relations SET subject_id = ? WHERE subject_type = 'user' AND subject_id = ?").run(u,a)),r["relations.written_by"]=s(await e.prepare("UPDATE relations SET written_by = ? WHERE written_by = ?").run(t,n)),r["revisions.acting_for"]=s(await e.prepare("UPDATE revisions SET acting_for = ? WHERE acting_for = ?").run(t,n)),await e.prepare(`INSERT INTO grantee_usage (grantee, month, usd_equivalent)
6
- SELECT ?::text, month, usd_equivalent FROM grantee_usage WHERE grantee = ?
7
- ON CONFLICT(grantee, month) DO UPDATE SET usd_equivalent = grantee_usage.usd_equivalent + excluded.usd_equivalent`).run(t,n),r["grantee_usage.grantee"]=s(await e.prepare("DELETE FROM grantee_usage WHERE grantee = ?").run(n)),await e.prepare(`INSERT INTO usage (grantee, month, payer, source, served_model, input_tokens, output_tokens, cached_tokens, usd_equivalent)
8
- SELECT ?::text, month, payer, source, served_model, input_tokens, output_tokens, cached_tokens, usd_equivalent FROM usage WHERE grantee = ?
9
- ON CONFLICT(grantee, month, payer, source, served_model) DO UPDATE SET
10
- input_tokens = usage.input_tokens + excluded.input_tokens,
11
- output_tokens = usage.output_tokens + excluded.output_tokens,
12
- cached_tokens = usage.cached_tokens + excluded.cached_tokens,
13
- usd_equivalent = CASE WHEN usage.usd_equivalent IS NULL AND excluded.usd_equivalent IS NULL THEN NULL
14
- ELSE COALESCE(usage.usd_equivalent, 0) + COALESCE(excluded.usd_equivalent, 0) END`).run(t,n),r["usage.grantee"]=s(await e.prepare("DELETE FROM usage WHERE grantee = ?").run(n)),r}o(L,"substitutePerson");async function Y(e){const n=`SELECT o.id FROM objects o JOIN identities i ON i.object = o.owner JOIN nodes n ON n.node_id = o.id
15
- WHERE o.type = 'node' AND i.scheme = 'local-user' AND i.external_id = '${p}'`;return(await e.prepareUncached(`${n} LIMIT 1`).get()!==void 0?s(await e.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${n})`).run((await H(e,"app:terminal")).id)):0)+s(await e.prepare("UPDATE node_tokens SET grantee = 'app:terminal' WHERE grantee = ?").run(p))+s(await e.prepare("UPDATE run_events SET grantee = 'app:terminal' WHERE grantee = ?").run(p))+s(await e.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = 'app:terminal')").run(p))+s(await e.prepare("UPDATE idempotency_keys SET grantee = 'app:terminal' WHERE grantee = ?").run(p))}o(Y,"rewriteLegacyGrantees");async function z(e,n,t){const r=[b,`terminal:${n}`].filter(c=>c!==t),a=r.map(()=>"?").join(", "),u={};u["nodes.grant_id"]=s(await e.prepareUncached(`UPDATE nodes SET grant_id = ? WHERE grant_id IN (${a})`).run(t,...r)),u["node_tokens.grant"]=s(await e.prepareUncached(`UPDATE node_tokens SET "grant" = ? WHERE "grant" IN (${a})`).run(t,...r)),u["crons.grant_id"]=s(await e.prepareUncached(`UPDATE crons SET grant_id = ? WHERE grant_id IN (${a})`).run(t,...r));const i=t===null?"'null'::jsonb":"to_jsonb(?::text)";return u["daemon_actions.principal_grant_id"]=s(await e.prepareUncached(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,grant_id}', ${i})::text
5
+ AND n.resource_id = r.resource_id AND n.relation = r.relation AND n.subject_relation = r.subject_relation)`).run(a,p))+s(await e.prepare("UPDATE relations SET subject_id = ? WHERE subject_type = 'user' AND subject_id = ?").run(p,a)),t["relations.written_by"]=s(await e.prepare("UPDATE relations SET written_by = ? WHERE written_by = ?").run(r,n)),t["revisions.acting_for"]=s(await e.prepare("UPDATE revisions SET acting_for = ? WHERE acting_for = ?").run(r,n));for(const[o,c]of B)t[`${o}.${c}`]=s(await e.prepareUncached(`UPDATE ${o} SET ${c} = ? WHERE ${c} = ?`).run(r,n));return Object.entries(t).some(([o,c])=>o.startsWith("charges.")&&c!==0)&&(await x(e,n,r),t["charge_totals.rebuilt"]=1),t}i(P,"substitutePerson");async function K(e){const n=`SELECT o.id FROM objects o JOIN identities i ON i.object = o.owner JOIN nodes n ON n.node_id = o.id
6
+ WHERE o.type = 'node' AND i.scheme = 'local-user' AND i.external_id = '${u}'`;return(await e.prepareUncached(`${n} LIMIT 1`).get()!==void 0?s(await e.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${n})`).run((await H(e,"app:terminal")).id)):0)+s(await e.prepare("UPDATE node_tokens SET grantee = 'app:terminal' WHERE grantee = ?").run(u))+s(await e.prepare("UPDATE run_events SET grantee = 'app:terminal' WHERE grantee = ?").run(u))+s(await e.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key IN (SELECT key FROM idempotency_keys WHERE grantee = 'app:terminal')").run(u))+s(await e.prepare("UPDATE idempotency_keys SET grantee = 'app:terminal' WHERE grantee = ?").run(u))}i(K,"rewriteLegacyGrantees");async function q(e,n,r){const t=[C,`terminal:${n}`].filter(c=>c!==r),a=t.map(()=>"?").join(", "),p={};p["nodes.grant_id"]=s(await e.prepareUncached(`UPDATE nodes SET grant_id = ? WHERE grant_id IN (${a})`).run(r,...t)),p["node_tokens.grant"]=s(await e.prepareUncached(`UPDATE node_tokens SET "grant" = ? WHERE "grant" IN (${a})`).run(r,...t)),p["crons.grant_id"]=s(await e.prepareUncached(`UPDATE crons SET grant_id = ? WHERE grant_id IN (${a})`).run(r,...t));const o=r===null?"'null'::jsonb":"to_jsonb(?::text)";return p["daemon_actions.principal_grant_id"]=s(await e.prepareUncached(`UPDATE daemon_actions SET adapter_json = jsonb_set(adapter_json::jsonb, '{principal,grant_id}', ${o})::text
16
7
  WHERE state IN ('pending', 'handed_off') AND adapter_json::jsonb ->> 'adapter' = 'cron_spawn'
17
- AND adapter_json::jsonb #>> '{principal,grant_id}' IN (${a})`).run(...t===null?[]:[t],...r)),u}o(z,"repointGrantReferences");async function oe(){const e=await S(async n=>{const t={},r=o((w,g="")=>{for(const[y,R]of Object.entries(w))R!==0&&(t[g+y]=(t[g+y]??0)+R)},"add");let a=await N(n,E);if(a===null)throw new m;let u=null;const i=f();i!==null&&i!==a&&(r(await L(n,a,i),"join."),await l(n,E,i),u=a,a=i);const c=D("user").runtime?.issuer,_=await N(n,"install_id");if(_===null)throw new Error("retireLocalPerson: the install id is not stored");const A=await N(n,h)!==a;if(A){r({legacy_grantees:await Y(n)}),r(await L(n,p,a));const w=`SELECT o.id FROM objects o JOIN peer_stubs s ON s.object = o.id JOIN identities i ON i.object = o.owner
18
- WHERE o.type = 'node' AND o.reception = 2 AND s.envelope_json ->> 'app' IS NULL AND i.scheme = 'app' AND i.external_id = 'app:terminal'`;if(await n.prepareUncached(`${w} LIMIT 1`).get()!==void 0){const g=await W(n,"person","user",a,"user");r({repaired_stubs:s(await n.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${w})`).run(g.id))})}r({"nodes.sub_null_terminal":s(await n.prepare(`UPDATE nodes SET sub = ? WHERE sub IS NULL AND node_id IN (
8
+ AND adapter_json::jsonb #>> '{principal,grant_id}' IN (${a})`).run(...r===null?[]:[r],...t)),p}i(q,"repointGrantReferences");async function pe(){const e=await R(async n=>{const r={},t=i((w,m="")=>{for(const[y,A]of Object.entries(w))A!==0&&(r[m+y]=(r[m+y]??0)+A)},"add");let a=await N(n,E);if(a===null)throw new f;let p=null;const o=D();o!==null&&o!==a&&(t(await P(n,a,o),"join."),await d(n,E,o),p=a,a=o);const c=j("user").runtime?.issuer,_=await N(n,"install_id");if(_===null)throw new Error("retireLocalPerson: the install id is not stored");const g=await N(n,h)!==a;if(g){t({legacy_grantees:await K(n)}),t(await P(n,u,a));const w=`SELECT o.id FROM objects o JOIN peer_stubs s ON s.object = o.id JOIN identities i ON i.object = o.owner
9
+ WHERE o.type = 'node' AND o.reception = 2 AND s.envelope_json ->> 'app' IS NULL AND i.scheme = 'app' AND i.external_id = 'app:terminal'`;if(await n.prepareUncached(`${w} LIMIT 1`).get()!==void 0){const m=await W(n,"person","user",a,"user");t({repaired_stubs:s(await n.prepareUncached(`UPDATE objects SET owner = ? WHERE id IN (${w})`).run(m.id))})}t({"nodes.sub_null_terminal":s(await n.prepare(`UPDATE nodes SET sub = ? WHERE sub IS NULL AND node_id IN (
19
10
  SELECT n.node_id FROM nodes n JOIN identities i ON i.object = n.app
20
- WHERE i.scheme = 'app' AND i.external_id = 'app:terminal')`).run(a))}),r(await z(n,_,c?null:`terminal:${_}`)),c&&r({local_grant_mirror:await k(n)}),await l(n,h,a)}return c||await P(_,a),{person:a,joinedFrom:u,retired:A,columns:t}});return $(e.person),e}o(oe,"retireLocalPerson");export{p as LEGACY_LOCAL_PERSON,m as PersonUnresolvedError,ae as personId,se as personIdAsync,q as resolvePerson,G as resolvePersonId,oe as retireLocalPerson,$ as setPersonId,L as substitutePerson};
11
+ WHERE i.scheme = 'app' AND i.external_id = 'app:terminal')`).run(a))}),t(await q(n,_,c?null:`terminal:${_}`)),c&&t({local_grant_mirror:await v(n)}),await d(n,h,a)}return c||await b(_,a),{person:a,joinedFrom:p,retired:g,columns:r}});return F(e.person),e}i(pe,"retireLocalPerson");export{u as LEGACY_LOCAL_PERSON,f as PersonUnresolvedError,ie as personId,ce as personIdAsync,J as resolvePerson,Y as resolvePersonId,pe as retireLocalPerson,F as setPersonId,P as substitutePerson};
@@ -0,0 +1,34 @@
1
+ import type pg from 'pg';
2
+ /** The version `schema-pg.sql` writes; every delta number is above it. */
3
+ export declare const BASELINE_VERSION = 1;
4
+ export declare const DEFAULT_STEP_LOCK_TIMEOUT_MS = 5000;
5
+ export declare const DEFAULT_STEP_RETRY_BUDGET_MS = 120000;
6
+ export interface SchemaDelta {
7
+ version: number;
8
+ name: string;
9
+ file: string;
10
+ }
11
+ /** The delta files in `dir`, in number order. They must be numbered contiguously from {@link BASELINE_VERSION} + 1; any other
12
+ * entry except `.gitkeep` (which keeps the directory in git while it is empty) is an error. */
13
+ export declare function listDeltas(dir?: string): SchemaDelta[];
14
+ export type SchemaEmit = (level: 'info' | 'warn', event: string, fields: Record<string, string | number>) => void;
15
+ export interface SchemaChainOptions {
16
+ /** The Postgres schema holding the canvas tables (`CRTR_DATABASE_SCHEMA`, see `canvasDatabaseSchema`). Created when absent and set as the session's `search_path`. Default: the role's own (`public`). */
17
+ schema?: string;
18
+ /** The build's schema version. Default {@link CANVAS_SCHEMA_VERSION}; a test passes another to drive fixture deltas. */
19
+ version?: number;
20
+ /** The directory of delta files. Default `schema-pg/` beside this module; a test passes a fixture directory. */
21
+ deltaDir?: string;
22
+ /** `lock_timeout` of each step. */
23
+ lockTimeoutMs?: number;
24
+ /** How long one step keeps retrying after lock timeouts before the open fails. */
25
+ retryBudgetMs?: number;
26
+ /** Receives `schema.step.applied` after each committed step and `schema.step.lock_timeout` before each retry. */
27
+ emit?: SchemaEmit;
28
+ }
29
+ /** Bring the connected database to this build's schema. Takes `pg_advisory_lock` for the session and reads the version under
30
+ * it, so processes opening together queue and a late one applies nothing. Refuses a database whose `schema_min_reader` is above
31
+ * this build's version; opens one whose `schema_version` is above it when `schema_min_reader` allows. A database with no
32
+ * `schema_version` gets `schema-pg.sql`, which runs as one multi-statement simple query, one transaction. A database at the
33
+ * baseline version first runs the file's `-- <name>:begin … -- <name>:end` blocks, in file order, each idempotent (S1 deletes this). */
34
+ export declare function ensureSchema(client: pg.Client, options?: SchemaChainOptions): Promise<void>;
@@ -0,0 +1 @@
1
+ var _=Object.defineProperty;var u=(a,e)=>_(a,"name",{value:e,configurable:!0});import{createHash as v}from"node:crypto";import{readdirSync as S,readFileSync as E}from"node:fs";import{dirname as T,join as d}from"node:path";import{fileURLToPath as p}from"node:url";import{CANVAS_SCHEMA_VERSION as $,readStoredVersions as f}from"./schema-version.js";const w=T(p(import.meta.url)),g=d(w,"schema-pg.sql"),k=d(w,"schema-pg"),y=7427001,h=1,A=5e3,L=12e4,q="55P03",C=/^(\d{3})-([a-z0-9][a-z0-9-]*)\.sql$/;function M(a=k){const e=[];for(const t of S(a).sort()){if(t===".gitkeep")continue;const o=C.exec(t);if(o===null)throw new Error(`schema delta directory ${a}: ${t} is not named NNN-<name>.sql`);e.push({version:Number(o[1]),name:o[2],file:d(a,t)})}return e.forEach((t,o)=>{if(t.version!==h+1+o)throw new Error(`schema delta directory ${a}: expected delta ${h+1+o} but found ${t.version}-${t.name}.sql; deltas are numbered contiguously from ${h+1}`)}),e}u(M,"listDeltas");async function l(a,e){await a.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",[e.key,e.hash])}u(l,"recordBlock");const b=u(a=>new Promise(e=>setTimeout(e,a)),"sleep");async function D(a,e,t){const o=E(e.file,"utf8"),c=Date.now();let m=250;for(let r=1;;r++)try{if(await a.query("BEGIN"),await a.query(`SET LOCAL lock_timeout = '${t.lockTimeoutMs}ms'`),await a.query(o),(await a.query("UPDATE canvas_meta SET value = $1 WHERE key = 'schema_version' AND value = $2",[String(e.version),String(e.version-1)])).rowCount!==1)throw new Error(`schema step ${e.version} (${e.name}): schema_version was not ${e.version-1}; another writer changed the schema outside the chain`);await a.query("COMMIT"),t.emit?.("info","schema.step.applied",{version:e.version,name:e.name,ms:Date.now()-c,attempts:r});return}catch(s){if(await a.query("ROLLBACK").catch(()=>{}),s.code!==q)throw s;if(Date.now()+m-c>t.retryBudgetMs)throw new Error(`schema step ${e.version} (${e.name}) could not get its locks within ${t.retryBudgetMs} ms (${r} attempts); a long transaction holds a table it needs`,{cause:s});t.emit?.("warn","schema.step.lock_timeout",{version:e.version,name:e.name,attempt:r,retry_in_ms:m}),await b(m),m=Math.min(m*2,1e4)}}u(D,"applyStep");async function F(a,e={}){const t=e.version??$,o=M(e.deltaDir);if(e.schema!==void 0){const c=`"${e.schema.replace(/"/g,'""')}"`;await a.query(`CREATE SCHEMA IF NOT EXISTS ${c}`),await a.query(`SET search_path TO ${c}`)}await a.query("SELECT pg_advisory_lock($1)",[y]);try{const c=E(g,"utf8"),m=[...c.matchAll(/^-- (\w+):begin\n([\s\S]*?)^-- \1:end$/gm)].map(([,i,n])=>({key:`schema_block.${i}`,sql:n,hash:v("sha256").update(n).digest("hex")}));let r=await f(a);if(r!==void 0&&t<r.minReader)throw new Error(`unsupported canvas schema: this build is at version ${t} but the database requires readers at version ${r.minReader} or above (it is at ${r.version}); this build is older than the database. Select a newer runtime generation.`);let s;if(r===void 0){if(await a.query(c),r=await f(a),r===void 0)throw new Error("schema-pg.sql did not write canvas_meta.schema_version");s=r.version;for(const i of m)await l(a,i)}else if(s=r.version,s===h){const i=new Map((await a.query("SELECT key, value FROM canvas_meta WHERE key LIKE 'schema\\_block.%'")).rows.map(n=>[n.key,n.value]));for(const n of m)i.get(n.key)!==n.hash&&(await a.query(n.sql),await l(a,n))}for(const i of o)i.version<=s||(s=i.version,await D(a,i,{lockTimeoutMs:e.lockTimeoutMs??A,retryBudgetMs:e.retryBudgetMs??L,emit:e.emit}));if(s<t)throw new Error(`canvas schema version ${s} does not match this build's ${t}: no delta reaches it`)}finally{await a.query("SELECT pg_advisory_unlock($1)",[y]).catch(()=>{})}}u(F,"ensureSchema");export{h as BASELINE_VERSION,A as DEFAULT_STEP_LOCK_TIMEOUT_MS,L as DEFAULT_STEP_RETRY_BUDGET_MS,F as ensureSchema,M as listDeltas};
File without changes