@north-light/cli 0.3.555 → 0.3.557

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
- var v=Object.defineProperty;var d=(e,a)=>v(e,"name",{value:a,configurable:!0});import{envNodeId as w}from"../../shared/env.js";import{defineBranch as $,defineLeaf as h}from"../../core/command.js";import{InputError as r}from"../../core/io.js";import{ExitCode as T}from"../../types.js";import{existsSync as g,mkdirSync as I,rmSync as D,writeFileSync as f}from"node:fs";import{activeBackgroundBashJobs as E,backgroundRunningBashJobs as R,bashJobCommandLine as q,bashJobPaths as j,clearJobDeadline as N,formatBashElapsed as _,isSafeBashJobId as y,jobStartedAtMs as S,newBashJobId as M,readJobPgid as B,tailJobLog as L,writeJobDeadlineMs as J}from"../../core/bash-jobs.js";import{parseDurationMs as U}from"../../core/wake.js";import{cliClient as A,getNodeOrNull as u}from"../api-client.js";import{PREVIEW_RESULT_PATH_ENV as C}from"../../core/preview-result-path.js";import{nodeInPane as P}from"../surface/node/placement.js";function b(e,a){const n=e.pane??process.env.TMUX_PANE;let t=e.node;if((t===void 0||t==="")&&(t=P(n)),(t===void 0||t==="")&&(t=w()),t===void 0||t==="")throw new r({error:"no_node",message:a,next:"Run from inside a node, pass --node <id>, or --pane <pane>."});return{id:t,pane:n}}d(b,"resolveBashTarget");const O=h({name:"run",description:"start a shell command as a background job and return at once",whenToUse:"a command needs longer than one tool call \u2014 a test lane, a build, a soak \u2014 and you should be woken with its exit code instead of waiting, backgrounding with `&`, or polling a log",tier:"normal",help:{name:"node bash run",summary:"start one shell command as a background job owned by the calling node and return before it finishes",params:[{kind:"positional",name:"command",required:!0,constraint:"The shell command, run by bash from the current directory with this process\u2019s environment. The same refusals apply as for a bash tool call, judged on this command."}],output:[{name:"job_id",type:"string",required:!0,constraint:"The job object\u2019s id; read or watch it with `crtr canvas`, stop it with `node bash kill`."},{name:"log_path",type:"string",required:!0,constraint:"File receiving the command\u2019s combined stdout and stderr, capped at 10 MiB."}],outputKind:"object",effects:["Works only inside a node\u2019s bash tool call; the job belongs to the calling node.","Starts the command detached from this call, so it keeps running after the call and the turn end. A `timeout` around this leaf bounds only the launch, not the job.","Records the job as a canvas object; when the command exits, its watchers (the calling node by default) get an urgent `exited` push with the exit code and log path.","Sets no cancel deadline: the job runs to completion unless stopped with `node bash kill`.","Fails, with the job id and log path, if the job could not be recorded; the command is then running but will not message the node when it exits."]},run:d(async e=>{const a=e.command,n=w(),t=process.env.CRTR_CONTEXT_DIR;if(n===void 0||n===""||t===void 0||t==="")throw new r({error:"no_node",message:"`node bash run` starts a job for the calling node and works only inside a node\u2019s bash tool",next:"Run it from a node\u2019s bash tool call."});const s=process.cwd(),{bashCommandRefusal:i}=await import("../../core/bash-guard.js"),l=i(a,s);if(l!==null)throw new r({error:"refused",message:l,next:"Run a different command."});const o=j(t,M());I(o.dir,{recursive:!0}),f(o.cmdSh,a),f(o.jobLog,""),f(o.jobBg,"");const{launchBashJobSupervisor:c}=await import("../../core/runtime/broker/bash-job-launch.js"),{[C]:m,...k}=process.env;try{await c(n,o,t,s,k)}catch(p){throw D(o.dir,{recursive:!0,force:!0}),p}const{recordBackgroundedJob:x}=await import("../../core/runtime/broker/daemon-ops.js");try{await x(n,o.jobId,{command:a,job_dir:o.dir,log_path:o.jobLog})}catch(p){throw new r({error:"job_not_recorded",message:`job ${o.jobId} is running but could not be recorded (${p.message}); it will NOT message you when it exits. Log: ${o.jobLog}`,next:`Watch it with \`tail -f ${o.jobLog}\`, or stop it with \`crtr node bash kill ${o.jobId}\`.`},T.GENERAL)}return{job_id:o.jobId,log_path:o.jobLog}},"run"),render:d(e=>`[backgrounded as job ${e.job_id}. It will interrupt you with a message when it exits. Watch: tail -f ${e.log_path} ; stop: crtr node bash kill ${e.job_id}]`,"render")}),K=h({name:"background",description:"hand the focused node\u2019s running bash command to its background job system",whenToUse:"the tmux prefix menu invokes this when a foreground bash command should stop blocking the agent immediately but keep running as a file-backed background job",tier:"hidden",help:{name:"node bash background",summary:"hand every running foreground bash command for one node to its file-backed background job system",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node whose running bash command to background. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE. The prefix menu passes this for you."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The target node."},{name:"jobs",type:"object[]",required:!0,constraint:"Per handed-off job {job_id, elapsed_ms} \u2014 the foreground elapsed time in milliseconds at the moment of handoff."}],outputKind:"object",effects:["Signals every foreground bash command currently running for the node to return from its tool call immediately while its detached supervisor keeps the command running.","Each handed-off command retains its log under the node context jobs directory and becomes a job object; when it exits, the job\u2019s watchers (its node by default) get an urgent `exited` push carrying the exit code and log path."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash handoff"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=R(n.paths.context_dir),s=Date.now(),i=t.map(l=>({job_id:l.jobId,elapsed_ms:s-(S(l.jobId)??s)}));return{node_id:a,jobs:i}},"run"),render:d(e=>{const a=e.jobs;if(a.length===0)return`No foreground bash command is running for ${e.node_id}.`;const n=Math.max(...a.map(t=>t.elapsed_ms));return`Handed ${a.length} bash command(s) to the background job system for ${e.node_id} \xB7 ran ${_(n)} in the foreground.`},"render")}),X=h({name:"list",description:"list a node\u2019s live background bash jobs",whenToUse:"you need the ids, commands, and ages of your jobs that are still running",help:{name:"node bash list",summary:"read every still-running background bash job owned by one node",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node whose background jobs to list. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."},{kind:"flag",name:"tail",type:"int",required:!1,default:0,constraint:"Include this many trailing log lines per job (0 = none)."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The target node."},{name:"jobs",type:"object[]",required:!0,constraint:"Oldest-first {job_id, command, purpose, started_at, elapsed_ms, deadline_at, log_path, pgid, log_tail} for every live background job; purpose is a human-readable label or null; deadline_at is the auto-cancel time or null for a job that runs to completion."}],outputKind:"object",effects:["Reads the node context jobs directory. Changes nothing."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash job list"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=Date.now(),s=Math.max(0,Math.trunc(Number(e.tail??0))),i=n.paths.context_dir,l=E(i).map(o=>({job_id:o.jobId,command:o.command,purpose:o.purpose,started_at:new Date(o.startedAtMs).toISOString(),elapsed_ms:t-o.startedAtMs,deadline_at:o.deadlineAtMs===null?null:new Date(o.deadlineAtMs).toISOString(),log_path:o.logPath,pgid:B(i,o.jobId)??null,log_tail:L(o.logPath,s)}));return{node_id:n.node_id,jobs:l}},"run"),render:d(e=>{const a=e.jobs;return a.length===0?`No background bash jobs for ${e.node_id}.`:a.map(n=>`${n.job_id} \xB7 ${_(n.elapsed_ms)} \xB7 ${q(n.command)}${n.purpose===null?"":`
1
+ var $=Object.defineProperty;var i=(e,a)=>$(e,"name",{value:a,configurable:!0});import{envNodeId as _}from"../../shared/env.js";import{defineBranch as T,defineLeaf as c}from"../../core/command.js";import{InputError as r}from"../../core/io.js";import{ExitCode as I}from"../../types.js";import{existsSync as u,mkdirSync as q,rmSync as D,writeFileSync as f}from"node:fs";import{activeBackgroundBashJobs as E,backgroundRunningBashJobs as R,bashJobCommandLine as N,bashJobPaths as w,clearJobDeadline as B,formatBashElapsed as y,isSafeBashJobId as j,jobStartedAtMs as S,markJobQuietOk as k,newBashJobId as M,readJobPgid as L,tailJobLog as U,writeJobDeadlineMs as A}from"../../core/bash-jobs.js";import{parseDurationMs as J}from"../../core/wake.js";import{cliClient as O,getNodeOrNull as b}from"../api-client.js";import{PREVIEW_RESULT_PATH_ENV as P}from"../../core/preview-result-path.js";import{nodeInPane as C}from"../surface/node/placement.js";function m(e,a){const n=e.pane??process.env.TMUX_PANE;let o=e.node;if((o===void 0||o==="")&&(o=C(n)),(o===void 0||o==="")&&(o=_()),o===void 0||o==="")throw new r({error:"no_node",message:a,next:"Run from inside a node, pass --node <id>, or --pane <pane>."});return{id:o,pane:n}}i(m,"resolveBashTarget");const K=c({name:"run",description:"start a shell command as a background job and return at once",whenToUse:"a command needs longer than one tool call \u2014 a test lane, a build, a soak \u2014 and you should be woken with its exit code instead of waiting, backgrounding with `&`, or polling a log",tier:"normal",help:{name:"node bash run",summary:"start one shell command as a background job owned by the calling node and return before it finishes",params:[{kind:"positional",name:"command",required:!0,constraint:"The shell command, run by bash from the current directory with this process\u2019s environment. The same refusals apply as for a bash tool call, judged on this command."},{kind:"flag",name:"quiet-ok",type:"bool",required:!1,constraint:"Pass for a job that legitimately sits idle \u2014 `gh run watch`, `sleep`, waiting on a lock, a `docker build` whose work runs outside the process tree \u2014 so it gets no silence messages. Omit it for anything that should be making progress."}],output:[{name:"job_id",type:"string",required:!0,constraint:"The job object\u2019s id; read or watch it with `crtr canvas`, stop it with `node bash kill`."},{name:"log_path",type:"string",required:!0,constraint:"File receiving the command\u2019s combined stdout and stderr, capped at 10 MiB."}],outputKind:"object",effects:["Works only inside a node\u2019s bash tool call; the job belongs to the calling node.","Starts the command detached from this call, so it keeps running after the call and the turn end. A `timeout` around this leaf bounds only the launch, not the job.","Records the job as a canvas object; when the command exits, its watchers (the calling node by default) get an urgent `exited` push with the exit code and log path.","Sets no cancel deadline: the job runs to completion unless stopped with `node bash kill`.","A job whose processes use almost no CPU and write to no open file for 15 minutes messages the calling node about it, again at 30 and 60 minutes, then hourly; activity restarts the schedule and nothing is stopped. `--quiet-ok` turns these messages off.","Fails, with the job id and log path, if the job could not be recorded; the command is then running but will not message the node when it exits."]},run:i(async e=>{const a=e.command,n=_(),o=process.env.CRTR_CONTEXT_DIR;if(n===void 0||n===""||o===void 0||o==="")throw new r({error:"no_node",message:"`node bash run` starts a job for the calling node and works only inside a node\u2019s bash tool",next:"Run it from a node\u2019s bash tool call."});const s=process.cwd(),{bashCommandRefusal:d}=await import("../../core/bash-guard.js"),l=d(a,s);if(l!==null)throw new r({error:"refused",message:l,next:"Run a different command."});const t=w(o,M());q(t.dir,{recursive:!0}),f(t.cmdSh,a),f(t.jobLog,""),f(t.jobBg,""),e.quietOk===!0&&k(t);const{launchBashJobSupervisor:h}=await import("../../core/runtime/broker/bash-job-launch.js"),{[P]:g,...x}=process.env;try{await h(n,t,o,s,x)}catch(p){throw D(t.dir,{recursive:!0,force:!0}),p}const{recordBackgroundedJob:v}=await import("../../core/runtime/broker/daemon-ops.js");try{await v(n,t.jobId,{command:a,job_dir:t.dir,log_path:t.jobLog})}catch(p){throw new r({error:"job_not_recorded",message:`job ${t.jobId} is running but could not be recorded (${p.message}); it will NOT message you when it exits. Log: ${t.jobLog}`,next:`Watch it with \`tail -f ${t.jobLog}\`, or stop it with \`crtr node bash kill ${t.jobId}\`.`},I.GENERAL)}return{job_id:t.jobId,log_path:t.jobLog}},"run"),render:i(e=>`[backgrounded as job ${e.job_id}. It will interrupt you with a message when it exits. Watch: tail -f ${e.log_path} ; stop: crtr node bash kill ${e.job_id}]`,"render")}),X=c({name:"background",description:"hand the focused node\u2019s running bash command to its background job system",whenToUse:"the tmux prefix menu invokes this when a foreground bash command should stop blocking the agent immediately but keep running as a file-backed background job",tier:"hidden",help:{name:"node bash background",summary:"hand every running foreground bash command for one node to its file-backed background job system",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node whose running bash command to background. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE. The prefix menu passes this for you."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The target node."},{name:"jobs",type:"object[]",required:!0,constraint:"Per handed-off job {job_id, elapsed_ms} \u2014 the foreground elapsed time in milliseconds at the moment of handoff."}],outputKind:"object",effects:["Signals every foreground bash command currently running for the node to return from its tool call immediately while its detached supervisor keeps the command running.","Each handed-off command retains its log under the node context jobs directory and becomes a job object; when it exits, the job\u2019s watchers (its node by default) get an urgent `exited` push carrying the exit code and log path."]},run:i(async e=>{const{id:a}=m(e,"no node found for the bash handoff"),n=await b(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const o=R(n.paths.context_dir),s=Date.now(),d=o.map(l=>({job_id:l.jobId,elapsed_ms:s-(S(l.jobId)??s)}));return{node_id:a,jobs:d}},"run"),render:i(e=>{const a=e.jobs;if(a.length===0)return`No foreground bash command is running for ${e.node_id}.`;const n=Math.max(...a.map(o=>o.elapsed_ms));return`Handed ${a.length} bash command(s) to the background job system for ${e.node_id} \xB7 ran ${y(n)} in the foreground.`},"render")}),W=c({name:"list",description:"list a node\u2019s live background bash jobs",whenToUse:"you need the ids, commands, and ages of your jobs that are still running",help:{name:"node bash list",summary:"read every still-running background bash job owned by one node",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node whose background jobs to list. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."},{kind:"flag",name:"tail",type:"int",required:!1,default:0,constraint:"Include this many trailing log lines per job (0 = none)."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The target node."},{name:"jobs",type:"object[]",required:!0,constraint:"Oldest-first {job_id, command, purpose, started_at, elapsed_ms, deadline_at, log_path, pgid, log_tail} for every live background job; purpose is a human-readable label or null; deadline_at is the auto-cancel time or null for a job that runs to completion."}],outputKind:"object",effects:["Reads the node context jobs directory. Changes nothing."]},run:i(async e=>{const{id:a}=m(e,"no node found for the bash job list"),n=await b(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const o=Date.now(),s=Math.max(0,Math.trunc(Number(e.tail??0))),d=n.paths.context_dir,l=E(d).map(t=>({job_id:t.jobId,command:t.command,purpose:t.purpose,started_at:new Date(t.startedAtMs).toISOString(),elapsed_ms:o-t.startedAtMs,deadline_at:t.deadlineAtMs===null?null:new Date(t.deadlineAtMs).toISOString(),log_path:t.logPath,pgid:L(d,t.jobId)??null,log_tail:U(t.logPath,s)}));return{node_id:n.node_id,jobs:l}},"run"),render:i(e=>{const a=e.jobs;return a.length===0?`No background bash jobs for ${e.node_id}.`:a.map(n=>`${n.job_id} \xB7 ${y(n.elapsed_ms)} \xB7 ${N(n.command)}${n.purpose===null?"":`
2
2
  purpose: ${n.purpose}`}${n.deadline_at===null?"":`
3
3
  cancels at: ${n.deadline_at}`}
4
4
  log: ${n.log_path}`).join(`
5
- `)},"render")}),G=h({name:"kill",description:"stop one of a node\u2019s background bash jobs",whenToUse:"a job you started is no longer wanted or is stuck, and it should stop now",help:{name:"node bash kill",summary:"best-effort stop a background bash job and tell the owning node it was canceled",params:[{kind:"positional",name:"job",required:!0,constraint:"Job id, as reported by node bash list."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The owning node."},{name:"signaled",type:"boolean",required:!0,constraint:"True when at least one observed supervisor-tree process received SIGTERM."}],outputKind:"object",effects:["Snapshots the supervisor\u2019s observable process tree, sends SIGTERM then SIGKILL to those processes, and cannot stop a descendant that detached and reparented before the snapshot.","Records the job\u2019s exit so it stops showing as live, and ends its job object as killed; the job\u2019s watchers (its node by default) get an urgent `killed` push."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash job"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=e.job;if(!y(t))throw new r({error:"invalid_job_id",message:`invalid background job id: ${t}`,next:"Use a job id reported by `crtr node bash list`."});const s=await A().stopBashJob(n.node_id,t);return{node_id:n.node_id,signaled:s.signaled}},"run"),render:d(e=>`Canceled a background bash job for ${e.node_id}${e.signaled===!0?"":" (no observed job process was still running)"}.`,"render")}),W=h({name:"extend",description:"claim an auto-backgrounded bash job so the daemon does not cancel it at its deadline",whenToUse:"a command that was moved to a job automatically is worth keeping past its cancel deadline \u2014 give it more time with --for, or let it run to completion with --keep",help:{name:"node bash extend",summary:"move or clear the auto-cancel deadline on one of a node\u2019s live background bash jobs",params:[{kind:"positional",name:"job",required:!0,constraint:"Job id, as reported by node bash list or the auto-background notice."},{kind:"flag",name:"for",type:"string",required:!1,constraint:"New window from now before the job is canceled, as a duration (e.g. 90s, 10m, 1h30m). Mutually exclusive with --keep; give exactly one."},{kind:"flag",name:"keep",type:"bool",required:!1,constraint:"Remove the deadline entirely so the job runs to completion. Mutually exclusive with --for; give exactly one."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The owning node."},{name:"deadline_at",type:"string",required:!1,constraint:"The job\u2019s new auto-cancel time (ISO), or absent when --keep cleared the deadline so the job runs to completion."}],outputKind:"object",effects:["Rewrites or removes the job\u2019s deadline file. The daemon\u2019s deadline lane reads it on its next scan. Signals nothing."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash job"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=e.job;if(!y(t))throw new r({error:"invalid_job_id",message:`invalid background job id: ${t}`,next:"Use a job id reported by `crtr node bash list`."});const s=e.for,i=e.keep===!0;if(i===(s!==void 0&&s!==""))throw new r({error:"invalid_args",message:"give exactly one of --for <duration> or --keep",next:"Extend by a window with --for 10m, or drop the deadline with --keep."});const l=n.paths.context_dir,o=j(l,t);if(!g(o.dir))throw new r({error:"not_found",message:`no background job ${t} for ${n.node_id}`,next:"List live jobs with `crtr node bash list`."});if(!g(o.jobBg)||g(o.jobExit))throw new r({error:"not_running",message:`background job ${t} for ${n.node_id} is no longer running`,next:`It finished or was stopped, so there is no deadline to change. Log: ${o.jobLog}`});if(i)return N(o),{node_id:n.node_id};const c=U(s);if(c===null||c<=0)throw new r({error:"invalid_duration",message:`not a duration: ${s}`,next:"Use a value like 90s, 10m, or 1h30m."});const m=Date.now()+c;return J(o,m),{node_id:n.node_id,deadline_at:new Date(m).toISOString()}},"run"),render:d(e=>e.deadline_at===void 0?`Cleared the deadline \u2014 the background job will run to completion for ${e.node_id}.`:`Background job for ${e.node_id} will be canceled at ${e.deadline_at} unless extended again.`,"render")}),ae=$({name:"bash",description:"start, list, stop, and extend your background bash jobs",whenToUse:"a command needs longer than one tool call \u2014 start it as a job and be woken with its exit code instead of using `&`, `nohup`, or a cron that polls a log \u2014 or you need to list, stop, or extend the deadline of a job you already have",help:{name:"node bash",summary:"start, list, stop, and extend background bash jobs owned by a node",model:"Every bash call runs under a detached supervisor with its output in a log file, so a command that outlives its call keeps running as a job: a canvas object its node owns, readable and watchable with `crtr canvas`. `run` starts a job on purpose and it is never auto-canceled. A foreground command still running after 5 minutes is moved to a job automatically, with a cancel deadline unless `extend` claims it."},children:[O,K,X,G,W]});export{ae as nodeBash};
5
+ `)},"render")}),G=c({name:"kill",description:"stop one of a node\u2019s background bash jobs",whenToUse:"a job you started is no longer wanted or is stuck, and it should stop now",help:{name:"node bash kill",summary:"best-effort stop a background bash job and tell the owning node it was canceled",params:[{kind:"positional",name:"job",required:!0,constraint:"Job id, as reported by node bash list."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The owning node."},{name:"signaled",type:"boolean",required:!0,constraint:"True when at least one observed supervisor-tree process received SIGTERM."}],outputKind:"object",effects:["Snapshots the supervisor\u2019s observable process tree, sends SIGTERM then SIGKILL to those processes, and cannot stop a descendant that detached and reparented before the snapshot.","Records the job\u2019s exit so it stops showing as live, and ends its job object as killed; the job\u2019s watchers (its node by default) get an urgent `killed` push."]},run:i(async e=>{const{id:a}=m(e,"no node found for the bash job"),n=await b(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const o=e.job;if(!j(o))throw new r({error:"invalid_job_id",message:`invalid background job id: ${o}`,next:"Use a job id reported by `crtr node bash list`."});const s=await O().stopBashJob(n.node_id,o);return{node_id:n.node_id,signaled:s.signaled}},"run"),render:i(e=>`Canceled a background bash job for ${e.node_id}${e.signaled===!0?"":" (no observed job process was still running)"}.`,"render")}),F=c({name:"quiet",description:"mark a running background bash job as an expected wait so it sends no silence messages",whenToUse:"a silence message arrived for a job that is meant to sit idle",help:{name:"node bash quiet",summary:"stop silence messages for one of a node\u2019s live background bash jobs",params:[{kind:"positional",name:"job",required:!0,constraint:"Job id, as reported by node bash list or the silence message."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The owning node."}],outputKind:"object",effects:["Writes a marker in the job\u2019s directory; the daemon sends no further silence messages for it. Signals nothing."]},run:i(async e=>{const{id:a}=m(e,"no node found for the bash job"),n=await b(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const o=e.job;if(!j(o))throw new r({error:"invalid_job_id",message:`invalid background job id: ${o}`,next:"Use a job id reported by `crtr node bash list`."});const s=w(n.paths.context_dir,o);if(!u(s.jobBg)||u(s.jobExit))throw new r({error:"not_running",message:`background job ${o} for ${n.node_id} is not running`,next:"List live jobs with `crtr node bash list`."});return k(s),{node_id:n.node_id}},"run"),render:i(e=>`Background job for ${e.node_id} will send no more silence messages.`,"render")}),H=c({name:"extend",description:"claim an auto-backgrounded bash job so the daemon does not cancel it at its deadline",whenToUse:"a command that was moved to a job automatically is worth keeping past its cancel deadline \u2014 give it more time with --for, or let it run to completion with --keep",help:{name:"node bash extend",summary:"move or clear the auto-cancel deadline on one of a node\u2019s live background bash jobs",params:[{kind:"positional",name:"job",required:!0,constraint:"Job id, as reported by node bash list or the auto-background notice."},{kind:"flag",name:"for",type:"string",required:!1,constraint:"New window from now before the job is canceled, as a duration (e.g. 90s, 10m, 1h30m). Mutually exclusive with --keep; give exactly one."},{kind:"flag",name:"keep",type:"bool",required:!1,constraint:"Remove the deadline entirely so the job runs to completion. Mutually exclusive with --for; give exactly one."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The owning node."},{name:"deadline_at",type:"string",required:!1,constraint:"The job\u2019s new auto-cancel time (ISO), or absent when --keep cleared the deadline so the job runs to completion."}],outputKind:"object",effects:["Rewrites or removes the job\u2019s deadline file. The daemon\u2019s deadline lane reads it on its next scan. Signals nothing."]},run:i(async e=>{const{id:a}=m(e,"no node found for the bash job"),n=await b(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const o=e.job;if(!j(o))throw new r({error:"invalid_job_id",message:`invalid background job id: ${o}`,next:"Use a job id reported by `crtr node bash list`."});const s=e.for,d=e.keep===!0;if(d===(s!==void 0&&s!==""))throw new r({error:"invalid_args",message:"give exactly one of --for <duration> or --keep",next:"Extend by a window with --for 10m, or drop the deadline with --keep."});const l=n.paths.context_dir,t=w(l,o);if(!u(t.dir))throw new r({error:"not_found",message:`no background job ${o} for ${n.node_id}`,next:"List live jobs with `crtr node bash list`."});if(!u(t.jobBg)||u(t.jobExit))throw new r({error:"not_running",message:`background job ${o} for ${n.node_id} is no longer running`,next:`It finished or was stopped, so there is no deadline to change. Log: ${t.jobLog}`});if(d)return B(t),{node_id:n.node_id};const h=J(s);if(h===null||h<=0)throw new r({error:"invalid_duration",message:`not a duration: ${s}`,next:"Use a value like 90s, 10m, or 1h30m."});const g=Date.now()+h;return A(t,g),{node_id:n.node_id,deadline_at:new Date(g).toISOString()}},"run"),render:i(e=>e.deadline_at===void 0?`Cleared the deadline \u2014 the background job will run to completion for ${e.node_id}.`:`Background job for ${e.node_id} will be canceled at ${e.deadline_at} unless extended again.`,"render")}),re=T({name:"bash",description:"start, list, stop, and extend your background bash jobs",whenToUse:"a command needs longer than one tool call \u2014 start it as a job and be woken with its exit code instead of using `&`, `nohup`, or a cron that polls a log \u2014 or you need to list, stop, or extend the deadline of a job you already have",help:{name:"node bash",summary:"start, list, stop, and extend background bash jobs owned by a node",model:"Every bash call runs under a detached supervisor with its output in a log file, so a command that outlives its call keeps running as a job: a canvas object its node owns, readable and watchable with `crtr canvas`. `run` starts a job on purpose and it is never auto-canceled. A job that shows no activity for 15 minutes messages its node, and is never stopped for it. A foreground command still running after 5 minutes is moved to a job automatically, with a cancel deadline unless `extend` claims it."},children:[K,X,W,G,F,H]});export{re as nodeBash};
@@ -1,5 +1,5 @@
1
1
  export declare const MAX_BACKGROUND_BASH_LOG_BYTES: number;
2
2
  export declare const BACKGROUND_BASH_LOG_TRUNCATION_MARKER = "\n[background bash job output truncated at 10 MiB; remaining output discarded]\n";
3
- /** File-backed command supervisor. It writes the exit code to job.exit and, for a foreground
3
+ /** File-backed command supervisor. Beside the job it runs the activity sampler. It writes the exit code to job.exit and, for a foreground
4
4
  * command, claims job.done; the daemon reads job.exit of a backgrounded job and publishes `exited`. */
5
5
  export declare const BASH_JOB_SUPERVISOR_SCRIPT: string;
@@ -1,4 +1,4 @@
1
- const t=10485760,n=`
1
+ const c=10485760,t=`
2
2
  [background bash job output truncated at 10 MiB; remaining output discarded]
3
- `,e=['const fs = require("node:fs");',"const cap = 10485760;",`const marker = Buffer.from(${JSON.stringify(n)});`,"const prefixCap = cap - marker.length;",'const fd = fs.openSync(process.argv[1], "w");',"let written = 0;","let truncated = false;",'process.stdin.on("data", (chunk) => {'," if (truncated) return;"," const available = prefixCap - written;"," if (available > 0) {"," const prefix = chunk.subarray(0, available);"," fs.writeSync(fd, prefix);"," written += prefix.length;"," if (prefix.length === chunk.length) return;"," }"," fs.writeSync(fd, marker);"," truncated = true;","});",'process.stdin.on("end", () => fs.closeSync(fd));'].join(" "),r=["n=$(command -v nice)",'${n:+"$n" -n "$7"} "$6" "$1" 2>&1 | "$8" -e '+JSON.stringify(e)+' "$2"','echo "${PIPESTATUS[0]}" > "$3"','mv "$4" "$5" 2>/dev/null || true'].join(`
4
- `);export{n as BACKGROUND_BASH_LOG_TRUNCATION_MARKER,r as BASH_JOB_SUPERVISOR_SCRIPT,t as MAX_BACKGROUND_BASH_LOG_BYTES};
3
+ `,e=['const fs = require("node:fs");',"const cap = 10485760;",`const marker = Buffer.from(${JSON.stringify(t)});`,"const prefixCap = cap - marker.length;",'const fd = fs.openSync(process.argv[1], "w");',"let written = 0;","let truncated = false;",'process.stdin.on("data", (chunk) => {'," if (truncated) return;"," const available = prefixCap - written;"," if (available > 0) {"," const prefix = chunk.subarray(0, available);"," fs.writeSync(fd, prefix);"," written += prefix.length;"," if (prefix.length === chunk.length) return;"," }"," fs.writeSync(fd, marker);"," truncated = true;","});",'process.stdin.on("end", () => fs.closeSync(fd));'].join(" "),s=['const fs = require("node:fs"), cp = require("node:child_process");',"const rootPid = Number(process.argv[1]), out = process.argv[3];","let logPath = process.argv[2];","try { logPath = fs.realpathSync(logPath); } catch {}","const interval = Number(process.env.CRTR_BASH_ACTIVITY_SAMPLE_MS) || 60000;",'const linux = fs.existsSync("/proc/self/stat");',"function processes() {"," const procs = new Map();"," if (linux) {",' for (const e of fs.readdirSync("/proc")) {'," if (!/^[0-9]+$/.test(e)) continue;"," try {",' const s = fs.readFileSync("/proc/" + e + "/stat", "utf8");',' const r = s.slice(s.lastIndexOf(")") + 2).split(" ");'," procs.set(Number(e), { ppid: Number(r[1]), cpu: (Number(r[11]) + Number(r[12])) / 100 });"," } catch {}"," }"," return procs;"," }",' const ps = cp.execFileSync("ps", ["-ax", "-o", "pid=,ppid=,time="], { encoding: "utf8", timeout: 5000 });',' for (const line of ps.split("\\n")) {'," const f = line.trim().split(/\\s+/);"," if (f.length < 3) continue;",' const [days, clock] = f[2].includes("-") ? f[2].split("-") : ["0", f[2]];',' const cpu = clock.split(":").reduce((t, x) => t * 60 + Number(x), 0) + Number(days) * 86400;'," if (Number.isFinite(cpu)) procs.set(Number(f[0]), { ppid: Number(f[1]), cpu });"," }"," return procs;","}","function tree(procs) {"," const cpus = new Map(), stack = [rootPid];"," while (stack.length > 0) {"," const pid = stack.pop();"," if (pid === process.pid || cpus.has(pid) || !procs.has(pid)) continue;"," cpus.set(pid, procs.get(pid).cpu);"," for (const [child, p] of procs) if (p.ppid === pid) stack.push(child);"," }"," return cpus;","}","function writableFiles(pids) {"," const files = new Set();"," if (linux) {"," for (const pid of pids) {"," let fds = [];",' try { fds = fs.readdirSync("/proc/" + pid + "/fd"); } catch {}'," for (const fd of fds) {"," try {",' const info = fs.readFileSync("/proc/" + pid + "/fdinfo/" + fd, "utf8").match(/flags:\\s*([0-7]+)/);'," if (info === null || (parseInt(info[1], 8) & 3) === 0) continue;",' files.add(fs.readlinkSync("/proc/" + pid + "/fd/" + fd));'," } catch {}"," }"," }"," return files;"," }",' const r = cp.spawnSync("lsof", ["-nP", "-w", "-F", "pfatn", "-p", pids.join(",")], { encoding: "utf8", timeout: 10000 });',' if (r.error || typeof r.stdout !== "string") return files;',' let access = "", type = "";',' for (const line of r.stdout.split("\\n")) {'," const key = line[0], value = line.slice(1);",' if (key === "f") { access = ""; type = ""; }',' else if (key === "a") access = value;',' else if (key === "t") type = value;',' else if (key === "n" && (access === "w" || access === "u") && type === "REG") files.add(value);'," }"," return files;","}","let lastActivity = Date.now(), prevAt = lastActivity, prevCpu = new Map(), watched = [logPath];","try { prevCpu = tree(processes()); } catch {}","function record() {",' fs.writeFileSync(out + ".tmp", lastActivity + " " + prevAt + "\\n" + watched.join("\\n") + "\\n");',' fs.renameSync(out + ".tmp", out);',"}","function sample() {",' try { process.kill(rootPid, 0); } catch (e) { if (e.code !== "EPERM") process.exit(0); }'," const at = Date.now();"," const cpus = tree(processes());"," let used = 0;"," for (const [pid, cpu] of cpus) used += Math.max(0, cpu - (prevCpu.get(pid) || 0));"," const paths = new Set([logPath]);"," let changed = false;"," for (const file of writableFiles([...cpus.keys()])) {"," try {"," const st = fs.statSync(file);"," if (!st.isFile()) continue;"," paths.add(fs.realpathSync(file));"," if (st.mtimeMs > prevAt) changed = true;"," } catch {}"," }"," try { if (fs.statSync(logPath).mtimeMs > prevAt) changed = true; } catch {}"," if (at - prevAt > 3 * interval) changed = true;"," if (changed || used > 0.02 * (at - prevAt) / 1000) lastActivity = at;"," watched = [...paths];"," prevAt = at;"," prevCpu = cpus;"," record();","}","record();","setInterval(() => { try { sample(); } catch {} }, interval);"].join(" "),n=['"$8" -e '+JSON.stringify(s)+' "$$" "$2" "$9" &',"sampler=$!","n=$(command -v nice)",'${n:+"$n" -n "$7"} "$6" "$1" 2>&1 | "$8" -e '+JSON.stringify(e)+' "$2"',"status=${PIPESTATUS[0]}",'kill "$sampler" 2>/dev/null','echo "$status" > "$3"','mv "$4" "$5" 2>/dev/null || true'].join(`
4
+ `);export{t as BACKGROUND_BASH_LOG_TRUNCATION_MARKER,n as BASH_JOB_SUPERVISOR_SCRIPT,c as MAX_BACKGROUND_BASH_LOG_BYTES};
@@ -25,6 +25,13 @@ export interface BashJobPaths {
25
25
  * subject to the daemon's claim-or-cancel deadline. Absent for a deliberate
26
26
  * handoff (tmux menu, broker termination), which runs to completion. */
27
27
  jobDeadline: string;
28
+ /** Written beside the job by its supervisor's activity sampler: when the job last
29
+ * showed life and which files it was watched through (see `readJobActivity`). */
30
+ jobActivity: string;
31
+ /** Present when the job's owner declared that it legitimately sits idle: it gets no silence messages. */
32
+ jobQuietOk: string;
33
+ /** The daemon's record of the last silence message sent: `<last-activity-ms> <threshold-ms>`. */
34
+ jobSilence: string;
28
35
  }
29
36
  /** A purpose is display text, never shell input. Keep the file-backed control
30
37
  * plane to one bounded, visible line so old or hand-written job directories
@@ -78,6 +85,25 @@ export declare function writeJobDeadlineMs(paths: BashJobPaths, deadlineMs: numb
78
85
  /** Drop a job's cancel deadline so it runs to completion — the node has claimed
79
86
  * it. Best-effort: a job with no deadline file is already in this state. */
80
87
  export declare function clearJobDeadline(paths: BashJobPaths): void;
88
+ /** What the job's activity sampler last recorded: when the job last showed life (its process tree
89
+ * used CPU, or a regular file one of its processes holds open for writing changed), when that was
90
+ * sampled, and the files it was watched through. undefined for a job with no readable record. */
91
+ export declare function readJobActivity(contextDir: string, jobId: string): {
92
+ lastActivityMs: number;
93
+ sampledAtMs: number;
94
+ files: string[];
95
+ } | undefined;
96
+ /** Whether the owner declared the job an expected wait, so it gets no silence messages. */
97
+ export declare function isJobQuietOk(contextDir: string, jobId: string): boolean;
98
+ /** Declare a job an expected wait (`node bash run --quiet-ok`, `node bash quiet`). */
99
+ export declare function markJobQuietOk(paths: BashJobPaths): void;
100
+ /** The silence message the daemon last sent for a job: the last-activity time it was sent for and
101
+ * the threshold it announced. undefined when none was sent. */
102
+ export declare function readJobSilence(contextDir: string, jobId: string): {
103
+ lastActivityMs: number;
104
+ thresholdMs: number;
105
+ } | undefined;
106
+ export declare function writeJobSilence(paths: BashJobPaths, lastActivityMs: number, thresholdMs: number): void;
81
107
  /** Launcher jobs have a root-owned bwrap pid, not a process group; direct
82
108
  * jobs keep their detached supervisor process group. */
83
109
  export declare function isBashJobProcessGroupAlive(pgid: number, identity?: string): boolean;
@@ -1,8 +1,10 @@
1
- var $=Object.defineProperty;var o=(e,t)=>$(e,"name",{value:t,configurable:!0});import{randomBytes as R}from"node:crypto";import{closeSync as B,openSync as j,readSync as y,statSync as T}from"node:fs";import{dirname as g,join as a}from"node:path";import{captureTeardownSnapshot as I,isAnyPidAlive as _,isPidAlive as S,killProcessTreePids as x}from"./canvas/pid.js";import{launcherRequest as C}from"../launcher/client.js";import{LaunchRefused as F}from"../launcher/spec.js";import{createFileBeneath as P,existsBeneath as O,readdirBeneath as v,readFileBeneath as z,renameBeneath as G,unlinkBeneath as H,writeFileBeneath as E}from"./spaces/open-beneath.js";const X=280;function q(e){if(typeof e!="string"||/[\p{Cc}\p{Cf}]/u.test(e))return null;const t=e.trim();return t===""?null:Buffer.byteLength(t,"utf8")<=X?t:null}o(q,"normalizeBashJobPurpose");function m(e){return a(e,"jobs")}o(m,"bashJobsDir");function p(e,t){return z(g(e),t).toString("utf8")}o(p,"readJobText");function J(e){return v(g(e),m(e)).map(t=>t.name)}o(J,"jobIds");function c(e,t){return O(e.root,t)}o(c,"has");function M(e,t){try{H(e,t)}catch(n){if(n.code!=="ENOENT")throw n}}o(M,"unlinkIfPresent");function ae(e){return typeof e=="string"&&e!==""&&e!=="."&&e!==".."&&/^[A-Za-z0-9._~-]+$/u.test(e)&&Buffer.byteLength(e,"utf8")<=128}o(ae,"isSafeBashJobId");function l(e,t){const n=a(m(e),t);return{jobId:t,root:g(e),dir:n,cmdSh:a(n,"cmd.sh"),jobLog:a(n,"job.log"),jobExit:a(n,"job.exit"),jobRun:a(n,"job.run"),jobBg:a(n,"job.bg"),jobDone:a(n,"job.done"),jobPgid:a(n,"job.pgid"),jobPgidIdentity:a(n,"job.pgid-identity"),jobPurpose:a(n,"purpose"),jobDeadline:a(n,"job.deadline")}}o(l,"bashJobPaths");function U(e,t){try{const n=Number(p(e,l(e,t).jobPgid).trim());return Number.isInteger(n)&&n>1?n:void 0}catch{return}}o(U,"readJobPgid");function W(e,t){try{const n=p(e,l(e,t).jobPgidIdentity).trim();return n===""?void 0:n}catch{return}}o(W,"readJobPgidIdentity");function K(e,t){try{return q(p(e,l(e,t).jobPurpose))}catch{return null}}o(K,"readJobPurpose");const A=10,Y=19;function k(e){return a(m(e),"niceness")}o(k,"bashNicenessPath");function ce(e){try{const t=Number(p(e,k(e)).trim());return!Number.isInteger(t)||t<0||t>Y?A:t}catch{return A}}o(ce,"readBashNiceness");function fe(e,t){E(g(e),k(e),`${Math.trunc(t)}
2
- `)}o(fe,"writeBashNiceness");function Z(e,t){try{const n=Number(p(e,l(e,t).jobDeadline).trim());return Number.isFinite(n)?n:void 0}catch{return}}o(Z,"readJobDeadlineMs");function Q(e,t){E(e.root,e.jobDeadline,`${Math.trunc(t)}
3
- `)}o(Q,"writeJobDeadlineMs");function de(e){try{M(e.root,e.jobDeadline)}catch{}}o(de,"clearJobDeadline");function le(e,t){if(t===`launcher:${e}`)return S(e);try{return process.kill(-e,0),!0}catch(n){return n.code==="EPERM"}}o(le,"isBashJobProcessGroupAlive");function he(e,t){if(e===void 0||t===void 0)return!1;if(t===`launcher:${e}`)return!S(e);try{return process.kill(-e,0),!1}catch(n){return n.code==="ESRCH"}}o(he,"isBashJobProcessConfirmedGone");async function V(e,t){const n=I(e,t);if(n.reused||n.tree.length===0)return!1;const r=[...n.tree].reverse(),u=_(r);return x(r,"SIGTERM",n.identities),await new Promise(i=>setTimeout(i,400)),x(r,"SIGKILL",n.identities),u}o(V,"cancelBashJobProcessTree");async function be(e,t,n=!1,r,u){const i=l(e,t);if(!c(i,i.dir))return{kind:"missing"};if(!c(i,i.jobBg)&&!(n&&c(i,i.jobRun))||c(i,i.jobExit))return{kind:"retired",logPath:i.jobLog};const s=U(e,t),f=W(e,t);if(s===void 0||f===void 0)return{kind:"not-stoppable",logPath:i.jobLog};const h=a(i.dir,"job.stop");try{P(i.root,h,u===void 0?"":`${u}
4
- `)}catch(d){if(d.code==="EEXIST")return{kind:"retired",logPath:i.jobLog};throw d}try{let d;if(f===`launcher:${s}`){if(r===void 0)throw new Error("launcher job stop requires its owning node");try{if(!("ok"in await C({operation:"stop",pid:s,nodeId:r})))throw new Error("launcher did not stop job");d=!0}catch(b){if(!(b instanceof F&&b.refusal==="invalid_spec"))throw b;d=!1}}else d=await V(s,f);try{P(i.root,i.jobExit,`143
5
- `)}catch(b){if(b.code!=="EEXIST")throw b}return{kind:"stopped",signaled:d,pgid:s,logPath:i.jobLog}}catch(d){throw M(i.root,h),d}}o(be,"stopBackgroundBashJob");function pe(e,t){if(t<=0)return[];const n=64*1024;let r;try{const u=T(e).size,i=Math.max(0,u-n),s=Buffer.alloc(Math.min(u,n));r=j(e,"r"),y(r,s,0,s.length,i);const f=s.toString("utf8"),h=(i>0?f.slice(f.indexOf(`
6
- `)+1):f).split(`
7
- `);return h[h.length-1]===""&&h.pop(),h.slice(-t)}catch{return[]}finally{r!==void 0&&B(r)}}o(pe,"tailJobLog");function ge(e){let t;try{t=j(e,"r");const n=Buffer.alloc(64*1024);let r=0,u=0;for(;;){const i=y(t,n,0,n.length,null);if(i===0)break;for(let s=0;s<i;s+=1)n[s]===10&&(r+=1);u=n[i-1]}return u!==0&&u!==10?r+1:r}catch{return 0}finally{t!==void 0&&B(t)}}o(ge,"countJobLogLines");function me(){return`${Date.now()}-${R(4).toString("hex")}`}o(me,"newBashJobId");function w(e){const t=e.indexOf("-");if(t<=0||t===e.length-1)return;const n=Number(e.slice(0,t));return Number.isFinite(n)?n:void 0}o(w,"jobStartedAtMs");function Be(e){const t=Math.max(0,Math.floor(e/1e3));if(t<60)return`${t}s`;const n=Math.floor(t/60),r=t%60;if(n<60)return`${n}m ${String(r).padStart(2,"0")}s`;const u=Math.floor(n/60),i=n%60;return`${u}h ${String(i).padStart(2,"0")}m`}o(Be,"formatBashElapsed");function je(e){return(e.split(`
8
- `).map(n=>n.trim()).find(n=>n!=="")??"").replace(/\s+/g," ")}o(je,"bashJobCommandLine");function N(e,t=!1){let n;try{n=J(e)}catch{return[]}const r=[];for(const u of n){const i=w(u);if(i===void 0)continue;const s=l(e,u);let f;try{if(!c(s,s.jobBg)&&!(t&&c(s,s.jobRun))||c(s,s.jobExit))continue;f=p(e,s.cmdSh)}catch{continue}r.push({jobId:u,command:f,purpose:K(e,u),startedAtMs:i,logPath:s.jobLog,deadlineAtMs:Z(e,u)??null})}return r.sort((u,i)=>u.startedAtMs-i.startedAtMs)}o(N,"activeBackgroundBashJobs");function ye(e,t){return N(e).filter(n=>n.deadlineAtMs!==null&&t>=n.deadlineAtMs)}o(ye,"expiredAutoBackgroundBashJobs");function Se(e,t){if(!Number.isFinite(t)||t<=0)return 0;let n=0;for(const r of N(e))if(r.deadlineAtMs!==null)try{Q(l(e,r.jobId),r.deadlineAtMs+t),n+=1}catch{}return n}o(Se,"shiftAutoBackgroundBashDeadlines");function D(e){try{return G(e.root,e.jobRun,e.jobBg),"backgrounded"}catch(t){if(t.code!=="ENOENT")throw t;return c(e,e.jobBg)?"already-backgrounded":c(e,e.jobDone)?"finished":"missing"}}o(D,"backgroundBashJob");function L(e){let t;try{t=J(e)}catch{return[]}return t.flatMap(n=>{const r=l(e,n);try{return c(r,r.cmdSh)&&c(r,r.jobLog)&&c(r,r.jobRun)?[r]:[]}catch{return[]}})}o(L,"runningBashJobs");function xe(e){let t;for(const n of L(e)){const r=w(n.jobId);r!==void 0&&(t===void 0||r<t)&&(t=r)}return t}o(xe,"oldestRunningBashJobStartedAtMs");function Pe(e){const t=[];for(const n of L(e))D(n)==="backgrounded"&&t.push(n);return t}o(Pe,"backgroundRunningBashJobs");export{A as DEFAULT_BASH_NICENESS,Y as MAX_BASH_NICENESS,N as activeBackgroundBashJobs,D as backgroundBashJob,Pe as backgroundRunningBashJobs,je as bashJobCommandLine,l as bashJobPaths,m as bashJobsDir,k as bashNicenessPath,V as cancelBashJobProcessTree,de as clearJobDeadline,ge as countJobLogLines,ye as expiredAutoBackgroundBashJobs,Be as formatBashElapsed,he as isBashJobProcessConfirmedGone,le as isBashJobProcessGroupAlive,ae as isSafeBashJobId,w as jobStartedAtMs,me as newBashJobId,q as normalizeBashJobPurpose,xe as oldestRunningBashJobStartedAtMs,ce as readBashNiceness,Z as readJobDeadlineMs,U as readJobPgid,W as readJobPgidIdentity,K as readJobPurpose,Se as shiftAutoBackgroundBashDeadlines,be as stopBackgroundBashJob,pe as tailJobLog,fe as writeBashNiceness,Q as writeJobDeadlineMs};
1
+ var $=Object.defineProperty;var o=(t,e)=>$(t,"name",{value:e,configurable:!0});import{randomBytes as R}from"node:crypto";import{closeSync as y,openSync as B,readSync as S,statSync as v}from"node:fs";import{dirname as m,join as c}from"node:path";import{captureTeardownSnapshot as O,isAnyPidAlive as T,isPidAlive as M,killProcessTreePids as x}from"./canvas/pid.js";import{launcherRequest as F}from"../launcher/client.js";import{LaunchRefused as _}from"../launcher/spec.js";import{createFileBeneath as A,existsBeneath as I,readdirBeneath as C,readFileBeneath as Q,renameBeneath as z,unlinkBeneath as G,writeFileBeneath as g}from"./spaces/open-beneath.js";const H=280;function X(t){if(typeof t!="string"||/[\p{Cc}\p{Cf}]/u.test(t))return null;const e=t.trim();return e===""?null:Buffer.byteLength(e,"utf8")<=H?e:null}o(X,"normalizeBashJobPurpose");function j(t){return c(t,"jobs")}o(j,"bashJobsDir");function b(t,e){return Q(m(t),e).toString("utf8")}o(b,"readJobText");function J(t){return C(m(t),j(t)).map(e=>e.name)}o(J,"jobIds");function a(t,e){return I(t.root,e)}o(a,"has");function k(t,e){try{G(t,e)}catch(n){if(n.code!=="ENOENT")throw n}}o(k,"unlinkIfPresent");function ct(t){return typeof t=="string"&&t!==""&&t!=="."&&t!==".."&&/^[A-Za-z0-9._~-]+$/u.test(t)&&Buffer.byteLength(t,"utf8")<=128}o(ct,"isSafeBashJobId");function f(t,e){const n=c(j(t),e);return{jobId:e,root:m(t),dir:n,cmdSh:c(n,"cmd.sh"),jobLog:c(n,"job.log"),jobExit:c(n,"job.exit"),jobRun:c(n,"job.run"),jobBg:c(n,"job.bg"),jobDone:c(n,"job.done"),jobPgid:c(n,"job.pgid"),jobPgidIdentity:c(n,"job.pgid-identity"),jobPurpose:c(n,"purpose"),jobDeadline:c(n,"job.deadline"),jobActivity:c(n,"job.activity"),jobQuietOk:c(n,"job.quiet-ok"),jobSilence:c(n,"job.silence")}}o(f,"bashJobPaths");function q(t,e){try{const n=Number(b(t,f(t,e).jobPgid).trim());return Number.isInteger(n)&&n>1?n:void 0}catch{return}}o(q,"readJobPgid");function U(t,e){try{const n=b(t,f(t,e).jobPgidIdentity).trim();return n===""?void 0:n}catch{return}}o(U,"readJobPgidIdentity");function W(t,e){try{return X(b(t,f(t,e).jobPurpose))}catch{return null}}o(W,"readJobPurpose");const P=10,K=19;function N(t){return c(j(t),"niceness")}o(N,"bashNicenessPath");function at(t){try{const e=Number(b(t,N(t)).trim());return!Number.isInteger(e)||e<0||e>K?P:e}catch{return P}}o(at,"readBashNiceness");function ft(t,e){g(m(t),N(t),`${Math.trunc(e)}
2
+ `)}o(ft,"writeBashNiceness");function Y(t,e){try{const n=Number(b(t,f(t,e).jobDeadline).trim());return Number.isFinite(n)?n:void 0}catch{return}}o(Y,"readJobDeadlineMs");function Z(t,e){g(t.root,t.jobDeadline,`${Math.trunc(e)}
3
+ `)}o(Z,"writeJobDeadlineMs");function dt(t){try{k(t.root,t.jobDeadline)}catch{}}o(dt,"clearJobDeadline");function lt(t,e){try{const[n,...r]=b(t,f(t,e).jobActivity).split(`
4
+ `),[s,i]=(n??"").split(" ").map(Number);return!Number.isFinite(s)||!Number.isFinite(i)?void 0:{lastActivityMs:s,sampledAtMs:i,files:r.filter(u=>u!=="")}}catch{return}}o(lt,"readJobActivity");function bt(t,e){return a(f(t,e),f(t,e).jobQuietOk)}o(bt,"isJobQuietOk");function ht(t){g(t.root,t.jobQuietOk,"")}o(ht,"markJobQuietOk");function pt(t,e){try{const[n,r]=b(t,f(t,e).jobSilence).trim().split(" ").map(Number);return Number.isFinite(n)&&Number.isFinite(r)?{lastActivityMs:n,thresholdMs:r}:void 0}catch{return}}o(pt,"readJobSilence");function mt(t,e,n){g(t.root,t.jobSilence,`${Math.trunc(e)} ${Math.trunc(n)}
5
+ `)}o(mt,"writeJobSilence");function gt(t,e){if(e===`launcher:${t}`)return M(t);try{return process.kill(-t,0),!0}catch(n){return n.code==="EPERM"}}o(gt,"isBashJobProcessGroupAlive");function jt(t,e){if(t===void 0||e===void 0)return!1;if(e===`launcher:${t}`)return!M(t);try{return process.kill(-t,0),!1}catch(n){return n.code==="ESRCH"}}o(jt,"isBashJobProcessConfirmedGone");async function V(t,e){const n=O(t,e);if(n.reused||n.tree.length===0)return!1;const r=[...n.tree].reverse(),s=T(r);return x(r,"SIGTERM",n.identities),await new Promise(i=>setTimeout(i,400)),x(r,"SIGKILL",n.identities),s}o(V,"cancelBashJobProcessTree");async function yt(t,e,n=!1,r,s){const i=f(t,e);if(!a(i,i.dir))return{kind:"missing"};if(!a(i,i.jobBg)&&!(n&&a(i,i.jobRun))||a(i,i.jobExit))return{kind:"retired",logPath:i.jobLog};const u=q(t,e),d=U(t,e);if(u===void 0||d===void 0)return{kind:"not-stoppable",logPath:i.jobLog};const h=c(i.dir,"job.stop");try{A(i.root,h,s===void 0?"":`${s}
6
+ `)}catch(l){if(l.code==="EEXIST")return{kind:"retired",logPath:i.jobLog};throw l}try{let l;if(d===`launcher:${u}`){if(r===void 0)throw new Error("launcher job stop requires its owning node");try{if(!("ok"in await F({operation:"stop",pid:u,nodeId:r})))throw new Error("launcher did not stop job");l=!0}catch(p){if(!(p instanceof _&&p.refusal==="invalid_spec"))throw p;l=!1}}else l=await V(u,d);try{A(i.root,i.jobExit,`143
7
+ `)}catch(p){if(p.code!=="EEXIST")throw p}return{kind:"stopped",signaled:l,pgid:u,logPath:i.jobLog}}catch(l){throw k(i.root,h),l}}o(yt,"stopBackgroundBashJob");function Bt(t,e){if(e<=0)return[];const n=64*1024;let r;try{const s=v(t).size,i=Math.max(0,s-n),u=Buffer.alloc(Math.min(s,n));r=B(t,"r"),S(r,u,0,u.length,i);const d=u.toString("utf8"),h=(i>0?d.slice(d.indexOf(`
8
+ `)+1):d).split(`
9
+ `);return h[h.length-1]===""&&h.pop(),h.slice(-e)}catch{return[]}finally{r!==void 0&&y(r)}}o(Bt,"tailJobLog");function St(t){let e;try{e=B(t,"r");const n=Buffer.alloc(64*1024);let r=0,s=0;for(;;){const i=S(e,n,0,n.length,null);if(i===0)break;for(let u=0;u<i;u+=1)n[u]===10&&(r+=1);s=n[i-1]}return s!==0&&s!==10?r+1:r}catch{return 0}finally{e!==void 0&&y(e)}}o(St,"countJobLogLines");function Mt(){return`${Date.now()}-${R(4).toString("hex")}`}o(Mt,"newBashJobId");function E(t){const e=t.indexOf("-");if(e<=0||e===t.length-1)return;const n=Number(t.slice(0,e));return Number.isFinite(n)?n:void 0}o(E,"jobStartedAtMs");function xt(t){const e=Math.max(0,Math.floor(t/1e3));if(e<60)return`${e}s`;const n=Math.floor(e/60),r=e%60;if(n<60)return`${n}m ${String(r).padStart(2,"0")}s`;const s=Math.floor(n/60),i=n%60;return`${s}h ${String(i).padStart(2,"0")}m`}o(xt,"formatBashElapsed");function At(t){return(t.split(`
10
+ `).map(n=>n.trim()).find(n=>n!=="")??"").replace(/\s+/g," ")}o(At,"bashJobCommandLine");function w(t,e=!1){let n;try{n=J(t)}catch{return[]}const r=[];for(const s of n){const i=E(s);if(i===void 0)continue;const u=f(t,s);let d;try{if(!a(u,u.jobBg)&&!(e&&a(u,u.jobRun))||a(u,u.jobExit))continue;d=b(t,u.cmdSh)}catch{continue}r.push({jobId:s,command:d,purpose:W(t,s),startedAtMs:i,logPath:u.jobLog,deadlineAtMs:Y(t,s)??null})}return r.sort((s,i)=>s.startedAtMs-i.startedAtMs)}o(w,"activeBackgroundBashJobs");function Jt(t,e){return w(t).filter(n=>n.deadlineAtMs!==null&&e>=n.deadlineAtMs)}o(Jt,"expiredAutoBackgroundBashJobs");function kt(t,e){if(!Number.isFinite(e)||e<=0)return 0;let n=0;for(const r of w(t))if(r.deadlineAtMs!==null)try{Z(f(t,r.jobId),r.deadlineAtMs+e),n+=1}catch{}return n}o(kt,"shiftAutoBackgroundBashDeadlines");function D(t){try{return z(t.root,t.jobRun,t.jobBg),"backgrounded"}catch(e){if(e.code!=="ENOENT")throw e;return a(t,t.jobBg)?"already-backgrounded":a(t,t.jobDone)?"finished":"missing"}}o(D,"backgroundBashJob");function L(t){let e;try{e=J(t)}catch{return[]}return e.flatMap(n=>{const r=f(t,n);try{return a(r,r.cmdSh)&&a(r,r.jobLog)&&a(r,r.jobRun)?[r]:[]}catch{return[]}})}o(L,"runningBashJobs");function Pt(t){let e;for(const n of L(t)){const r=E(n.jobId);r!==void 0&&(e===void 0||r<e)&&(e=r)}return e}o(Pt,"oldestRunningBashJobStartedAtMs");function Nt(t){const e=[];for(const n of L(t))D(n)==="backgrounded"&&e.push(n);return e}o(Nt,"backgroundRunningBashJobs");export{P as DEFAULT_BASH_NICENESS,K as MAX_BASH_NICENESS,w as activeBackgroundBashJobs,D as backgroundBashJob,Nt as backgroundRunningBashJobs,At as bashJobCommandLine,f as bashJobPaths,j as bashJobsDir,N as bashNicenessPath,V as cancelBashJobProcessTree,dt as clearJobDeadline,St as countJobLogLines,Jt as expiredAutoBackgroundBashJobs,xt as formatBashElapsed,jt as isBashJobProcessConfirmedGone,gt as isBashJobProcessGroupAlive,bt as isJobQuietOk,ct as isSafeBashJobId,E as jobStartedAtMs,ht as markJobQuietOk,Mt as newBashJobId,X as normalizeBashJobPurpose,Pt as oldestRunningBashJobStartedAtMs,at as readBashNiceness,lt as readJobActivity,Y as readJobDeadlineMs,q as readJobPgid,U as readJobPgidIdentity,W as readJobPurpose,pt as readJobSilence,kt as shiftAutoBackgroundBashDeadlines,yt as stopBackgroundBashJob,Bt as tailJobLog,ft as writeBashNiceness,Z as writeJobDeadlineMs,mt as writeJobSilence};
@@ -1,3 +1,3 @@
1
- var V=Object.defineProperty;var w=(i,a)=>V(i,"name",{value:a,configurable:!0});import{DatabaseSync as W}from"node:sqlite";import{existsSync as Y,mkdirSync as D,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{canvasSearchPathOptions as ee}from"./schema-version.js";import{EXPORT_MARK_KEY as F,IMPORT_MARK_KEY as te,OBJECT_COLUMNS as ne,pgColumns as se,quote as c,TABLE_ORDER as C}from"./import-sqlite.js";const oe=5e3,re=new Set(["environments","relations","schemas","charges","charge_totals","limits","viewer_attachments","reconciler_leases","environment_repos","conversation_entries","conversation_parts","charge_chunks","profiles","profile_projects","profile_defaults","scope_settings","scope_bookkeeping","scope_marketplaces","scope_plugins","person_settings","model_accounts","model_account_cooldowns","model_api_keys","model_settings","model_sign_in_flows","env_vars"]),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-")),s=process.env.CRTR_HOME;process.env.CRTR_HOME=a,D(A(a,"runtime","nodes"),{recursive:!0});try{const o=new W(i);return o.exec("PRAGMA journal_mode = DELETE;"),o.exec("PRAGMA foreign_keys = ON;"),z(o),o.exec("DELETE FROM canvas_meta; DELETE FROM message_sequence;"),o}finally{s===void 0?delete process.env.CRTR_HOME:process.env.CRTR_HOME=s,P(a,{recursive:!0,force:!0})}}w(ce,"createEmptyDatabase");async function Re(i,a=()=>{}){const s=Z();if(Y(s))throw new Error(`${s} already exists; refusing to overwrite it`);D(A(s,".."),{recursive:!0,mode:448});const o=`${s}.exporting-${process.pid}`,p=Date.now(),t=new j.Client({connectionString:i,application_name:`crtr-export-${process.pid}`,options:ee(),types:{getTypeParser:w(((f,r)=>f===20?d=>Number(d):j.types.getTypeParser(f,r)),"getTypeParser")}});await t.connect();let m;try{if((await t.query("SELECT value FROM canvas_meta WHERE key = 'schema_version'").catch(()=>({rows:[]}))).rows.length===0)throw new Error("the Postgres database has no canvas schema; nothing to export");m=ce(o);const r=m;r.exec("PRAGMA foreign_keys = OFF;");const d=await se(t),L=[...d.keys()].filter(e=>!re.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 E={};for(const[e,S]of Object.entries(ie)){const h=new Map;for(const _ of(await t.query(`SELECT ${S} AS id, seq FROM ${e}`)).rows)h.set(_.id,Number(_.seq));E[e]=h}const B=E.objects;let M=0;r.exec("BEGIN");for(const e of C){const S=Date.now(),h=new Set(r.prepare(`PRAGMA table_info(${c(e)})`).all().map(n=>n.name)),_=d.get(e);if(_===void 0)throw new Error(`export: Postgres has no table ${e}`);const G=_.some(n=>n.name==="seq"),O=_.filter(n=>h.has(n.name)),K=new Set(ne[e]??[]),g=e==="objects"||e==="revisions"||e==="watches"||e==="watch_subscriptions",$=O.map(n=>n.type==="jsonb"?`${c(n.name)}::text AS ${c(n.name)}`:c(n.name));e==="watches"&&$.push("watch_id AS _watch_id"),g&&$.push("seq AS _seq");const R=O.map(n=>c(n.name)),U=r.prepare(`INSERT INTO ${c(e)} (${g?["rowid",...R]:R}) VALUES (${(g?["?",...R]:R).map(()=>"?").join(", ")})`),q=G?"seq":ae[e],b=`export_${e}`;await t.query(`DECLARE ${b} NO SCROLL CURSOR FOR SELECT ${$.join(", ")} FROM ${c(e)}${e==="canvas_meta"?` WHERE key NOT IN (${[...H].map(n=>`'${n}'`).join(", ")})`:""}${q===void 0?"":` ORDER BY ${q}`}`);let T=0;for(;;){const n=await t.query(`FETCH ${oe} FROM ${b}`);if(n.rows.length===0)break;for(const k of n.rows){const N=g?[Number(k._seq)]:[];for(const u of O){let l=k[u.name];e==="revision_parents"&&(u.name==="child"||u.name==="parent")?l=x(E.revisions,"revision",e,u.name,l):e==="watch_subscriptions"&&u.name==="watch"?l=x(E.watches,"watch",e,u.name,l):K.has(u.name)&&(l=x(B,"object",e,u.name,l)),N.push(l??null)}U.run(...N)}T+=n.rows.length}await t.query(`CLOSE ${b}`),M+=T,a(`export ${e}: ${T} rows in ${Date.now()-S} ms`)}r.exec("COMMIT"),await t.query("COMMIT");const y=r.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 pe(t,r);const v=r.prepare("PRAGMA user_version").get().user_version;if(v!==I.length)throw new Error(`export: exported canvas.db is at user_version ${v}, expected ${I.length}`);return r.close(),m=void 0,await t.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",[F,JSON.stringify({at:new Date().toISOString(),path:s})]),X(o,s),{path:s,rows:M,ms:Date.now()-p,userVersion:v}}catch(f){await t.query("ROLLBACK").catch(()=>{});try{m?.close()}catch{}for(const r of["","-wal","-shm","-journal"])P(`${o}${r}`,{force:!0});throw f}finally{await t.end().catch(()=>{})}}w(Re,"exportPostgresToSqlite");function x(i,a,s,o,p){if(p==null)return null;const t=i.get(p);if(t===void 0)throw new Error(`export: ${s}.${o} references ${a} ${String(p)}, which was not exported (`);return t}w(x,"mapId");async function pe(i,a){const s=[];for(const o of C){const p=Number((await i.query(`SELECT count(*) AS n FROM ${c(o)}`)).rows[0].n);let t=Number(a.prepare(`SELECT count(*) AS n FROM ${c(o)}`).get().n),m=p;o==="canvas_meta"&&(m=Number((await i.query("SELECT count(*) AS n FROM canvas_meta WHERE key <> ALL($1)",[[...H]])).rows[0].n),t=Number(a.prepare("SELECT count(*) AS n FROM canvas_meta").get().n)),m!==t&&s.push(`${o}: Postgres ${m} rows, SQLite ${t}`)}if(s.length>0)throw new Error(`Postgres \u2192 SQLite export verification failed:
1
+ var V=Object.defineProperty;var w=(i,a)=>V(i,"name",{value:a,configurable:!0});import{DatabaseSync as W}from"node:sqlite";import{existsSync as Y,mkdirSync as D,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{canvasSearchPathOptions as ee}from"./schema-version.js";import{EXPORT_MARK_KEY as F,IMPORT_MARK_KEY as te,OBJECT_COLUMNS as ne,pgColumns as se,quote as c,TABLE_ORDER as C}from"./import-sqlite.js";const re=5e3,oe=new Set(["environments","relations","schemas","charges","charge_totals","limits","viewer_attachments","reconciler_leases","environment_repos","conversation_entries","conversation_parts","charge_chunks","profiles","profile_projects","profile_defaults","scope_settings","scope_bookkeeping","scope_marketplaces","scope_plugins","person_settings","model_accounts","model_account_cooldowns","model_api_keys","model_settings","model_sign_in_flows","env_vars","usage","grantee_usage","provider_receipts"]),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-")),s=process.env.CRTR_HOME;process.env.CRTR_HOME=a,D(A(a,"runtime","nodes"),{recursive:!0});try{const r=new W(i);return r.exec("PRAGMA journal_mode = DELETE;"),r.exec("PRAGMA foreign_keys = ON;"),z(r),r.exec("DELETE FROM canvas_meta; DELETE FROM message_sequence;"),r}finally{s===void 0?delete process.env.CRTR_HOME:process.env.CRTR_HOME=s,P(a,{recursive:!0,force:!0})}}w(ce,"createEmptyDatabase");async function Re(i,a=()=>{}){const s=Z();if(Y(s))throw new Error(`${s} already exists; refusing to overwrite it`);D(A(s,".."),{recursive:!0,mode:448});const r=`${s}.exporting-${process.pid}`,p=Date.now(),t=new j.Client({connectionString:i,application_name:`crtr-export-${process.pid}`,options:ee(),types:{getTypeParser:w(((d,o)=>d===20?f=>Number(f):j.types.getTypeParser(d,o)),"getTypeParser")}});await t.connect();let m;try{if((await t.query("SELECT value FROM canvas_meta WHERE key = 'schema_version'").catch(()=>({rows:[]}))).rows.length===0)throw new Error("the Postgres database has no canvas schema; nothing to export");m=ce(r);const o=m;o.exec("PRAGMA foreign_keys = OFF;");const f=await se(t),L=[...f.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 E={};for(const[e,S]of Object.entries(ie)){const h=new Map;for(const _ of(await t.query(`SELECT ${S} AS id, seq FROM ${e}`)).rows)h.set(_.id,Number(_.seq));E[e]=h}const B=E.objects;let M=0;o.exec("BEGIN");for(const e of C){const S=Date.now(),h=new Set(o.prepare(`PRAGMA table_info(${c(e)})`).all().map(n=>n.name)),_=f.get(e);if(_===void 0)throw new Error(`export: Postgres has no table ${e}`);const G=_.some(n=>n.name==="seq"),O=_.filter(n=>h.has(n.name)),K=new Set(ne[e]??[]),g=e==="objects"||e==="revisions"||e==="watches"||e==="watch_subscriptions",$=O.map(n=>n.type==="jsonb"?`${c(n.name)}::text AS ${c(n.name)}`:c(n.name));e==="watches"&&$.push("watch_id AS _watch_id"),g&&$.push("seq AS _seq");const R=O.map(n=>c(n.name)),U=o.prepare(`INSERT INTO ${c(e)} (${g?["rowid",...R]:R}) VALUES (${(g?["?",...R]:R).map(()=>"?").join(", ")})`),q=G?"seq":ae[e],b=`export_${e}`;await t.query(`DECLARE ${b} NO SCROLL CURSOR FOR SELECT ${$.join(", ")} FROM ${c(e)}${e==="canvas_meta"?` WHERE key NOT IN (${[...H].map(n=>`'${n}'`).join(", ")})`:""}${q===void 0?"":` ORDER BY ${q}`}`);let T=0;for(;;){const n=await t.query(`FETCH ${re} FROM ${b}`);if(n.rows.length===0)break;for(const k of n.rows){const N=g?[Number(k._seq)]:[];for(const u of O){let l=k[u.name];e==="revision_parents"&&(u.name==="child"||u.name==="parent")?l=x(E.revisions,"revision",e,u.name,l):e==="watch_subscriptions"&&u.name==="watch"?l=x(E.watches,"watch",e,u.name,l):K.has(u.name)&&(l=x(B,"object",e,u.name,l)),N.push(l??null)}U.run(...N)}T+=n.rows.length}await t.query(`CLOSE ${b}`),M+=T,a(`export ${e}: ${T} rows in ${Date.now()-S} ms`)}o.exec("COMMIT"),await t.query("COMMIT");const v=o.prepare("PRAGMA foreign_key_check").all();if(v.length>0)throw new Error(`export: ${v.length} foreign key violation(s) in the exported canvas.db, first: ${JSON.stringify(v[0])}`);await pe(t,o);const y=o.prepare("PRAGMA user_version").get().user_version;if(y!==I.length)throw new Error(`export: exported canvas.db is at user_version ${y}, expected ${I.length}`);return o.close(),m=void 0,await t.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",[F,JSON.stringify({at:new Date().toISOString(),path:s})]),X(r,s),{path:s,rows:M,ms:Date.now()-p,userVersion:y}}catch(d){await t.query("ROLLBACK").catch(()=>{});try{m?.close()}catch{}for(const o of["","-wal","-shm","-journal"])P(`${r}${o}`,{force:!0});throw d}finally{await t.end().catch(()=>{})}}w(Re,"exportPostgresToSqlite");function x(i,a,s,r,p){if(p==null)return null;const t=i.get(p);if(t===void 0)throw new Error(`export: ${s}.${r} references ${a} ${String(p)}, which was not exported (`);return t}w(x,"mapId");async function pe(i,a){const s=[];for(const r of C){const p=Number((await i.query(`SELECT count(*) AS n FROM ${c(r)}`)).rows[0].n);let t=Number(a.prepare(`SELECT count(*) AS n FROM ${c(r)}`).get().n),m=p;r==="canvas_meta"&&(m=Number((await i.query("SELECT count(*) AS n FROM canvas_meta WHERE key <> ALL($1)",[[...H]])).rows[0].n),t=Number(a.prepare("SELECT count(*) AS n FROM canvas_meta").get().n)),m!==t&&s.push(`${r}: Postgres ${m} rows, SQLite ${t}`)}if(s.length>0)throw new Error(`Postgres \u2192 SQLite export verification failed:
2
2
  ${s.join(`
3
3
  `)}`)}w(pe,"verify");export{Re as exportPostgresToSqlite};
@@ -5,7 +5,7 @@ export declare const IMPORT_MARK_KEY = "imported_from_sqlite";
5
5
  export declare const EXPORT_MARK_KEY = "exported_to_sqlite";
6
6
  /** Every canvas table, in dependency order (a table follows every table its foreign keys name). The import and the
7
7
  * rollback export both copy exactly this list. */
8
- export declare const TABLE_ORDER: readonly ["objects", "documents", "revisions", "revision_parents", "jobs", "identities", "custom_objects", "context_sessions", "peer_stubs", "object_kinds", "watches", "watch_subscriptions", "exposures", "rules", "edges", "unresolved_links", "nodes", "focuses", "canvas_meta", "runtime_state", "warm_pool", "sessions", "executions", "worktrees", "node_faults", "node_tokens", "node_outcome_deliveries", "deleted_isolated_nodes", "create_requests", "waits", "messages", "message_sequence", "message_causes", "deliveries", "structured_requests", "daemon_actions", "human_requests", "human_action_deliveries", "review_comments", "review_comment_events", "run_events", "idempotency_keys", "grants", "memory_reads", "app_users", "uploads", "upload_uses", "shares", "audit_rows", "grantee_usage", "provider_receipts", "provider_approvals", "usage", "crons", "cron_runs", "peer_outbox", "peer_locations", "peer_ingested", "peer_ingested_destinations"];
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", "provider_approvals", "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
11
  /** `optional`: the column may be left out of an INSERT (nullable, or it has a default or is an identity). */
@@ -1,7 +1,19 @@
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
1
+ var X=Object.defineProperty;var p=(s,t)=>X(s,"name",{value:t,configurable:!0});import{DatabaseSync as z}from"node:sqlite";import{createHash as Z,randomBytes as ee}from"node:crypto";import{existsSync as L,mkdirSync as te,renameSync as H}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{LEGACY_LOCAL_PERSON as ce}from"./legacy-person.js";import{REBUILD_ALL_TOTALS as ue}from"../ledger/totals.js";import{canvasDbPath as U,onDiskMigrationLockPath as de}from"./paths.js";import{canvasSearchPathOptions as pe}from"./schema-version.js";const k="imported_from_sqlite",B="exported_to_sqlite",M=1e4,N=["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","provider_approvals","crons","cron_runs","peer_outbox","peer_locations","peer_ingested","peer_ingested_destinations"],me=new Set(["sqlite_sequence","sqlite_stat1","sqlite_stat4","reviews","usage","grantee_usage","provider_receipts"]),Ee={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"]),fe={canvas_meta:"key",message_sequence:"id"};function G(){return ee(16).toString("hex")}p(G,"newId");function f(s){return`"${s.replace(/"/g,'""')}"`}p(f,"quote");function we(s){switch(s){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 ${s}`)}}p(we,"arrayType");async function le(s){const t=await s.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 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):
4
+ ORDER BY c.relname, a.attnum`),e=new Map;for(const a of t.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(le,"pgColumns");const _e="\uFFFD";function he(s){if(!s.includes("\\u0000"))return{text:s,replaced:0};let t;try{t=JSON.parse(s)}catch{return{text:s,replaced:0}}let e=0;const a=p(d=>{const i=d.split("\0");return e+=i.length-1,i.join(_e)},"clean"),n=p(d=>typeof d=="string"?a(d):Array.isArray(d)?d.map(n):d!==null&&typeof d=="object"?Object.fromEntries(Object.entries(d).map(([i,_])=>[a(i),n(_)])):d,"walk"),c=n(t);return e===0?{text:s,replaced:e}:{text:JSON.stringify(c),replaced:e}}p(he,"scrubNul");async function ye(s,t,e,a,n,c,d){for(let i=0;i<a.length;i++)for(const _ of e){const o=n[c.indexOf(_)][i];if(o!=null){await s.query("SAVEPOINT import_probe");try{await s.query("SELECT $1::jsonb",[o]),await s.query("RELEASE SAVEPOINT import_probe")}catch(u){return await s.query("ROLLBACK TO SAVEPOINT import_probe"),new Error(`import: ${t}.${_.name} at ${d(a[i])} is not a value jsonb accepts: ${u.message}`,{cause:u})}}}}p(ye,"describeJsonbFailure");function v(s,t,e,a,n){if(n==null)return null;const c=s.get(Number(n));if(c===void 0)throw new Error(`import: ${e}.${a} references ${t} rowid ${String(n)}, which does not exist`);return c}p(v,"lookup");async function $e(s,t,e,a,n,c){const d=s.prepare(`PRAGMA table_info(${f(e)})`).all(),i=new Set(d.map(r=>r.name)),_=p(r=>r==="seq"||r==="watch_id"||r==="subscription_id","minted"),o=a.filter(r=>i.has(r.name)||_(r.name)||!r.optional),w=!s.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(e).sql.toUpperCase().includes("WITHOUT ROWID"),$=new Set(Ee[e]??[]),y=[];for(const r of o)if(r.name==="seq"&&!i.has("seq")){if(!w)throw new Error(`import: ${e} needs seq from rowid but has none`);y.push("rowid AS seq")}else if(r.name==="watch_id"||r.name==="subscription_id")y.push("NULL AS "+f(r.name));else if(i.has(r.name))y.push(f(r.name));else throw new Error(`import: SQLite table ${e} has no column ${r.name}`);w&&y.push("rowid AS _rowid");const h=e==="canvas_meta"?` WHERE key NOT IN (${[...W].map(r=>`'${r}'`).join(",")})`:"",b=s.prepare(`SELECT ${y.join(", ")} FROM ${f(e)}${h}${w?" ORDER BY rowid":""}`),S=o.map(r=>f(r.name)),R=o.map((r,m)=>`$${m+1}::${we(r.type)}`),O=o.map((r,m)=>`c${m}`),A=o.map((r,m)=>r.type==="jsonb"?`c${m}::jsonb`:`c${m}`);let j=`INSERT INTO ${f(e)} (${S.join(", ")}) SELECT ${A.join(", ")} FROM unnest(${R.join(", ")}) AS u(${O.join(", ")})`;const C=fe[e];if(C!==void 0){const r=o.filter(m=>m.name!==C).map(m=>`${f(m.name)} = EXCLUDED.${f(m.name)}`);j+=` ON CONFLICT (${f(C)}) DO UPDATE SET ${r.join(", ")}`}const D=e==="objects",x=o.filter(r=>r.type==="jsonb"),I=w?"_rowid":o[0].name,J=p(r=>`${w?"rowid":I} ${String(r[I])}`,"key");let P=0,g=[];const F=p(async()=>{if(g.length===0)return;const r=o.map(()=>new Array(g.length));if(g.forEach((m,T)=>{o.forEach((E,Q)=>{let l=m[E.name];if(e==="objects"&&E.name==="privacy"?l=ie(l):E.name==="watch_id"||E.name==="subscription_id"?l=G():e==="revision_parents"&&(E.name==="child"||E.name==="parent")?l=v(n.revisions,"revision",e,E.name,l):e==="watch_subscriptions"&&E.name==="watch"?l=v(n.watches,"watch",e,E.name,l):$.has(E.name)&&(l=D?null:v(n.objects,"object",e,E.name,l)),E.type==="bytea"&&l instanceof Uint8Array&&!Buffer.isBuffer(l)&&(l=Buffer.from(l.buffer,l.byteOffset,l.byteLength)),E.type==="jsonb"&&typeof l=="string"){const q=he(l);q.replaced>0&&(l=q.text,c.set(`${e}.${E.name}`,(c.get(`${e}.${E.name}`)??0)+q.replaced))}r[Q][T]=l??null}),e==="watches"&&n.watches.set(Number(m._rowid),r[o.findIndex(E=>E.name==="watch_id")][T])}),x.length===0)await t.query(j,r);else{await t.query("SAVEPOINT import_batch");try{await t.query(j,r)}catch(m){throw await t.query("ROLLBACK TO SAVEPOINT import_batch"),await ye(t,e,x,g,r,o,J)??m}await t.query("RELEASE SAVEPOINT import_batch")}P+=g.length,g=[]},"flush");for(const r of b.iterate())e==="objects"&&n.objects.set(Number(r._rowid),r.id),e==="revisions"&&n.revisions.set(Number(r._rowid),r.rev_id),g.push(r),g.length>=M&&await F();if(await F(),D){const r=s.prepare("SELECT id, owner FROM objects WHERE owner IS NOT NULL").all();for(let m=0;m<r.length;m+=M){const T=r.slice(m,m+M);await t.query("UPDATE objects o SET owner = v.owner FROM unnest($1::text[], $2::text[]) AS v(id, owner) WHERE o.id = v.id",[T.map(E=>E.id),T.map(E=>v(n.objects,"object","objects","owner",E.owner))])}}return P}p($e,"copyTable");async function Se(s,t){for(const[e,a]of t)for(const n of a)n.identity&&await s.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(Se,"advanceIdentities");function Y(s,t,e=""){return Number(s.prepare(`SELECT count(*) AS n FROM ${f(t)}${e}`).get().n)}p(Y,"sqliteCount");async function ge(s,t,e){const a=[];for(const o of N){if(o==="canvas_meta")continue;const u=Y(s,o),w=Number((await t.query(`SELECT count(*) AS n FROM ${f(o)}`)).rows[0].n);u!==w&&a.push(`${o}: SQLite ${u} rows, Postgres ${w}`)}for(const o of s.prepare(`SELECT key, value FROM canvas_meta WHERE key NOT IN (${[...W].map(u=>`'${u}'`).join(",")})`).all())(await t.query("SELECT value FROM canvas_meta WHERE key = $1",[o.key])).rows[0]?.value!==o.value&&a.push(`canvas_meta ${o.key}: value differs`);const n=p(o=>{const u=Z("sha256");for(const w of o)u.update(w).update(`
5
+ `);return u.digest("hex")},"hash"),c=s.prepare("SELECT id FROM objects ORDER BY rowid").all().map(o=>o.id),d=(await t.query("SELECT id FROM objects ORDER BY seq")).rows.map(o=>o.id);n(c)!==n(d)&&a.push("objects: ids in insertion order differ");const i=s.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 t.query("SELECT id, owner FROM objects WHERE owner IS NOT NULL ORDER BY seq")).rows;n(i.map(o=>`${o.id}>${o.owner}`))!==n(_.map(o=>`${o.id}>${o.owner}`))&&a.push("objects: owner links differ");for(const[o,u]of[["run_events","body_json"],["messages","body_json"],["deliveries","last_failure_json"]]){const w=Number(s.prepare(`SELECT COALESCE(sum(length(CAST(${f(u)} AS TEXT))), 0) AS n FROM ${f(o)}`).get().n),$=e.get(o).find(b=>b.name===u).type,y=$==="jsonb"?`${f(u)}::text`:f(u);if($==="jsonb")continue;const h=Number((await t.query(`SELECT COALESCE(sum(length(${y})), 0)::bigint AS n FROM ${f(o)}`)).rows[0].n);w!==h&&a.push(`${o}.${u}: total length SQLite ${w}, Postgres ${h}`)}if(a.length>0)throw new Error(`SQLite \u2192 Postgres import verification failed (rolled back; canvas.db untouched):
6
6
  ${a.join(`
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};
7
+ `)}`)}p(ge,"verify");async function be(s,t){const e=s.prepare("SELECT grantee, month, payer, source, served_model, input_tokens, output_tokens, cached_tokens, usd_equivalent FROM usage").all();if(e.length===0)return 0;const n=(await t.query("SELECT value FROM canvas_meta WHERE key = 'person_id'")).rows[0]?.value??ce,c=p((o,u=String)=>e.map(w=>w[o]===null?null:u(w[o])),"column"),d=await t.query(`INSERT INTO charges (id, request, at, month, kind, what, units, usd, payer, funding, sub, grantee, provider)
8
+ SELECT 'legacy:' || u.grantee || ':' || u.month || ':' || u.payer || ':' || u.source || ':' || u.served_model,
9
+ NULL, u.month || '-01T00:00:00.000Z', u.month,
10
+ CASE WHEN u.source = 'model' THEN 'model' ELSE 'tool' END,
11
+ CASE WHEN u.source = 'model' THEN u.served_model ELSE substr(u.source, 10) END,
12
+ CASE WHEN u.source = 'model' THEN jsonb_build_object('input', u.input_tokens, 'output', u.output_tokens, 'cache_read', u.cached_tokens, 'cache_write', 0) END,
13
+ u.usd_equivalent,
14
+ CASE WHEN u.payer = 'app' THEN u.grantee ELSE $1::text END,
15
+ CASE WHEN u.payer = 'app' THEN 'endpoint' WHEN u.source = 'model' THEN 'legacy' ELSE 'bill' END,
16
+ $1::text, u.grantee,
17
+ CASE WHEN u.source = 'model' THEN NULL ELSE 'app:' || substr(u.source, 10) END
18
+ FROM unnest($2::text[], $3::text[], $4::text[], $5::text[], $6::text[], $7::bigint[], $8::bigint[], $9::bigint[], $10::float8[])
19
+ AS u(grantee, month, payer, source, served_model, input_tokens, output_tokens, cached_tokens, usd_equivalent)`,[n,c("grantee"),c("month"),c("payer"),c("source"),c("served_model"),c("input_tokens"),c("output_tokens"),c("cached_tokens"),c("usd_equivalent",Number)]);for(const o of ue)await t.query(o);const i=(await t.query("SELECT count(*)::int AS n, COALESCE(SUM(usd), 0)::float8 AS usd FROM charges WHERE id LIKE 'legacy:%'")).rows[0],_=e.reduce((o,u)=>o+Number(u.usd_equivalent??0),0);if(d.rowCount!==e.length||i.n!==e.length||Math.abs(i.usd-_)>1e-4)throw new Error(`import: the usage \u2192 charges copy does not match (${e.length} usage rows, ${d.rowCount} charges inserted, ${i.n} legacy charges; usd ${_} vs ${i.usd})`);return e.length}p(be,"importLegacyUsage");async function Re(s,t,e={}){const a=e.log??(()=>{}),n=Date.now(),c=await le(t),d=s.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map(h=>h.name),i=d.filter(h=>!me.has(h)&&!N.includes(h));if(i.length>0)throw new Error(`import: SQLite tables with no copy rule: ${i.join(", ")}`);const _=N.filter(h=>!c.has(h));if(_.length>0)throw new Error(`import: Postgres tables missing from the schema: ${_.join(", ")}`);const o=d.includes("reviews")?Y(s,"reviews"):0;if(o>0)throw new Error(`import: the legacy reviews table still holds ${o} rows after the SQLite migration chain`);const u={objects:new Map,revisions:new Map,watches:new Map},w=[],$=new Map;let y=0;await t.query("BEGIN");try{await t.query("ALTER TABLE nodes DISABLE TRIGGER USER"),await t.query("ALTER TABLE deliveries DISABLE TRIGGER USER");for(const S of N){const R=Date.now(),O=await $e(s,t,S,c.get(S),u,$),A=Date.now()-R;w.push({table:S,rows:O,ms:A}),a(`import ${S}: ${O} rows in ${A} ms`)}await t.query("ALTER TABLE nodes ENABLE TRIGGER USER"),await t.query("ALTER TABLE deliveries ENABLE TRIGGER USER"),await Se(t,c);const h=await be(s,t);h>0&&a(`import usage: ${h} rows into charges`);const b=Date.now();await ge(s,t,c),y=Date.now()-b,a(`import verify: ${y} ms`),await t.query("INSERT INTO canvas_meta (key, value) VALUES ($1, $2)",[k,JSON.stringify({at:new Date().toISOString(),rows:w.reduce((S,R)=>S+R.rows,0)})]),await t.query("COMMIT")}catch(h){throw await t.query("ROLLBACK").catch(()=>{}),h}return $.size>0&&a(`import: replaced NUL (\\u0000) with U+FFFD in jsonb values: ${[...$].map(([h,b])=>`${h} ${b}`).join(", ")}`),{tables:w,verifyMs:y,totalMs:Date.now()-n,nulReplaced:Object.fromEntries($)}}p(Re,"copySqliteToPostgres");const K=["","-wal","-shm"];function V(s=U()){if(L(`${s}.imported`)){const t=`${s}.imported.${Date.now()}`;for(const e of K)L(`${s}.imported${e}`)&&H(`${s}.imported${e}`,`${t}${e}`)}for(const t of K)L(`${s}${t}`)&&H(`${s}${t}`,`${s}.imported${t}`)}p(V,"renameImportedDatabase");async function Pe(s,t=()=>{}){const e=U();if(!L(e))return null;const a=de();return te(oe(a),{recursive:!0,mode:448}),re(a,async()=>{if(!L(e))return null;const n=new se.Client({connectionString:s,application_name:`crtr-import-${process.pid}`,options:pe()});await n.connect();try{const c=await n.query("SELECT key FROM canvas_meta WHERE key = ANY($1)",[[k,B]]),d=new Set(c.rows.map(o=>o.key));if(d.has(k)){if(d.has(B))throw new Error(`${e} was written by \`crtr sys db export-sqlite\` and the Postgres database predates it; importing would drop the exported writes. Stop the daemon and delete the Postgres cluster (runtime/pg/data) to import ${e} afresh, or move ${e} aside to keep using Postgres.`);return V(e),t(`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 Re(i,n,{log:t})}finally{i.close()}return V(e),t(`import: ${_.tables.reduce((o,u)=>o+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(Pe,"importSqliteCanvasIfPresent");export{B as EXPORT_MARK_KEY,k as IMPORT_MARK_KEY,Ee as OBJECT_COLUMNS,N as TABLE_ORDER,Re as copySqliteToPostgres,Pe as importSqliteCanvasIfPresent,le as pgColumns,f as quote,V as renameImportedDatabase};
@@ -0,0 +1,52 @@
1
+ -- 003-fleet-removals.sql — cycle 5 removals (design-fleet "Data model", D72). One transaction; ends by raising schema_min_reader.
2
+ -- The previous release (version 2, the build that shipped 002) opens the result: it reads none of what is removed here (see the
3
+ -- reader floor at the end).
4
+
5
+ -- The version-3 build no longer copies `usage` into `charges` at boot (cycle 4's first boot did; `ledger_migrated` marks it done), so a
6
+ -- database that still holds un-migrated rows in `usage`, `grantee_usage` or `provider_receipts` would strand them. Refuse until a
7
+ -- cycle 4 build has run against it. A fresh database, or one that never had cycle-3 rows, has all three empty, so the chain from the
8
+ -- baseline to here still applies (D71). The three tables stay: the version-2 build still lists them in its SQLite import and export,
9
+ -- and a later delta (004, with the `jobs` columns, once the reader floor reaches 3) drops them.
10
+ DO $$
11
+ BEGIN
12
+ IF NOT EXISTS (SELECT 1 FROM canvas_meta WHERE key = 'ledger_migrated')
13
+ AND (EXISTS (SELECT 1 FROM usage) OR EXISTS (SELECT 1 FROM grantee_usage) OR EXISTS (SELECT 1 FROM provider_receipts)) THEN
14
+ RAISE EXCEPTION 'schema step 3 (003-fleet-removals): usage, grantee_usage or provider_receipts still hold rows and canvas_meta.ledger_migrated is not set: the cycle 4 money ledger step has not run, and this release no longer runs it. Run a cycle 4 build (v0.3.552 or later, whose first boot copies usage into charges) against this database before this release.';
15
+ END IF;
16
+ END $$;
17
+
18
+ -- 002 replaced the unique (grantee, sub) with unique (runtime, sub, grantee) and kept the old one for this release.
19
+ DROP INDEX idx_grants_grantee_sub;
20
+
21
+ -- A machine environment needs its machine id (002 made the column nullable for Blaxel environments; S1 review finding 2).
22
+ ALTER TABLE environments ADD CONSTRAINT environments_machine_id_check CHECK (kind <> 'machine' OR machine_id IS NOT NULL);
23
+
24
+ -- 002's trigger function changed after it shipped (a negative spend_cap now fails the limits CHECK instead of deleting the limit).
25
+ -- A database that applied the earlier 002 text never re-runs it, so the current body is carried here; on a database built from the
26
+ -- current 002 this replaces the function with itself.
27
+ CREATE OR REPLACE FUNCTION crtr_grant_limit() RETURNS trigger LANGUAGE plpgsql AS $$
28
+ BEGIN
29
+ IF TG_OP = 'DELETE' THEN
30
+ DELETE FROM limits WHERE source = 'grant_sync' AND id = 'grant:' || OLD.id;
31
+ RETURN OLD;
32
+ END IF;
33
+ IF NEW.status = 'active' AND NEW.spend_cap IS NOT NULL
34
+ AND NEW.payer IS DISTINCT FROM 'app' AND NEW.grantee <> 'app:terminal' THEN
35
+ INSERT INTO limits (id, payer, grantee, usd_per_month, source, updated_at)
36
+ VALUES ('grant:' || NEW.id, NEW.sub, NEW.grantee, NEW.spend_cap::numeric, 'grant_sync',
37
+ to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'))
38
+ ON CONFLICT (id) DO UPDATE SET payer = EXCLUDED.payer, funding = NULL, grantee = EXCLUDED.grantee, provider = NULL, sub = NULL,
39
+ account = NULL, usd_per_month = EXCLUDED.usd_per_month, source = 'grant_sync', updated_at = EXCLUDED.updated_at
40
+ WHERE limits.source = 'grant_sync';
41
+ ELSE
42
+ DELETE FROM limits WHERE source = 'grant_sync' AND id = 'grant:' || NEW.id;
43
+ END IF;
44
+ RETURN NULL;
45
+ END $$;
46
+
47
+ -- ---------------------------------------------------------------- reader floor
48
+
49
+ -- Version 2 reads nothing removed above: its grant reads name no index and it writes environments with a machine id; the money
50
+ -- tables it reads (boot backfill, SQLite import and export) still exist. Version 1 (before 002) writes environments of kind machine
51
+ -- without a machine id check and lacks the 002 tables, so it is refused.
52
+ INSERT INTO canvas_meta (key, value) VALUES ('schema_min_reader', '2') ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
@@ -1,7 +1,7 @@
1
1
  /** The canvas schema version this build supports: the number of the highest delta in `schema-pg/` (the baseline
2
2
  * `schema-pg.sql` is version 1). `scripts/install-runtime.mjs` reads this module from a candidate generation to refuse
3
3
  * one below the live database's `schema_min_reader`. */
4
- export declare const CANVAS_SCHEMA_VERSION = 2;
4
+ export declare const CANVAS_SCHEMA_VERSION = 3;
5
5
  /** The Postgres schema holding the canvas tables: `CRTR_DATABASE_SCHEMA` (`crtr` in the cloud deploy). Unset or empty
6
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
7
  * it is put in a `search_path` connection option. */
@@ -1 +1 @@
1
- var m=Object.defineProperty;var r=(n,e)=>m(n,"name",{value:e,configurable:!0});const A=2;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
+ var m=Object.defineProperty;var r=(n,e)=>m(n,"name",{value:e,configurable:!0});const A=3;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,2 +1,2 @@
1
- import{jobRow as d}from"../jobs.js";import{objectDTO as c}from"./dto.js";import{withBase as l}from"./kind.js";import{recordLiveRead as p}from"./live-read.js";import{attr as r,header as w}from"./render.js";const u={name:"job",addressable:"id",events:l({exited:{level:7},killed:{level:7}}),async read(a,i,e,s){const o=await d(a,e);if(!o)throw new Error(`job object ${e.id} has no jobs row`);await p(a,i,e,s.reposFn);const m=await c(a,e),t={command:o.command,status:o.status,exit_code:o.exit_code,log_path:o.log_path},n=[w("object",m,r("status",t.status)+r("exit_code",t.exit_code)),`Command: ${t.command}`,`Log: ${t.log_path}`].join(`
2
- `);return{data:t,text:n}}};export{u as jobKind};
1
+ import{jobRow as d}from"../jobs.js";import{objectDTO as c}from"./dto.js";import{withBase as l}from"./kind.js";import{recordLiveRead as p}from"./live-read.js";import{attr as r,header as w}from"./render.js";const u={name:"job",addressable:"id",events:l({exited:{level:7},killed:{level:7},silent:{level:7}}),async read(a,i,e,s){const o=await d(a,e);if(!o)throw new Error(`job object ${e.id} has no jobs row`);await p(a,i,e,s.reposFn);const n=await c(a,e),t={command:o.command,status:o.status,exit_code:o.exit_code,log_path:o.log_path},m=[w("object",n,r("status",t.status)+r("exit_code",t.exit_code)),`Command: ${t.command}`,`Log: ${t.log_path}`].join(`
2
+ `);return{data:t,text:m}}};export{u as jobKind};
@@ -5,5 +5,4 @@ export { writeCharge, receiptChargeId, takePct, chargeOf, type WriteResult } fro
5
5
  export { check, findRefusal, refusalError, refusedMark, userActionOf, type Candidate, type CallCandidate, type StartCandidate, type Refusal } from './check.js';
6
6
  export { upsertLimit, deleteLimit, listLimits } from './limits.js';
7
7
  export { usageFor, listCharges, monthTotals, assertReference, type ChargePage, type ListFilter, type MonthTotals } from './read.js';
8
- export { backfillLedger, LEDGER_MIGRATED_KEY, type BackfillResult } from './backfill.js';
9
- export { rebuildTotals, rebuildTotalsFor } from './totals.js';
8
+ export { REBUILD_ALL_TOTALS, rebuildTotalsFor } from './totals.js';
@@ -1 +1 @@
1
- export*from"./types.js";export*from"./months.js";import{payerOf as f}from"./payer.js";import{writeCharge as i,receiptChargeId as s,takePct as m,chargeOf as l}from"./charges.js";import{check as x,findRefusal as c,refusalError as u,refusedMark as d,userActionOf as g}from"./check.js";import{upsertLimit as E,deleteLimit as L,listLimits as k}from"./limits.js";import{usageFor as R,listCharges as T,monthTotals as b,assertReference as C}from"./read.js";import{backfillLedger as A,LEDGER_MIGRATED_KEY as D}from"./backfill.js";import{rebuildTotals as G,rebuildTotalsFor as I}from"./totals.js";export{D as LEDGER_MIGRATED_KEY,C as assertReference,A as backfillLedger,l as chargeOf,x as check,L as deleteLimit,c as findRefusal,T as listCharges,k as listLimits,b as monthTotals,f as payerOf,G as rebuildTotals,I as rebuildTotalsFor,s as receiptChargeId,u as refusalError,d as refusedMark,m as takePct,E as upsertLimit,R as usageFor,g as userActionOf,i as writeCharge};
1
+ export*from"./types.js";export*from"./months.js";import{payerOf as f}from"./payer.js";import{writeCharge as a,receiptChargeId as i,takePct as m,chargeOf as p}from"./charges.js";import{check as x,findRefusal as c,refusalError as u,refusedMark as L,userActionOf as h}from"./check.js";import{upsertLimit as g,deleteLimit as n,listLimits as O}from"./limits.js";import{usageFor as k,listCharges as A,monthTotals as C,assertReference as R}from"./read.js";import{REBUILD_ALL_TOTALS as F,rebuildTotalsFor as I}from"./totals.js";export{F as REBUILD_ALL_TOTALS,R as assertReference,p as chargeOf,x as check,n as deleteLimit,c as findRefusal,A as listCharges,O as listLimits,C as monthTotals,f as payerOf,I as rebuildTotalsFor,i as receiptChargeId,u as refusalError,L as refusedMark,m as takePct,g as upsertLimit,k as usageFor,h as userActionOf,a as writeCharge};
@@ -13,8 +13,9 @@ export declare function addToTotals(db: CanvasDb, charge: {
13
13
  usd: number | null;
14
14
  }): Promise<void>;
15
15
  /** Rebuild the whole rollup from `charges`, `billed_usd` with `usd`: a rebuild that dropped it would zero the billed totals
16
- * 002's trigger keeps, and an older build that may still open the database must not do that either. */
17
- export declare function rebuildTotals(db: CanvasDb): Promise<void>;
16
+ * 002's trigger keeps, and an older build that may still open the database must not do that either. Plain SQL, so the SQLite
17
+ * import runs it on its own client. */
18
+ export declare const REBUILD_ALL_TOTALS: readonly ["DELETE FROM charge_totals", "INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd, billed_usd) SELECT month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, ''), COALESCE(SUM(usd), 0), COALESCE(SUM(billed_usd), 0)\n FROM charges WHERE refused IS NULL AND kind <> 'take' GROUP BY month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, '')"];
18
19
  /** Delete the rollup rows that name `a` or `b` as payer, sub or grantee and rebuild them from `charges`, so two rows that
19
20
  * now share a key are summed once. For person substitution, in the same transaction as the rename. */
20
21
  export declare function rebuildTotalsFor(db: CanvasDb, a: string, b: string): Promise<void>;
@@ -1,4 +1,4 @@
1
- var o=Object.defineProperty;var t=(r,e)=>o(r,"name",{value:e,configurable:!0});const a=t(r=>r??"","absent");async function E(r,e){await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd) VALUES (?, ?, ?, ?, ?, ?, ?)
2
- ON CONFLICT (month, payer, funding, sub, grantee, provider) DO UPDATE SET usd = charge_totals.usd + excluded.usd`).run(e.month,e.payer,e.funding,a(e.sub),e.grantee,a(e.provider),e.usd??0)}t(E,"addToTotals");const u=`SELECT month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, ''), COALESCE(SUM(usd), 0), COALESCE(SUM(billed_usd), 0)
3
- FROM charges WHERE refused IS NULL AND kind <> 'take'`,s="GROUP BY month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, '')";async function p(r){await r.prepare("DELETE FROM charge_totals").run(),await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd, billed_usd) ${u} ${s}`).run()}t(p,"rebuildTotals");async function i(r,e,n){await r.prepare("DELETE FROM charge_totals WHERE payer IN (?, ?) OR sub IN (?, ?) OR grantee IN (?, ?)").run(e,n,e,n,e,n),await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd, billed_usd) ${u}
4
- AND (payer IN (?, ?) OR sub IN (?, ?) OR grantee IN (?, ?)) ${s}`).run(e,n,e,n,e,n)}t(i,"rebuildTotalsFor");export{a as absent,E as addToTotals,p as rebuildTotals,i as rebuildTotalsFor};
1
+ var E=Object.defineProperty;var t=(r,e)=>E(r,"name",{value:e,configurable:!0});const s=t(r=>r??"","absent");async function o(r,e){await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd) VALUES (?, ?, ?, ?, ?, ?, ?)
2
+ ON CONFLICT (month, payer, funding, sub, grantee, provider) DO UPDATE SET usd = charge_totals.usd + excluded.usd`).run(e.month,e.payer,e.funding,s(e.sub),e.grantee,s(e.provider),e.usd??0)}t(o,"addToTotals");const a=`SELECT month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, ''), COALESCE(SUM(usd), 0), COALESCE(SUM(billed_usd), 0)
3
+ FROM charges WHERE refused IS NULL AND kind <> 'take'`,u="GROUP BY month, payer, funding, COALESCE(sub, ''), grantee, COALESCE(provider, '')",p=["DELETE FROM charge_totals",`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd, billed_usd) ${a} ${u}`];async function i(r,e,n){await r.prepare("DELETE FROM charge_totals WHERE payer IN (?, ?) OR sub IN (?, ?) OR grantee IN (?, ?)").run(e,n,e,n,e,n),await r.prepare(`INSERT INTO charge_totals (month, payer, funding, sub, grantee, provider, usd, billed_usd) ${a}
4
+ AND (payer IN (?, ?) OR sub IN (?, ?) OR grantee IN (?, ?)) ${u}`).run(e,n,e,n,e,n)}t(i,"rebuildTotalsFor");export{p as REBUILD_ALL_TOTALS,s as absent,o as addToTotals,i as rebuildTotalsFor};
@@ -1 +1 @@
1
- var u=Object.defineProperty;var m=(e,o)=>u(e,"name",{value:o,configurable:!0});import{spawn as S}from"node:child_process";import{writeFileSync as a}from"node:fs";import{BASH_JOB_SUPERVISOR_SCRIPT as l}from"../../bash-job-supervisor.js";import{readBashNiceness as h}from"../../bash-jobs.js";import{capturePidIdentities as j}from"../../canvas/pid.js";import{freshChildNodeExecPath as g}from"../branded-host.js";import{resolveAdmittedHostCommand as B}from"../spawn-env.js";import{startBrokerBashJob as I}from"./daemon-ops.js";const P=process.platform==="linux";async function J(e,o,f,n,d){const b=await g();if(P){const{pid:t}=await I(e,{job_id:o.jobId,cwd:n,env:d});return{pgid:t,identity:`launcher:${t}`}}const c=B("bash",d,n),r=S(c,["-c",l,"--",o.cmdSh,o.jobLog,o.jobExit,o.jobRun,o.jobDone,c,String(h(f)),b],{cwd:n,detached:!0,stdio:"ignore",env:d}),i=await new Promise((t,p)=>{r.once("error",p),r.pid!==void 0&&t(r.pid)});r.unref();const s=j([i])?.get(i);if(s===void 0){try{process.kill(-i,"SIGKILL")}catch{}throw new Error(`bash job ${o.jobId} started without a process identity`)}return a(o.jobPgid,String(i)),a(o.jobPgidIdentity,s),{pgid:i,identity:s}}m(J,"launchBashJobSupervisor");export{P as BASH_JOBS_USE_LAUNCHER,J as launchBashJobSupervisor};
1
+ var p=Object.defineProperty;var m=(e,o)=>p(e,"name",{value:o,configurable:!0});import{spawn as S}from"node:child_process";import{writeFileSync as a}from"node:fs";import{BASH_JOB_SUPERVISOR_SCRIPT as l}from"../../bash-job-supervisor.js";import{readBashNiceness as j}from"../../bash-jobs.js";import{capturePidIdentities as g}from"../../canvas/pid.js";import{freshChildNodeExecPath as h}from"../branded-host.js";import{resolveAdmittedHostCommand as B}from"../spawn-env.js";import{startBrokerBashJob as y}from"./daemon-ops.js";const I=process.platform==="linux";async function J(e,o,f,n,d){const b=await h();if(I){const{pid:t}=await y(e,{job_id:o.jobId,cwd:n,env:d});return{pgid:t,identity:`launcher:${t}`}}const s=B("bash",d,n),r=S(s,["-c",l,"--",o.cmdSh,o.jobLog,o.jobExit,o.jobRun,o.jobDone,s,String(j(f)),b,o.jobActivity],{cwd:n,detached:!0,stdio:"ignore",env:d}),i=await new Promise((t,u)=>{r.once("error",u),r.pid!==void 0&&t(r.pid)});r.unref();const c=g([i])?.get(i);if(c===void 0){try{process.kill(-i,"SIGKILL")}catch{}throw new Error(`bash job ${o.jobId} started without a process identity`)}return a(o.jobPgid,String(i)),a(o.jobPgidIdentity,c),{pgid:i,identity:c}}m(J,"launchBashJobSupervisor");export{I as BASH_JOBS_USE_LAUNCHER,J as launchBashJobSupervisor};