@north-light/cli 0.3.552 → 0.3.554

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.
@@ -1,5 +1,5 @@
1
1
  /** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
2
- export declare const CONTRACT_LEVEL = "1.32";
2
+ export declare const CONTRACT_LEVEL = "1.33";
3
3
  /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
4
4
  export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
5
5
  /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
@@ -1 +1 @@
1
- var c=Object.defineProperty;var e=(r,t)=>c(r,"name",{value:t,configurable:!0});const f="1.32",i="Crouter-Required-Contract",p="Crouter-Contract";function l(r){const t=/^(\d+)\.(\d+)$/.exec(r.trim());return t===null?null:{major:Number(t[1]),minor:Number(t[2])}}e(l,"parseContractLevel");function a(r){return r==null?[]:r.split(",").map(t=>l(t)).filter(t=>t!==null)}e(a,"parseServedContracts");function s(r,t){const n=l(r);return n===null?!1:(typeof t=="string"||t===null||t===void 0?a(t):t).some(u=>u.major===n.major&&u.minor>=n.minor)}e(s,"contractSatisfies");function C(r,t,n={}){if(r===void 0)return null;if(l(r)===null)return{status:400,code:"invalid_request",param:i,message:`${i} must be a contract level like 1.0.`};if(s(r,t))return null;const o=r.trim();return{status:426,code:"runtime_version_unsupported",message:t===null||a(t).length===0?`The runtime did not name its contract level; the caller requires contract ${o} or later. Roll the runtime forward.`:`The runtime serves contract ${t}; the caller requires contract ${o} or later within the same major. Roll the runtime forward, or pin an SDK whose crouterContract the runtime serves.`,details:{sdk_version:(n.legacyMinimum??"").trim(),runtime_version:n.runtimeVersion??null,required_contract:o,runtime_contract:t}}}e(C,"contractRefusal");export{p as CONTRACT_HEADER,f as CONTRACT_LEVEL,i as REQUIRED_CONTRACT_HEADER,C as contractRefusal,s as contractSatisfies,l as parseContractLevel,a as parseServedContracts};
1
+ var c=Object.defineProperty;var e=(r,t)=>c(r,"name",{value:t,configurable:!0});const f="1.33",i="Crouter-Required-Contract",p="Crouter-Contract";function l(r){const t=/^(\d+)\.(\d+)$/.exec(r.trim());return t===null?null:{major:Number(t[1]),minor:Number(t[2])}}e(l,"parseContractLevel");function a(r){return r==null?[]:r.split(",").map(t=>l(t)).filter(t=>t!==null)}e(a,"parseServedContracts");function s(r,t){const n=l(r);return n===null?!1:(typeof t=="string"||t===null||t===void 0?a(t):t).some(u=>u.major===n.major&&u.minor>=n.minor)}e(s,"contractSatisfies");function C(r,t,n={}){if(r===void 0)return null;if(l(r)===null)return{status:400,code:"invalid_request",param:i,message:`${i} must be a contract level like 1.0.`};if(s(r,t))return null;const o=r.trim();return{status:426,code:"runtime_version_unsupported",message:t===null||a(t).length===0?`The runtime did not name its contract level; the caller requires contract ${o} or later. Roll the runtime forward.`:`The runtime serves contract ${t}; the caller requires contract ${o} or later within the same major. Roll the runtime forward, or pin an SDK whose crouterContract the runtime serves.`,details:{sdk_version:(n.legacyMinimum??"").trim(),runtime_version:n.runtimeVersion??null,required_contract:o,runtime_contract:t}}}e(C,"contractRefusal");export{p as CONTRACT_HEADER,f as CONTRACT_LEVEL,i as REQUIRED_CONTRACT_HEADER,C as contractRefusal,s as contractSatisfies,l as parseContractLevel,a as parseServedContracts};
@@ -9,6 +9,10 @@ when-and-why-to-read: When you need Release notes, this knowledge should be read
9
9
 
10
10
  Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
11
11
 
12
+ ## 0.3.554
13
+
14
+ `RunObject.recap` (`runs.get`, `runs.list`): the root's latest idle recap, `{ goal, state, at }`, where `goal` says what the conversation is for and `state` where it stands. The runtime writes it after a turn once the root goes idle; it is absent before the first one. Additive.
15
+
12
16
  ## 0.3.552
13
17
 
14
18
  One ledger records every charge. Additive, except that the per-app cap now counts more.
@@ -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 l=(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","charges","charge_totals","limits"]),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","ledger_migrated",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})}}l(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}`,m=Date.now(),t=new j.Client({connectionString:i,application_name:`crtr-export-${process.pid}`,types:{getTypeParser:l(((f,o)=>f===20?d=>Number(d):j.types.getTypeParser(f,o)),"getTypeParser")}});await t.connect();let u;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");u=ie(s);const o=u;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 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":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 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 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(),u=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{u?.close()}catch{}for(const o of["","-wal","-shm","-journal"])P(`${s}${o}`,{force:!0});throw f}finally{await t.end().catch(()=>{})}}l(_e,"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 ce(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),u=m;s==="canvas_meta"&&(u=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)),u!==t&&r.push(`${s}: Postgres ${u} 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
- `)}`)}l(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","caused_by"],revisions:["acting_app","acting_for"]},W=new Set(["daemon_owner","schema_version","ledger_migrated"]),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};
@@ -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
@@ -1,4 +1,30 @@
1
- /** The canvas schema version `schema-pg.sql` writes to `canvas_meta.schema_version`. `db.ts` refuses a database at
2
- * any other version, and `scripts/install-runtime.mjs` reads this module from a candidate generation to refuse one older
3
- * than the live cluster. Kept free of imports so the installer can load it alone. */
1
+ /** The canvas schema version this build supports: the number of the highest delta in `schema-pg/` (the baseline
2
+ * `schema-pg.sql` is version 1). `scripts/install-runtime.mjs` reads this module from a candidate generation to refuse
3
+ * one below the live database's `schema_min_reader`. */
4
4
  export declare const CANVAS_SCHEMA_VERSION = 1;
5
+ /** The Postgres schema holding the canvas tables: `CRTR_DATABASE_SCHEMA` (`crtr` in the cloud deploy). Unset or empty
6
+ * means the role's own search_path (`public` on a Mac and on a VM). A name that is not a plain identifier is an error:
7
+ * it is put in a `search_path` connection option. */
8
+ export declare function canvasDatabaseSchema(env?: Record<string, string | undefined>): string | undefined;
9
+ /** The direct, non-pooled connection the schema chain runs on when `CRTR_SCHEMA_DATABASE_URL` is set (its advisory lock
10
+ * needs one session). Unset: the chain runs on the daemon's own connection. */
11
+ export declare function canvasSchemaDatabaseUrl(env?: Record<string, string | undefined>): string | undefined;
12
+ /** The pg `options` connection parameter that makes every connection resolve unqualified names in the canvas schema. */
13
+ export declare function canvasSearchPathOptions(env?: Record<string, string | undefined>): string | undefined;
14
+ interface Queryable {
15
+ query(text: string): Promise<{
16
+ rows: Array<Record<string, unknown>>;
17
+ }>;
18
+ }
19
+ export interface StoredVersions {
20
+ /** `canvas_meta.schema_version`: the number of the last committed step. */
21
+ version: number;
22
+ /** `canvas_meta.schema_min_reader`: the lowest build version that may open the database; absent means `version`. */
23
+ minReader: number;
24
+ }
25
+ /** The versions stored in `schema` (default: the session's search_path), or undefined when the database has no canvas schema yet. */
26
+ export declare function readStoredVersions(client: Queryable, schema?: string): Promise<StoredVersions | undefined>;
27
+ /** The installer's rollback gate: refuse a candidate whose version is below the live database's `schema_min_reader`. A
28
+ * database with no canvas schema yet accepts any candidate. */
29
+ export declare function assertCandidateMayOpen(client: Queryable, supported: number, schema?: string): Promise<void>;
30
+ export {};
@@ -1 +1 @@
1
- const A=1;export{A as CANVAS_SCHEMA_VERSION};
1
+ var m=Object.defineProperty;var r=(n,e)=>m(n,"name",{value:e,configurable:!0});const A=1;function _(n=process.env){const e=n.CRTR_DATABASE_SCHEMA?.trim();if(e){if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(e))throw new Error(`CRTR_DATABASE_SCHEMA must be a plain identifier (letters, digits, underscore); got ${JSON.stringify(e)}`);return e}}r(_,"canvasDatabaseSchema");function h(n=process.env){return n.CRTR_SCHEMA_DATABASE_URL?.trim()||void 0}r(h,"canvasSchemaDatabaseUrl");function E(n=process.env){const e=_(n);return e===void 0?void 0:`-c search_path=${e}`}r(E,"canvasSearchPathOptions");async function v(n,e){const t=e===void 0?"canvas_meta":`"${e.replace(/"/g,'""')}".canvas_meta`;if((await n.query(`SELECT to_regclass('${t.replace(/'/g,"''")}') IS NOT NULL AS found`)).rows[0].found!==!0)return;const u=(await n.query(`SELECT key, value FROM ${t} WHERE key IN ('schema_version', 'schema_min_reader')`)).rows,o=r(c=>{const i=u.find(f=>f.key===c);if(i===void 0)return;const d=Number(i.value);if(!Number.isSafeInteger(d))throw new Error(`canvas_meta.${c} is not an integer: ${String(i.value)}`);return d},"value"),s=o("schema_version");return s===void 0?void 0:{version:s,minReader:o("schema_min_reader")??s}}r(v,"readStoredVersions");async function l(n,e,t){const a=await v(n,t);if(a!==void 0&&a.minReader>e)throw new Error(`refusing to select runtime with canvas schema ${e}: the live database requires readers at version ${a.minReader} or above (it is at ${a.version}); install a descendant build instead`)}r(l,"assertCandidateMayOpen");export{A as CANVAS_SCHEMA_VERSION,l as assertCandidateMayOpen,_ as canvasDatabaseSchema,h as canvasSchemaDatabaseUrl,E as canvasSearchPathOptions,v as readStoredVersions};
@@ -1,14 +1,14 @@
1
- var Oe=Object.defineProperty;var u=(e,t)=>Oe(e,"name",{value:t,configurable:!0});import{coveredByAny as be}from"../../shared/env.js";import{createHash as G}from"node:crypto";import{ApiError as De}from"../../api/errors.js";import{CrtrError as Me}from"../errors.js";import{isAbsolute as z,relative as Ie,resolve as Ae,sep as Te,join as Le}from"node:path";import{SessionManager as Ce}from"@earendil-works/pi-coding-agent";import{covers as U}from"@north-light/identity";import{openDb as B,withCanvasWrite as W}from"../canvas/db.js";import{deleteNodeWithOwned as ke}from"../graph/deletion.js";import{getById as xe}from"../graph/objects.js";import{repairRunConversations as Q}from"../canvas/sessions.js";import{getNode as O,nodeFiles as We,removeNodeFiles as Fe,updateNode as qe}from"../canvas/canvas.js";import{fullName as He}from"../canvas/labels.js";import{renameWindow as Ue}from"../runtime/placement.js";import{liveExecutionForNode as V}from"../canvas/executions.js";import{conversationStore as Z,isConversationNotFound as Be}from"../conversation-store/index.js";import{PARKED_RESIDENT_SQL as Pe}from"../canvas/canvas.js";import{WAKE_MAIL_SQL as $e}from"../canvas/deliveries.js";import{enqueueNodeMail as je}from"../feed/messages.js";import{newestReportBody as Je}from"../feed/reports.js";import{ensureAppProfile as Ye,loadExactProfileManifest as Xe}from"../profiles/manifest.js";import{stopNodeSandboxes as K}from"../spaces/stop.js";import{BrokerUnreachableError as Ge}from"../runtime/broker-request.js";import{interruptLive as ze}from"../runtime/interactive-deliver.js";import{isBrokerLive as ee}from"../runtime/model-swap.js";import{newNodeId as Qe}from"../runtime/nodes.js";import{reviveNode as P}from"../runtime/revive.js";import{spawnChildPrepared as Ve}from"../runtime/spawn.js";import{isPortableModelToken as Ze}from"../runtime/launch.js";import{parseDurationMs as Ke}from"../wake.js";import{expandModelCandidates as et,parseModelSpec as tt}from"../model-routes.js";import{payerOfGrant as $}from"../grants/mirror.js";import{isolatedBrokerLauncherAvailable as nt}from"../runtime/host.js";import{resolveRunKind as st,resolveRunModel as at}from"../runtime/launch-target.js";import{cycleAwareMessages as it}from"../runtime/session-cycles.js";import{originFor as te}from"../runtime/stamp/protocol.js";import{readOutputRequest as rt,readOutputResult as ne}from"../runtime/structured-output.js";import{admitLegacyStructuredOutput as se}from"../../daemon/structured-requests/sidecar.js";import{runSchemaRepromptPending as ot}from"../runtime/stop-guard.js";import{toRunOutcomeDTO as dt}from"../../daemon/api/map.js";import{openAssistantMessageStartSequence as ct}from"../../daemon/runs/observer.js";import{closeNode as ae}from"../runtime/close.js";import{parseScope as ut}from"@north-light/identity";import{appendRunEvent as ie,subscribeRunEvents as mt}from"./events.js";import{runActivity as lt}from"./activity.js";import{cancelOpenQuestions as re,openNoticeIds as pt,openQuestionIds as oe}from"./questions.js";import{appSpace as j}from"../layout.js";import{ensureAppSpace as _t}from"../spaces/space.js";import{assertUploadsUsable as de,resolveStorageUrls as ft}from"../storage/attachments.js";import{storageClient as wt,storageConfig as gt}from"../storage/client.js";import{deleteUploadRow as Et,insertUploadUses as ce,releaseRunUploads as yt}from"../storage/tables.js";import{abortProviderCalls as ue}from"../providers/inflight.js";import{filterAsync as me,mapAsync as k}from"../async-array.js";const le=u((e,t,s)=>ft(t,e,s),"defaultResolver"),ht=3e3;class N extends Error{static{u(this,"RunError")}status;code;param;details;scope;retryable;constructor(t,s,n,d,l,i,r=t===503){super(n),this.status=t,this.code=s,this.param=d,this.details=l,this.scope=i,this.retryable=r}}const S=u((e,t)=>new N(400,"invalid_request",e,t),"invalid"),J=u(()=>new N(404,"not_found","run not found"),"missing"),Y=u(()=>({input_tokens:0,output_tokens:0,cache_read_tokens:0,cache_write_tokens:0,cost_usd:0}),"usage"),I=u(async()=>await B(),"db");async function F(e,t,s=!1){const n=await O(e);if(!n||n.parent!==null||n.run_id!==e||n.grantee!==t||!s&&n.deleting_at)throw J();return n}u(F,"ownedRun");const pe=["waiting_on_user","idle","settled"];async function C(e){if(e.status==="canceled"||e.status==="dead"||e.status==="done"&&e.intent!=="parked")return"settled";const t=await(await I()).prepare(`SELECT 1 FROM node_rows n WHERE n.run_id = ? AND n.status = 'active' AND
1
+ var Oe=Object.defineProperty;var u=(e,t)=>Oe(e,"name",{value:t,configurable:!0});import{coveredByAny as be}from"../../shared/env.js";import{createHash as G}from"node:crypto";import{ApiError as De}from"../../api/errors.js";import{CrtrError as Me}from"../errors.js";import{isAbsolute as z,relative as Ie,resolve as Ae,sep as Te,join as Le}from"node:path";import{SessionManager as Ce}from"@earendil-works/pi-coding-agent";import{covers as U}from"@north-light/identity";import{openDb as B,withCanvasWrite as W}from"../canvas/db.js";import{deleteNodeWithOwned as ke}from"../graph/deletion.js";import{getById as xe}from"../graph/objects.js";import{repairRunConversations as Q}from"../canvas/sessions.js";import{getNode as O,nodeFiles as We,removeNodeFiles as Fe,updateNode as qe}from"../canvas/canvas.js";import{fullName as He}from"../canvas/labels.js";import{renameWindow as Ue}from"../runtime/placement.js";import{liveExecutionForNode as V}from"../canvas/executions.js";import{conversationStore as Z,isConversationNotFound as Be}from"../conversation-store/index.js";import{PARKED_RESIDENT_SQL as Pe}from"../canvas/canvas.js";import{WAKE_MAIL_SQL as $e}from"../canvas/deliveries.js";import{readNodeRecap as je}from"../canvas/node-recap.js";import{enqueueNodeMail as Je}from"../feed/messages.js";import{newestReportBody as Ye}from"../feed/reports.js";import{ensureAppProfile as Xe,loadExactProfileManifest as Ge}from"../profiles/manifest.js";import{stopNodeSandboxes as K}from"../spaces/stop.js";import{BrokerUnreachableError as ze}from"../runtime/broker-request.js";import{interruptLive as Qe}from"../runtime/interactive-deliver.js";import{isBrokerLive as ee}from"../runtime/model-swap.js";import{newNodeId as Ve}from"../runtime/nodes.js";import{reviveNode as P}from"../runtime/revive.js";import{spawnChildPrepared as Ze}from"../runtime/spawn.js";import{isPortableModelToken as Ke}from"../runtime/launch.js";import{parseDurationMs as et}from"../wake.js";import{expandModelCandidates as tt,parseModelSpec as nt}from"../model-routes.js";import{payerOfGrant as $}from"../grants/mirror.js";import{isolatedBrokerLauncherAvailable as st}from"../runtime/host.js";import{resolveRunKind as at,resolveRunModel as it}from"../runtime/launch-target.js";import{cycleAwareMessages as rt}from"../runtime/session-cycles.js";import{originFor as te}from"../runtime/stamp/protocol.js";import{readOutputRequest as ot,readOutputResult as ne}from"../runtime/structured-output.js";import{admitLegacyStructuredOutput as se}from"../../daemon/structured-requests/sidecar.js";import{runSchemaRepromptPending as dt}from"../runtime/stop-guard.js";import{toRunOutcomeDTO as ct}from"../../daemon/api/map.js";import{openAssistantMessageStartSequence as ut}from"../../daemon/runs/observer.js";import{closeNode as ae}from"../runtime/close.js";import{parseScope as mt}from"@north-light/identity";import{appendRunEvent as ie,subscribeRunEvents as lt}from"./events.js";import{runActivity as pt}from"./activity.js";import{cancelOpenQuestions as re,openNoticeIds as _t,openQuestionIds as oe}from"./questions.js";import{appSpace as j}from"../layout.js";import{ensureAppSpace as ft}from"../spaces/space.js";import{assertUploadsUsable as de,resolveStorageUrls as wt}from"../storage/attachments.js";import{storageClient as gt,storageConfig as Et}from"../storage/client.js";import{deleteUploadRow as yt,insertUploadUses as ce,releaseRunUploads as ht}from"../storage/tables.js";import{abortProviderCalls as ue}from"../providers/inflight.js";import{filterAsync as me,mapAsync as k}from"../async-array.js";const le=u((e,t,s)=>wt(t,e,s),"defaultResolver"),Rt=3e3;class N extends Error{static{u(this,"RunError")}status;code;param;details;scope;retryable;constructor(t,s,n,d,l,i,r=t===503){super(n),this.status=t,this.code=s,this.param=d,this.details=l,this.scope=i,this.retryable=r}}const S=u((e,t)=>new N(400,"invalid_request",e,t),"invalid"),J=u(()=>new N(404,"not_found","run not found"),"missing"),Y=u(()=>({input_tokens:0,output_tokens:0,cache_read_tokens:0,cache_write_tokens:0,cost_usd:0}),"usage"),I=u(async()=>await B(),"db");async function F(e,t,s=!1){const n=await O(e);if(!n||n.parent!==null||n.run_id!==e||n.grantee!==t||!s&&n.deleting_at)throw J();return n}u(F,"ownedRun");const pe=["waiting_on_user","idle","settled"];async function C(e){if(e.status==="canceled"||e.status==="dead"||e.status==="done"&&e.intent!=="parked")return"settled";const t=await(await I()).prepare(`SELECT 1 FROM node_rows n WHERE n.run_id = ? AND n.status = 'active' AND
2
2
  (n.last_settled_at IS NULL OR EXISTS (
3
3
  SELECT 1 FROM run_events e WHERE e.run_id = ? AND e.node_id = n.node_id AND e.type = 'node.turn.started'
4
4
  AND e.sequence_number > COALESCE((SELECT MAX(c.sequence_number) FROM run_events c
5
5
  WHERE c.run_id = e.run_id AND c.node_id = e.node_id AND c.type = 'node.turn.completed'), 0)
6
- )) LIMIT 1`).get(e.node_id,e.node_id),s=!e.last_settled_at&&e.status!=="done";return t||s||await ot(e)?"running":(await oe(e.node_id)).length?"waiting_on_user":await(await I()).prepare(`SELECT 1 FROM deliveries d JOIN nodes n ON n.node_id = d.destination_id
6
+ )) LIMIT 1`).get(e.node_id,e.node_id),s=!e.last_settled_at&&e.status!=="done";return t||s||await dt(e)?"running":(await oe(e.node_id)).length?"waiting_on_user":await(await I()).prepare(`SELECT 1 FROM deliveries d JOIN nodes n ON n.node_id = d.destination_id
7
7
  WHERE n.run_id = ? AND ${$e("d","'armed', 'pending'")}
8
- AND (n.status IN ('active', 'idle') OR ${Pe("n")}) LIMIT 1`).get(e.node_id)?"running":"idle"}u(C,"runStatus");async function On(e){const t=await O(e);return t!==null&&await C(t)==="running"}u(On,"runIsRunning");async function x(e){await Q(e.node_id),e=await O(e.node_id)??e;const t=await C(e),s=t==="settled"?[]:await oe(e.node_id),n=await Je(e.node_id)||void 0,d=t==="settled"?dt(e):null,l=d?.kind==="result"?ne(e.node_id,e.grantee):void 0;return{run_id:e.node_id,name:e.name,status:t,...d?{outcome:d}:{},...l===void 0?{}:{result:l},...n!==void 0?{last_report:n}:{},pending_question_ids:s,open_notice_ids:t==="settled"?[]:await pt(e.node_id),...e.last_turn_error_json?{last_turn_error:JSON.parse(e.last_turn_error_json)}:{},usage:JSON.parse(e.usage_json??JSON.stringify(Y())),payer:await $(e.grant_id),scopes:e.scopes,metadata:e.metadata_json?JSON.parse(e.metadata_json):{},created_at:e.created,...t==="settled"&&e.outcome_at?{settled_at:e.outcome_at}:{},latest_sequence:(e.event_cursor??1)-1,last_activity_at:e.last_activity_at,activity:await lt(e.node_id,t)}}u(x,"runObject");const X=864e5,_e=255;function bn(e){if(e!==void 0){if(typeof e!="string"||!e.length||e.length>_e)throw S(`Idempotency-Key must be 1-${_e} characters`);return e}}u(bn,"idempotencyKeyOf");async function q(e,t,s,n){n??=await B();const d=await n.prepare("SELECT body_hash, run_id, response_json, created_at FROM idempotency_keys WHERE grantee = ? AND key = ?").get(e,t);if(!(!d||Date.now()-Date.parse(d.created_at)>=X)){if(d.body_hash!==s)throw new N(409,"idempotency_conflict","key used with a different request");return{run_id:d.run_id,response:JSON.parse(d.response_json)}}}u(q,"findIdempotent");async function fe(e,t,s,n,d,l,i){await e.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key = ? AND created_at <= ?").run(t,s,new Date(Date.now()-X).toISOString()),await e.prepare("INSERT INTO idempotency_keys (grantee, key, body_hash, run_id, response_json, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(t,s,n,d,JSON.stringify(l),i)}u(fe,"recordIdempotent");async function Dn(e=Date.now()){return Number((await(await B()).prepare("DELETE FROM idempotency_keys WHERE created_at <= ?").run(new Date(e-X).toISOString())).changes)}u(Dn,"pruneExpiredIdempotencyKeys");async function we(e,t,s,n,d){const l=n.text;let i=0;return{messageId:(await je(t,{kind:"message",label:l.slice(0,120),senderNodeId:"human",senderName:s,bodyJson:d?{body:l}:{body:l,start_turn:!1},urgency:d?"normal":"deferred",wake:d?1:0,withinEnqueue:u(async(c,m)=>{n.uses.length&&(await de(s,n.uses,c),await ce(n.uses.map(({upload_id:h,path:E})=>({upload_id:h,path:E,run_id:t})),c)),i=(await ie(c,t,t,"node.message.accepted",{message_id:m,start_turn:d})).sequence_number},"withinEnqueue")})).messageId,sequence:i}}u(we,"acceptRunMessage");class ge extends Error{static{u(this,"IdempotentReplay")}winner;constructor(t){super("idempotent replay"),this.winner=t}}const Rt=1440*60*1e3;function Ee(e){const t=typeof e=="string"?Ke(e):null;if(t===null||!Number.isSafeInteger(t))throw S(`deadline '${String(e)}' is not a duration; use one or more <int>[smhd] parts such as 10m or 1h30m`,"deadline");if(t<=0)throw S("deadline must be greater than zero","deadline");if(t>Rt)throw S("deadline must be at most 24h","deadline");return t}u(Ee,"parseRunDeadlineMs");async function Mn(e,t,s,n=le){if(t.isolated&&!nt())throw new N(503,"isolated_unavailable","isolated runs require the Linux root launcher","isolated");t.deadline!==void 0&&Ee(t.deadline);const{causedBy:d,...l}=t,i=G("sha256").update(JSON.stringify(l)).digest("hex");if(s){const a=await q(e.grantee,s,i);if(a){const p=await F(a.run_id,e.grantee);return!p.pi_pid&&await C(p)==="running"&&(t.output_schema!==void 0&&!p.outcome_at&&rt(p.node_id).state==="absent"&&ne(p.node_id,p.grantee)===void 0&&await se(p.node_id,"terminal",t.output_schema),await P(p.node_id,{resume:!0,capacity:"freeze"})),a.response}}await _t(e.grantee);const r=t.profile===void 0?Ye(e.grantee):Xe(t.profile),c=r.manifest.grantee??"app:terminal";if(c!==e.grantee){const a=`crtr:memory:rw:${c}:profile:${r.profileId}`;if(!e.resolved.some(p=>U(p,a))||t.scopes!==null&&!t.scopes.some(p=>U(p,a)))throw new N(403,"scope_missing",`missing scope ${a}`,"profile",void 0,a)}if(t.model!==void 0){const a=t.model;if(Ze(a)){if(a.includes("/")&&await $(e.grant)!=="app"){const p=a.lastIndexOf("/"),D=a.slice(0,p),v=a.slice(p+1);if(["ultra","strong","medium","light"].includes(v)&&et({kind:"logical",family:D,strength:v,spec:a},j(e.grantee).files,r.profileId).every(R=>R.family!==D))throw S(`model '${a}' names a model family that is not configured in this daemon`,"model")}}else if(tt(a)===void 0)throw S(`model '${a}' is not a strength (ultra, strong, medium, light), a family/strength, or a provider/model id`,"model")}const m=await Qe(),h=Le(j(e.grantee).nodes,m);if(t.isolated&&t.cwd!==void 0){const a=Ie(h,Ae(t.cwd));if(!z(t.cwd)||a===".."||a.startsWith(`..${Te}`)||z(a))throw S("cwd must be inside the isolated root node folder","cwd")}const E=await n(m,e.grantee,t.prompt),g=E.text;let o;try{o=t.message===void 0?void 0:await n(m,e.grantee,t.message)}catch(a){throw E.cleanup(),a}const _=await $(e.grant);let y;try{const a=at(r,t.model);await Ve({kind:st(r),mode:"base",root:!0,rootLifecycle:"resident",readiness:"launch",cwd:t.isolated?t.cwd??h:j(e.grantee).files,profile:r.profileId,prompt:g,nodeId:m,isolated:t.isolated??!1,...t.causedBy===void 0?{}:{causedBy:t.causedBy},...a===void 0?{}:{model:a},...t.deadline===void 0?{}:{deadlineAt:new Date(Date.now()+Ee(t.deadline)).toISOString()},...t.scopes===null?{}:{scopes:t.scopes},run:{grantee:e.grantee,sub:e.sub,grant_id:e.grant,client_id:e.client_id,metadata_json:JSON.stringify(t.metadata),run_id:m,event_cursor:1,usage_json:JSON.stringify(Y())},withinCreate:u(async(p,D)=>{if(s){const R=await q(e.grantee,s,i,p);if(R)throw new ge(R)}const v=E.uses;if(v.length&&(await de(e.grantee,v,p),await ce(v.map(({upload_id:R,path:A})=>({upload_id:R,path:A,run_id:m})),p)),y={run_id:m,node_id:m,grantee:e.grantee,grant:e.grant,rev:e.rev,scopes:t.scopes,payer:_,status:"running",created_at:D.created,metadata:t.metadata,sequence_number:0},await ie(p,m,m,"run.started",{scopes:t.scopes,payer:_,usage:Y()}),o){const R=await we(p,m,e.grantee,o,!1);y.message={run_id:m,message_id:R.messageId,sequence_number:R.sequence}}s&&await fe(p,e.grantee,s,i,m,y,D.created)},"withinCreate")},t.output_schema===void 0?void 0:async p=>{await se(p,"terminal",t.output_schema)})}catch(a){if(await O(m)||(E.cleanup(),o?.cleanup()),a instanceof ge)return a.winner.response;if(t.model!==void 0&&a instanceof Me&&a.code==="usage")throw S(a.message,"model");if(a instanceof De&&(a.code==="isolated_run"||a.code==="invalid_request")){const p=a.details&&typeof a.details=="object"&&"param"in a.details&&typeof a.details.param=="string"?a.details.param:void 0;throw new N(a.status,a.code,a.message,p)}throw a}if(!y)throw new Error("run creation did not commit");return y}u(Mn,"startRun");async function In(e,t){let s;if(t.after)try{const r=JSON.parse(Buffer.from(t.after,"base64url").toString());if(!r||typeof r!="object"||!("activity"in r)||typeof r.activity!="string"||!("id"in r)||typeof r.id!="string")throw new Error("invalid cursor");s={activity:r.activity,id:r.id}}catch{throw S("invalid list cursor","after")}const n=await(await I()).prepareUncached(`SELECT node_id, COALESCE(last_activity_at, created) AS activity FROM node_rows
8
+ AND (n.status IN ('active', 'idle') OR ${Pe("n")}) LIMIT 1`).get(e.node_id)?"running":"idle"}u(C,"runStatus");async function Dn(e){const t=await O(e);return t!==null&&await C(t)==="running"}u(Dn,"runIsRunning");async function x(e){await Q(e.node_id),e=await O(e.node_id)??e;const t=await C(e),s=t==="settled"?[]:await oe(e.node_id),n=await Ye(e.node_id)||void 0,d=t==="settled"?ct(e):null,l=d?.kind==="result"?ne(e.node_id,e.grantee):void 0,i=je(e.node_id);return{run_id:e.node_id,name:e.name,status:t,...d?{outcome:d}:{},...l===void 0?{}:{result:l},...n!==void 0?{last_report:n}:{},...i?{recap:i}:{},pending_question_ids:s,open_notice_ids:t==="settled"?[]:await _t(e.node_id),...e.last_turn_error_json?{last_turn_error:JSON.parse(e.last_turn_error_json)}:{},usage:JSON.parse(e.usage_json??JSON.stringify(Y())),payer:await $(e.grant_id),scopes:e.scopes,metadata:e.metadata_json?JSON.parse(e.metadata_json):{},created_at:e.created,...t==="settled"&&e.outcome_at?{settled_at:e.outcome_at}:{},latest_sequence:(e.event_cursor??1)-1,last_activity_at:e.last_activity_at,activity:await pt(e.node_id,t)}}u(x,"runObject");const X=864e5,_e=255;function Mn(e){if(e!==void 0){if(typeof e!="string"||!e.length||e.length>_e)throw S(`Idempotency-Key must be 1-${_e} characters`);return e}}u(Mn,"idempotencyKeyOf");async function q(e,t,s,n){n??=await B();const d=await n.prepare("SELECT body_hash, run_id, response_json, created_at FROM idempotency_keys WHERE grantee = ? AND key = ?").get(e,t);if(!(!d||Date.now()-Date.parse(d.created_at)>=X)){if(d.body_hash!==s)throw new N(409,"idempotency_conflict","key used with a different request");return{run_id:d.run_id,response:JSON.parse(d.response_json)}}}u(q,"findIdempotent");async function fe(e,t,s,n,d,l,i){await e.prepare("DELETE FROM idempotency_keys WHERE grantee = ? AND key = ? AND created_at <= ?").run(t,s,new Date(Date.now()-X).toISOString()),await e.prepare("INSERT INTO idempotency_keys (grantee, key, body_hash, run_id, response_json, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(t,s,n,d,JSON.stringify(l),i)}u(fe,"recordIdempotent");async function In(e=Date.now()){return Number((await(await B()).prepare("DELETE FROM idempotency_keys WHERE created_at <= ?").run(new Date(e-X).toISOString())).changes)}u(In,"pruneExpiredIdempotencyKeys");async function we(e,t,s,n,d){const l=n.text;let i=0;return{messageId:(await Je(t,{kind:"message",label:l.slice(0,120),senderNodeId:"human",senderName:s,bodyJson:d?{body:l}:{body:l,start_turn:!1},urgency:d?"normal":"deferred",wake:d?1:0,withinEnqueue:u(async(c,m)=>{n.uses.length&&(await de(s,n.uses,c),await ce(n.uses.map(({upload_id:h,path:E})=>({upload_id:h,path:E,run_id:t})),c)),i=(await ie(c,t,t,"node.message.accepted",{message_id:m,start_turn:d})).sequence_number},"withinEnqueue")})).messageId,sequence:i}}u(we,"acceptRunMessage");class ge extends Error{static{u(this,"IdempotentReplay")}winner;constructor(t){super("idempotent replay"),this.winner=t}}const St=1440*60*1e3;function Ee(e){const t=typeof e=="string"?et(e):null;if(t===null||!Number.isSafeInteger(t))throw S(`deadline '${String(e)}' is not a duration; use one or more <int>[smhd] parts such as 10m or 1h30m`,"deadline");if(t<=0)throw S("deadline must be greater than zero","deadline");if(t>St)throw S("deadline must be at most 24h","deadline");return t}u(Ee,"parseRunDeadlineMs");async function An(e,t,s,n=le){if(t.isolated&&!st())throw new N(503,"isolated_unavailable","isolated runs require the Linux root launcher","isolated");t.deadline!==void 0&&Ee(t.deadline);const{causedBy:d,...l}=t,i=G("sha256").update(JSON.stringify(l)).digest("hex");if(s){const a=await q(e.grantee,s,i);if(a){const p=await F(a.run_id,e.grantee);return!p.pi_pid&&await C(p)==="running"&&(t.output_schema!==void 0&&!p.outcome_at&&ot(p.node_id).state==="absent"&&ne(p.node_id,p.grantee)===void 0&&await se(p.node_id,"terminal",t.output_schema),await P(p.node_id,{resume:!0,capacity:"freeze"})),a.response}}await ft(e.grantee);const r=t.profile===void 0?Xe(e.grantee):Ge(t.profile),c=r.manifest.grantee??"app:terminal";if(c!==e.grantee){const a=`crtr:memory:rw:${c}:profile:${r.profileId}`;if(!e.resolved.some(p=>U(p,a))||t.scopes!==null&&!t.scopes.some(p=>U(p,a)))throw new N(403,"scope_missing",`missing scope ${a}`,"profile",void 0,a)}if(t.model!==void 0){const a=t.model;if(Ke(a)){if(a.includes("/")&&await $(e.grant)!=="app"){const p=a.lastIndexOf("/"),D=a.slice(0,p),v=a.slice(p+1);if(["ultra","strong","medium","light"].includes(v)&&tt({kind:"logical",family:D,strength:v,spec:a},j(e.grantee).files,r.profileId).every(R=>R.family!==D))throw S(`model '${a}' names a model family that is not configured in this daemon`,"model")}}else if(nt(a)===void 0)throw S(`model '${a}' is not a strength (ultra, strong, medium, light), a family/strength, or a provider/model id`,"model")}const m=await Ve(),h=Le(j(e.grantee).nodes,m);if(t.isolated&&t.cwd!==void 0){const a=Ie(h,Ae(t.cwd));if(!z(t.cwd)||a===".."||a.startsWith(`..${Te}`)||z(a))throw S("cwd must be inside the isolated root node folder","cwd")}const E=await n(m,e.grantee,t.prompt),g=E.text;let o;try{o=t.message===void 0?void 0:await n(m,e.grantee,t.message)}catch(a){throw E.cleanup(),a}const _=await $(e.grant);let y;try{const a=it(r,t.model);await Ze({kind:at(r),mode:"base",root:!0,rootLifecycle:"resident",readiness:"launch",cwd:t.isolated?t.cwd??h:j(e.grantee).files,profile:r.profileId,prompt:g,nodeId:m,isolated:t.isolated??!1,...t.causedBy===void 0?{}:{causedBy:t.causedBy},...a===void 0?{}:{model:a},...t.deadline===void 0?{}:{deadlineAt:new Date(Date.now()+Ee(t.deadline)).toISOString()},...t.scopes===null?{}:{scopes:t.scopes},run:{grantee:e.grantee,sub:e.sub,grant_id:e.grant,client_id:e.client_id,metadata_json:JSON.stringify(t.metadata),run_id:m,event_cursor:1,usage_json:JSON.stringify(Y())},withinCreate:u(async(p,D)=>{if(s){const R=await q(e.grantee,s,i,p);if(R)throw new ge(R)}const v=E.uses;if(v.length&&(await de(e.grantee,v,p),await ce(v.map(({upload_id:R,path:A})=>({upload_id:R,path:A,run_id:m})),p)),y={run_id:m,node_id:m,grantee:e.grantee,grant:e.grant,rev:e.rev,scopes:t.scopes,payer:_,status:"running",created_at:D.created,metadata:t.metadata,sequence_number:0},await ie(p,m,m,"run.started",{scopes:t.scopes,payer:_,usage:Y()}),o){const R=await we(p,m,e.grantee,o,!1);y.message={run_id:m,message_id:R.messageId,sequence_number:R.sequence}}s&&await fe(p,e.grantee,s,i,m,y,D.created)},"withinCreate")},t.output_schema===void 0?void 0:async p=>{await se(p,"terminal",t.output_schema)})}catch(a){if(await O(m)||(E.cleanup(),o?.cleanup()),a instanceof ge)return a.winner.response;if(t.model!==void 0&&a instanceof Me&&a.code==="usage")throw S(a.message,"model");if(a instanceof De&&(a.code==="isolated_run"||a.code==="invalid_request")){const p=a.details&&typeof a.details=="object"&&"param"in a.details&&typeof a.details.param=="string"?a.details.param:void 0;throw new N(a.status,a.code,a.message,p)}throw a}if(!y)throw new Error("run creation did not commit");return y}u(An,"startRun");async function Tn(e,t){let s;if(t.after)try{const r=JSON.parse(Buffer.from(t.after,"base64url").toString());if(!r||typeof r!="object"||!("activity"in r)||typeof r.activity!="string"||!("id"in r)||typeof r.id!="string")throw new Error("invalid cursor");s={activity:r.activity,id:r.id}}catch{throw S("invalid list cursor","after")}const n=await(await I()).prepareUncached(`SELECT node_id, COALESCE(last_activity_at, created) AS activity FROM node_rows
9
9
  WHERE run_id = node_id AND deleting_at IS NULL ${e===null?"":"AND grantee = ?"}
10
- ORDER BY activity DESC, node_id DESC`).all(...e===null?[]:[e]),d=await me(await k((await me(n,async({node_id:r})=>!t.visible||await t.visible(r))).filter(({activity:r,node_id:c})=>!s||r<s.activity||r===s.activity&&c<s.id),async r=>({root:await O(r.node_id),activity:r.activity})),async({root:r})=>!t.status||await C(r)===t.status),l=d.slice(0,t.limit),i=l.at(-1);return{runs:await k(l,async({root:r})=>await x(r)),next:d.length>t.limit&&i?Buffer.from(JSON.stringify({activity:i.activity,id:i.root.node_id})).toString("base64url"):null}}u(In,"listRuns");async function An(e,t,s){const n=await F(e,t),d=u(async()=>await x(await F(e,t)),"read");return!s||pe.includes(await C(n))?await d():new Promise((l,i)=>{let r=u(()=>{},"unsubscribe");const c=u(()=>{clearTimeout(o),clearInterval(g),r()},"settle"),m=u(async()=>{c();try{l(await d())}catch(_){i(_)}},"finish"),h=u(async()=>{pe.includes(String((await d()).status))&&await m()},"check"),E=u(()=>{h().catch(_=>{c(),i(_)})},"run"),g=setInterval(E,250),o=setTimeout(()=>{m()},s*1e3);mt(e,E).then(_=>{r=_},_=>{c(),i(_)})})}u(An,"waitRun");async function Tn(e,t,s=le,n=!0,d){const l=e.status==="canceled"||e.outcome_reason==="closed",i=e.grantee??"app:terminal",r=G("sha256").update(JSON.stringify(["run.message",e.node_id,t,n])).digest("hex"),c=u(async g=>{const o=await O(e.node_id),_=await(await I()).prepare("SELECT 1 FROM deliveries WHERE message_id = ? AND state = 'pending' AND canceled_at IS NULL").get(String(g.message_id));return o&&!l&&n&&!o.pi_pid&&_&&await P(e.node_id,{resume:!0,capacity:"freeze"}),g},"replay");if(d){const g=await q(i,d,r);if(g)return c(g.response)}if(l)throw new N(409,"run_settled","canceled run cannot receive a message");const m=await s(e.node_id,i,t),h=n&&e.final_report&&!e.pi_pid?(await import("../runtime/reopen.js")).commitReopenResident:void 0;let E;try{E=await W(async g=>{if(d){const y=await q(i,d,r,g);if(y)return{replayed:y.response}}await h?.(e.node_id);const o=await we(g,e.node_id,i,m,n),_={run_id:e.node_id,message_id:o.messageId,sequence_number:o.sequence};return d&&await fe(g,i,d,r,e.node_id,_,new Date().toISOString()),{result:_}})}catch(g){throw m.cleanup(),g}return"replayed"in E?(m.cleanup(),c(E.replayed)):(n&&!e.pi_pid&&await P(e.node_id,{resume:!0,capacity:"freeze"}),E.result)}u(Tn,"messageRun");async function St(e){const t=u(()=>new N(503,"service_unavailable",`node ${e} broker was unavailable before the interrupt reached it`),"unavailable");for(let s=0;s<2;s++){const n=await O(e);if(!n)throw J();if(!ee(n)){if(n.streaming)throw t();return}const d=(await V(e))?.executionId;try{await ze(e)==="turn"&&ue({nodes:[e]});return}catch(l){if(!(l instanceof Ge))throw l;const i=await O(e);if(!i)throw J();if(!i.streaming)return;if(s===0&&ee(i)&&(await V(e))?.executionId!==d)continue;throw t()}}throw t()}u(St,"interruptNode");async function Ln(e,t="stop"){const s=t==="stop"?(await(await I()).prepare("SELECT node_id FROM nodes WHERE run_id = ?").all(e.node_id)).map(({node_id:n})=>n):[e.node_id];return await Promise.all(s.map(n=>St(n))),await x(await O(e.node_id)??e)}u(Ln,"interruptRun");async function Cn(e,t={}){const s=(await(await I()).prepare("SELECT node_id FROM nodes WHERE run_id = ? ORDER BY created DESC").all(e.node_id)).map(i=>i.node_id);ue({run:e.node_id});const n=[];if(await C(e)!=="settled")n.push(...(await ae(e.node_id,{preserveEmpty:!0,stopReason:"run_canceled"})).closed);else for(const i of s){if(i===e.node_id)continue;const r=await O(i);r&&r.status!=="done"&&r.status!=="canceled"&&r.status!=="dead"&&n.push(...(await ae(i,{preserveEmpty:!0,stopReason:"run_canceled"})).closed)}if(await re({run_id:e.node_id},"the run was canceled"),(await Promise.all([...new Set([e.node_id,...n])].map(i=>K(i,"run_canceled")))).some(i=>!i.confirmed))throw new N(500,"server_error","run process teardown could not be confirmed",void 0,void 0,void 0,!0);const l=await O(e.node_id);return await x(l??e)}u(Cn,"cancelRun");const ye=200;async function kn(e,t){if(typeof t!="string"||!t.trim())throw S("name is required","name");const s=t.trim();if(s.length>ye)throw S(`name exceeds ${ye} characters`,"name");const n=await qe(e.node_id,{name:s});return n.window&&Ue(n.window,He(n)),await x(n)}u(kn,"renameRun");async function xn(e,t={config:gt,client:wt}){await re({run_id:e.node_id},"the run was deleted");const s=await W(async c=>(await c.prepare("UPDATE nodes SET deleting_at = COALESCE(deleting_at, ?) WHERE node_id = ?").run(new Date().toISOString(),e.node_id),(await c.prepare("SELECT node_id FROM nodes WHERE run_id = ? ORDER BY created DESC").all(e.node_id)).map(m=>m.node_id)));if((await Promise.all(s.map(c=>K(c,"run_deleted",{deleting:!0})))).some(c=>!c.confirmed))throw new N(500,"server_error","run process teardown could not be confirmed",void 0,void 0,void 0,!0);const d=await k(s,async c=>{const m=await We(c);if(m.app===null)throw new Error(`run node missing during deletion: ${c}`);return m}),l=await(await I()).prepare("SELECT DISTINCT session_id FROM sessions WHERE session_id IS NOT NULL AND node_id IN (SELECT node_id FROM nodes WHERE run_id = ?)").all(e.node_id);for(const{session_id:c}of l)try{await Z().delete(c)}catch(m){if(!Be(m))throw m}const i=await W(async c=>{for(const g of s){const o=await xe(c,g);o!==null&&o.deleted_at===null&&await ke(c,o)}const m=await yt([e.node_id],c);await c.prepare("DELETE FROM run_events WHERE run_id = ?").run(e.node_id),await c.prepare("DELETE FROM idempotency_keys WHERE run_id = ?").run(e.node_id);const h=s.map(()=>"?").join(","),E=await c.prepareUncached(`SELECT message_id FROM messages WHERE sender_node_id IN (${h}) UNION SELECT message_id FROM deliveries WHERE destination_id IN (${h})`).all(...s,...s);await c.prepareUncached(`DELETE FROM deliveries WHERE destination_id IN (${h})`).run(...s);for(const{message_id:g}of E)await c.prepare("DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)").run(g,g);return m});for(const c of d)await Fe(c);const{notifyRunDeleted:r}=await import("./events.js");return r(e.node_id),{uploads_pending:await Nt(i,t)}}u(xn,"deleteRun");async function Nt(e,t){if(!e.length||!t.config())return e.length;let s;try{s=t.client()}catch{return e.length}const n=new Set,d=e.map(i=>s.deleteUpload(i).then(async()=>{await Et(i),n.add(i)},()=>{}));let l;return await Promise.race([Promise.all(d),new Promise(i=>{l=setTimeout(i,t.budgetMs??ht),l.unref?.()})]),clearTimeout(l),e.length-n.size}u(Nt,"deleteReleasedUploads");async function Wn(e,t,s){await F(e,t),await Q(e);const n=await W(async o=>{const _=await o.prepare("SELECT node_id, parent FROM nodes WHERE run_id = ? ORDER BY CASE WHEN node_id = ? THEN 0 ELSE 1 END, created, node_id").all(e,e),y=new Map;for(const R of _)y.set(R.node_id,(await o.prepare("SELECT session_id FROM sessions WHERE node_id = ? GROUP BY session_id ORDER BY min(created_at)").all(R.node_id)).map(A=>A.session_id));const p=(await o.prepare("SELECT event_cursor FROM nodes WHERE node_id = ?").get(e)).event_cursor-1,D=await o.prepare(`SELECT node_id, sequence_number FROM run_events
10
+ ORDER BY activity DESC, node_id DESC`).all(...e===null?[]:[e]),d=await me(await k((await me(n,async({node_id:r})=>!t.visible||await t.visible(r))).filter(({activity:r,node_id:c})=>!s||r<s.activity||r===s.activity&&c<s.id),async r=>({root:await O(r.node_id),activity:r.activity})),async({root:r})=>!t.status||await C(r)===t.status),l=d.slice(0,t.limit),i=l.at(-1);return{runs:await k(l,async({root:r})=>await x(r)),next:d.length>t.limit&&i?Buffer.from(JSON.stringify({activity:i.activity,id:i.root.node_id})).toString("base64url"):null}}u(Tn,"listRuns");async function Ln(e,t,s){const n=await F(e,t),d=u(async()=>await x(await F(e,t)),"read");return!s||pe.includes(await C(n))?await d():new Promise((l,i)=>{let r=u(()=>{},"unsubscribe");const c=u(()=>{clearTimeout(o),clearInterval(g),r()},"settle"),m=u(async()=>{c();try{l(await d())}catch(_){i(_)}},"finish"),h=u(async()=>{pe.includes(String((await d()).status))&&await m()},"check"),E=u(()=>{h().catch(_=>{c(),i(_)})},"run"),g=setInterval(E,250),o=setTimeout(()=>{m()},s*1e3);lt(e,E).then(_=>{r=_},_=>{c(),i(_)})})}u(Ln,"waitRun");async function Cn(e,t,s=le,n=!0,d){const l=e.status==="canceled"||e.outcome_reason==="closed",i=e.grantee??"app:terminal",r=G("sha256").update(JSON.stringify(["run.message",e.node_id,t,n])).digest("hex"),c=u(async g=>{const o=await O(e.node_id),_=await(await I()).prepare("SELECT 1 FROM deliveries WHERE message_id = ? AND state = 'pending' AND canceled_at IS NULL").get(String(g.message_id));return o&&!l&&n&&!o.pi_pid&&_&&await P(e.node_id,{resume:!0,capacity:"freeze"}),g},"replay");if(d){const g=await q(i,d,r);if(g)return c(g.response)}if(l)throw new N(409,"run_settled","canceled run cannot receive a message");const m=await s(e.node_id,i,t),h=n&&e.final_report&&!e.pi_pid?(await import("../runtime/reopen.js")).commitReopenResident:void 0;let E;try{E=await W(async g=>{if(d){const y=await q(i,d,r,g);if(y)return{replayed:y.response}}await h?.(e.node_id);const o=await we(g,e.node_id,i,m,n),_={run_id:e.node_id,message_id:o.messageId,sequence_number:o.sequence};return d&&await fe(g,i,d,r,e.node_id,_,new Date().toISOString()),{result:_}})}catch(g){throw m.cleanup(),g}return"replayed"in E?(m.cleanup(),c(E.replayed)):(n&&!e.pi_pid&&await P(e.node_id,{resume:!0,capacity:"freeze"}),E.result)}u(Cn,"messageRun");async function Nt(e){const t=u(()=>new N(503,"service_unavailable",`node ${e} broker was unavailable before the interrupt reached it`),"unavailable");for(let s=0;s<2;s++){const n=await O(e);if(!n)throw J();if(!ee(n)){if(n.streaming)throw t();return}const d=(await V(e))?.executionId;try{await Qe(e)==="turn"&&ue({nodes:[e]});return}catch(l){if(!(l instanceof ze))throw l;const i=await O(e);if(!i)throw J();if(!i.streaming)return;if(s===0&&ee(i)&&(await V(e))?.executionId!==d)continue;throw t()}}throw t()}u(Nt,"interruptNode");async function kn(e,t="stop"){const s=t==="stop"?(await(await I()).prepare("SELECT node_id FROM nodes WHERE run_id = ?").all(e.node_id)).map(({node_id:n})=>n):[e.node_id];return await Promise.all(s.map(n=>Nt(n))),await x(await O(e.node_id)??e)}u(kn,"interruptRun");async function xn(e,t={}){const s=(await(await I()).prepare("SELECT node_id FROM nodes WHERE run_id = ? ORDER BY created DESC").all(e.node_id)).map(i=>i.node_id);ue({run:e.node_id});const n=[];if(await C(e)!=="settled")n.push(...(await ae(e.node_id,{preserveEmpty:!0,stopReason:"run_canceled"})).closed);else for(const i of s){if(i===e.node_id)continue;const r=await O(i);r&&r.status!=="done"&&r.status!=="canceled"&&r.status!=="dead"&&n.push(...(await ae(i,{preserveEmpty:!0,stopReason:"run_canceled"})).closed)}if(await re({run_id:e.node_id},"the run was canceled"),(await Promise.all([...new Set([e.node_id,...n])].map(i=>K(i,"run_canceled")))).some(i=>!i.confirmed))throw new N(500,"server_error","run process teardown could not be confirmed",void 0,void 0,void 0,!0);const l=await O(e.node_id);return await x(l??e)}u(xn,"cancelRun");const ye=200;async function Wn(e,t){if(typeof t!="string"||!t.trim())throw S("name is required","name");const s=t.trim();if(s.length>ye)throw S(`name exceeds ${ye} characters`,"name");const n=await qe(e.node_id,{name:s});return n.window&&Ue(n.window,He(n)),await x(n)}u(Wn,"renameRun");async function Fn(e,t={config:Et,client:gt}){await re({run_id:e.node_id},"the run was deleted");const s=await W(async c=>(await c.prepare("UPDATE nodes SET deleting_at = COALESCE(deleting_at, ?) WHERE node_id = ?").run(new Date().toISOString(),e.node_id),(await c.prepare("SELECT node_id FROM nodes WHERE run_id = ? ORDER BY created DESC").all(e.node_id)).map(m=>m.node_id)));if((await Promise.all(s.map(c=>K(c,"run_deleted",{deleting:!0})))).some(c=>!c.confirmed))throw new N(500,"server_error","run process teardown could not be confirmed",void 0,void 0,void 0,!0);const d=await k(s,async c=>{const m=await We(c);if(m.app===null)throw new Error(`run node missing during deletion: ${c}`);return m}),l=await(await I()).prepare("SELECT DISTINCT session_id FROM sessions WHERE session_id IS NOT NULL AND node_id IN (SELECT node_id FROM nodes WHERE run_id = ?)").all(e.node_id);for(const{session_id:c}of l)try{await Z().delete(c)}catch(m){if(!Be(m))throw m}const i=await W(async c=>{for(const g of s){const o=await xe(c,g);o!==null&&o.deleted_at===null&&await ke(c,o)}const m=await ht([e.node_id],c);await c.prepare("DELETE FROM run_events WHERE run_id = ?").run(e.node_id),await c.prepare("DELETE FROM idempotency_keys WHERE run_id = ?").run(e.node_id);const h=s.map(()=>"?").join(","),E=await c.prepareUncached(`SELECT message_id FROM messages WHERE sender_node_id IN (${h}) UNION SELECT message_id FROM deliveries WHERE destination_id IN (${h})`).all(...s,...s);await c.prepareUncached(`DELETE FROM deliveries WHERE destination_id IN (${h})`).run(...s);for(const{message_id:g}of E)await c.prepare("DELETE FROM messages WHERE message_id = ? AND NOT EXISTS (SELECT 1 FROM deliveries WHERE message_id = ?)").run(g,g);return m});for(const c of d)await Fe(c);const{notifyRunDeleted:r}=await import("./events.js");return r(e.node_id),{uploads_pending:await vt(i,t)}}u(Fn,"deleteRun");async function vt(e,t){if(!e.length||!t.config())return e.length;let s;try{s=t.client()}catch{return e.length}const n=new Set,d=e.map(i=>s.deleteUpload(i).then(async()=>{await yt(i),n.add(i)},()=>{}));let l;return await Promise.race([Promise.all(d),new Promise(i=>{l=setTimeout(i,t.budgetMs??Rt),l.unref?.()})]),clearTimeout(l),e.length-n.size}u(vt,"deleteReleasedUploads");async function qn(e,t,s){await F(e,t),await Q(e);const n=await W(async o=>{const _=await o.prepare("SELECT node_id, parent FROM nodes WHERE run_id = ? ORDER BY CASE WHEN node_id = ? THEN 0 ELSE 1 END, created, node_id").all(e,e),y=new Map;for(const R of _)y.set(R.node_id,(await o.prepare("SELECT session_id FROM sessions WHERE node_id = ? GROUP BY session_id ORDER BY min(created_at)").all(R.node_id)).map(A=>A.session_id));const p=(await o.prepare("SELECT event_cursor FROM nodes WHERE node_id = ?").get(e)).event_cursor-1,D=await o.prepare(`SELECT node_id, sequence_number FROM run_events
11
11
  WHERE run_id = ? AND sequence_number <= ? AND type IN ('node.thinking.done', 'node.output_text.done', 'usage')
12
12
  ORDER BY sequence_number`).all(e,p),v=await o.prepare(`SELECT node_id, sequence_number, type, body_json FROM run_events
13
13
  WHERE run_id = ? AND sequence_number <= ? AND type IN ('node.thinking.delta', 'node.output_text.delta')
14
- ORDER BY sequence_number`).all(e,p);return{nodes:_,sessionIds:y,latestSequence:p,completed:D,deltas:v}}),{nodes:d,sessionIds:l,latestSequence:i,completed:r,deltas:c}=n,m=await k(d,async o=>({node_id:o.node_id,parent_node_id:o.parent,conversations:await k(l.get(o.node_id)??[],async _=>{const{header:y,entries:a}=await Z().read(_);if(s==="entries")return{conversation_id:_,header:y,entries:a};const p=Ce.inMemory(y.cwd,void 0,[y,...a]),D=it(p)??[],v=new Map(a.map((w,b)=>[w.id,b])),R=a.filter(w=>w.type==="custom"&&w.customType==="crtr-system-prompt").map(w=>({id:w.id,message:{role:"custom",customType:"crtr-system-prompt",data:"data"in w?w.data:void 0,timestamp:w.timestamp}})),A=a.filter(w=>w.type==="custom_message"&&w.customType==="app_message").map(w=>({id:w.id,message:{role:"custom",customType:"app_message",content:"content"in w?w.content:"",timestamp:w.timestamp}})),he=o.node_id===e?a.find(w=>{if(w.type!=="message"||w.message.role!=="user")return!1;const b=te(a,w.message.timestamp);return b?.type==="runtime"&&b.kind==="kickoff"})?.id:void 0,Re=[...D.filter(({id:w})=>!A.some(b=>b.id===w)),...R,...A].sort((w,b)=>(v.get(w.id)??Number.MAX_SAFE_INTEGER)-(v.get(b.id)??Number.MAX_SAFE_INTEGER)).map(({id:w,message:b})=>{const f=b,T=f.usage,H=f.role==="user"?te(a,Number(f.timestamp)):null,Se=H?.type==="human"||w===he,L=f.role==="toolResult"?"tool_result":f.role==="bashExecution"?"bash_execution":f.role==="compactionSummary"?"compaction_summary":f.role==="branchSummary"?"branch_summary":f.role==="custom"&&f.customType==="crtr-system-prompt"?"system":f.role==="user"?Se?"user":"custom":f.role,Ne=L==="custom"?f.role==="custom"?f.customType:H?.type==="runtime"?H.kind:"unknown":void 0,ve=L==="system"&&f.customType==="crtr-system-prompt"?[{type:"text",text:f.data?.text??""}]:typeof f.content=="string"?[{type:"text",text:f.content}]:Array.isArray(f.content)?f.content.map(M=>M.type==="toolCall"?{type:"tool_call",tool_call_id:M.id,name:M.name,arguments:M.arguments}:M.type==="image"?{type:"image",mime_type:M.mimeType}:M.type==="thinking"?{type:"thinking",text:M.thinking}:M):[];return{entry_id:w,role:L,...L==="custom"?{custom_type:Ne}:{},time:typeof f.timestamp=="string"?f.timestamp:new Date(Number(f.timestamp??Date.now())).toISOString(),content:ve,...L==="assistant"?{model:f.model,provider:f.provider,stop_reason:f.stopReason,...f.errorMessage?{error_message:f.errorMessage}:{},...T?{usage:{input_tokens:T.input??0,output_tokens:T.output??0,cache_read_tokens:T.cacheRead??0,cache_write_tokens:T.cacheWrite??0,cost_usd:T.cost?.total??0}}:{}}:{},...L==="tool_result"?{tool_call_id:f.toolCallId,tool_name:f.toolName,is_error:f.isError}:{}}})??[];return{conversation_id:_,messages:Re}})})),h=new Map(m.map(o=>[o.node_id,ct(o.node_id)]));for(const o of r){const _=h.get(o.node_id);_!=null&&o.sequence_number>=_&&h.set(o.node_id,null)}const E=new Map;for(const o of c){const _=h.get(o.node_id);if(_==null||o.sequence_number<_)continue;const y=JSON.parse(o.body_json),a=E.get(o.node_id)??{thinking:"",text:""};o.type==="node.thinking.delta"?a.thinking+=y.delta:a.text+=y.delta,E.set(o.node_id,a)}const g=m.map(o=>({...o,in_progress_message:h.get(o.node_id)==null?null:E.get(o.node_id)??{thinking:"",text:""}}));return{run_id:e,latest_sequence:i,nodes:g}}u(Wn,"traceRun");function Fn(e,t,s){if(t===void 0){if(!e.some(n=>U(n,"crtr:llm")))throw new N(403,"scope_missing","missing scope crtr:llm",void 0,void 0,"crtr:llm");return null}if(!Array.isArray(t)||t.some(n=>typeof n!="string"))throw S("scopes must be a list of scope strings","scopes");for(const n of t){try{ut(n)}catch{throw S(`invalid scope ${n}`,"scopes")}if(/^crtr:files:(?:read|rw):app:/.test(n)&&n.split(":")[4]===s.slice(4))throw S(`own-app files cannot narrow a run: ${n}`,"scopes");if(!(/^crtr:memory:(?:read|rw):app:/.test(n)&&n.split(":")[4]===s.slice(4))&&!be(e,n))throw new N(403,"scope_missing",`missing scope ${n}`,void 0,void 0,n)}if(!t.includes("crtr:llm"))throw new N(403,"scope_missing","missing scope crtr:llm",void 0,void 0,"crtr:llm");return t}u(Fn,"requestedScopes");export{X as IDEMPOTENCY_WINDOW_MS,Rt as RUN_DEADLINE_MAX_MS,ht as RUN_DELETE_STORAGE_BUDGET_MS,ye as RUN_NAME_MAX,N as RunError,Cn as cancelRun,xn as deleteRun,bn as idempotencyKeyOf,Ln as interruptRun,In as listRuns,Tn as messageRun,F as ownedRun,Ee as parseRunDeadlineMs,Dn as pruneExpiredIdempotencyKeys,kn as renameRun,Fn as requestedScopes,On as runIsRunning,x as runObject,Mn as startRun,Wn as traceRun,An as waitRun};
14
+ ORDER BY sequence_number`).all(e,p);return{nodes:_,sessionIds:y,latestSequence:p,completed:D,deltas:v}}),{nodes:d,sessionIds:l,latestSequence:i,completed:r,deltas:c}=n,m=await k(d,async o=>({node_id:o.node_id,parent_node_id:o.parent,conversations:await k(l.get(o.node_id)??[],async _=>{const{header:y,entries:a}=await Z().read(_);if(s==="entries")return{conversation_id:_,header:y,entries:a};const p=Ce.inMemory(y.cwd,void 0,[y,...a]),D=rt(p)??[],v=new Map(a.map((w,b)=>[w.id,b])),R=a.filter(w=>w.type==="custom"&&w.customType==="crtr-system-prompt").map(w=>({id:w.id,message:{role:"custom",customType:"crtr-system-prompt",data:"data"in w?w.data:void 0,timestamp:w.timestamp}})),A=a.filter(w=>w.type==="custom_message"&&w.customType==="app_message").map(w=>({id:w.id,message:{role:"custom",customType:"app_message",content:"content"in w?w.content:"",timestamp:w.timestamp}})),he=o.node_id===e?a.find(w=>{if(w.type!=="message"||w.message.role!=="user")return!1;const b=te(a,w.message.timestamp);return b?.type==="runtime"&&b.kind==="kickoff"})?.id:void 0,Re=[...D.filter(({id:w})=>!A.some(b=>b.id===w)),...R,...A].sort((w,b)=>(v.get(w.id)??Number.MAX_SAFE_INTEGER)-(v.get(b.id)??Number.MAX_SAFE_INTEGER)).map(({id:w,message:b})=>{const f=b,T=f.usage,H=f.role==="user"?te(a,Number(f.timestamp)):null,Se=H?.type==="human"||w===he,L=f.role==="toolResult"?"tool_result":f.role==="bashExecution"?"bash_execution":f.role==="compactionSummary"?"compaction_summary":f.role==="branchSummary"?"branch_summary":f.role==="custom"&&f.customType==="crtr-system-prompt"?"system":f.role==="user"?Se?"user":"custom":f.role,Ne=L==="custom"?f.role==="custom"?f.customType:H?.type==="runtime"?H.kind:"unknown":void 0,ve=L==="system"&&f.customType==="crtr-system-prompt"?[{type:"text",text:f.data?.text??""}]:typeof f.content=="string"?[{type:"text",text:f.content}]:Array.isArray(f.content)?f.content.map(M=>M.type==="toolCall"?{type:"tool_call",tool_call_id:M.id,name:M.name,arguments:M.arguments}:M.type==="image"?{type:"image",mime_type:M.mimeType}:M.type==="thinking"?{type:"thinking",text:M.thinking}:M):[];return{entry_id:w,role:L,...L==="custom"?{custom_type:Ne}:{},time:typeof f.timestamp=="string"?f.timestamp:new Date(Number(f.timestamp??Date.now())).toISOString(),content:ve,...L==="assistant"?{model:f.model,provider:f.provider,stop_reason:f.stopReason,...f.errorMessage?{error_message:f.errorMessage}:{},...T?{usage:{input_tokens:T.input??0,output_tokens:T.output??0,cache_read_tokens:T.cacheRead??0,cache_write_tokens:T.cacheWrite??0,cost_usd:T.cost?.total??0}}:{}}:{},...L==="tool_result"?{tool_call_id:f.toolCallId,tool_name:f.toolName,is_error:f.isError}:{}}})??[];return{conversation_id:_,messages:Re}})})),h=new Map(m.map(o=>[o.node_id,ut(o.node_id)]));for(const o of r){const _=h.get(o.node_id);_!=null&&o.sequence_number>=_&&h.set(o.node_id,null)}const E=new Map;for(const o of c){const _=h.get(o.node_id);if(_==null||o.sequence_number<_)continue;const y=JSON.parse(o.body_json),a=E.get(o.node_id)??{thinking:"",text:""};o.type==="node.thinking.delta"?a.thinking+=y.delta:a.text+=y.delta,E.set(o.node_id,a)}const g=m.map(o=>({...o,in_progress_message:h.get(o.node_id)==null?null:E.get(o.node_id)??{thinking:"",text:""}}));return{run_id:e,latest_sequence:i,nodes:g}}u(qn,"traceRun");function Hn(e,t,s){if(t===void 0){if(!e.some(n=>U(n,"crtr:llm")))throw new N(403,"scope_missing","missing scope crtr:llm",void 0,void 0,"crtr:llm");return null}if(!Array.isArray(t)||t.some(n=>typeof n!="string"))throw S("scopes must be a list of scope strings","scopes");for(const n of t){try{mt(n)}catch{throw S(`invalid scope ${n}`,"scopes")}if(/^crtr:files:(?:read|rw):app:/.test(n)&&n.split(":")[4]===s.slice(4))throw S(`own-app files cannot narrow a run: ${n}`,"scopes");if(!(/^crtr:memory:(?:read|rw):app:/.test(n)&&n.split(":")[4]===s.slice(4))&&!be(e,n))throw new N(403,"scope_missing",`missing scope ${n}`,void 0,void 0,n)}if(!t.includes("crtr:llm"))throw new N(403,"scope_missing","missing scope crtr:llm",void 0,void 0,"crtr:llm");return t}u(Hn,"requestedScopes");export{X as IDEMPOTENCY_WINDOW_MS,St as RUN_DEADLINE_MAX_MS,Rt as RUN_DELETE_STORAGE_BUDGET_MS,ye as RUN_NAME_MAX,N as RunError,xn as cancelRun,Fn as deleteRun,Mn as idempotencyKeyOf,kn as interruptRun,Tn as listRuns,Cn as messageRun,F as ownedRun,Ee as parseRunDeadlineMs,In as pruneExpiredIdempotencyKeys,Wn as renameRun,Hn as requestedScopes,Dn as runIsRunning,x as runObject,An as startRun,qn as traceRun,Ln as waitRun};
@@ -7,6 +7,10 @@ description: Behavior changes in @north-light/sdk that an app must act on, newes
7
7
 
8
8
  Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
9
9
 
10
+ ## 0.3.554
11
+
12
+ `RunObject.recap` (`runs.get`, `runs.list`): the root's latest idle recap, `{ goal, state, at }`, where `goal` says what the conversation is for and `state` where it stands. The runtime writes it after a turn once the root goes idle; it is absent before the first one. Additive.
13
+
10
14
  ## 0.3.552
11
15
 
12
16
  One ledger records every charge. Additive, except that the per-app cap now counts more.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.552",
3
+ "version": "0.3.554",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -71,6 +71,7 @@
71
71
  "scripts/patch-pi-tree-root-ordering.mjs",
72
72
  "scripts/install-runtime.mjs",
73
73
  "scripts/lib/isolation-env.mjs",
74
+ "scripts/lib/canvas-schema-gate.mjs",
74
75
  "scripts/lib/pack-args.mjs",
75
76
  "scripts/lib/update-target.mjs",
76
77
  "scripts/lib/runtime-deps-key.mjs",
@@ -140,7 +141,7 @@
140
141
  },
141
142
  "license": "UNLICENSED",
142
143
  "dependencies": {
143
- "@north-light/identity": "0.3.552",
144
+ "@north-light/identity": "0.3.554",
144
145
  "@earendil-works/pi-agent-core": "1.1.0",
145
146
  "@earendil-works/pi-ai": "1.1.0",
146
147
  "@earendil-works/pi-coding-agent": "1.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.552",
3
+ "version": "0.3.554",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.552",
3
+ "version": "0.3.554",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.552",
9
+ "version": "0.3.554",
10
10
  "hasInstallScript": true,
11
11
  "license": "UNLICENSED",
12
12
  "workspaces": [
@@ -24,7 +24,7 @@
24
24
  "@milkdown/preset-gfm": "^7.22.3",
25
25
  "@milkdown/prose": "^7.22.3",
26
26
  "@milkdown/utils": "^7.22.3",
27
- "@north-light/identity": "0.3.552",
27
+ "@north-light/identity": "0.3.554",
28
28
  "cron-parser": "^5.6.0",
29
29
  "esbuild": "^0.27.7",
30
30
  "jose": "^6.2.12",
@@ -12455,17 +12455,17 @@
12455
12455
  },
12456
12456
  "packages/crouter-api": {
12457
12457
  "name": "@north-light/api",
12458
- "version": "0.3.552",
12458
+ "version": "0.3.554",
12459
12459
  "license": "UNLICENSED"
12460
12460
  },
12461
12461
  "packages/crouter-env-docker": {
12462
12462
  "name": "@north-light/env-docker",
12463
- "version": "0.3.552",
12463
+ "version": "0.3.554",
12464
12464
  "license": "UNLICENSED"
12465
12465
  },
12466
12466
  "packages/crouter-identity": {
12467
12467
  "name": "@north-light/identity",
12468
- "version": "0.3.552",
12468
+ "version": "0.3.554",
12469
12469
  "license": "UNLICENSED",
12470
12470
  "dependencies": {
12471
12471
  "jose": "^6.2.1"
@@ -12473,21 +12473,21 @@
12473
12473
  },
12474
12474
  "packages/crouter-plugin": {
12475
12475
  "name": "@north-light/plugin",
12476
- "version": "0.3.552",
12476
+ "version": "0.3.554",
12477
12477
  "license": "UNLICENSED",
12478
12478
  "dependencies": {
12479
- "@north-light/api": "0.3.552",
12480
- "@north-light/identity": "0.3.552"
12479
+ "@north-light/api": "0.3.554",
12480
+ "@north-light/identity": "0.3.554"
12481
12481
  }
12482
12482
  },
12483
12483
  "packages/crouter-sdk": {
12484
12484
  "name": "@north-light/sdk",
12485
- "version": "0.3.552",
12485
+ "version": "0.3.554",
12486
12486
  "license": "UNLICENSED",
12487
12487
  "dependencies": {
12488
- "@north-light/api": "0.3.552",
12489
- "@north-light/identity": "0.3.552",
12490
- "@north-light/plugin": "0.3.552",
12488
+ "@north-light/api": "0.3.554",
12489
+ "@north-light/identity": "0.3.554",
12490
+ "@north-light/plugin": "0.3.554",
12491
12491
  "jose": "^6.2.1"
12492
12492
  },
12493
12493
  "bin": {
@@ -13,6 +13,7 @@ import { smokeValidate } from './runtime-smoke.mjs';
13
13
  import { scrubInheritedBrokerEnv, unsafeIsolationReason } from './lib/isolation-env.mjs';
14
14
  import { packArgs } from './lib/pack-args.mjs';
15
15
  import { runtimeDepsKeyFromFiles } from './lib/runtime-deps-key.mjs';
16
+ import { assertCandidateMayOpenLiveDatabase, candidateSchemaVersion } from './lib/canvas-schema-gate.mjs';
16
17
 
17
18
  scrubInheritedBrokerEnv();
18
19
  // CI has no account store to protect, and its checkout sits inside the runner's
@@ -198,18 +199,11 @@ async function assertCanvasSchemaCompatible(stage) {
198
199
  }
199
200
  }
200
201
 
201
- /** The candidate's Postgres canvas schema version, read from its own `schema-version.js`. */
202
- async function candidatePostgresSchemaVersion(stage) {
203
- const { CANVAS_SCHEMA_VERSION: version } = await import(pathToFileURL(join(stage, 'dist', 'core', 'canvas', 'schema-version.js')).href);
204
- if (!Number.isSafeInteger(version) || version < 1) throw new Error('candidate runtime does not expose a valid canvas schema version');
205
- return version;
206
- }
207
-
208
- /** Read `canvas_meta.schema_version` from the live Postgres cluster and refuse a candidate older than it. A cluster that
209
- * is not running cannot be read, so the candidate is refused until the cluster is started. */
202
+ /** Refuse a candidate whose version is below `canvas_meta.schema_min_reader` (absent: `schema_version`) in the live Postgres
203
+ * cluster, read from `CRTR_DATABASE_SCHEMA` as the daemon reads it (`scripts/lib/canvas-schema-gate.mjs`). A cluster that is
204
+ * not running cannot be read, so the candidate is refused until the cluster is started. */
210
205
  async function assertPostgresSchemaCompatible(stage, runtime) {
211
206
  const { default: pg } = await import('pg');
212
- const supported = await candidatePostgresSchemaVersion(stage);
213
207
  let socketDir = join(runtime, 'pg');
214
208
  try { socketDir = (await readFile(join(runtime, 'pg', 'socket-dir'), 'utf8')).trim(); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
215
209
  const connectionString = process.env.CRTR_DATABASE_URL
@@ -220,22 +214,14 @@ async function assertPostgresSchemaCompatible(stage, runtime) {
220
214
  } catch (error) {
221
215
  if (!process.env.CRTR_DATABASE_URL && (error?.code === 'ENOENT' || error?.code === 'ECONNREFUSED')) {
222
216
  throw new Error(
223
- `refusing to select runtime: the Postgres cluster at ${socketDir} is not running, so its canvas schema version cannot be checked against this build (${supported}). ` +
217
+ `refusing to select runtime: the Postgres cluster at ${socketDir} is not running, so its canvas schema version cannot be checked against this build (${await candidateSchemaVersion(stage)}). ` +
224
218
  'Start it with the currently selected generation (`crtr sys daemon start`) and install again.',
225
219
  );
226
220
  }
227
221
  throw error;
228
222
  }
229
223
  try {
230
- const exists = await client.query(`SELECT to_regclass('canvas_meta') AS t`);
231
- const row = exists.rows[0].t === null ? { rows: [] } : await client.query(`SELECT value FROM canvas_meta WHERE key = 'schema_version'`);
232
- const current = row.rows[0] === undefined ? undefined : Number(row.rows[0].value);
233
- if (current !== undefined && current > supported) {
234
- throw new Error(
235
- `refusing to select runtime with canvas schema ${supported}: ` +
236
- `the live database is already at version ${current}; install a descendant build instead`,
237
- );
238
- }
224
+ await assertCandidateMayOpenLiveDatabase(stage, client);
239
225
  } finally {
240
226
  await client.end();
241
227
  }
@@ -0,0 +1,20 @@
1
+ // The installer's rollback gate on the live Postgres canvas: loads the candidate generation's own schema module (its
2
+ // supported version, which Postgres schema holds the canvas — `CRTR_DATABASE_SCHEMA`, read by the same resolver the daemon
3
+ // uses — and the check itself) and refuses a candidate below the database's `schema_min_reader`. Takes a connected client so
4
+ // a test can drive it without the installer's side effects.
5
+ import { join } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ /** `client` is a connected `pg` client for the live database; `env` is the installer's environment. */
9
+ export async function assertCandidateMayOpenLiveDatabase(stage, client, env = process.env) {
10
+ const candidate = await import(pathToFileURL(join(stage, 'dist', 'core', 'canvas', 'schema-version.js')).href);
11
+ const supported = candidate.CANVAS_SCHEMA_VERSION;
12
+ if (!Number.isSafeInteger(supported) || supported < 1) throw new Error('candidate runtime does not expose a valid canvas schema version');
13
+ await candidate.assertCandidateMayOpen(client, supported, candidate.canvasDatabaseSchema(env));
14
+ }
15
+
16
+ /** The candidate's supported canvas schema version, for messages. */
17
+ export async function candidateSchemaVersion(stage) {
18
+ const candidate = await import(pathToFileURL(join(stage, 'dist', 'core', 'canvas', 'schema-version.js')).href);
19
+ return candidate.CANVAS_SCHEMA_VERSION;
20
+ }