@north-light/cli 0.3.520 → 0.3.521

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 +1 @@
1
- var c=Object.defineProperty;var s=(r,e)=>c(r,"name",{value:e,configurable:!0});import{defineLeaf as d,defineBranch as w}from"../../core/command.js";import{InputError as o}from"../../core/io.js";import{parseDurationMs as f}from"../../core/wake.js";import{DaemonNotPreparedError as g,probeDaemonHealth as u,probeErrorMessage as y,spawnDaemon as h,stopDaemonProcess as b,sweepStrayDaemons as v}from"../../daemon/manage.js";import{isDaemonRunning as _,readPidfile as l,isPidAlive as m}from"../../daemon/pidfile.js";import{cliClient as k}from"../api-client.js";import{localClient as T}from"../../api/node-transport.js";import{defineSysBranch as q}from"./branch.js";const x=d({name:"start",description:"start the daemon in the background",whenToUse:"bringing the crtrd supervisor up for the first time in a session so node window-exits get handled (no-op only when its Unix API socket already serves)",help:{name:"sys daemon start",summary:"start the crtrd supervisor daemon in the background (no-op only if its Unix API socket serves)",params:[{kind:"flag",name:"prepare",type:"bool",required:!1,constraint:"Hold normal daemon work after storage preparation until `sys daemon admit`; omit for ordinary startup."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,constraint:"Start even while `crtr sys join` holds the daemon down. Joining then fails `daemon_running`; use only to clear a join that is stuck."}],output:[{name:"started",type:"boolean",required:!0,constraint:"True when a new daemon process was spawned; false when one was already running."},{name:"pid",type:"number",required:!1,constraint:"PID of the newly spawned daemon."},{name:"existing_pid",type:"number",required:!1,constraint:"PID of the already-running daemon (when started=false)."},{name:"running",type:"boolean",required:!1,constraint:"True only when a live pidfile owner does not serve its Unix API socket."},{name:"serving",type:"boolean",required:!1,constraint:"False only when a live pidfile owner does not answer /healthz."},{name:"probe_error",type:"string",required:!1,constraint:"The final /healthz probe failure when a live owner missed readiness."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon phase when it was reachable."}],outputKind:"object",effects:["Refuses `join_in_progress` while a live `crtr sys join` holds the daemon down, unless `--force`. `sys daemon restart` and `ensureDaemon` stand down the same way.","Spawns dist/daemon/crtrd-cli.js detached + unref'd; it outlives this process.","Writes a pidfile at CRTR_PIDFILE if set, otherwise crtrHome()/crtrd.pid."]},run:s(async r=>{try{return await h({targetPhase:r.prepare===!0?"prepared":"ready",ignoreJoinLock:r.force===!0})}catch(e){throw e instanceof g?new o({error:"daemon_not_prepared",message:e.message,next:"Stop the live daemon, then start a new one with `crtr sys daemon start --prepare` before setup."}):e}},"run")}),D=100;async function I(r,e){const a=Date.now(),n=a+e;let i="initializing",p="the daemon socket was not reachable";for(;Date.now()<n;){try{const t=await u(Math.min(1e3,Math.max(1,n-Date.now())));if(i=t.startup_phase,p=t.startup_blocked??`daemon is ${t.startup_phase}`,t.startup_phase==="blocked")throw new o({error:"startup_blocked",message:t.startup_blocked??"daemon startup is blocked",received:t.startup_phase,field:"phase",next:"Repair the reported blocker paths with `crtr sys migrate`, then retry the await."});if(t.startup_phase===r)return{startup_phase:r,waited_ms:Date.now()-a};if(r==="prepared"&&t.startup_phase==="ready")throw new o({error:"await_missed_phase",message:"daemon is already ready and cannot return to prepared in this process",received:t.startup_phase,field:"phase",next:"Start a new daemon with `crtr sys daemon start --prepare` before awaiting prepared."})}catch(t){if(t instanceof o)throw t;p=t instanceof Error?t.message:String(t)}await new Promise(t=>setTimeout(t,Math.min(D,Math.max(0,n-Date.now()))))}throw new o({error:"await_timeout",message:`daemon did not reach ${r} within ${e}ms (last phase: ${i}; last observation: ${p})`,received:i,field:"phase",next:"Inspect `crtr sys daemon status` and resolve any reported startup blocker before retrying."})}s(I,"awaitDaemonPhase");const S=d({name:"await",description:"wait for a daemon startup phase",whenToUse:"a caller started or was given a daemon that must reach prepared or ready before its next step.",help:{name:"sys daemon await",summary:"wait for the running daemon to report one startup phase",params:[{kind:"flag",name:"phase",type:"enum",choices:["prepared","ready"],required:!0,constraint:"The phase that permits the caller\u2019s next step."},{kind:"flag",name:"timeout",type:"string",required:!1,default:"5m",constraint:"Maximum wait as a positive duration such as 90s, 5m, or 1h30m."}],output:[{name:"startup_phase",type:"string",required:!0,constraint:"The requested phase."},{name:"waited_ms",type:"number",required:!0,constraint:"Elapsed wall-clock time before the phase was observed."}],outputKind:"object",effects:["Read-only: polls /healthz until the requested phase is observed or the caller-bounded timeout expires."]},run:s(async r=>{const e=r.timeout,a=f(e);if(a===null||a<=0)throw new o({error:"invalid_duration",message:`not a positive duration: ${e}`,received:e,field:"timeout",next:"Use a positive duration such as 90s, 5m, or 1h30m."});return I(r.phase,a)},"run")}),E=d({name:"admit",description:"admit normal work after daemon preparation",whenToUse:"the caller has completed its setup against a prepared daemon and is ready for normal daemon work to start.",help:{name:"sys daemon admit",summary:"ask a prepared daemon to start normal services",params:[],output:[{name:"admitted",type:"boolean",required:!0,constraint:"True only when this request began admission."},{name:"already_admitted",type:"boolean",required:!0,constraint:"True when admission had already begun or completed."},{name:"startup_phase",type:"string",required:!0,constraint:"The phase after admission settles."}],outputKind:"object",effects:["Starts normal daemon services when the daemon is prepared."]},run:s(async r=>await T({autostart:!1}).admitDaemon(),"run")}),R=d({name:"status",description:"check whether the daemon is running",whenToUse:"operator/dev diagnostic \u2014 probing whether the crtrd supervisor process is alive while troubleshooting revive/supervision behavior",help:{name:"sys daemon status",summary:"check whether the crtrd supervisor daemon is currently running",params:[],output:[{name:"running",type:"boolean",required:!0,constraint:"True when the daemon pidfile owner is alive."},{name:"serving",type:"boolean",required:!1,constraint:"True when the live daemon answers /healthz; false when one probe does not."},{name:"pid",type:"number",required:!1,constraint:"PID of the running daemon."},{name:"probe_error",type:"string",required:!1,constraint:"The failed /healthz observation when the live owner did not answer."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon startup phase when the socket answered."}],outputKind:"object",effects:["Read-only: reads the pidfile, probes the pid, and requests /healthz without autostart."]},run:s(async r=>{const e=l();if(!(e!==null&&m(e)))return{running:!1};try{const n=await u();return{running:!0,serving:n.ok,pid:e,...n.startup_phase===void 0?{}:{startup_phase:n.startup_phase}}}catch(n){return{running:!0,serving:!1,pid:e,probe_error:y(n)}}},"run")}),P=d({name:"restart",description:"put the selected runtime generation in service via a daemon-owned handover",whenToUse:"adopting a newly installed runtime generation \u2014 the routine restart. Use this instead of stop+start: the daemon answers before it hands over, so a caller running inside a node (whose broker the handover tears down) still gets a settled result and is resumed automatically.",help:{name:"sys daemon restart",summary:"ask the running daemon to replace itself with a successor on the selected runtime generation",params:[],output:[{name:"restarted",type:"boolean",required:!0,constraint:"True when a running daemon accepted the handover. False when none was running \u2014 one was started instead."},{name:"started",type:"boolean",required:!1,constraint:"True when no daemon was running and this call started one (restarted=false)."},{name:"pid",type:"number",required:!1,constraint:"The daemon that accepted the handover, or the one just started."},{name:"grace_ms",type:"number",required:!1,constraint:"How long the daemon waits after answering before teardown begins."},{name:"note",type:"string",required:!1,constraint:"What happens next to the calling node. Read it before acting on an interrupted turn."}],outputKind:"object",effects:["The daemon acknowledges immediately, then after a short grace tears its whole broker fleet down, releases its claim, spawns a successor on the runtime generation currently selected, and exits.","Each torn-down broker hands its foreground bash commands to the file-backed background job system; they keep running and report completion by urgent inbox message.","Every node it tore down \u2014 including the caller \u2014 is resolved by the successor through the ordinary row-driven supervision tick.","The calling process is killed as part of that teardown, AFTER this result is returned."]},run:s(async r=>{if(!_()){const e=await h();return{restarted:!1,started:e.started,...e.pid!==void 0?{pid:e.pid}:{},...e.existing_pid!==void 0?{pid:e.existing_pid}:{},note:"No daemon was running, so there was nothing to hand over; one was started on the selected runtime generation."}}try{const e=await k().restartDaemon();return{restarted:e.accepted,pid:e.predecessor_pid,grace_ms:e.grace_ms,note:"Handover accepted. If you are running inside a node, your broker is about to be torn down and resumed on the new generation \u2014 THIS RESTART ALREADY SUCCEEDED. When your turn resumes with this result in the transcript, do not re-run it."}}catch(e){throw new o({error:"daemon_restart_failed",message:`the daemon did not accept a handover: ${e.message}`,next:"Run `crtr sys daemon status` to inspect the daemon."})}},"run")}),j=d({name:"stop",description:"stop the running daemon",whenToUse:"shutting the crtrd supervisor down for good, ending auto-revival of nodes on window exit. Not the way to pick up a new runtime generation \u2014 `restart` is, because stopping kills every broker of this epoch, including the caller's own, so this command cannot report its result to an agent that runs it.",help:{name:"sys daemon stop",summary:"stop the crtrd supervisor daemon and confirm it exited",params:[],output:[{name:"stopped",type:"boolean",required:!0,constraint:"True when a daemon (recorded or stray) was found and killed. False when none was running."},{name:"pid",type:"number",required:!1,constraint:"The recorded pidfile pid that exited (when one was recorded and alive)."},{name:"strays_killed",type:"array",required:!1,constraint:"PIDs of stray crtrd daemons (alive but not in the pidfile) swept alongside the recorded one \u2014 the backstop against a daemon that survived a prior stop."},{name:"force_killed",type:"boolean",required:!1,constraint:"True when the recorded daemon ignored SIGTERM and had to be SIGKILLed."}],outputKind:"object",effects:["Sends SIGTERM to the recorded pid and waits for it to exit and release its pidfile, escalating to SIGKILL if it does not.","Then sweeps any OTHER live crtrd daemon process (SIGTERM, escalating to SIGKILL after 2s) so a stray daemon orphaned from the pidfile cannot keep supervising."]},run:s(async r=>{const e=l(),a=e!==null&&m(e);try{let n=!1;a&&(n=await b(e)==="killed");const i=v(null);return!a&&i.length===0?{stopped:!1}:{stopped:!0,...a?{pid:e}:{},...i.length>0?{strays_killed:i}:{},...n?{force_killed:!0}:{}}}catch(n){throw new o({error:"daemon_stop_failed",message:`failed to stop daemon${a?` pid ${e}`:""}: ${n.message}`,next:"Run `crtr sys daemon status` to inspect the daemon before retrying."})}},"run")}),A=w({name:"daemon",description:"manage the crtrd supervisor process",whenToUse:"managing the crtrd supervisor that auto-revives nodes on window exit \u2014 start it, restart it onto a new runtime generation, check its status, or stop it",help:{name:"sys daemon",summary:"manage the crtrd canvas supervisor daemon",model:"crtrd is a thin background daemon that polls active+idle nodes and acts on window exit: crashed windows become dead; refresh-yield windows get a fresh respawn. It holds no orchestration logic \u2014 just process supervision. These are operator/dev self-management, not something a normal agent turn needs to check. Picking up a newly installed runtime generation is `restart` (a handover the daemon performs on itself); `stop` is the terminal one, and it kills the caller along with the rest of the fleet."},children:[x,S,E,P,R,j]});function F(){return q([A])}s(F,"registerDaemonControlSys");export{A as daemonBranch,F as registerDaemonControlSys};
1
+ var w=Object.defineProperty;var o=(t,e)=>w(t,"name",{value:e,configurable:!0});import{defineLeaf as u,defineBranch as f}from"../../core/command.js";import{InputError as d}from"../../core/io.js";import{parseDurationMs as g}from"../../core/wake.js";import{DaemonNotPreparedError as y,probeDaemonHealth as p,probeErrorMessage as b,spawnDaemon as h,stopDaemonProcess as v,sweepStrayDaemons as _}from"../../daemon/manage.js";import{isDaemonRunning as k,readPidfile as l,isPidAlive as m}from"../../daemon/pidfile.js";import{cliClient as T}from"../api-client.js";import{localClient as c}from"../../api/node-transport.js";import{defineSysBranch as q}from"./branch.js";import{selectedGeneration as x}from"./shared.js";const I=u({name:"start",description:"start the daemon in the background",whenToUse:"bringing the crtrd supervisor up for the first time in a session so node window-exits get handled (no-op only when its Unix API socket already serves)",help:{name:"sys daemon start",summary:"start the crtrd supervisor daemon in the background (no-op only if its Unix API socket serves)",params:[{kind:"flag",name:"prepare",type:"bool",required:!1,constraint:"Hold normal daemon work after storage preparation until `sys daemon admit`; omit for ordinary startup."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,constraint:"Start even while `crtr sys join` holds the daemon down. Joining then fails `daemon_running`; use only to clear a join that is stuck."}],output:[{name:"started",type:"boolean",required:!0,constraint:"True when a new daemon process was spawned; false when one was already running."},{name:"pid",type:"number",required:!1,constraint:"PID of the newly spawned daemon."},{name:"existing_pid",type:"number",required:!1,constraint:"PID of the already-running daemon (when started=false)."},{name:"running",type:"boolean",required:!1,constraint:"True only when a live pidfile owner does not serve its Unix API socket."},{name:"serving",type:"boolean",required:!1,constraint:"False only when a live pidfile owner does not answer /healthz."},{name:"probe_error",type:"string",required:!1,constraint:"The final /healthz probe failure when a live owner missed readiness."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon phase when it was reachable."}],outputKind:"object",effects:["Refuses `join_in_progress` while a live `crtr sys join` holds the daemon down, unless `--force`. `sys daemon restart` and `ensureDaemon` stand down the same way.","Spawns dist/daemon/crtrd-cli.js detached + unref'd; it outlives this process.","Writes a pidfile at CRTR_PIDFILE if set, otherwise crtrHome()/crtrd.pid."]},run:o(async t=>{try{return await h({targetPhase:t.prepare===!0?"prepared":"ready",ignoreJoinLock:t.force===!0})}catch(e){throw e instanceof y?new d({error:"daemon_not_prepared",message:e.message,next:"Stop the live daemon, then start a new one with `crtr sys daemon start --prepare` before setup."}):e}},"run")}),D=100;async function S(t,e){const a=Date.now(),r=a+e;let s="initializing",i="the daemon socket was not reachable";for(;Date.now()<r;){try{const n=await p(Math.min(1e3,Math.max(1,r-Date.now())));if(s=n.startup_phase,i=n.startup_blocked??`daemon is ${n.startup_phase}`,n.startup_phase==="blocked")throw new d({error:"startup_blocked",message:n.startup_blocked??"daemon startup is blocked",received:n.startup_phase,field:"phase",next:"Repair the reported blocker paths with `crtr sys migrate`, then retry the await."});if(n.startup_phase===t)return{startup_phase:t,waited_ms:Date.now()-a};if(t==="prepared"&&n.startup_phase==="ready")throw new d({error:"await_missed_phase",message:"daemon is already ready and cannot return to prepared in this process",received:n.startup_phase,field:"phase",next:"Start a new daemon with `crtr sys daemon start --prepare` before awaiting prepared."})}catch(n){if(n instanceof d)throw n;i=n instanceof Error?n.message:String(n)}await new Promise(n=>setTimeout(n,Math.min(D,Math.max(0,r-Date.now()))))}throw new d({error:"await_timeout",message:`daemon did not reach ${t} within ${e}ms (last phase: ${s}; last observation: ${i})`,received:s,field:"phase",next:"Inspect `crtr sys daemon status` and resolve any reported startup blocker before retrying."})}o(S,"awaitDaemonPhase");const E=u({name:"await",description:"wait for a daemon startup phase",whenToUse:"a caller started or was given a daemon that must reach prepared or ready before its next step.",help:{name:"sys daemon await",summary:"wait for the running daemon to report one startup phase",params:[{kind:"flag",name:"phase",type:"enum",choices:["prepared","ready"],required:!0,constraint:"The phase that permits the caller\u2019s next step."},{kind:"flag",name:"timeout",type:"string",required:!1,default:"5m",constraint:"Maximum wait as a positive duration such as 90s, 5m, or 1h30m."}],output:[{name:"startup_phase",type:"string",required:!0,constraint:"The requested phase."},{name:"waited_ms",type:"number",required:!0,constraint:"Elapsed wall-clock time before the phase was observed."}],outputKind:"object",effects:["Read-only: polls /healthz until the requested phase is observed or the caller-bounded timeout expires."]},run:o(async t=>{const e=t.timeout,a=g(e);if(a===null||a<=0)throw new d({error:"invalid_duration",message:`not a positive duration: ${e}`,received:e,field:"timeout",next:"Use a positive duration such as 90s, 5m, or 1h30m."});return S(t.phase,a)},"run")}),R=u({name:"admit",description:"admit normal work after daemon preparation",whenToUse:"the caller has completed its setup against a prepared daemon and is ready for normal daemon work to start.",help:{name:"sys daemon admit",summary:"ask a prepared daemon to start normal services",params:[],output:[{name:"admitted",type:"boolean",required:!0,constraint:"True only when this request began admission."},{name:"already_admitted",type:"boolean",required:!0,constraint:"True when admission had already begun or completed."},{name:"startup_phase",type:"string",required:!0,constraint:"The phase after admission settles."}],outputKind:"object",effects:["Starts normal daemon services when the daemon is prepared."]},run:o(async t=>await c({autostart:!1}).admitDaemon(),"run")});async function P(){try{return(await c({autostart:!1}).status()).generation_id??void 0}catch{return}}o(P,"servingGenerationId");const A=u({name:"status",description:"check whether the daemon is running",whenToUse:"operator/dev diagnostic \u2014 probing whether the crtrd supervisor process is alive while troubleshooting revive/supervision behavior",help:{name:"sys daemon status",summary:"check whether the crtrd supervisor daemon is currently running",params:[],output:[{name:"running",type:"boolean",required:!0,constraint:"True when the daemon pidfile owner is alive."},{name:"serving",type:"boolean",required:!1,constraint:"True when the live daemon answers /healthz; false when one probe does not."},{name:"pid",type:"number",required:!1,constraint:"PID of the running daemon."},{name:"probe_error",type:"string",required:!1,constraint:"The failed /healthz observation when the live owner did not answer."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon startup phase when the socket answered."},{name:"serving_generation",type:"string",required:!1,constraint:"Id of the runtime generation the daemon process was launched from. Absent when the daemon reports none (it predates the field, or runs from a development checkout) or its status could not be read."},{name:"selected_generation",type:"string",required:!1,constraint:"Id of the selected runtime generation. Absent when none is selected or it cannot be read."},{name:"generation_drift",type:"string",required:!1,constraint:"`none` when the serving and selected generation ids are equal; `differs` when they are not, meaning the daemon runs older bytes than the install selected until `sys daemon restart`; `unknown` when either id is absent. Never `none` on missing evidence."}],outputKind:"object",effects:["Read-only: reads the pidfile, probes the pid, and requests /healthz and /v1/status without autostart."]},run:o(async t=>{const e=l();if(!(e!==null&&m(e)))return{running:!1};try{const r=await p(),s=await P(),i=x().generation?.id;return{running:!0,serving:r.ok,pid:e,...r.startup_phase===void 0?{}:{startup_phase:r.startup_phase},...s===void 0?{}:{serving_generation:s},...i===void 0?{}:{selected_generation:i},generation_drift:s===void 0||i===void 0?"unknown":s===i?"none":"differs"}}catch(r){return{running:!0,serving:!1,pid:e,probe_error:b(r)}}},"run")}),j=u({name:"restart",description:"put the selected runtime generation in service via a daemon-owned handover",whenToUse:"adopting a newly installed runtime generation \u2014 the routine restart. Use this instead of stop+start: the daemon answers before it hands over, so a caller running inside a node (whose broker the handover tears down) still gets a settled result and is resumed automatically.",help:{name:"sys daemon restart",summary:"ask the running daemon to replace itself with a successor on the selected runtime generation",params:[],output:[{name:"restarted",type:"boolean",required:!0,constraint:"True when a running daemon accepted the handover. False when none was running \u2014 one was started instead."},{name:"started",type:"boolean",required:!1,constraint:"True when no daemon was running and this call started one (restarted=false)."},{name:"pid",type:"number",required:!1,constraint:"The daemon that accepted the handover, or the one just started."},{name:"grace_ms",type:"number",required:!1,constraint:"How long the daemon waits after answering before teardown begins."},{name:"note",type:"string",required:!1,constraint:"What happens next to the calling node. Read it before acting on an interrupted turn."}],outputKind:"object",effects:["The daemon acknowledges immediately, then after a short grace tears its whole broker fleet down, releases its claim, spawns a successor on the runtime generation currently selected, and exits.","Each torn-down broker hands its foreground bash commands to the file-backed background job system; they keep running and report completion by urgent inbox message.","Every node it tore down \u2014 including the caller \u2014 is resolved by the successor through the ordinary row-driven supervision tick.","The calling process is killed as part of that teardown, AFTER this result is returned."]},run:o(async t=>{if(!k()){const e=await h();return{restarted:!1,started:e.started,...e.pid!==void 0?{pid:e.pid}:{},...e.existing_pid!==void 0?{pid:e.existing_pid}:{},note:"No daemon was running, so there was nothing to hand over; one was started on the selected runtime generation."}}try{const e=await T().restartDaemon();return{restarted:e.accepted,pid:e.predecessor_pid,grace_ms:e.grace_ms,note:"Handover accepted. If you are running inside a node, your broker is about to be torn down and resumed on the new generation \u2014 THIS RESTART ALREADY SUCCEEDED. When your turn resumes with this result in the transcript, do not re-run it."}}catch(e){throw new d({error:"daemon_restart_failed",message:`the daemon did not accept a handover: ${e.message}`,next:"Run `crtr sys daemon status` to inspect the daemon."})}},"run")}),U=u({name:"stop",description:"stop the running daemon",whenToUse:"shutting the crtrd supervisor down for good, ending auto-revival of nodes on window exit. Not the way to pick up a new runtime generation \u2014 `restart` is, because stopping kills every broker of this epoch, including the caller's own, so this command cannot report its result to an agent that runs it.",help:{name:"sys daemon stop",summary:"stop the crtrd supervisor daemon and confirm it exited",params:[],output:[{name:"stopped",type:"boolean",required:!0,constraint:"True when a daemon (recorded or stray) was found and killed. False when none was running."},{name:"pid",type:"number",required:!1,constraint:"The recorded pidfile pid that exited (when one was recorded and alive)."},{name:"strays_killed",type:"array",required:!1,constraint:"PIDs of stray crtrd daemons (alive but not in the pidfile) swept alongside the recorded one \u2014 the backstop against a daemon that survived a prior stop."},{name:"force_killed",type:"boolean",required:!1,constraint:"True when the recorded daemon ignored SIGTERM and had to be SIGKILLed."}],outputKind:"object",effects:["Sends SIGTERM to the recorded pid and waits for it to exit and release its pidfile, escalating to SIGKILL if it does not.","Then sweeps any OTHER live crtrd daemon process (SIGTERM, escalating to SIGKILL after 2s) so a stray daemon orphaned from the pidfile cannot keep supervising."]},run:o(async t=>{const e=l(),a=e!==null&&m(e);try{let r=!1;a&&(r=await v(e)==="killed");const s=_(null);return!a&&s.length===0?{stopped:!1}:{stopped:!0,...a?{pid:e}:{},...s.length>0?{strays_killed:s}:{},...r?{force_killed:!0}:{}}}catch(r){throw new d({error:"daemon_stop_failed",message:`failed to stop daemon${a?` pid ${e}`:""}: ${r.message}`,next:"Run `crtr sys daemon status` to inspect the daemon before retrying."})}},"run")}),L=f({name:"daemon",description:"manage the crtrd supervisor process",whenToUse:"managing the crtrd supervisor that auto-revives nodes on window exit \u2014 start it, restart it onto a new runtime generation, check its status, or stop it",help:{name:"sys daemon",summary:"manage the crtrd canvas supervisor daemon",model:"crtrd is a thin background daemon that polls active+idle nodes and acts on window exit: crashed windows become dead; refresh-yield windows get a fresh respawn. It holds no orchestration logic \u2014 just process supervision. These are operator/dev self-management, not something a normal agent turn needs to check. Picking up a newly installed runtime generation is `restart` (a handover the daemon performs on itself); `stop` is the terminal one, and it kills the caller along with the rest of the fleet."},children:[I,E,R,j,A,U]});function W(){return q([L])}o(W,"registerDaemonControlSys");export{L as daemonBranch,W as registerDaemonControlSys};
@@ -1,4 +1,4 @@
1
- var Te=Object.defineProperty;var E=(e,t)=>Te(e,"name",{value:t,configurable:!0});import{randomBytes as P}from"node:crypto";import{parseScope as ae}from"@north-light/identity";import{importNodeSqlAuthority as de,recreateNodeRowsView as _e}from"./node-sql-migration.js";import{migrateRecoveryWitnesses as ce}from"./recovery.js";import{migrateMessagingState as Ne}from"./deliveries.js";import{migrateHumanRequestsState as Le}from"../human/requests.js";import{existsSync as U,readFileSync as M,readdirSync as k,realpathSync as le,statSync as ue}from"node:fs";import{basename as pe,join as j,resolve as Y}from"node:path";import{appSpace as Re}from"../layout.js";import{emitEvent as Ae}from"../events/emit.js";import{isSafeNodeId as Oe,nodesRoot as h,nodeMetaPath as x}from"./paths.js";import{convertLegacyEdges as Ie}from"../../migrations/004-canvas-documents/legacy-edges.js";import{customExtensionPaths as me}from"./extensions.js";import{readMetaObject as K,updateJsonFileDurably as q,withMetaLock as fe,writeMetaObjectLocked as Xe}from"./meta-file.js";function Ue(e){e.exec(`
1
+ var Te=Object.defineProperty;var E=(e,t)=>Te(e,"name",{value:t,configurable:!0});import{randomBytes as P}from"node:crypto";import{parseScope as ae}from"@north-light/identity";import{importNodeSqlAuthority as de,recreateNodeRowsView as _e}from"./node-sql-migration.js";import{migrateRecoveryWitnesses as Ne}from"./recovery.js";import{migrateMessagingState as ce}from"./deliveries.js";import{migrateHumanRequestsState as Le}from"../human/requests.js";import{existsSync as U,readFileSync as M,readdirSync as k,realpathSync as le,statSync as ue}from"node:fs";import{basename as pe,join as j,resolve as Y}from"node:path";import{appSpace as Re}from"../layout.js";import{emitEvent as Ae}from"../events/emit.js";import{isSafeNodeId as Oe,nodesRoot as h,nodeMetaPath as x}from"./paths.js";import{convertLegacyEdges as Ie}from"../../migrations/004-canvas-documents/legacy-edges.js";import{customExtensionPaths as me}from"./extensions.js";import{readMetaObject as K,updateJsonFileDurably as q,withMetaLock as fe,writeMetaObjectLocked as Xe}from"./meta-file.js";function Ue(e){e.exec(`
2
2
  CREATE TABLE IF NOT EXISTS nodes (
3
3
  node_id TEXT PRIMARY KEY,
4
4
  name TEXT NOT NULL,
@@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS edges (
23
23
  CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(type, to_id);
24
24
  CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
25
25
  CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
26
- `)}E(Ue,"baselineSchema");function b(e,t){return new Set(e.prepare(`PRAGMA table_info(${t})`).all().map(n=>n.name))}E(b,"tableColumns");function $(e,t,n,o){t.has(n)||(e.exec(o),t.add(n))}E($,"addColumnIfMissing");function O(e){return b(e,"nodes")}E(O,"nodeColumns");const N=$;function Ce(e){const t=O(e);N(e,t,"intent","ALTER TABLE nodes ADD COLUMN intent TEXT;"),N(e,t,"pi_pid","ALTER TABLE nodes ADD COLUMN pi_pid INTEGER;"),N(e,t,"window","ALTER TABLE nodes ADD COLUMN window TEXT;"),N(e,t,"tmux_session","ALTER TABLE nodes ADD COLUMN tmux_session TEXT;")}E(Ce,"addRuntimeColumns");function Se(e){const t=h();if(!U(t))return;const n=e.prepare('UPDATE nodes SET intent = ?, pi_pid = ?, "window" = ?, tmux_session = ? WHERE node_id = ?');for(const o of k(t)){const T=x(o);if(!U(T))continue;let i;try{i=JSON.parse(M(T,"utf8"))}catch{continue}n.run(i.intent??null,i.pi_pid??null,i.window??null,i.tmux_session??null,o)}}E(Se,"backfillRuntime");function De(e){B(e,3,()=>{if(!(e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'edges'").get()!==void 0)){e.exec(`
26
+ `)}E(Ue,"baselineSchema");function b(e,t){return new Set(e.prepare(`PRAGMA table_info(${t})`).all().map(n=>n.name))}E(b,"tableColumns");function $(e,t,n,o){t.has(n)||(e.exec(o),t.add(n))}E($,"addColumnIfMissing");function O(e){return b(e,"nodes")}E(O,"nodeColumns");const c=$;function Ce(e){const t=O(e);c(e,t,"intent","ALTER TABLE nodes ADD COLUMN intent TEXT;"),c(e,t,"pi_pid","ALTER TABLE nodes ADD COLUMN pi_pid INTEGER;"),c(e,t,"window","ALTER TABLE nodes ADD COLUMN window TEXT;"),c(e,t,"tmux_session","ALTER TABLE nodes ADD COLUMN tmux_session TEXT;")}E(Ce,"addRuntimeColumns");function Se(e){const t=h();if(!U(t))return;const n=e.prepare('UPDATE nodes SET intent = ?, pi_pid = ?, "window" = ?, tmux_session = ? WHERE node_id = ?');for(const o of k(t)){const T=x(o);if(!U(T))continue;let i;try{i=JSON.parse(M(T,"utf8"))}catch{continue}n.run(i.intent??null,i.pi_pid??null,i.window??null,i.tmux_session??null,o)}}E(Se,"backfillRuntime");function De(e){B(e,3,()=>{if(!(e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'edges'").get()!==void 0)){e.exec(`
27
27
  CREATE TABLE edges (
28
28
  type TEXT NOT NULL,
29
29
  from_id TEXT NOT NULL,
@@ -57,7 +57,7 @@ DROP TABLE edges;
57
57
  ALTER TABLE edges_new RENAME TO edges;
58
58
  CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(type, to_id);
59
59
  CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
60
- `);const o=e.prepare("SELECT COUNT(*) AS n FROM edges").get().n;if(o!==n)throw new Error(`edges FK rebuild lost rows: expected ${n} non-orphan edge(s), got ${o}`);const T=e.prepare("PRAGMA foreign_key_check").all();if(T.length>0)throw new Error(`edges FK rebuild left ${T.length} foreign-key violation(s)`)})}E(De,"edgesForeignKeyCascade");function ge(e){const t=O(e);N(e,t,"pane","ALTER TABLE nodes ADD COLUMN pane TEXT;")}E(ge,"addPaneColumn");function we(e){e.exec(`
60
+ `);const o=e.prepare("SELECT COUNT(*) AS n FROM edges").get().n;if(o!==n)throw new Error(`edges FK rebuild lost rows: expected ${n} non-orphan edge(s), got ${o}`);const T=e.prepare("PRAGMA foreign_key_check").all();if(T.length>0)throw new Error(`edges FK rebuild left ${T.length} foreign-key violation(s)`)})}E(De,"edgesForeignKeyCascade");function ge(e){const t=O(e);c(e,t,"pane","ALTER TABLE nodes ADD COLUMN pane TEXT;")}E(ge,"addPaneColumn");function we(e){e.exec(`
61
61
  CREATE TABLE IF NOT EXISTS focuses (
62
62
  focus_id TEXT PRIMARY KEY, -- stable internal id for the viewport
63
63
  pane TEXT, -- the durable %pane_id realizing the focus
@@ -131,12 +131,12 @@ SELECT
131
131
  FROM wakeups;
132
132
 
133
133
  DROP TABLE wakeups;
134
- `)}E(ve,"migrateWakeupsToTriggers");function he(e){const t=O(e);N(e,t,"host_kind","ALTER TABLE nodes ADD COLUMN host_kind TEXT;")}E(he,"addHostKindColumn");function xe(e){e.exec(`
134
+ `)}E(ve,"migrateWakeupsToTriggers");function he(e){const t=O(e);c(e,t,"host_kind","ALTER TABLE nodes ADD COLUMN host_kind TEXT;")}E(he,"addHostKindColumn");function xe(e){e.exec(`
135
135
  CREATE TABLE IF NOT EXISTS canvas_meta (
136
136
  key TEXT PRIMARY KEY,
137
137
  value TEXT NOT NULL
138
138
  );
139
- `)}E(xe,"addCanvasMetaTable");function Fe(e){const t=O(e);N(e,t,"profile_id","ALTER TABLE nodes ADD COLUMN profile_id TEXT;")}E(Fe,"addProfileIdColumn");function Me(e){const t=O(e);N(e,t,"pi_pid_identity","ALTER TABLE nodes ADD COLUMN pi_pid_identity TEXT;")}E(Me,"addPidIdentityColumn");function ke(e){const t=O(e);N(e,t,"waiting_for","ALTER TABLE nodes ADD COLUMN waiting_for TEXT;")}E(ke,"addMessageWaitColumn");function J(e){const t=O(e);if(t.size===0)return;N(e,t,"final_report","ALTER TABLE nodes ADD COLUMN final_report TEXT;"),N(e,t,"finalized_at","ALTER TABLE nodes ADD COLUMN finalized_at TEXT;");const n=e.prepare("SELECT node_id FROM nodes WHERE status = 'done' AND final_report IS NULL").all(),o=e.prepare("UPDATE nodes SET final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL");for(const{node_id:T}of n){const i=j(h(),T,"reports");if(!U(i))continue;let a=null;for(const c of k(i))if(c.endsWith(".md"))try{const L=M(j(i,c),"utf8"),l=/^---\nnode: [^\n]+\nkind: final\nts: ([^\n]+)\n---\n/.exec(L);if(l===null||Number.isNaN(Date.parse(l[1])))continue;const r={name:c,ts:l[1]};(a===null||r.ts>a.ts||r.ts===a.ts&&r.name>a.name)&&(a=r)}catch{}a!==null&&o.run(a.name,a.ts,T)}}E(J,"addCanonicalFinalColumns");function je(e){const t=h();if(U(t))for(const n of k(t)){const o=x(n);U(o)&&q(o,T=>{const i=T.launch;if(typeof i!="object"||i===null||Array.isArray(i))return!1;const a=i.extensions;if(!Array.isArray(a)||!a.every(L=>typeof L=="string"))return!1;const c=me(a);return c.length===a.length?!1:(i.extensions=c,!0)})}}E(je,"normalizePersistedLaunchExtensions");function He(e){const t=O(e);N(e,t,"waiting_for_generation","ALTER TABLE nodes ADD COLUMN waiting_for_generation INTEGER NOT NULL DEFAULT 0;")}E(He,"addMessageWaitGenerationColumn");function Ge(e){e.exec(`
139
+ `)}E(xe,"addCanvasMetaTable");function Fe(e){const t=O(e);c(e,t,"profile_id","ALTER TABLE nodes ADD COLUMN profile_id TEXT;")}E(Fe,"addProfileIdColumn");function Me(e){const t=O(e);c(e,t,"pi_pid_identity","ALTER TABLE nodes ADD COLUMN pi_pid_identity TEXT;")}E(Me,"addPidIdentityColumn");function ke(e){const t=O(e);c(e,t,"waiting_for","ALTER TABLE nodes ADD COLUMN waiting_for TEXT;")}E(ke,"addMessageWaitColumn");function J(e){const t=O(e);if(t.size===0)return;c(e,t,"final_report","ALTER TABLE nodes ADD COLUMN final_report TEXT;"),c(e,t,"finalized_at","ALTER TABLE nodes ADD COLUMN finalized_at TEXT;");const n=e.prepare("SELECT node_id FROM nodes WHERE status = 'done' AND final_report IS NULL").all(),o=e.prepare("UPDATE nodes SET final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL");for(const{node_id:T}of n){const i=j(h(),T,"reports");if(!U(i))continue;let a=null;for(const N of k(i))if(N.endsWith(".md"))try{const L=M(j(i,N),"utf8"),l=/^---\nnode: [^\n]+\nkind: final\nts: ([^\n]+)\n---\n/.exec(L);if(l===null||Number.isNaN(Date.parse(l[1])))continue;const r={name:N,ts:l[1]};(a===null||r.ts>a.ts||r.ts===a.ts&&r.name>a.name)&&(a=r)}catch{}a!==null&&o.run(a.name,a.ts,T)}}E(J,"addCanonicalFinalColumns");function je(e){const t=h();if(U(t))for(const n of k(t)){const o=x(n);U(o)&&q(o,T=>{const i=T.launch;if(typeof i!="object"||i===null||Array.isArray(i))return!1;const a=i.extensions;if(!Array.isArray(a)||!a.every(L=>typeof L=="string"))return!1;const N=me(a);return N.length===a.length?!1:(i.extensions=N,!0)})}}E(je,"normalizePersistedLaunchExtensions");function He(e){const t=O(e);c(e,t,"waiting_for_generation","ALTER TABLE nodes ADD COLUMN waiting_for_generation INTEGER NOT NULL DEFAULT 0;")}E(He,"addMessageWaitGenerationColumn");function Ge(e){e.exec(`
140
140
  CREATE TABLE IF NOT EXISTS consult_outbox (
141
141
  node_id TEXT PRIMARY KEY, -- legacy work node id
142
142
  dir TEXT NOT NULL, -- legacy ticket directory
@@ -216,19 +216,19 @@ CREATE INDEX IF NOT EXISTS idx_cron_runs_cron ON cron_runs(cron_id, started);
216
216
  `)}E(We,"addCronsTables");function g(e){return`'${e.replace(/'/g,`'"'"'`)}'`}E(g,"shellSingleQuote");function Pe(e,t){let n=0;for(;;){const o=`${t}_${n}`;if(!e.split(`
217
217
  `).includes(o))return o;n++}}E(Pe,"uniqueHeredocDelimiter");function Q(e,t,n){const o=Pe(t,n);return`${e} <<'${o}'
218
218
  ${t}
219
- ${o}`}E(Q,"heredocCommand");function Ye(e){if(typeof e!="string")return{};try{const t=JSON.parse(e);return typeof t!="object"||t===null||Array.isArray(t)?{}:t}catch{return{}}}E(Ye,"migrationPayload");function X(e,t,n){const o=e[t];if(typeof o!="string"||o.trim()==="")throw new Error(`trigger ${n} payload requires non-empty ${t}`);return o}E(X,"requiredString");function H(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}E(H,"optionalString");function w(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}E(w,"optionalColumn");function z(e,t,n,o){return Q(`crtr node message send --to ${g(e)} --tier ${g(t)}`,n,`CRTR_MIGRATED_${o.replace(/[^A-Za-z0-9]/g,"_")}`)}E(z,"migratedMessageCommand");function Ke(e,t){return`printf '%s\\n' "$CRTR_GATE_STDOUT" | crtr node message send --to ${g(e)} --tier ${g(t)}`}E(Ke,"migratedGateStdoutMessageCommand");function qe(e,t){const n=[`crtr node new --kind ${g(X(e,"kind",t))}`],o=[["mode","mode"],["cwd","cwd"],["name","name"],["parent","parent"],["worktreeBase","worktree"],["forkFrom","fork-from"],["model","model"],["profile","profile"],["situationalContext","situational-context"]];for(const[i,a]of o){const c=H(e,i);c!==void 0&&n.push(`--${a} ${g(c)}`)}e.root===!0&&n.push("--root");const T=X(e,"prompt",t);return Q(n.join(" "),T,`CRTR_MIGRATED_${t.replace(/[^A-Za-z0-9]/g,"_")}`)}E(qe,"migratedSpawnCommand");const V="migration.v22.skipped";function $e(e){const t=e.prepare("SELECT cwd FROM nodes WHERE parent IS NULL ORDER BY created LIMIT 1").get();return(t!==void 0?w(t,"cwd"):void 0)??process.cwd()}E($e,"ambientMigrationCwd");function Je(e){let t;try{t=e.prepare("SELECT eval_pid FROM triggers WHERE eval_state = 'running' AND eval_pid IS NOT NULL").all()}catch{return}for(const n of t){const o=n.eval_pid;if(!(typeof o!="number"||!Number.isInteger(o)||o<=1))try{process.kill(-o,"SIGTERM")}catch{}}}E(Je,"reapRunningPredicateEvaluations");function Qe(e){const t=[],n=e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'triggers'").get()!==void 0;n&&Je(e);let o=[];if(n)try{o=e.prepare("SELECT * FROM triggers ORDER BY created, trigger_id").all()}catch(a){t.push({trigger_id:"<unreadable table>",reason:`triggers table could not be read: ${a instanceof Error?a.message:String(a)}`})}const T=$e(e),i=e.prepare(`
219
+ ${o}`}E(Q,"heredocCommand");function Ye(e){if(typeof e!="string")return{};try{const t=JSON.parse(e);return typeof t!="object"||t===null||Array.isArray(t)?{}:t}catch{return{}}}E(Ye,"migrationPayload");function X(e,t,n){const o=e[t];if(typeof o!="string"||o.trim()==="")throw new Error(`trigger ${n} payload requires non-empty ${t}`);return o}E(X,"requiredString");function H(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}E(H,"optionalString");function w(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}E(w,"optionalColumn");function z(e,t,n,o){return Q(`crtr node message send --to ${g(e)} --tier ${g(t)}`,n,`CRTR_MIGRATED_${o.replace(/[^A-Za-z0-9]/g,"_")}`)}E(z,"migratedMessageCommand");function Ke(e,t){return`printf '%s\\n' "$CRTR_GATE_STDOUT" | crtr node message send --to ${g(e)} --tier ${g(t)}`}E(Ke,"migratedGateStdoutMessageCommand");function qe(e,t){const n=[`crtr node new --kind ${g(X(e,"kind",t))}`],o=[["mode","mode"],["cwd","cwd"],["name","name"],["parent","parent"],["worktreeBase","worktree"],["forkFrom","fork-from"],["model","model"],["profile","profile"],["situationalContext","situational-context"]];for(const[i,a]of o){const N=H(e,i);N!==void 0&&n.push(`--${a} ${g(N)}`)}e.root===!0&&n.push("--root");const T=X(e,"prompt",t);return Q(n.join(" "),T,`CRTR_MIGRATED_${t.replace(/[^A-Za-z0-9]/g,"_")}`)}E(qe,"migratedSpawnCommand");const V="migration.v22.skipped";function $e(e){const t=e.prepare("SELECT cwd FROM nodes WHERE parent IS NULL ORDER BY created LIMIT 1").get();return(t!==void 0?w(t,"cwd"):void 0)??process.cwd()}E($e,"ambientMigrationCwd");function Je(e){let t;try{t=e.prepare("SELECT eval_pid FROM triggers WHERE eval_state = 'running' AND eval_pid IS NOT NULL").all()}catch{return}for(const n of t){const o=n.eval_pid;if(!(typeof o!="number"||!Number.isInteger(o)||o<=1))try{process.kill(-o,"SIGTERM")}catch{}}}E(Je,"reapRunningPredicateEvaluations");function Qe(e){const t=[],n=e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'triggers'").get()!==void 0;n&&Je(e);let o=[];if(n)try{o=e.prepare("SELECT * FROM triggers ORDER BY created, trigger_id").all()}catch(a){t.push({trigger_id:"<unreadable table>",reason:`triggers table could not be read: ${a instanceof Error?a.message:String(a)}`})}const T=$e(e),i=e.prepare(`
220
220
  INSERT OR REPLACE INTO crons (
221
221
  cron_id, name, created_by, command, fire_at, recur, tz, expires_at,
222
222
  anchor_node, cancel_on_wake, cwd, env_json, profile, scope,
223
223
  run_timeout_s, overlap, on_output, sink, tier, created, updated
224
- ) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, NULL, ?, 'profile', ?, 'skip', 'on-failure', '{}', ?, ?, ?)`);for(const a of o){const c=w(a,"trigger_id");if(c===void 0){t.push({trigger_id:"<unidentified row>",reason:"row carries no trigger_id"});continue}try{ze(e,i,a,c,T)}catch(L){t.push({trigger_id:c,reason:L instanceof Error?L.message:String(L)})}}t.length>0&&(e.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(V,JSON.stringify(t)),console.warn(`[canvas migration v22] ${t.length} retired schedule(s) could not be converted to crons and were dropped; see canvas_meta['${V}']: `+t.map(a=>`${a.trigger_id} (${a.reason})`).join("; "))),e.exec(`
224
+ ) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, NULL, ?, 'profile', ?, 'skip', 'on-failure', '{}', ?, ?, ?)`);for(const a of o){const N=w(a,"trigger_id");if(N===void 0){t.push({trigger_id:"<unidentified row>",reason:"row carries no trigger_id"});continue}try{ze(e,i,a,N,T)}catch(L){t.push({trigger_id:N,reason:L instanceof Error?L.message:String(L)})}}t.length>0&&(e.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(V,JSON.stringify(t)),console.warn(`[canvas migration v22] ${t.length} retired schedule(s) could not be converted to crons and were dropped; see canvas_meta['${V}']: `+t.map(a=>`${a.trigger_id} (${a.reason})`).join("; "))),e.exec(`
225
225
  DROP INDEX IF EXISTS idx_triggers_fire;
226
226
  DROP INDEX IF EXISTS idx_triggers_node;
227
227
  DROP INDEX IF EXISTS idx_triggers_owner;
228
228
  DROP INDEX IF EXISTS idx_triggers_eval;
229
229
  DROP INDEX IF EXISTS idx_triggers_deadline;
230
230
  DROP TABLE IF EXISTS triggers;
231
- `)}E(Qe,"migrateTriggersToCrons");function ze(e,t,n,o,T){const i=X(n,"owner_id",o),a=e.prepare("SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(i),c=(a===void 0?void 0:w(a,"cwd"))??T,L=a===void 0?null:w(a,"profile_id")??null,l=w(n,"node_id")??null,r=X(n,"action",o),_=X(n,"action_mode",o),d=X(n,"trigger_type",o),u=Ye(n.payload),p=w(n,"created")??new Date().toISOString(),I=_==="deadline_message"?"urgent":H(u,"tier")??"normal",R=H(u,"label");let s,A,m=!1;if(r==="msg"&&(_==="inbox_message"||_==="deadline_message")){if(l===null)throw new Error("has no message target");const f=H(u,"body");f!==void 0?s=z(l,I,f,o):d==="when"?(m=!0,s=Ke(l,I)):s=z(l,I,R??"Scheduled message (no body was recorded when this schedule was armed).",o),A=R??(_==="deadline_message"?"deadline":"scheduled message")}else if(r==="msg"&&_==="fresh_revive"){if(l===null)throw new Error("has no revive target");s=`crtr node lifecycle revive ${g(l)} --fresh`,A=R??"scheduled fresh revive"}else if(r==="new"&&_==="node_birth")s=qe(u,o),A=H(u,"name")??`scheduled ${X(u,"kind",o)} node`;else throw new Error(`has unsupported action ${r}/${_}`);let D,y,G=null;if(d==="when"){D=X(n,"next_eval_at",o),y=X(n,"eval_every",o),G=w(n,"timeout_at")??null;const f=X(n,"when_script",o);s=`${m?`if ! CRTR_GATE_STDOUT="$(
231
+ `)}E(Qe,"migrateTriggersToCrons");function ze(e,t,n,o,T){const i=X(n,"owner_id",o),a=e.prepare("SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(i),N=(a===void 0?void 0:w(a,"cwd"))??T,L=a===void 0?null:w(a,"profile_id")??null,l=w(n,"node_id")??null,r=X(n,"action",o),_=X(n,"action_mode",o),d=X(n,"trigger_type",o),u=Ye(n.payload),p=w(n,"created")??new Date().toISOString(),I=_==="deadline_message"?"urgent":H(u,"tier")??"normal",R=H(u,"label");let s,A,m=!1;if(r==="msg"&&(_==="inbox_message"||_==="deadline_message")){if(l===null)throw new Error("has no message target");const f=H(u,"body");f!==void 0?s=z(l,I,f,o):d==="when"?(m=!0,s=Ke(l,I)):s=z(l,I,R??"Scheduled message (no body was recorded when this schedule was armed).",o),A=R??(_==="deadline_message"?"deadline":"scheduled message")}else if(r==="msg"&&_==="fresh_revive"){if(l===null)throw new Error("has no revive target");s=`crtr node lifecycle revive ${g(l)} --fresh`,A=R??"scheduled fresh revive"}else if(r==="new"&&_==="node_birth")s=qe(u,o),A=H(u,"name")??`scheduled ${X(u,"kind",o)} node`;else throw new Error(`has unsupported action ${r}/${_}`);let D,y,G=null;if(d==="when"){D=X(n,"next_eval_at",o),y=X(n,"eval_every",o),G=w(n,"timeout_at")??null;const f=X(n,"when_script",o);s=`${m?`if ! CRTR_GATE_STDOUT="$(
232
232
  ${f}
233
233
  )"; then`:`if ! (
234
234
  ${f}
@@ -237,7 +237,7 @@ ${f}
237
237
  fi
238
238
  {
239
239
  ${s}
240
- } && crtr cron cancel "$CRTR_CRON_ID"`}else if(d==="at"||d==="every"||d==="until")D=X(n,"fire_at",o),y=d==="every"?X(n,"recur",o):null;else throw new Error(`has unknown timing type ${d}`);if(_==="deadline_message"&&l!==null){const f=e.prepare("SELECT cron_id FROM crons WHERE anchor_node = ? AND cancel_on_wake = 1 AND cron_id <> ?").get(l,o);if(f!==void 0)throw new Error(`node ${l} already has a migrated deadline (${String(f.cron_id)}); only one is allowed`)}t.run(o,A,i,s,D,y,G,l,_==="deadline_message"?1:0,c,L,300,I,p,p)}E(ze,"convertTriggerRow");function Ve(e){const t=b(e,"crons");t.size===0||t.has("run_pid_identity")||e.exec("ALTER TABLE crons ADD COLUMN run_pid_identity TEXT;")}E(Ve,"addCronRunPidIdentityColumn");function Ze(e){const t=b(e,"crons");t.size!==0&&(t.has("run_out_of_band")||e.exec("ALTER TABLE crons ADD COLUMN run_out_of_band INTEGER NOT NULL DEFAULT 0;"),t.has("queued")&&e.exec("ALTER TABLE crons DROP COLUMN queued;"))}E(Ze,"addCronOutOfBandColumn");function et(e){e.exec(`
240
+ } && crtr cron cancel "$CRTR_CRON_ID"`}else if(d==="at"||d==="every"||d==="until")D=X(n,"fire_at",o),y=d==="every"?X(n,"recur",o):null;else throw new Error(`has unknown timing type ${d}`);if(_==="deadline_message"&&l!==null){const f=e.prepare("SELECT cron_id FROM crons WHERE anchor_node = ? AND cancel_on_wake = 1 AND cron_id <> ?").get(l,o);if(f!==void 0)throw new Error(`node ${l} already has a migrated deadline (${String(f.cron_id)}); only one is allowed`)}t.run(o,A,i,s,D,y,G,l,_==="deadline_message"?1:0,N,L,300,I,p,p)}E(ze,"convertTriggerRow");function Ve(e){const t=b(e,"crons");t.size===0||t.has("run_pid_identity")||e.exec("ALTER TABLE crons ADD COLUMN run_pid_identity TEXT;")}E(Ve,"addCronRunPidIdentityColumn");function Ze(e){const t=b(e,"crons");t.size!==0&&(t.has("run_out_of_band")||e.exec("ALTER TABLE crons ADD COLUMN run_out_of_band INTEGER NOT NULL DEFAULT 0;"),t.has("queued")&&e.exec("ALTER TABLE crons DROP COLUMN queued;"))}E(Ze,"addCronOutOfBandColumn");function et(e){e.exec(`
241
241
  CREATE TABLE IF NOT EXISTS warm_pool (
242
242
  node_id TEXT PRIMARY KEY REFERENCES nodes(node_id) ON DELETE CASCADE,
243
243
  recipe_key TEXT NOT NULL, -- frozen launch tuple this spare was minted for
@@ -378,18 +378,18 @@ CREATE TABLE human_action_deliveries (
378
378
  updated_at INTEGER NOT NULL
379
379
  );
380
380
  CREATE INDEX idx_human_action_deliveries_due ON human_action_deliveries(state, next_attempt_at);
381
- `)}E(Tt,"addHumanActionDeliveries");function at(e){const t=O(e);N(e,t,"pi_session_file","ALTER TABLE nodes ADD COLUMN pi_session_file TEXT;");for(const n of k(h(),{withFileTypes:!0}))if(n.isDirectory())try{const o=JSON.parse(M(x(n.name),"utf8"));typeof o.pi_session_file=="string"&&e.prepare("UPDATE nodes SET pi_session_file = ? WHERE node_id = ?").run(o.pi_session_file,n.name)}catch{}e.exec("CREATE INDEX IF NOT EXISTS idx_nodes_pi_session_file ON nodes(pi_session_file);")}E(at,"addNodeSessionFileIndex");function Z(e,t){const n=E(c=>{const L=Y(c);return U(L)?le(L):L},"canonical"),o=e.prepare("UPDATE nodes SET pi_session_file = ? WHERE node_id = ?");e.exec("DROP INDEX IF EXISTS idx_nodes_pi_session_file;");const T=e.prepare("SELECT node_id FROM nodes ORDER BY created ASC, node_id ASC").all();for(const{node_id:c}of T){const L=x(c);if(K(L)===null){o.run(null,c);continue}const l=fe(L,()=>{const r=K(L);if(r===null)throw new Error(`node ${c} session metadata changed during ownership migration`);if(typeof r.pi_session_file!="string")return null;const _=r.pi_session_file,d=n(_);return d!==_&&(r.pi_session_file=d,Xe(L,r)),d});o.run(l,c)}const i=e.prepare(`
381
+ `)}E(Tt,"addHumanActionDeliveries");function at(e){const t=O(e);c(e,t,"pi_session_file","ALTER TABLE nodes ADD COLUMN pi_session_file TEXT;");for(const n of k(h(),{withFileTypes:!0}))if(n.isDirectory())try{const o=JSON.parse(M(x(n.name),"utf8"));typeof o.pi_session_file=="string"&&e.prepare("UPDATE nodes SET pi_session_file = ? WHERE node_id = ?").run(o.pi_session_file,n.name)}catch{}e.exec("CREATE INDEX IF NOT EXISTS idx_nodes_pi_session_file ON nodes(pi_session_file);")}E(at,"addNodeSessionFileIndex");function Z(e,t){const n=E(N=>{const L=Y(N);return U(L)?le(L):L},"canonical"),o=e.prepare("UPDATE nodes SET pi_session_file = ? WHERE node_id = ?");e.exec("DROP INDEX IF EXISTS idx_nodes_pi_session_file;");const T=e.prepare("SELECT node_id FROM nodes ORDER BY created ASC, node_id ASC").all();for(const{node_id:N}of T){const L=x(N);if(K(L)===null){o.run(null,N);continue}const l=fe(L,()=>{const r=K(L);if(r===null)throw new Error(`node ${N} session metadata changed during ownership migration`);if(typeof r.pi_session_file!="string")return null;const _=r.pi_session_file,d=n(_);return d!==_&&(r.pi_session_file=d,Xe(L,r)),d});o.run(l,N)}const i=e.prepare(`
382
382
  SELECT pi_session_file
383
383
  FROM nodes
384
384
  WHERE pi_session_file IS NOT NULL
385
385
  GROUP BY pi_session_file
386
386
  HAVING COUNT(*) > 1
387
- `).all();if(!t&&i.length>0){const c=i.map(({pi_session_file:L})=>L).join(", ");throw new Error(`cannot restore unique Pi session ownership: authoritative metadata has concurrent claimants for ${c}`)}const a=e.prepare("UPDATE nodes SET pi_session_file = NULL WHERE node_id = ?");for(const{pi_session_file:c}of i){const L=e.prepare(`
387
+ `).all();if(!t&&i.length>0){const N=i.map(({pi_session_file:L})=>L).join(", ");throw new Error(`cannot restore unique Pi session ownership: authoritative metadata has concurrent claimants for ${N}`)}const a=e.prepare("UPDATE nodes SET pi_session_file = NULL WHERE node_id = ?");for(const{pi_session_file:N}of i){const L=e.prepare(`
388
388
  SELECT node_id
389
389
  FROM nodes
390
390
  WHERE pi_session_file = ?
391
391
  ORDER BY created ASC, node_id ASC
392
- `).all(c);for(const{node_id:l}of L.slice(1)){if(!q(x(l),_=>{const d=_.pi_session_file;if(typeof d!="string"||n(d)!==c)throw new Error(`node ${l} session metadata changed during ownership migration`);return _.pi_session_id=null,_.pi_session_file=null,!0}))throw new Error(`node ${l} session metadata changed during ownership migration`);a.run(l)}}e.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_nodes_pi_session_file ON nodes(pi_session_file);")}E(Z,"reconcileNodeSessionFileOwners");function dt(e){Z(e,!0)}E(dt,"makeNodeSessionFileUnique");function _t(e){Z(e,!1)}E(_t,"repairNodeSessionFileUniqueIndex");function ct(e){const t=O(e);N(e,t,"frozen_at","ALTER TABLE nodes ADD COLUMN frozen_at TEXT;"),N(e,t,"respawn_failures","ALTER TABLE nodes ADD COLUMN respawn_failures INTEGER NOT NULL DEFAULT 0;"),N(e,t,"respawn_not_before","ALTER TABLE nodes ADD COLUMN respawn_not_before TEXT;"),N(e,t,"launched_at","ALTER TABLE nodes ADD COLUMN launched_at TEXT;"),N(e,t,"last_settled_at","ALTER TABLE nodes ADD COLUMN last_settled_at TEXT;"),N(e,t,"terminal_reason","ALTER TABLE nodes ADD COLUMN terminal_reason TEXT;"),e.exec(`
392
+ `).all(N);for(const{node_id:l}of L.slice(1)){if(!q(x(l),_=>{const d=_.pi_session_file;if(typeof d!="string"||n(d)!==N)throw new Error(`node ${l} session metadata changed during ownership migration`);return _.pi_session_id=null,_.pi_session_file=null,!0}))throw new Error(`node ${l} session metadata changed during ownership migration`);a.run(l)}}e.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_nodes_pi_session_file ON nodes(pi_session_file);")}E(Z,"reconcileNodeSessionFileOwners");function dt(e){Z(e,!0)}E(dt,"makeNodeSessionFileUnique");function _t(e){Z(e,!1)}E(_t,"repairNodeSessionFileUniqueIndex");function Nt(e){const t=O(e);c(e,t,"frozen_at","ALTER TABLE nodes ADD COLUMN frozen_at TEXT;"),c(e,t,"respawn_failures","ALTER TABLE nodes ADD COLUMN respawn_failures INTEGER NOT NULL DEFAULT 0;"),c(e,t,"respawn_not_before","ALTER TABLE nodes ADD COLUMN respawn_not_before TEXT;"),c(e,t,"launched_at","ALTER TABLE nodes ADD COLUMN launched_at TEXT;"),c(e,t,"last_settled_at","ALTER TABLE nodes ADD COLUMN last_settled_at TEXT;"),c(e,t,"terminal_reason","ALTER TABLE nodes ADD COLUMN terminal_reason TEXT;"),e.exec(`
393
393
  UPDATE nodes SET terminal_reason = CASE
394
394
  WHEN status = 'done' AND final_report IS NOT NULL THEN 'finalized'
395
395
  WHEN status = 'done' AND intent = 'parked' THEN 'parked'
@@ -398,7 +398,7 @@ UPDATE nodes SET terminal_reason = CASE
398
398
  WHEN status = 'dead' THEN 'crashed'
399
399
  END
400
400
  WHERE status IN ('done','canceled','dead') AND terminal_reason IS NULL;
401
- `)}E(ct,"addNodeLifecycleColumns");function Nt(e){const t=O(e);N(e,t,"description","ALTER TABLE nodes ADD COLUMN description TEXT;"),N(e,t,"cycles","ALTER TABLE nodes ADD COLUMN cycles INTEGER;");const n=h();if(!U(n))return;const o=e.prepare("UPDATE nodes SET description = ?, cycles = ? WHERE node_id = ?");for(const T of k(n)){const i=x(T);if(!U(i))continue;let a;try{a=JSON.parse(M(i,"utf8"))}catch{continue}const c=typeof a.description=="string"?a.description:null,L=typeof a.cycles=="number"?a.cycles:null;o.run(c,L,T)}}E(Nt,"addNodeLabelColumns");function Lt(e){const t=O(e);N(e,t,"outcome_kind","ALTER TABLE nodes ADD COLUMN outcome_kind TEXT;"),N(e,t,"outcome_reason","ALTER TABLE nodes ADD COLUMN outcome_reason TEXT;"),N(e,t,"outcome_at","ALTER TABLE nodes ADD COLUMN outcome_at TEXT;"),N(e,t,"outcome_revision","ALTER TABLE nodes ADD COLUMN outcome_revision INTEGER NOT NULL DEFAULT 0;"),N(e,t,"outcome_detail_json","ALTER TABLE nodes ADD COLUMN outcome_detail_json TEXT;"),N(e,t,"deadline_at","ALTER TABLE nodes ADD COLUMN deadline_at TEXT;"),e.exec(`
401
+ `)}E(Nt,"addNodeLifecycleColumns");function ct(e){const t=O(e);c(e,t,"description","ALTER TABLE nodes ADD COLUMN description TEXT;"),c(e,t,"cycles","ALTER TABLE nodes ADD COLUMN cycles INTEGER;");const n=h();if(!U(n))return;const o=e.prepare("UPDATE nodes SET description = ?, cycles = ? WHERE node_id = ?");for(const T of k(n)){const i=x(T);if(!U(i))continue;let a;try{a=JSON.parse(M(i,"utf8"))}catch{continue}const N=typeof a.description=="string"?a.description:null,L=typeof a.cycles=="number"?a.cycles:null;o.run(N,L,T)}}E(ct,"addNodeLabelColumns");function Lt(e){const t=O(e);c(e,t,"outcome_kind","ALTER TABLE nodes ADD COLUMN outcome_kind TEXT;"),c(e,t,"outcome_reason","ALTER TABLE nodes ADD COLUMN outcome_reason TEXT;"),c(e,t,"outcome_at","ALTER TABLE nodes ADD COLUMN outcome_at TEXT;"),c(e,t,"outcome_revision","ALTER TABLE nodes ADD COLUMN outcome_revision INTEGER NOT NULL DEFAULT 0;"),c(e,t,"outcome_detail_json","ALTER TABLE nodes ADD COLUMN outcome_detail_json TEXT;"),c(e,t,"deadline_at","ALTER TABLE nodes ADD COLUMN deadline_at TEXT;"),e.exec(`
402
402
  CREATE TABLE IF NOT EXISTS node_outcome_deliveries (
403
403
  node_id TEXT PRIMARY KEY,
404
404
  state TEXT NOT NULL,
@@ -431,17 +431,17 @@ UPDATE nodes SET outcome_kind='failure',
431
431
  outcome_reason=COALESCE(terminal_reason,'crashed'),
432
432
  outcome_at=COALESCE(finalized_at, created), outcome_revision=1
433
433
  WHERE final_report IS NULL AND status IN ('done','dead','canceled') AND outcome_at IS NULL;
434
- `)}E(Lt,"addNodeOutcomeAndDeadline");function lt(e){const t=O(e);N(e,t,"telemetry_tokens_in","ALTER TABLE nodes ADD COLUMN telemetry_tokens_in INTEGER;"),N(e,t,"telemetry_context_tokens","ALTER TABLE nodes ADD COLUMN telemetry_context_tokens INTEGER;"),N(e,t,"telemetry_last_activity","ALTER TABLE nodes ADD COLUMN telemetry_last_activity TEXT;"),N(e,t,"telemetry_updated_at","ALTER TABLE nodes ADD COLUMN telemetry_updated_at TEXT;");const n=e.prepare(`UPDATE nodes
434
+ `)}E(Lt,"addNodeOutcomeAndDeadline");function lt(e){const t=O(e);c(e,t,"telemetry_tokens_in","ALTER TABLE nodes ADD COLUMN telemetry_tokens_in INTEGER;"),c(e,t,"telemetry_context_tokens","ALTER TABLE nodes ADD COLUMN telemetry_context_tokens INTEGER;"),c(e,t,"telemetry_last_activity","ALTER TABLE nodes ADD COLUMN telemetry_last_activity TEXT;"),c(e,t,"telemetry_updated_at","ALTER TABLE nodes ADD COLUMN telemetry_updated_at TEXT;");const n=e.prepare(`UPDATE nodes
435
435
  SET telemetry_tokens_in = ?, telemetry_context_tokens = ?,
436
436
  telemetry_last_activity = ?, telemetry_updated_at = ?
437
- WHERE node_id = ?`);for(const{node_id:o}of e.prepare("SELECT node_id FROM nodes").all()){const T=j(h(),o,"job","telemetry.json");if(U(T))try{const i=JSON.parse(M(T,"utf8")),a=typeof i.tokens_in=="number"&&Number.isFinite(i.tokens_in)?i.tokens_in:null,c=typeof i.context_tokens=="number"&&Number.isFinite(i.context_tokens)?i.context_tokens:null,L=typeof i.last_activity=="string"?i.last_activity:null,l=ue(T).mtime.toISOString();n.run(a,c,L,l,o)}catch{}}}E(lt,"addNodeTelemetryColumns");function ut(e){e.exec(`
437
+ WHERE node_id = ?`);for(const{node_id:o}of e.prepare("SELECT node_id FROM nodes").all()){const T=j(h(),o,"job","telemetry.json");if(U(T))try{const i=JSON.parse(M(T,"utf8")),a=typeof i.tokens_in=="number"&&Number.isFinite(i.tokens_in)?i.tokens_in:null,N=typeof i.context_tokens=="number"&&Number.isFinite(i.context_tokens)?i.context_tokens:null,L=typeof i.last_activity=="string"?i.last_activity:null,l=ue(T).mtime.toISOString();n.run(a,N,L,l,o)}catch{}}}E(lt,"addNodeTelemetryColumns");function ut(e){e.exec(`
438
438
  CREATE INDEX IF NOT EXISTS deliveries_external_due_order
439
439
  ON deliveries(state, next_attempt_at, created_at, delivery_id)
440
440
  WHERE destination_kind = 'external_action' AND canceled_at IS NULL;
441
441
  DROP INDEX IF EXISTS deliveries_external_due;
442
442
  CREATE INDEX IF NOT EXISTS idx_nodes_parent_created ON nodes(parent, created);
443
443
  CREATE INDEX IF NOT EXISTS idx_edges_type_created ON edges(type, created);
444
- `)}E(ut,"addRecurringLaneIndexes");function pt(e){_e(e)}E(pt,"projectLiveNodeState");function Rt(e){N(e,O(e),"scopes_json","ALTER TABLE nodes ADD COLUMN scopes_json TEXT;")}E(Rt,"addNodeScopes");function At(e){const t=O(e);N(e,t,"grantee","ALTER TABLE nodes ADD COLUMN grantee TEXT NOT NULL DEFAULT 'user:local';"),N(e,t,"sub","ALTER TABLE nodes ADD COLUMN sub TEXT;"),N(e,t,"grant_id","ALTER TABLE nodes ADD COLUMN grant_id TEXT;"),N(e,t,"client_id","ALTER TABLE nodes ADD COLUMN client_id TEXT;"),N(e,t,"metadata_json","ALTER TABLE nodes ADD COLUMN metadata_json TEXT;"),N(e,t,"run_id","ALTER TABLE nodes ADD COLUMN run_id TEXT;"),N(e,t,"event_cursor","ALTER TABLE nodes ADD COLUMN event_cursor INTEGER;"),N(e,t,"usage_json","ALTER TABLE nodes ADD COLUMN usage_json TEXT;"),N(e,t,"deleting_at","ALTER TABLE nodes ADD COLUMN deleting_at TEXT;"),e.exec(`
444
+ `)}E(ut,"addRecurringLaneIndexes");function pt(e){_e(e)}E(pt,"projectLiveNodeState");function Rt(e){c(e,O(e),"scopes_json","ALTER TABLE nodes ADD COLUMN scopes_json TEXT;")}E(Rt,"addNodeScopes");function At(e){const t=O(e);c(e,t,"grantee","ALTER TABLE nodes ADD COLUMN grantee TEXT NOT NULL DEFAULT 'user:local';"),c(e,t,"sub","ALTER TABLE nodes ADD COLUMN sub TEXT;"),c(e,t,"grant_id","ALTER TABLE nodes ADD COLUMN grant_id TEXT;"),c(e,t,"client_id","ALTER TABLE nodes ADD COLUMN client_id TEXT;"),c(e,t,"metadata_json","ALTER TABLE nodes ADD COLUMN metadata_json TEXT;"),c(e,t,"run_id","ALTER TABLE nodes ADD COLUMN run_id TEXT;"),c(e,t,"event_cursor","ALTER TABLE nodes ADD COLUMN event_cursor INTEGER;"),c(e,t,"usage_json","ALTER TABLE nodes ADD COLUMN usage_json TEXT;"),c(e,t,"deleting_at","ALTER TABLE nodes ADD COLUMN deleting_at TEXT;"),e.exec(`
445
445
  ALTER TABLE sessions ADD COLUMN event_offset INTEGER;
446
446
  CREATE INDEX idx_nodes_run_id ON nodes(run_id);
447
447
  -- Child nodes may be pruned while their root run and event history remain.
@@ -503,7 +503,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
503
503
  );
504
504
  CREATE UNIQUE INDEX idx_grants_grantee_sub ON grants(grantee, sub);
505
505
  CREATE TABLE runtime_state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
506
- `)}E(mt,"addGrantMirror");function ft(e){e.exec("UPDATE nodes SET grantee = 'app:terminal' WHERE grantee = 'user:local';")}E(ft,"migrateTerminalGrantees");function Xt(e){const t={llm:"crtr:llm",act:"crtr:act",schedule:"crtr:schedule","memory:read":"crtr:memory:read:user","memory:write":"crtr:memory:rw:user"},n=e.prepare("SELECT node_id, scopes_json FROM nodes WHERE scopes_json IS NOT NULL").all(),o=e.prepare("UPDATE nodes SET scopes_json = ? WHERE node_id = ?");for(const T of n){const i=JSON.parse(T.scopes_json);if(!Array.isArray(i))throw new Error(`invalid launched scopes for node ${T.node_id}`);const a=[];for(const c of i){if(typeof c!="string")continue;const L=t[c]??c;try{ae(L),a.push(L)}catch(l){if(!(l instanceof TypeError))throw l}}o.run(JSON.stringify([...new Set(a)]),T.node_id)}}E(Xt,"migrateLaunchedScopes");function Ut(e){e.exec(`CREATE TABLE node_tokens (
506
+ `)}E(mt,"addGrantMirror");function ft(e){e.exec("UPDATE nodes SET grantee = 'app:terminal' WHERE grantee = 'user:local';")}E(ft,"migrateTerminalGrantees");function Xt(e){const t={llm:"crtr:llm",act:"crtr:act",schedule:"crtr:schedule","memory:read":"crtr:memory:read:user","memory:write":"crtr:memory:rw:user"},n=e.prepare("SELECT node_id, scopes_json FROM nodes WHERE scopes_json IS NOT NULL").all(),o=e.prepare("UPDATE nodes SET scopes_json = ? WHERE node_id = ?");for(const T of n){const i=JSON.parse(T.scopes_json);if(!Array.isArray(i))throw new Error(`invalid launched scopes for node ${T.node_id}`);const a=[];for(const N of i){if(typeof N!="string")continue;const L=t[N]??N;try{ae(L),a.push(L)}catch(l){if(!(l instanceof TypeError))throw l}}o.run(JSON.stringify([...new Set(a)]),T.node_id)}}E(Xt,"migrateLaunchedScopes");function Ut(e){e.exec(`CREATE TABLE node_tokens (
507
507
  broker_process_id TEXT PRIMARY KEY REFERENCES executions(execution_id) ON DELETE CASCADE,
508
508
  node_id TEXT NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,
509
509
  token_hash TEXT NOT NULL UNIQUE, grantee TEXT NOT NULL, uid INTEGER NOT NULL,
@@ -656,7 +656,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
656
656
  ALTER TABLE messages ADD COLUMN level INTEGER;
657
657
  ALTER TABLE messages ADD COLUMN actor_id TEXT;
658
658
  `),ie(e),e.exec("ANALYZE"))}E(oe,"canvasObjectsSchema");function ie(e){const t=new Date().toISOString(),n=e.prepare("INSERT INTO objects (id, type, kind, owner, privacy, name, preview, created_at) VALUES (?, ?, NULL, NULL, 'public', ?, '', ?) RETURNING rowid"),o=e.prepare("INSERT INTO identities (object, scheme, external_id) VALUES (?, ?, ?)"),T=e.prepare(`SELECT DISTINCT n.grantee FROM nodes n
659
- WHERE NOT EXISTS (SELECT 1 FROM identities i WHERE i.external_id = n.grantee AND i.scheme IN ('app', 'local-user'))`).all();for(const{grantee:i}of T){let a;if(i==="user:local")a={type:"person",scheme:"local-user",handle:"user"};else if(/^app:[a-z0-9][a-z0-9._-]*$/.test(i))a={type:"app",scheme:"app",handle:i.slice(4)};else throw new Error(`canvas objects migration: node grantee ${JSON.stringify(i)} is neither app:<name> nor user:local`);const{rowid:c}=n.get(`${Date.now().toString(36)}-${P(4).toString("hex")}`,a.type,a.handle,t);o.run(c,a.scheme,i)}e.exec(`INSERT INTO objects (id, type, kind, owner, privacy, name, preview, created_at)
659
+ WHERE NOT EXISTS (SELECT 1 FROM identities i WHERE i.external_id = n.grantee AND i.scheme IN ('app', 'local-user'))`).all();for(const{grantee:i}of T){let a;if(i==="user:local")a={type:"person",scheme:"local-user",handle:"user"};else if(/^app:[a-z0-9][a-z0-9._-]*$/.test(i))a={type:"app",scheme:"app",handle:i.slice(4)};else throw new Error(`canvas objects migration: node grantee ${JSON.stringify(i)} is neither app:<name> nor user:local`);const{rowid:N}=n.get(`${Date.now().toString(36)}-${P(4).toString("hex")}`,a.type,a.handle,t);o.run(N,a.scheme,i)}e.exec(`INSERT INTO objects (id, type, kind, owner, privacy, name, preview, created_at)
660
660
  SELECT n.node_id, 'node', n.kind, i.object, 'public', n.name, '', n.created
661
661
  FROM nodes n JOIN identities i ON i.external_id = n.grantee AND i.scheme IN ('app', 'local-user')
662
662
  WHERE NOT EXISTS (SELECT 1 FROM objects o WHERE o.id = n.node_id)`)}E(ie,"copyNodesIntoObjects");function Dt(e){if(oe(e),!S(e,"nodes","name"))return;ie(e);const t=e.prepare("SELECT COUNT(*) AS n FROM nodes WHERE node_id NOT IN (SELECT id FROM objects)").get();if(t.n>0)throw new Error(`canvasObjectsCut: ${t.n} node(s) have no objects row`);e.exec(`
@@ -877,11 +877,11 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
877
877
  JOIN objects o ON o.id = n.node_id AND o.type = 'node'
878
878
  WHERE d.idempotency_key LIKE 'node-outcome:%'`).all(),a=e.prepare(`INSERT INTO objects
879
879
  (id, type, kind, owner, privacy, name, preview, created_at, reception)
880
- VALUES (?, 'action', NULL, ?, ?, ?, '', ?, 2) RETURNING rowid`),c=e.prepare("INSERT INTO custom_objects (object, receiver_json, payload_json) VALUES (?, ?, ?)"),L=e.prepare(`INSERT INTO watches (watcher, object, in_tree, read_heads_key, created_at)
880
+ VALUES (?, 'action', NULL, ?, ?, ?, '', ?, 2) RETURNING rowid`),N=e.prepare("INSERT INTO custom_objects (object, receiver_json, payload_json) VALUES (?, ?, ?)"),L=e.prepare(`INSERT INTO watches (watcher, object, in_tree, read_heads_key, created_at)
881
881
  VALUES (?, ?, 1, NULL, ?) RETURNING rowid`),l=e.prepare(`INSERT INTO watch_subscriptions
882
882
  (watch, pattern, filter_json, config_json, started_by, ends_on, created_at)
883
883
  VALUES (?, 'outcome', NULL, '{"1":"after-turn"}', 'spawn', NULL, ?)`),r=e.prepare("UPDATE deliveries SET destination_id = ?, idempotency_key = NULL WHERE delivery_id = ?"),_=e.prepare("SELECT body_json FROM messages WHERE message_id = ?"),d=e.prepare(`UPDATE messages SET body_json = ?, event = 'outcome', object_id = ?, level = 1
884
- WHERE message_id = ?`);for(const s of i){if(s.owner===null)throw new Error(`v67 outcome node ${s.node_id} has no owner`);const A=JSON.parse(s.adapter_json);if(A.adapter!=="subprocess"||!Array.isArray(A.argv)||typeof A.cwd!="string"||typeof A.config_origin!="string")throw new Error(`v67 invalid outcome adapter on ${s.delivery_id}`);const m=P(16).toString("hex"),D=new Date(s.created_at).toISOString(),y=a.get(m,s.owner,s.privacy,`outcome/${s.node_id}`,D);c.run(y.rowid,JSON.stringify({argv:A.argv,cwd:A.cwd,config_origin:A.config_origin,name:s.destination_id}),JSON.stringify(A.payload??null));const G=L.get(y.rowid,e.prepare("SELECT rowid FROM objects WHERE id = ?").get(s.node_id).rowid,D);if(l.run(G.rowid,D),s.message_id===null){if(s.state!=="armed")throw new Error(`v67 delivery ${s.delivery_id} has no message but is ${s.state}`);continue}const f=_.get(s.message_id);if(!f||f.body_json===null)throw new Error(`v67 delivery ${s.delivery_id} has no frozen outcome document`);if(e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE message_id = ?").get(s.message_id).n!==1)throw new Error(`v67 outcome message ${s.message_id} is shared`);const v=JSON.parse(f.body_json);if(v===null||Array.isArray(v)||typeof v!="object")throw new Error(`v67 invalid outcome body on ${s.delivery_id}`);delete v.action,delete v.delivery,d.run(JSON.stringify(v),s.node_id,s.message_id),r.run(m,s.delivery_id)}const u=e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE state = 'armed'").get();if(u.n!==i.filter(s=>s.state==="armed").length)throw new Error(`v67 found ${u.n} armed deliveries but only ${i.length} mapped outcome rows`);const p=e.prepare("SELECT COUNT(*) AS n FROM deliveries").get().n-u.n,I=e.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'view' AND sql LIKE '%deliveries%'").all();for(const s of I)e.exec(`DROP VIEW "${s.name.replaceAll('"','""')}"`);e.exec(`
884
+ WHERE message_id = ?`);for(const s of i){if(s.owner===null)throw new Error(`v67 outcome node ${s.node_id} has no owner`);const A=JSON.parse(s.adapter_json);if(A.adapter!=="subprocess"||!Array.isArray(A.argv)||typeof A.cwd!="string"||typeof A.config_origin!="string")throw new Error(`v67 invalid outcome adapter on ${s.delivery_id}`);const m=P(16).toString("hex"),D=new Date(s.created_at).toISOString(),y=a.get(m,s.owner,s.privacy,`outcome/${s.node_id}`,D);N.run(y.rowid,JSON.stringify({argv:A.argv,cwd:A.cwd,config_origin:A.config_origin,name:s.destination_id}),JSON.stringify(A.payload??null));const G=L.get(y.rowid,e.prepare("SELECT rowid FROM objects WHERE id = ?").get(s.node_id).rowid,D);if(l.run(G.rowid,D),s.message_id===null){if(s.state!=="armed")throw new Error(`v67 delivery ${s.delivery_id} has no message but is ${s.state}`);continue}const f=_.get(s.message_id);if(!f||f.body_json===null)throw new Error(`v67 delivery ${s.delivery_id} has no frozen outcome document`);if(e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE message_id = ?").get(s.message_id).n!==1)throw new Error(`v67 outcome message ${s.message_id} is shared`);const v=JSON.parse(f.body_json);if(v===null||Array.isArray(v)||typeof v!="object")throw new Error(`v67 invalid outcome body on ${s.delivery_id}`);delete v.action,delete v.delivery,d.run(JSON.stringify(v),s.node_id,s.message_id),r.run(m,s.delivery_id)}const u=e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE state = 'armed'").get();if(u.n!==i.filter(s=>s.state==="armed").length)throw new Error(`v67 found ${u.n} armed deliveries but only ${i.length} mapped outcome rows`);const p=e.prepare("SELECT COUNT(*) AS n FROM deliveries").get().n-u.n,I=e.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'view' AND sql LIKE '%deliveries%'").all();for(const s of I)e.exec(`DROP VIEW "${s.name.replaceAll('"','""')}"`);e.exec(`
885
885
  CREATE TABLE deliveries_new (
886
886
  delivery_id TEXT PRIMARY KEY,
887
887
  message_id TEXT NOT NULL REFERENCES messages(message_id),
@@ -924,7 +924,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
924
924
  state TEXT NOT NULL DEFAULT 'reserved' CHECK (state IN ('reserved', 'warm_claimed', 'complete'))
925
925
  );
926
926
  CREATE INDEX IF NOT EXISTS idx_create_requests_expires ON create_requests(expires_at);
927
- `)}E(Ht,"addCreateRequests");function Gt(e){for(const[r,_]of[["sub","TEXT"],["grant_id","TEXT"],["scopes_json","TEXT"],["origin_run_id","TEXT"],["run_execution_id","TEXT"]])S(e,"crons",r)||e.exec(`ALTER TABLE crons ADD COLUMN ${r} ${_}`);const t=[],n=E(r=>e.prepare("SELECT id, sub, resolved_scopes_json FROM grants WHERE grantee = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r),"grantRow"),o=E(r=>e.prepare("SELECT grantee, sub, resolved_scopes_json FROM grants WHERE id = ?").get(r),"grantById"),T=E(r=>{try{const _=JSON.parse(r);return Array.isArray(_)?_.flatMap(d=>d&&d.mode==="always"&&typeof d.scope=="string"?[d.scope]:[]):[]}catch{return[]}},"snapshot"),i=E((r,_)=>{if(_===null)return null;const d=e.prepare("SELECT grantee, sub, grant_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(_);if(d===void 0||d.grantee!==r)return null;const u=r==="app:terminal";let p=null;if(d.scopes_json!==null){let A;try{A=JSON.parse(d.scopes_json)}catch{return{pause:"bad_scopes"}}if(!Array.isArray(A)||!A.every(m=>typeof m=="string"))return{pause:"bad_scopes"};p=A}const I=u?n(r):void 0;let R=d.grant_id??I?.id??null;R===null&&!u&&d.sub!==null&&(R=e.prepare("SELECT id FROM grants WHERE grantee = ? AND sub = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r,d.sub)?.id??null);const s=R===null?void 0:o(R);return R!==null&&(s===void 0||s.grantee!==r||d.sub!==null&&s.sub!==d.sub)?{pause:"no_grant"}:R===null&&!u?{pause:"no_grant"}:(p===null&&!u&&(p=T(s.resolved_scopes_json)),{principal:{sub:d.sub??s?.sub??I?.sub??"user:local",grant_id:R,scopes:p,origin_run_id:d.run_id}})},"fromCreator"),a=E(r=>{const _=n(r);return r==="app:terminal"?{sub:_?.sub??"user:local",grant_id:_?.id??null,scopes:null,origin_run_id:null}:_===void 0?null:{sub:_.sub,grant_id:_.id,scopes:T(_.resolved_scopes_json),origin_run_id:null}},"fromGrantee"),c=e.prepare("SELECT cron_id, grantee, created_by, isolated, cwd FROM crons WHERE sub IS NULL").all(),L=e.prepare("UPDATE crons SET sub = ?, grant_id = ?, scopes_json = ?, origin_run_id = ? WHERE cron_id = ?"),l=new Map;for(const r of c){const _=i(r.grantee,r.created_by),d=_===null?a(r.grantee):"principal"in _?_.principal:null;if(d===null){const u=_!==null&&"pause"in _?_.pause:"no_grant";L.run("user:local",null,null,null,r.cron_id),e.prepare("UPDATE crons SET state = 'paused' WHERE cron_id = ?").run(r.cron_id),t.push({event:`cron.migrated.${u}`,fields:{cron_id:r.cron_id,grantee:r.grantee}})}else l.set(r.cron_id,d),L.run(d.sub,d.grant_id,d.scopes===null?null:JSON.stringify(d.scopes),d.origin_run_id,r.cron_id);if(r.isolated!==0){const u=j(Re(r.grantee).root,"crons",r.cron_id);e.prepare("UPDATE crons SET cwd = ? WHERE cron_id = ?").run(u,r.cron_id),t.push({event:"cron.migrated.isolated_cwd",fields:{cron_id:r.cron_id,old_cwd:r.cwd,cwd:u}})}}if(C(e,"daemon_actions")){const r=e.prepare("SELECT action_id, adapter_json FROM daemon_actions WHERE state IN ('pending', 'handed_off') AND json_extract(adapter_json, '$.adapter') = 'cron_spawn' AND json_extract(adapter_json, '$.principal') IS NULL").all();for(const _ of r){const d=JSON.parse(_.adapter_json),u=typeof d.cronId=="string"?d.cronId:null,p=u===null?void 0:e.prepare("SELECT grantee, sub, grant_id, scopes_json, origin_run_id, isolated, created_by FROM crons WHERE cron_id = ?").get(u);let I=null;if(p!==void 0&&!(p.grant_id===null&&p.grantee!=="app:terminal"))I={cron_id:u,grantee:p.grantee,sub:p.sub,grant_id:p.grant_id,scopes:p.scopes_json===null?null:JSON.parse(p.scopes_json),origin_run_id:p.origin_run_id,isolated:p.isolated!==0,creator:p.created_by};else if(p===void 0){const R=typeof d.parent=="string"?d.parent:null,s=R===null?void 0:e.prepare("SELECT grantee FROM node_rows WHERE node_id = ?").get(R),A=s===void 0?null:i(s.grantee,R),m=A===null||!("principal"in A)?null:A.principal;s!==void 0&&m!==null&&(I={cron_id:u,grantee:s.grantee,sub:m.sub,grant_id:m.grant_id,scopes:m.scopes,origin_run_id:m.origin_run_id,isolated:d.isolated===!0,creator:R})}if(I!==null)e.prepare("UPDATE daemon_actions SET adapter_json = ? WHERE action_id = ?").run(JSON.stringify({...d,principal:I}),_.action_id);else{const R=Date.now();e.prepare("UPDATE daemon_actions SET state = 'permanent_failed', failed_at = ?, updated_at = ?, claimed_at = NULL, claim_owner = NULL, next_attempt_at = NULL, last_failure_json = ? WHERE action_id = ?").run(R,R,JSON.stringify({kind:"grant_refused",message:"the cron has no derivable principal to authorize this birth"}),_.action_id),t.push({event:"cron.birth.refused",fields:{cron_id:u,action_id:_.action_id,reason:"grant_missing"}})}}}for(const{event:r,fields:_}of t)Ae({level:"warn",event:r,fields:_})}E(Gt,"addCronPrincipal");const F=[Ue,Ce,Se,De,ge,we,ye,he,ve,xe,Fe,Me,ke,J,je,J,He,Ge,be,Be,We,Qe,Ve,Ze,et,tt,nt,Et,ot,it,st,rt,Tt,at,ct,dt,_t,Nt,Lt,e=>{de(e),ce(e),Ne(e),Le(e)},ut,pt,lt,Rt,At,Ot,mt,Ut,ft,Xt,ee,te,ne,Ee,Ct,St,oe,Dt,Ie,gt,wt,yt,vt,kt,Mt,Ft,jt,xt,e=>e.exec(`
927
+ `)}E(Ht,"addCreateRequests");function Gt(e){for(const[r,_]of[["sub","TEXT"],["grant_id","TEXT"],["scopes_json","TEXT"],["origin_run_id","TEXT"],["run_execution_id","TEXT"]])S(e,"crons",r)||e.exec(`ALTER TABLE crons ADD COLUMN ${r} ${_}`);const t=[],n=E(r=>e.prepare("SELECT id, sub, resolved_scopes_json FROM grants WHERE grantee = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r),"grantRow"),o=E(r=>e.prepare("SELECT grantee, sub, resolved_scopes_json FROM grants WHERE id = ?").get(r),"grantById"),T=E(r=>{try{const _=JSON.parse(r);return Array.isArray(_)?_.flatMap(d=>d&&d.mode==="always"&&typeof d.scope=="string"?[d.scope]:[]):[]}catch{return[]}},"snapshot"),i=E((r,_)=>{if(_===null)return null;const d=e.prepare("SELECT grantee, sub, grant_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(_);if(d===void 0||d.grantee!==r)return null;const u=r==="app:terminal";let p=null;if(d.scopes_json!==null){let A;try{A=JSON.parse(d.scopes_json)}catch{return{pause:"bad_scopes"}}if(!Array.isArray(A)||!A.every(m=>typeof m=="string"))return{pause:"bad_scopes"};p=A}const I=u?n(r):void 0;let R=d.grant_id??I?.id??null;R===null&&!u&&d.sub!==null&&(R=e.prepare("SELECT id FROM grants WHERE grantee = ? AND sub = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r,d.sub)?.id??null);const s=R===null?void 0:o(R);return R!==null&&(s===void 0||s.grantee!==r||d.sub!==null&&s.sub!==d.sub)?{pause:"no_grant"}:R===null&&!u?{pause:"no_grant"}:(p===null&&!u&&(p=T(s.resolved_scopes_json)),{principal:{sub:d.sub??s?.sub??I?.sub??"user:local",grant_id:R,scopes:p,origin_run_id:d.run_id}})},"fromCreator"),a=E(r=>{const _=n(r);return r==="app:terminal"?{sub:_?.sub??"user:local",grant_id:_?.id??null,scopes:null,origin_run_id:null}:_===void 0?null:{sub:_.sub,grant_id:_.id,scopes:T(_.resolved_scopes_json),origin_run_id:null}},"fromGrantee"),N=e.prepare("SELECT cron_id, grantee, created_by, isolated, cwd FROM crons WHERE sub IS NULL").all(),L=e.prepare("UPDATE crons SET sub = ?, grant_id = ?, scopes_json = ?, origin_run_id = ? WHERE cron_id = ?"),l=new Map;for(const r of N){const _=i(r.grantee,r.created_by),d=_===null?a(r.grantee):"principal"in _?_.principal:null;if(d===null){const u=_!==null&&"pause"in _?_.pause:"no_grant";L.run("user:local",null,null,null,r.cron_id),e.prepare("UPDATE crons SET state = 'paused' WHERE cron_id = ?").run(r.cron_id),t.push({event:`cron.migrated.${u}`,fields:{cron_id:r.cron_id,grantee:r.grantee}})}else l.set(r.cron_id,d),L.run(d.sub,d.grant_id,d.scopes===null?null:JSON.stringify(d.scopes),d.origin_run_id,r.cron_id);if(r.isolated!==0){const u=j(Re(r.grantee).root,"crons",r.cron_id);e.prepare("UPDATE crons SET cwd = ? WHERE cron_id = ?").run(u,r.cron_id),t.push({event:"cron.migrated.isolated_cwd",fields:{cron_id:r.cron_id,old_cwd:r.cwd,cwd:u}})}}if(C(e,"daemon_actions")){const r=e.prepare("SELECT action_id, adapter_json FROM daemon_actions WHERE state IN ('pending', 'handed_off') AND json_extract(adapter_json, '$.adapter') = 'cron_spawn' AND json_extract(adapter_json, '$.principal') IS NULL").all();for(const _ of r){const d=JSON.parse(_.adapter_json),u=typeof d.cronId=="string"?d.cronId:null,p=u===null?void 0:e.prepare("SELECT grantee, sub, grant_id, scopes_json, origin_run_id, isolated, created_by FROM crons WHERE cron_id = ?").get(u);let I=null;if(p!==void 0&&!(p.grant_id===null&&p.grantee!=="app:terminal"))I={cron_id:u,grantee:p.grantee,sub:p.sub,grant_id:p.grant_id,scopes:p.scopes_json===null?null:JSON.parse(p.scopes_json),origin_run_id:p.origin_run_id,isolated:p.isolated!==0,creator:p.created_by};else if(p===void 0){const R=typeof d.parent=="string"?d.parent:null,s=R===null?void 0:e.prepare("SELECT grantee FROM node_rows WHERE node_id = ?").get(R),A=s===void 0?null:i(s.grantee,R),m=A===null||!("principal"in A)?null:A.principal;s!==void 0&&m!==null&&(I={cron_id:u,grantee:s.grantee,sub:m.sub,grant_id:m.grant_id,scopes:m.scopes,origin_run_id:m.origin_run_id,isolated:d.isolated===!0,creator:R})}if(I!==null)e.prepare("UPDATE daemon_actions SET adapter_json = ? WHERE action_id = ?").run(JSON.stringify({...d,principal:I}),_.action_id);else{const R=Date.now();e.prepare("UPDATE daemon_actions SET state = 'permanent_failed', failed_at = ?, updated_at = ?, claimed_at = NULL, claim_owner = NULL, next_attempt_at = NULL, last_failure_json = ? WHERE action_id = ?").run(R,R,JSON.stringify({kind:"grant_refused",message:"the cron has no derivable principal to authorize this birth"}),_.action_id),t.push({event:"cron.birth.refused",fields:{cron_id:u,action_id:_.action_id,reason:"grant_missing"}})}}}for(const{event:r,fields:_}of t)Ae({level:"warn",event:r,fields:_})}E(Gt,"addCronPrincipal");const F=[Ue,Ce,Se,De,ge,we,ye,he,ve,xe,Fe,Me,ke,J,je,J,He,Ge,be,Be,We,Qe,Ve,Ze,et,tt,nt,Et,ot,it,st,rt,Tt,at,Nt,dt,_t,ct,Lt,e=>{de(e),Ne(e),ce(e),Le(e)},ut,pt,lt,Rt,At,Ot,mt,Ut,ft,Xt,ee,te,ne,Ee,Ct,St,oe,Dt,Ie,gt,wt,yt,vt,kt,Mt,Ft,jt,xt,e=>e.exec(`
928
928
  CREATE TABLE audit_rows (
929
929
  id INTEGER PRIMARY KEY, request_id TEXT NOT NULL, time TEXT NOT NULL, grantee TEXT, sub TEXT,
930
930
  client_id TEXT, class TEXT, operation TEXT NOT NULL, scope_checked TEXT,
@@ -1046,7 +1046,13 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
1046
1046
  CREATE UNIQUE INDEX objects_named ON objects(owner, name)
1047
1047
  WHERE deleted_at IS NULL AND home_machine IS NULL AND (type = 'document' OR instr(type, '.') > 0);
1048
1048
  CREATE UNIQUE INDEX objects_handle ON objects(type, name) WHERE home_machine IS NULL AND type IN ('person','app','repo','profile');
1049
- `),e=>e.exec("ALTER TABLE grants ADD COLUMN inference_endpoint_json TEXT"),e=>e.exec("ALTER TABLE nodes ADD COLUMN reply_at TEXT; ALTER TABLE nodes ADD COLUMN read_at TEXT;"),Bt,bt];function bt(e){e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'waits'").get()===void 0&&e.exec("CREATE TABLE waits (wait_id TEXT PRIMARY KEY, node_id TEXT NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE, kind TEXT, target_id TEXT, generation INTEGER, created_at INTEGER, satisfied_at INTEGER)");const t=e.prepare("SELECT COUNT(*) AS n FROM waits").get().n;e.exec(`
1049
+ `),e=>e.exec("ALTER TABLE grants ADD COLUMN inference_endpoint_json TEXT"),e=>e.exec("ALTER TABLE nodes ADD COLUMN reply_at TEXT; ALTER TABLE nodes ADD COLUMN read_at TEXT;"),Bt,bt,e=>e.exec(`
1050
+ CREATE TABLE IF NOT EXISTS unresolved_links (
1051
+ src INTEGER NOT NULL REFERENCES objects(rowid) ON DELETE CASCADE,
1052
+ name TEXT NOT NULL,
1053
+ PRIMARY KEY (src, name)
1054
+ ) WITHOUT ROWID;
1055
+ `)];function bt(e){e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'waits'").get()===void 0&&e.exec("CREATE TABLE waits (wait_id TEXT PRIMARY KEY, node_id TEXT NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE, kind TEXT, target_id TEXT, generation INTEGER, created_at INTEGER, satisfied_at INTEGER)");const t=e.prepare("SELECT COUNT(*) AS n FROM waits").get().n;e.exec(`
1050
1056
  CREATE TABLE waits_new (
1051
1057
  wait_id TEXT PRIMARY KEY,
1052
1058
  node_id TEXT NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,
@@ -2,7 +2,7 @@
2
2
  * delete, history, read, and the two import paths (migration, repo sync). Every act records
3
3
  * its actor's `acted-on` edge and watch and publishes its event in the caller's transaction. */
4
4
  import type { DatabaseSync } from 'node:sqlite';
5
- import type { ReposFn } from './reader.js';
5
+ import { type ReposFn } from './reader.js';
6
6
  import type { ObjectRow, Privacy, Reader, Revision, RuleSpec } from './types.js';
7
7
  export interface DocumentFields {
8
8
  preview?: string;
@@ -64,7 +64,8 @@ export declare function documentRecord(db: DatabaseSync, doc: ObjectRow): Docume
64
64
  /** The `[[name]]` links a body names, in order, each once. */
65
65
  export declare function linksIn(body: string): string[];
66
66
  /** Derive the document's `links-to` edges from `body`, resolving each name in the writer's
67
- * view. A name that resolves to nothing or to several objects stays text and is returned;
67
+ * view. A name that resolves to nothing or to several objects stays text and is returned,
68
+ * and remembered so {@link linkNewlyNamed} can link it when a document by that name appears;
68
69
  * one that may live on another runtime is neither linked nor reported. */
69
70
  export declare function setRevisionLinks(db: DatabaseSync, reader: Reader, doc: ObjectRow, body: string): string[];
70
71
  /** `body` with each `[[link]]` the document's `links-to` edges resolved shown under its target's
@@ -121,14 +122,14 @@ export declare function history(db: DatabaseSync, reader: Reader, doc: ObjectRow
121
122
  export declare function moveDocument(db: DatabaseSync, reader: Reader | null, doc: ObjectRow, to: {
122
123
  name?: string;
123
124
  owner?: ObjectRow;
124
- }): ObjectRow;
125
+ }, reposFn?: ReposFn): ObjectRow;
125
126
  /** Mark the document deleted; the row stays with its last preview. Publishes `deleted`
126
127
  * (level 8), then ends the watches on it and drops its rules. A null actor is repo sync or
127
128
  * node deletion; `deletedAt` is the tombstone's own time when sync imports one. What it linked to is re-checked by {@link collectUnlinked}. */
128
129
  export declare function deleteDocument(db: DatabaseSync, reader: Reader | null, doc: ObjectRow, reposFn?: ReposFn, deletedAt?: string): ObjectRow;
129
130
  /** Repo sync: bring a deleted document back under `name` (checked free first). Revisions, record
130
131
  * and links are untouched; the watches and rules deletion dropped are not recreated here. */
131
- export declare function restoreDocument(db: DatabaseSync, doc: ObjectRow, name: string): ObjectRow;
132
+ export declare function restoreDocument(db: DatabaseSync, doc: ObjectRow, name: string, reposFn?: ReposFn): ObjectRow;
132
133
  /** Delete each of `targets` that is a kept document nothing links to any more: a live document
133
134
  * whose owner is deleted (node deletion kept it because a live document linked to it) and that
134
135
  * no other live document links to. Each one deleted re-checks what it linked to, so chains of
@@ -150,7 +151,7 @@ export interface ImportSpec extends WriteSpec {
150
151
  /** Migration: a document with its linear history (oldest first; bodies already in the body
151
152
  * store). Explicit authors and times; records no edge or watch and publishes nothing — the
152
153
  * caller adds links (`setRevisionLinks`), acted-on edges and watches. */
153
- export declare function importDocument(db: DatabaseSync, spec: ImportSpec): ObjectRow;
154
+ export declare function importDocument(db: DatabaseSync, spec: ImportSpec, reposFn?: ReposFn): ObjectRow;
154
155
  export interface SyncedRevision {
155
156
  rev_id: string;
156
157
  parents: readonly string[];
@@ -1,10 +1,10 @@
1
- var ne=Object.defineProperty;var s=(t,n)=>ne(t,"name",{value:n,configurable:!0});import{prepared as c}from"../sqlite-statements.js";import{randomUUID as re}from"node:crypto";import{CrtrError as O,notFound as R,usage as m}from"../errors.js";import{ExitCode as D}from"../../types.js";import{canChange as k,canRead as J,missingMemoryScope as oe,missingMemoryScopeUnder as ie,ownerChain as se}from"./access.js";import{bodyOf as ae,get as ue,put as P}from"./bodies.js";import{recordActedOn as N,setLinks as ce}from"./edges.js";import{publish as x}from"./events.js";import{resolveName as le}from"./names.js";import{createObject as me,getById as we,getByRowid as d}from"./objects.js";import{replaceRules as B,setRulesOwner as pe}from"./rules.js";import{recordWatch as $}from"./watches.js";function y(t){return t.class==="node"?t.node:t.class==="app"?t.app:t.class==="object"?t.object:t.person}s(y,"actorOf");let de=0;function E(t,n){const e=`graph_doc_${++de}`;t.exec(`SAVEPOINT ${e}`);try{const r=n();return t.exec(`RELEASE ${e}`),r}catch(r){throw t.exec(`ROLLBACK TO ${e}`),t.exec(`RELEASE ${e}`),r}}s(E,"atomic");function fe(t,n){return new O("conflict",t,D.GENERAL,n)}s(fe,"conflict");function _(t){if(t.type!=="document")throw m(`${t.type} ${t.id} is not a document`)}s(_,"assertDocument");function K(t,n){const e=se(t,n);return(e.length?e[e.length-1]:n).type}s(K,"spaceType");function I(t,n,e){if(e!=="public"&&K(t,n)==="repo")throw m(`a repo owns only public documents; ${n.name} cannot own a ${e} one`,{rule:"repo-public-only",next:"Write it as public, or give it another owner (your own documents can carry `if: {repo: <handle>}`)."})}s(I,"assertPublicInRepo");function H(t,n,e,r){if(e.trim()===""||e!==e.trim())throw m(`invalid document name ${JSON.stringify(e)}`);const o=c(t,"SELECT id FROM objects WHERE (type = 'document' OR instr(type, '.') > 0) AND owner = ? AND name = ? AND deleted_at IS NULL AND home_machine IS NULL").get(n.rowid,e);if(o&&o.id!==r?.id)throw fe(`${n.name} already has a document named ${e}`,{name:e,holder:o.id})}s(H,"assertNameFree");function C(t,n,e){if(!J(t,n,e))throw R(`no document ${e.id}`)}s(C,"readable");function G(t){return new O("scope_missing",`missing scope ${t}`,D.GENERAL,{scope:t})}s(G,"scopeMissing");function T(t,n,e){C(t,n,e);const r=oe(t,n,e,"rw");if(r!==null)throw G(r);if(!k(t,n,e))throw new O("forbidden",`you cannot change ${e.name}`,D.GENERAL,{id:e.id})}s(T,"changeable");function V(t,n){const e=c(t,`SELECT p.rev_id FROM revision_parents rp JOIN revisions p ON p.rowid = rp.parent
2
- WHERE rp.child = ? ORDER BY p.rev_id`).all(n.rowid);return{rowid:n.rowid,rev_id:n.rev_id,number:n.number,body_hash:n.body_hash,author:n.author===null?null:d(t,n.author)?.id??null,rationale:n.rationale,verbatim:n.verbatim===1,created_at:n.created_at,parents:e.map(r=>r.rev_id)}}s(V,"toRevision");function W(t,n){return c(t,`SELECT * FROM revisions r WHERE r.document = ?
3
- AND NOT EXISTS (SELECT 1 FROM revision_parents p WHERE p.parent = r.rowid) ORDER BY r.number DESC, r.rev_id`).all(n.rowid).map(r=>V(t,r))}s(W,"heads");function he(t){return t.map(n=>n.rev_id).sort().join(",")}s(he,"headsKeyOf");function A(t,n){const e=c(t,"SELECT * FROM documents WHERE object = ?").get(n.rowid);if(!e)throw R(`no document record for ${n.id}`);return{summary:e.summary,heads_key:e.heads_key,revision:e.revision,why_it_exists:e.why_it_exists,unlisted:e.unlisted===1,lint_ignore:e.lint_ignore_json===null?null:JSON.parse(e.lint_ignore_json),extensions:e.extensions_json===null?null:JSON.parse(e.extensions_json),source_path:e.source_path}}s(A,"documentRecord");function L(t,n,e){const r=c(t,`INSERT INTO revisions (document, rev_id, number, body_hash, author, rationale, verbatim, created_at)
4
- VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`).get(n.rowid,e.rev_id??re(),e.number,e.body_hash,e.author?.rowid??null,e.rationale,e.verbatim?1:0,e.created_at??new Date().toISOString()),o=c(t,"INSERT INTO revision_parents (child, parent) VALUES (?, ?)");for(const i of e.parents)o.run(r.rowid,i);return r}s(L,"insertRevision");function Ee(t){const n=new Set;for(const e of t.matchAll(/\[\[([^[\]\n]+?)\]\]/g))n.add(e[1].trim());return[...n]}s(Ee,"linksIn");function Y(t,n,e,r){const o=[],i=[];for(const a of Ee(r)){const u=le(t,n,a);"object"in u?o.push({written_as:a,dst:u.object}):"deleted"in u?o.push({written_as:a,dst:u.deleted}):"remote"in u||i.push(a)}return b(t,ce(t,e,o)),i}s(Y,"setRevisionLinks");function _e(t,n,e){return n.type==="node"?n.id:c(t,`SELECT 1 FROM objects o JOIN objects d ON d.owner = o.rowid
1
+ var ie=Object.defineProperty;var a=(e,t)=>ie(e,"name",{value:t,configurable:!0});import{prepared as l}from"../sqlite-statements.js";import{randomUUID as se}from"node:crypto";import{CrtrError as O,notFound as N,usage as w}from"../errors.js";import{ExitCode as D}from"../../types.js";import{canChange as j,canRead as P,missingMemoryScope as ae,missingMemoryScopeUnder as ue,ownerChain as ce}from"./access.js";import{bodyOf as le,get as me,put as B}from"./bodies.js";import{dropUnresolved as we,linksOf as de,recordActedOn as R,setLinks as K,setUnresolved as pe,unresolvedLinksCouldName as fe}from"./edges.js";import{publish as x}from"./events.js";import{resolveName as he}from"./names.js";import{createObject as Ee,getById as _e,getByRowid as p}from"./objects.js";import{liveLookups as ye,readerForNode as ve,readerForObject as Ne}from"./reader.js";import{replaceRules as G,setRulesOwner as Re}from"./rules.js";import{recordWatch as $}from"./watches.js";function y(e){return e.class==="node"?e.node:e.class==="app"?e.app:e.class==="object"?e.object:e.person}a(y,"actorOf");let ge=0;function E(e,t){const n=`graph_doc_${++ge}`;e.exec(`SAVEPOINT ${n}`);try{const r=t();return e.exec(`RELEASE ${n}`),r}catch(r){throw e.exec(`ROLLBACK TO ${n}`),e.exec(`RELEASE ${n}`),r}}a(E,"atomic");function Se(e,t){return new O("conflict",e,D.GENERAL,t)}a(Se,"conflict");function _(e){if(e.type!=="document")throw w(`${e.type} ${e.id} is not a document`)}a(_,"assertDocument");function V(e,t){const n=ce(e,t);return(n.length?n[n.length-1]:t).type}a(V,"spaceType");function I(e,t,n){if(n!=="public"&&V(e,t)==="repo")throw w(`a repo owns only public documents; ${t.name} cannot own a ${n} one`,{rule:"repo-public-only",next:"Write it as public, or give it another owner (your own documents can carry `if: {repo: <handle>}`)."})}a(I,"assertPublicInRepo");function H(e,t,n,r){if(n.trim()===""||n!==n.trim())throw w(`invalid document name ${JSON.stringify(n)}`);const o=l(e,"SELECT id FROM objects WHERE (type = 'document' OR instr(type, '.') > 0) AND owner = ? AND name = ? AND deleted_at IS NULL AND home_machine IS NULL").get(t.rowid,n);if(o&&o.id!==r?.id)throw Se(`${t.name} already has a document named ${n}`,{name:n,holder:o.id})}a(H,"assertNameFree");function C(e,t,n){if(!P(e,t,n))throw N(`no document ${n.id}`)}a(C,"readable");function Y(e){return new O("scope_missing",`missing scope ${e}`,D.GENERAL,{scope:e})}a(Y,"scopeMissing");function T(e,t,n){C(e,t,n);const r=ae(e,t,n,"rw");if(r!==null)throw Y(r);if(!j(e,t,n))throw new O("forbidden",`you cannot change ${n.name}`,D.GENERAL,{id:n.id})}a(T,"changeable");function X(e,t){const n=l(e,`SELECT p.rev_id FROM revision_parents rp JOIN revisions p ON p.rowid = rp.parent
2
+ WHERE rp.child = ? ORDER BY p.rev_id`).all(t.rowid);return{rowid:t.rowid,rev_id:t.rev_id,number:t.number,body_hash:t.body_hash,author:t.author===null?null:p(e,t.author)?.id??null,rationale:t.rationale,verbatim:t.verbatim===1,created_at:t.created_at,parents:n.map(r=>r.rev_id)}}a(X,"toRevision");function U(e,t){return l(e,`SELECT * FROM revisions r WHERE r.document = ?
3
+ AND NOT EXISTS (SELECT 1 FROM revision_parents p WHERE p.parent = r.rowid) ORDER BY r.number DESC, r.rev_id`).all(t.rowid).map(r=>X(e,r))}a(U,"heads");function Oe(e){return e.map(t=>t.rev_id).sort().join(",")}a(Oe,"headsKeyOf");function k(e,t){const n=l(e,"SELECT * FROM documents WHERE object = ?").get(t.rowid);if(!n)throw N(`no document record for ${t.id}`);return{summary:n.summary,heads_key:n.heads_key,revision:n.revision,why_it_exists:n.why_it_exists,unlisted:n.unlisted===1,lint_ignore:n.lint_ignore_json===null?null:JSON.parse(n.lint_ignore_json),extensions:n.extensions_json===null?null:JSON.parse(n.extensions_json),source_path:n.source_path}}a(k,"documentRecord");function L(e,t,n){const r=l(e,`INSERT INTO revisions (document, rev_id, number, body_hash, author, rationale, verbatim, created_at)
4
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`).get(t.rowid,n.rev_id??se(),n.number,n.body_hash,n.author?.rowid??null,n.rationale,n.verbatim?1:0,n.created_at??new Date().toISOString()),o=l(e,"INSERT INTO revision_parents (child, parent) VALUES (?, ?)");for(const i of n.parents)o.run(r.rowid,i);return r}a(L,"insertRevision");function De(e){const t=new Set;for(const n of e.matchAll(/\[\[([^[\]\n]+?)\]\]/g))t.add(n[1].trim());return[...t]}a(De,"linksIn");function q(e,t,n){const r=[],o=[];for(const i of n){const s=he(e,t,i);"object"in s?r.push({written_as:i,dst:s.object}):"deleted"in s?r.push({written_as:i,dst:s.deleted}):"remote"in s||o.push(i)}return{links:r,unresolved:o}}a(q,"resolveLinks");function z(e,t,n,r){const{links:o,unresolved:i}=q(e,t,De(r));return te(e,K(e,n,o)),pe(e,n,i),i}a(z,"setRevisionLinks");function W(e,t,n){const r=new Map;for(const{src:o,name:i}of fe(e,t.name,t.id))r.set(o,[...r.get(o)??[],i]);for(const[o,i]of r){const s=p(e,o),u=s?.owner==null?null:p(e,s.owner);if(!s||s.deleted_at||s.home_machine!==null||!u||u.deleted_at)continue;const c=u.type==="node"?ve(e,u.id,n??(()=>[]),ye(e),!0):Ne(e,s,n),m=q(e,c,i).links.filter(d=>d.dst.rowid===t.rowid);m.length!==0&&(K(e,s,[...de(e,s),...m]),we(e,s,m.map(d=>d.written_as)))}}a(W,"linkNewlyNamed");function xe(e,t,n){return t.type==="node"?t.id:l(e,`SELECT 1 FROM objects o JOIN objects d ON d.owner = o.rowid
5
5
  WHERE o.name = ? AND o.type <> ? AND o.type IN ('person','app','repo','profile')
6
- AND d.type = 'document' AND d.name = ? AND d.deleted_at IS NULL LIMIT 1`).get(n.name,n.type,e)?`${n.type}:${n.name}`:n.name}s(_e,"ownerPrefix");function X(t,n,e){const r=c(t,`SELECT e.written_as, o.* FROM edges e JOIN objects o ON o.rowid = e.dst
7
- WHERE e.type = 'links-to' AND e.src = ? AND e.written_as IS NOT NULL`).all(n.rowid);if(r.length===0)return e;const o=new Map;for(const i of r){if(i.deleted_at||i.written_as===i.id)continue;const a=!i.written_as.includes("/");if(a&&i.name===i.written_as)continue;const u=i.owner===null?null:d(t,i.owner),l=u?`${_e(t,u,i.name)}/${i.name}`:i.name,w=a&&i.owner===n.owner?i.name:l;w!==i.written_as&&o.set(i.written_as,w)}return o.size===0?e:e.replace(/\[\[([^[\]\n]+?)\]\]/g,(i,a)=>{const u=o.get(a.trim());return u===void 0?i:`[[${u}]]`})}s(X,"withCurrentNames");function U(t,n,e){if(e.preview!==void 0){if(e.preview.trim()==="")throw m("a document needs a preview",{field:"preview"});c(t,"UPDATE objects SET preview = ? WHERE rowid = ?").run(e.preview,n.rowid)}e.privacy!==void 0&&(I(t,d(t,n.owner),e.privacy),c(t,"UPDATE objects SET privacy = ? WHERE rowid = ?").run(e.privacy,n.rowid));const r=s((i,a)=>t.prepare(`UPDATE documents SET ${i} = ? WHERE object = ?`).run(a,n.rowid),"set");e.summary!==void 0&&r("summary",e.summary),e.whyItExists!==void 0&&r("why_it_exists",e.whyItExists),e.unlisted!==void 0&&r("unlisted",e.unlisted?1:0),e.lintIgnore!==void 0&&r("lint_ignore_json",e.lintIgnore===null?null:JSON.stringify(e.lintIgnore)),e.extensions!==void 0&&r("extensions_json",e.extensions===null?null:JSON.stringify(e.extensions));const o=d(t,n.rowid);return e.rules!==void 0&&B(t,o,e.rules),o}s(U,"applyDocumentFields");function q(t,n){if(n.owner.deleted_at)throw m(`${n.owner.name} is deleted`);if(n.preview.trim()==="")throw m("a document needs a preview",{field:"preview"});I(t,n.owner,n.privacy),H(t,n.owner,n.name);const e=me(t,{...n.id?{id:n.id}:{},type:"document",kind:n.kind,owner:n.owner.rowid,privacy:n.privacy,name:n.name,preview:n.preview});return c(t,"INSERT INTO documents (object, heads_key, revision) VALUES (?, '', 0)").run(e.rowid),U(t,e,{...n,preview:void 0,privacy:void 0,rules:n.rules??[]})}s(q,"createDocument");function M(t,n){const e=W(t,n),r=he(e),o=Math.max(0,...e.map(i=>i.number));return c(t,"UPDATE documents SET heads_key = ?, revision = ? WHERE object = ?").run(r,o,n.rowid),{heads_key:r,revision:o,headCount:e.length}}s(M,"setHeads");function Ie(t,n,e,r,o){const i=P(r);return E(t,()=>{const a=y(n),u=ie(t,n,e.owner,"rw");if(u!==null)throw G(u);const l=q(t,e),w=L(t,l,{number:1,body_hash:i,author:a,rationale:null,parents:[]}),{heads_key:f}=M(t,l);if(!k(t,n,l))throw R(`no owner ${e.owner.name} you can write to`);const p=Y(t,n,l,r);return N(t,a,l),$(t,a,l,"write",{readHeadsKey:f}),x(t,{event:"created",object:l,actor:a,attrs:{revision:1},pointer:w.rev_id},o),{object:l,revision:1,unresolved:p}})}s(Ie,"writeDocument");function He(t,n,e){return c(t,"SELECT read_heads_key FROM watches WHERE watcher = ? AND object = ?").get(y(n).rowid,e.rowid)?.read_heads_key??null}s(He,"readBase");function ye(t,n){let e=t;for(const[r,{old:o,new:i}]of n.entries()){if(o==="")throw m(`block ${r+1}: a replacement needs non-empty old text`,{field:"replace"});const a=e.indexOf(o);if(a<0)throw m(`block ${r+1}: old text not found: ${JSON.stringify(o.slice(0,120))}`,{field:"replace",old:o});if(e.indexOf(o,a+1)>=0)throw m(`block ${r+1}: old text occurs more than once: ${JSON.stringify(o.slice(0,120))}`,{field:"replace",old:o});e=e.slice(0,a)+i+e.slice(a+o.length)}return e}s(ye,"applyReplacements");function Ce(t,n,e,r,o,i,a={},u){if(_(e),T(t,n,e),e.deleted_at)throw m(`${e.name} is deleted`);if(i===null)throw new O("read_first",`read ${e.name} before editing it`,D.GENERAL,{id:e.id});if(o.trim()==="")throw m("an edit needs a rationale",{field:"rationale"});const l=A(t,e);if(l.heads_key!==i)return{stale:{heads_key:l.heads_key,revision:l.revision}};const w=W(t,e);let f;if("body"in r)f=r.body;else{if(w.length!==1)throw m(`${e.name} has ${w.length} heads and needs a merge; pass the whole merged body`,{heads:w.length});f=ye(X(t,e,ue(w[0].body_hash)),r.replace)}const p=P(f);try{return E(t,()=>{const h=y(n),g=1+Math.max(0,...w.map(v=>v.number)),F=L(t,e,{number:g,body_hash:p,author:h,rationale:o,verbatim:r.verbatim===!0,parents:w.map(v=>v.rowid)}),j=F.rev_id,ee=c(t,"UPDATE documents SET heads_key = ?, revision = ? WHERE object = ? AND heads_key = ?").run(j,g,e.rowid,i);if(Number(ee.changes)===0){const v=A(t,e);throw new z(v.heads_key,v.revision)}const S=U(t,e,a),te=Y(t,n,S,f);return N(t,h,S),$(t,h,S,"write",{readHeadsKey:j}),x(t,{event:"edited",object:S,actor:h,pointer:F.rev_id,attrs:{revision:g,from_revision:l.revision,rationale:o}},u),{revision:g,heads_key:j,unresolved:te}})}catch(h){if(h instanceof z)return{stale:{heads_key:h.heads_key,revision:h.revision}};throw h}}s(Ce,"editDocument");class z extends Error{static{s(this,"StaleEdit")}heads_key;revision;constructor(n,e){super("stale"),this.heads_key=n,this.revision=e}}function We(t,n,e,r){return _(e),n&&T(t,n,e),E(t,()=>{const o=U(t,e,r);return n&&N(t,y(n),o),o})}s(We,"setDocumentRecord");function ve(t,n,e,r){const o=y(n),{heads_key:i}=A(t,e);$(t,o,e,"read",{readHeadsKey:i}),x(t,{event:"read",object:e,actor:o,attrs:{}},r)}s(ve,"recordRead");function Ue(t,n,e,r){return _(e),C(t,n,e),E(t,()=>{const o=A(t,e),i=W(t,e).map(u=>({...u,body:X(t,e,ae(u.body_hash,o.source_path))})),a=c(t,"SELECT dst, written_as FROM edges WHERE type = 'links-to' AND src = ? ORDER BY written_as").all(e.rowid).map(u=>({written_as:u.written_as??"",object:d(t,u.dst)})).filter(u=>u.object&&J(t,n,u.object)).map(u=>({...u,deleted:u.object.deleted_at!==null}));return e.deleted_at||ve(t,n,e,r),{object:e,record:o,heads:i,needsMerge:i.length>1,links:a}})}s(Ue,"readDocument");function Me(t,n,e){return _(e),C(t,n,e),c(t,"SELECT * FROM revisions WHERE document = ? ORDER BY number DESC, created_at DESC, rowid DESC").all(e.rowid).map(o=>V(t,o))}s(Me,"history");function Fe(t,n,e,r){if(_(e),e.deleted_at)throw m(`${e.name} is deleted`);return n&&T(t,n,e),E(t,()=>{const o=r.owner??d(t,e.owner),i=r.name??e.name;H(t,o,i,e),I(t,o,e.privacy),c(t,"UPDATE objects SET name = ?, owner = ? WHERE rowid = ?").run(i,o.rowid,e.rowid);const a=d(t,e.rowid);if(o.rowid!==e.owner&&(pe(t,a),n&&!k(t,n,a)))throw R(`no owner ${o.name} you can move documents to`);return n&&N(t,y(n),a),a})}s(Fe,"moveDocument");function Je(t,n,e,r,o){return _(e),e.deleted_at?e:(n&&T(t,n,e),E(t,()=>{const i=Q(t,n,e,r,o);return b(t,Z(t,e.rowid),r),i}))}s(Je,"deleteDocument");function Pe(t,n,e){return _(n),n.deleted_at?E(t,()=>(H(t,d(t,n.owner),e,n),c(t,"UPDATE objects SET deleted_at = NULL, name = ? WHERE rowid = ?").run(e,n.rowid),d(t,n.rowid))):n}s(Pe,"restoreDocument");function Q(t,n,e,r,o){c(t,"UPDATE objects SET deleted_at = ? WHERE rowid = ?").run(o??new Date().toISOString(),e.rowid);const i=d(t,e.rowid),a=n?y(n):null;return a&&N(t,a,i),x(t,{event:"deleted",object:i,actor:a,attrs:{}},r),c(t,"DELETE FROM watches WHERE object = ?").run(e.rowid),B(t,i,[]),i}s(Q,"markDeleted");function Z(t,n){return c(t,"SELECT dst FROM edges WHERE type = 'links-to' AND src = ?").all(n).map(({dst:e})=>e)}s(Z,"linkTargets");function b(t,n,e){const r=c(t,`SELECT 1 FROM objects d JOIN objects w ON w.rowid = d.owner
6
+ AND d.type = 'document' AND d.name = ? AND d.deleted_at IS NULL LIMIT 1`).get(t.name,t.type,n)?`${t.type}:${t.name}`:t.name}a(xe,"ownerPrefix");function Q(e,t,n){const r=l(e,`SELECT e.written_as, o.* FROM edges e JOIN objects o ON o.rowid = e.dst
7
+ WHERE e.type = 'links-to' AND e.src = ? AND e.written_as IS NOT NULL`).all(t.rowid);if(r.length===0)return n;const o=new Map;for(const i of r){if(i.deleted_at||i.written_as===i.id)continue;const s=!i.written_as.includes("/");if(s&&i.name===i.written_as)continue;const u=i.owner===null?null:p(e,i.owner),c=u?`${xe(e,u,i.name)}/${i.name}`:i.name,m=s&&i.owner===t.owner?i.name:c;m!==i.written_as&&o.set(i.written_as,m)}return o.size===0?n:n.replace(/\[\[([^[\]\n]+?)\]\]/g,(i,s)=>{const u=o.get(s.trim());return u===void 0?i:`[[${u}]]`})}a(Q,"withCurrentNames");function M(e,t,n){if(n.preview!==void 0){if(n.preview.trim()==="")throw w("a document needs a preview",{field:"preview"});l(e,"UPDATE objects SET preview = ? WHERE rowid = ?").run(n.preview,t.rowid)}n.privacy!==void 0&&(I(e,p(e,t.owner),n.privacy),l(e,"UPDATE objects SET privacy = ? WHERE rowid = ?").run(n.privacy,t.rowid));const r=a((i,s)=>e.prepare(`UPDATE documents SET ${i} = ? WHERE object = ?`).run(s,t.rowid),"set");n.summary!==void 0&&r("summary",n.summary),n.whyItExists!==void 0&&r("why_it_exists",n.whyItExists),n.unlisted!==void 0&&r("unlisted",n.unlisted?1:0),n.lintIgnore!==void 0&&r("lint_ignore_json",n.lintIgnore===null?null:JSON.stringify(n.lintIgnore)),n.extensions!==void 0&&r("extensions_json",n.extensions===null?null:JSON.stringify(n.extensions));const o=p(e,t.rowid);return n.rules!==void 0&&G(e,o,n.rules),o}a(M,"applyDocumentFields");function Z(e,t,n){if(t.owner.deleted_at)throw w(`${t.owner.name} is deleted`);if(t.preview.trim()==="")throw w("a document needs a preview",{field:"preview"});I(e,t.owner,t.privacy),H(e,t.owner,t.name);const r=Ee(e,{...t.id?{id:t.id}:{},type:"document",kind:t.kind,owner:t.owner.rowid,privacy:t.privacy,name:t.name,preview:t.preview});l(e,"INSERT INTO documents (object, heads_key, revision) VALUES (?, '', 0)").run(r.rowid);const o=M(e,r,{...t,preview:void 0,privacy:void 0,rules:t.rules??[]});return W(e,o,n),o}a(Z,"createDocument");function F(e,t){const n=U(e,t),r=Oe(n),o=Math.max(0,...n.map(i=>i.number));return l(e,"UPDATE documents SET heads_key = ?, revision = ? WHERE object = ?").run(r,o,t.rowid),{heads_key:r,revision:o,headCount:n.length}}a(F,"setHeads");function Ke(e,t,n,r,o){const i=B(r);return E(e,()=>{const s=y(t),u=ue(e,t,n.owner,"rw");if(u!==null)throw Y(u);const c=Z(e,n,o),m=L(e,c,{number:1,body_hash:i,author:s,rationale:null,parents:[]}),{heads_key:d}=F(e,c);if(!j(e,t,c))throw N(`no owner ${n.owner.name} you can write to`);const f=z(e,t,c,r);return R(e,s,c),$(e,s,c,"write",{readHeadsKey:d}),x(e,{event:"created",object:c,actor:s,attrs:{revision:1},pointer:m.rev_id},o),{object:c,revision:1,unresolved:f}})}a(Ke,"writeDocument");function Ge(e,t,n){return l(e,"SELECT read_heads_key FROM watches WHERE watcher = ? AND object = ?").get(y(t).rowid,n.rowid)?.read_heads_key??null}a(Ge,"readBase");function Te(e,t){let n=e;for(const[r,{old:o,new:i}]of t.entries()){if(o==="")throw w(`block ${r+1}: a replacement needs non-empty old text`,{field:"replace"});const s=n.indexOf(o);if(s<0)throw w(`block ${r+1}: old text not found: ${JSON.stringify(o.slice(0,120))}`,{field:"replace",old:o});if(n.indexOf(o,s+1)>=0)throw w(`block ${r+1}: old text occurs more than once: ${JSON.stringify(o.slice(0,120))}`,{field:"replace",old:o});n=n.slice(0,s)+i+n.slice(s+o.length)}return n}a(Te,"applyReplacements");function Ve(e,t,n,r,o,i,s={},u){if(_(n),T(e,t,n),n.deleted_at)throw w(`${n.name} is deleted`);if(i===null)throw new O("read_first",`read ${n.name} before editing it`,D.GENERAL,{id:n.id});if(o.trim()==="")throw w("an edit needs a rationale",{field:"rationale"});const c=k(e,n);if(c.heads_key!==i)return{stale:{heads_key:c.heads_key,revision:c.revision}};const m=U(e,n);let d;if("body"in r)d=r.body;else{if(m.length!==1)throw w(`${n.name} has ${m.length} heads and needs a merge; pass the whole merged body`,{heads:m.length});d=Te(Q(e,n,me(m[0].body_hash)),r.replace)}const f=B(d);try{return E(e,()=>{const h=y(t),g=1+Math.max(0,...m.map(v=>v.number)),J=L(e,n,{number:g,body_hash:f,author:h,rationale:o,verbatim:r.verbatim===!0,parents:m.map(v=>v.rowid)}),A=J.rev_id,re=l(e,"UPDATE documents SET heads_key = ?, revision = ? WHERE object = ? AND heads_key = ?").run(A,g,n.rowid,i);if(Number(re.changes)===0){const v=k(e,n);throw new b(v.heads_key,v.revision)}const S=M(e,n,s),oe=z(e,t,S,d);return R(e,h,S),$(e,h,S,"write",{readHeadsKey:A}),x(e,{event:"edited",object:S,actor:h,pointer:J.rev_id,attrs:{revision:g,from_revision:c.revision,rationale:o}},u),{revision:g,heads_key:A,unresolved:oe}})}catch(h){if(h instanceof b)return{stale:{heads_key:h.heads_key,revision:h.revision}};throw h}}a(Ve,"editDocument");class b extends Error{static{a(this,"StaleEdit")}heads_key;revision;constructor(t,n){super("stale"),this.heads_key=t,this.revision=n}}function Ye(e,t,n,r){return _(n),t&&T(e,t,n),E(e,()=>{const o=M(e,n,r);return t&&R(e,y(t),o),o})}a(Ye,"setDocumentRecord");function ke(e,t,n,r){const o=y(t),{heads_key:i}=k(e,n);$(e,o,n,"read",{readHeadsKey:i}),x(e,{event:"read",object:n,actor:o,attrs:{}},r)}a(ke,"recordRead");function Xe(e,t,n,r){return _(n),C(e,t,n),E(e,()=>{const o=k(e,n),i=U(e,n).map(u=>({...u,body:Q(e,n,le(u.body_hash,o.source_path))})),s=l(e,"SELECT dst, written_as FROM edges WHERE type = 'links-to' AND src = ? ORDER BY written_as").all(n.rowid).map(u=>({written_as:u.written_as??"",object:p(e,u.dst)})).filter(u=>u.object&&P(e,t,u.object)).map(u=>({...u,deleted:u.object.deleted_at!==null}));return n.deleted_at||ke(e,t,n,r),{object:n,record:o,heads:i,needsMerge:i.length>1,links:s}})}a(Xe,"readDocument");function qe(e,t,n){return _(n),C(e,t,n),l(e,"SELECT * FROM revisions WHERE document = ? ORDER BY number DESC, created_at DESC, rowid DESC").all(n.rowid).map(o=>X(e,o))}a(qe,"history");function ze(e,t,n,r,o){if(_(n),n.deleted_at)throw w(`${n.name} is deleted`);return t&&T(e,t,n),E(e,()=>{const i=r.owner??p(e,n.owner),s=r.name??n.name;H(e,i,s,n),I(e,i,n.privacy),l(e,"UPDATE objects SET name = ?, owner = ? WHERE rowid = ?").run(s,i.rowid,n.rowid);const u=p(e,n.rowid);if(i.rowid!==n.owner&&(Re(e,u),t&&!j(e,t,u)))throw N(`no owner ${i.name} you can move documents to`);return t&&R(e,y(t),u),(s!==n.name||i.rowid!==n.owner)&&W(e,u,o),u})}a(ze,"moveDocument");function Qe(e,t,n,r,o){return _(n),n.deleted_at?n:(t&&T(e,t,n),E(e,()=>{const i=ee(e,t,n,r,o);return te(e,ne(e,n.rowid),r),i}))}a(Qe,"deleteDocument");function Ze(e,t,n,r){return _(t),t.deleted_at?E(e,()=>{H(e,p(e,t.owner),n,t),l(e,"UPDATE objects SET deleted_at = NULL, name = ? WHERE rowid = ?").run(n,t.rowid);const o=p(e,t.rowid);return W(e,o,r),o}):t}a(Ze,"restoreDocument");function ee(e,t,n,r,o){l(e,"UPDATE objects SET deleted_at = ? WHERE rowid = ?").run(o??new Date().toISOString(),n.rowid);const i=p(e,n.rowid),s=t?y(t):null;return s&&R(e,s,i),x(e,{event:"deleted",object:i,actor:s,attrs:{}},r),l(e,"DELETE FROM watches WHERE object = ?").run(n.rowid),G(e,i,[]),i}a(ee,"markDeleted");function ne(e,t){return l(e,"SELECT dst FROM edges WHERE type = 'links-to' AND src = ?").all(t).map(({dst:n})=>n)}a(ne,"linkTargets");function te(e,t,n){const r=l(e,`SELECT 1 FROM objects d JOIN objects w ON w.rowid = d.owner
8
8
  WHERE d.rowid = ? AND d.type = 'document' AND d.deleted_at IS NULL AND w.deleted_at IS NOT NULL
9
9
  AND NOT EXISTS (SELECT 1 FROM edges e JOIN objects s ON s.rowid = e.src
10
- WHERE e.type = 'links-to' AND e.dst = d.rowid AND e.src != d.rowid AND s.deleted_at IS NULL)`),o=[...n];for(;o.length>0;){const i=o.pop();r.get(i)!==void 0&&(Q(t,null,d(t,i),e),o.push(...Z(t,i)))}}s(b,"collectUnlinked");function Be(t,n){if(n.revisions.length===0)throw m(`import of ${n.name}: no revisions`);return E(t,()=>{const e=q(t,n);c(t,"UPDATE objects SET created_at = ? WHERE rowid = ?").run(n.revisions[0].created_at,e.rowid);let r=null;return n.revisions.forEach((o,i)=>{r=L(t,e,{...o,number:i+1,parents:r===null?[]:[r]}).rowid}),M(t,e),d(t,e.rowid)})}s(Be,"importDocument");function Ke(t,n,e){if(_(n),n.privacy!=="public"&&K(t,n)==="repo")throw m(`a repo owns only public documents; refusing revisions of ${n.name}`);return E(t,()=>{const r=c(t,"SELECT rowid, number, document FROM revisions WHERE rev_id = ?");let o=e.filter(u=>!r.get(u.rev_id));for(;o.length>0;){const u=[];for(const l of o){const w=l.parents.map(p=>r.get(p));if(w.some(p=>p===void 0)){u.push(l);continue}const f=w;if(f.some(p=>p.document!==n.rowid))throw m(`revision ${l.rev_id} names a parent of another document`);L(t,n,{...l,author:null,number:1+Math.max(0,...f.map(p=>p.number)),parents:f.map(p=>p.rowid)})}if(u.length===o.length)throw m(`revisions of ${n.name} name unknown parents: ${u.map(l=>l.rev_id).join(", ")}`);o=u}const{heads_key:i,headCount:a}=M(t,n);return{heads_key:i,headCount:a}})}s(Ke,"insertImportedRevisions");function Ge(t,n){const e=we(t,n);if(!e||e.type!=="document")throw R(`no document ${n}`);return e}s(Ge,"documentById");export{y as actorOf,U as applyDocumentFields,ye as applyReplacements,b as collectUnlinked,Je as deleteDocument,Ge as documentById,A as documentRecord,Ce as editDocument,W as heads,Me as history,Be as importDocument,Ke as insertImportedRevisions,Ee as linksIn,Fe as moveDocument,He as readBase,Ue as readDocument,ve as recordRead,Pe as restoreDocument,We as setDocumentRecord,Y as setRevisionLinks,X as withCurrentNames,Ie as writeDocument};
10
+ WHERE e.type = 'links-to' AND e.dst = d.rowid AND e.src != d.rowid AND s.deleted_at IS NULL)`),o=[...t];for(;o.length>0;){const i=o.pop();r.get(i)!==void 0&&(ee(e,null,p(e,i),n),o.push(...ne(e,i)))}}a(te,"collectUnlinked");function be(e,t,n){if(t.revisions.length===0)throw w(`import of ${t.name}: no revisions`);return E(e,()=>{const r=Z(e,t,n);l(e,"UPDATE objects SET created_at = ? WHERE rowid = ?").run(t.revisions[0].created_at,r.rowid);let o=null;return t.revisions.forEach((i,s)=>{o=L(e,r,{...i,number:s+1,parents:o===null?[]:[o]}).rowid}),F(e,r),p(e,r.rowid)})}a(be,"importDocument");function en(e,t,n){if(_(t),t.privacy!=="public"&&V(e,t)==="repo")throw w(`a repo owns only public documents; refusing revisions of ${t.name}`);return E(e,()=>{const r=l(e,"SELECT rowid, number, document FROM revisions WHERE rev_id = ?");let o=n.filter(u=>!r.get(u.rev_id));for(;o.length>0;){const u=[];for(const c of o){const m=c.parents.map(f=>r.get(f));if(m.some(f=>f===void 0)){u.push(c);continue}const d=m;if(d.some(f=>f.document!==t.rowid))throw w(`revision ${c.rev_id} names a parent of another document`);L(e,t,{...c,author:null,number:1+Math.max(0,...d.map(f=>f.number)),parents:d.map(f=>f.rowid)})}if(u.length===o.length)throw w(`revisions of ${t.name} name unknown parents: ${u.map(c=>c.rev_id).join(", ")}`);o=u}const{heads_key:i,headCount:s}=F(e,t);return{heads_key:i,headCount:s}})}a(en,"insertImportedRevisions");function nn(e,t){const n=_e(e,t);if(!n||n.type!=="document")throw N(`no document ${t}`);return n}a(nn,"documentById");export{y as actorOf,M as applyDocumentFields,Te as applyReplacements,te as collectUnlinked,Qe as deleteDocument,nn as documentById,k as documentRecord,Ve as editDocument,U as heads,qe as history,be as importDocument,en as insertImportedRevisions,De as linksIn,ze as moveDocument,Ge as readBase,Xe as readDocument,ke as recordRead,Ze as restoreDocument,Ye as setDocumentRecord,z as setRevisionLinks,Q as withCurrentNames,Ke as writeDocument};
@@ -17,6 +17,22 @@ export declare function setLinks(db: DatabaseSync, doc: ObjectRow, links: Readon
17
17
  written_as: string;
18
18
  dst: ObjectRow;
19
19
  }>): number[];
20
+ /** Replace the names the document's current revision links that resolved to nothing or to several objects.
21
+ * Kept so that when a document with such a name comes into being, the link can gain its edge. */
22
+ export declare function setUnresolved(db: DatabaseSync, doc: ObjectRow, names: readonly string[]): void;
23
+ /** Forget `names` among the document's remembered unresolved links: they have gained an edge. */
24
+ export declare function dropUnresolved(db: DatabaseSync, doc: ObjectRow, names: Iterable<string>): void;
25
+ /** The unresolved links, as `{src, name}`, whose written name is the document's id or `name`, or ends in `/<name>`: every
26
+ * link that could name a document called `name`, bare or qualified by its owner. */
27
+ export declare function unresolvedLinksCouldName(db: DatabaseSync, name: string, id: string): Array<{
28
+ src: number;
29
+ name: string;
30
+ }>;
31
+ /** The document's current `links-to` edges as the links they were written from. */
32
+ export declare function linksOf(db: DatabaseSync, doc: ObjectRow): Array<{
33
+ written_as: string;
34
+ dst: ObjectRow;
35
+ }>;
20
36
  /** Documents whose body links to `object`. */
21
37
  export declare function backlinks(db: DatabaseSync, reader: Reader, object: ObjectRow): Edge[];
22
38
  export declare function edgesOf(db: DatabaseSync, reader: Reader, object: ObjectRow, opts?: {
@@ -1 +1 @@
1
- var O=Object.defineProperty;var d=(t,r)=>O(t,"name",{value:r,configurable:!0});import{prepared as o}from"../sqlite-statements.js";import{canRead as R}from"./access.js";import{getByRowid as l}from"./objects.js";function L(t,r,e){o(t,"INSERT OR IGNORE INTO edges (type, src, dst, written_as, created_at) VALUES ('acted-on', ?, ?, NULL, ?)").run(r.rowid,e.rowid,new Date().toISOString())}d(L,"recordActedOn");function _(t,r,e){const i=o(t,"SELECT dst FROM edges WHERE type = 'links-to' AND src = ?").all(r.rowid).map(({dst:s})=>s);o(t,"DELETE FROM edges WHERE type = 'links-to' AND src = ?").run(r.rowid);const a=o(t,"INSERT OR IGNORE INTO edges (type, src, dst, written_as, created_at) VALUES ('links-to', ?, ?, ?, ?)"),w=new Date().toISOString();for(const s of e)a.run(r.rowid,s.dst.rowid,s.written_as,w);const c=new Set(e.map(s=>s.dst.rowid));return i.filter(s=>!c.has(s))}d(_,"setLinks");function h(t,r,e){return S(t,r,e,{type:"links-to",direction:"in"})}d(h,"backlinks");function S(t,r,e,i={}){if(!R(t,r,e))return[];const a=i.direction??"both",w=i.type?[i.type]:["acted-on","links-to"],c=[];for(const n of w)a!=="in"&&c.push(...o(t,"SELECT * FROM edges WHERE type = ? AND src = ?").all(n,e.rowid)),a!=="out"&&c.push(...o(t,"SELECT * FROM edges WHERE type = ? AND dst = ?").all(n,e.rowid));const s=new Set,f=[];for(const n of c){const u=`${n.type}|${n.src}|${n.dst}`;if(s.has(u))continue;s.add(u);const p=n.src===e.rowid?e:l(t,n.src),E=n.dst===e.rowid?e:l(t,n.dst);if(!p||!E)continue;const y=p===e?E:p;y!==e&&!R(t,r,y)||f.push({type:n.type,src:p,dst:E,written_as:n.written_as,created_at:n.created_at})}return f}d(S,"edgesOf");export{h as backlinks,S as edgesOf,L as recordActedOn,_ as setLinks};
1
+ var m=Object.defineProperty;var c=(t,e)=>m(t,"name",{value:e,configurable:!0});import{prepared as i}from"../sqlite-statements.js";import{canRead as O}from"./access.js";import{getByRowid as p}from"./objects.js";function L(t,e,n){i(t,"INSERT OR IGNORE INTO edges (type, src, dst, written_as, created_at) VALUES ('acted-on', ?, ?, NULL, ?)").run(e.rowid,n.rowid,new Date().toISOString())}c(L,"recordActedOn");function T(t,e,n){const s=i(t,"SELECT dst FROM edges WHERE type = 'links-to' AND src = ?").all(e.rowid).map(({dst:o})=>o);i(t,"DELETE FROM edges WHERE type = 'links-to' AND src = ?").run(e.rowid);const d=i(t,"INSERT OR IGNORE INTO edges (type, src, dst, written_as, created_at) VALUES ('links-to', ?, ?, ?, ?)"),u=new Date().toISOString();for(const o of n)d.run(e.rowid,o.dst.rowid,o.written_as,u);const a=new Set(n.map(o=>o.dst.rowid));return s.filter(o=>!a.has(o))}c(T,"setLinks");function D(t,e,n){i(t,"DELETE FROM unresolved_links WHERE src = ?").run(e.rowid);const s=i(t,"INSERT OR IGNORE INTO unresolved_links (src, name) VALUES (?, ?)");for(const d of n)s.run(e.rowid,d)}c(D,"setUnresolved");function g(t,e,n){const s=i(t,"DELETE FROM unresolved_links WHERE src = ? AND name = ?");for(const d of n)s.run(e.rowid,d)}c(g,"dropUnresolved");function I(t,e,n){return i(t,"SELECT src, name FROM unresolved_links WHERE name = ? OR name = ? OR substr(name, -length(?) - 1) = '/' || ? ORDER BY src, name").all(e,n,e,e)}c(I,"unresolvedLinksCouldName");function A(t,e){return i(t,"SELECT e.dst, e.written_as FROM edges e WHERE e.type = 'links-to' AND e.src = ? ORDER BY e.written_as").all(e.rowid).map(s=>({written_as:s.written_as??"",dst:p(t,s.dst)}))}c(A,"linksOf");function h(t,e,n){return S(t,e,n,{type:"links-to",direction:"in"})}c(h,"backlinks");function S(t,e,n,s={}){if(!O(t,e,n))return[];const d=s.direction??"both",u=s.type?[s.type]:["acted-on","links-to"],a=[];for(const r of u)d!=="in"&&a.push(...i(t,"SELECT * FROM edges WHERE type = ? AND src = ?").all(r,n.rowid)),d!=="out"&&a.push(...i(t,"SELECT * FROM edges WHERE type = ? AND dst = ?").all(r,n.rowid));const o=new Set,w=[];for(const r of a){const R=`${r.type}|${r.src}|${r.dst}`;if(o.has(R))continue;o.add(R);const E=r.src===n.rowid?n:p(t,r.src),l=r.dst===n.rowid?n:p(t,r.dst);if(!E||!l)continue;const f=E===n?l:E;f!==n&&!O(t,e,f)||w.push({type:r.type,src:E,dst:l,written_as:r.written_as,created_at:r.created_at})}return w}c(S,"edgesOf");export{h as backlinks,g as dropUnresolved,S as edgesOf,A as linksOf,L as recordActedOn,T as setLinks,D as setUnresolved,I as unresolvedLinksCouldName};