@north-light/cli 0.3.520 → 0.3.522
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.
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/health.d.ts +3 -0
- package/dist/builtin-memory/crouter-sdk/release-notes.md +6 -0
- package/dist/clients/attach/viewer.js +533 -533
- package/dist/commands/sys/daemon.js +1 -1
- package/dist/core/canvas/migrations.js +28 -22
- package/dist/core/graph/documents.d.ts +6 -5
- package/dist/core/graph/documents.js +7 -7
- package/dist/core/graph/edges.d.ts +16 -0
- package/dist/core/graph/edges.js +1 -1
- package/dist/core/graph/repo-sync/exchange.js +2 -2
- package/dist/core/spaces/cache-env.d.ts +1 -1
- package/dist/core/spaces/cache-env.js +1 -1
- package/dist/daemon/api/handlers/crons.js +1 -1
- package/dist/daemon/api/handlers/docs.js +1 -1
- package/dist/daemon/api/handlers/health.js +3 -3
- package/docs/ci-runner.md +4 -5
- package/docs/sdk/release-notes.md +6 -0
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -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
|
|
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
|
|
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);
|
|
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);
|
|
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);
|
|
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
|
|
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
|
|
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),
|
|
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,
|
|
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);
|
|
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
|
|
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(
|
|
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(
|
|
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);
|
|
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,
|
|
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){
|
|
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
|
|
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:
|
|
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`),
|
|
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);
|
|
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"),
|
|
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
|
|
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
|
|
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
|
|
2
|
-
WHERE rp.child = ? ORDER BY p.rev_id`).all(
|
|
3
|
-
AND NOT EXISTS (SELECT 1 FROM revision_parents p WHERE p.parent = r.rowid) ORDER BY r.number DESC, r.rev_id`).all(
|
|
4
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`).get(
|
|
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(
|
|
7
|
-
WHERE e.type = 'links-to' AND e.src = ? AND e.written_as IS NOT NULL`).all(
|
|
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=[...
|
|
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?: {
|
package/dist/core/graph/edges.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
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};
|