@north-light/cli 0.3.526 → 0.3.528
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/builtin-memory/crouter-sdk/release-notes.md +6 -0
- package/dist/commands/node/bash.js +2 -2
- package/dist/commands/node.js +1 -1
- package/dist/core/bash-guard.js +4 -3
- package/dist/core/canvas/canvas.js +15 -13
- package/dist/core/runtime/broker/bash-job-launch.d.ts +10 -0
- package/dist/core/runtime/broker/bash-job-launch.js +1 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/pi-extensions/canvas-bash-valve.js +7 -7
- 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
|
@@ -9,6 +9,12 @@ when-and-why-to-read: When you need Release notes, this knowledge should be read
|
|
|
9
9
|
|
|
10
10
|
Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
|
|
11
11
|
|
|
12
|
+
## 0.3.527
|
|
13
|
+
|
|
14
|
+
`POST /v1/nodes/:id/jobs/start` accepts a job prepared as already backgrounded. Additive: no request or response an existing app sends or reads changes.
|
|
15
|
+
|
|
16
|
+
- The route used to refuse a job directory without `job.run` (404 `job_not_found`). It now also accepts one with `job.bg`, which `crtr node bash run` writes before launching. A job directory with neither marker is still refused.
|
|
17
|
+
|
|
12
18
|
## 0.3.521
|
|
13
19
|
|
|
14
20
|
`GET /v1/status` names the runtime generation the daemon is serving. Additive: no request or response an existing app sends or reads changes.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var
|
|
1
|
+
var v=Object.defineProperty;var d=(e,a)=>v(e,"name",{value:a,configurable:!0});import{envNodeId as w}from"../../shared/env.js";import{defineBranch as $,defineLeaf as h}from"../../core/command.js";import{InputError as r}from"../../core/io.js";import{ExitCode as T}from"../../types.js";import{existsSync as g,mkdirSync as I,rmSync as D,writeFileSync as f}from"node:fs";import{activeBackgroundBashJobs as E,backgroundRunningBashJobs as R,bashJobCommandLine as q,bashJobPaths as j,clearJobDeadline as N,formatBashElapsed as _,isSafeBashJobId as y,jobStartedAtMs as S,newBashJobId as M,readJobPgid as B,tailJobLog as L,writeJobDeadlineMs as J}from"../../core/bash-jobs.js";import{parseDurationMs as U}from"../../core/wake.js";import{cliClient as A,getNodeOrNull as u}from"../api-client.js";import{PREVIEW_RESULT_PATH_ENV as C}from"../../core/preview-result-path.js";import{nodeInPane as P}from"../surface/node/placement.js";function b(e,a){const n=e.pane??process.env.TMUX_PANE;let t=e.node;if((t===void 0||t==="")&&(t=P(n)),(t===void 0||t==="")&&(t=w()),t===void 0||t==="")throw new r({error:"no_node",message:a,next:"Run from inside a node, pass --node <id>, or --pane <pane>."});return{id:t,pane:n}}d(b,"resolveBashTarget");const O=h({name:"run",description:"start a shell command as a background job and return at once",whenToUse:"a command needs longer than one tool call \u2014 a test lane, a build, a soak \u2014 and you should be woken with its exit code instead of waiting, backgrounding with `&`, or polling a log",tier:"normal",help:{name:"node bash run",summary:"start one shell command as a background job owned by the calling node and return before it finishes",params:[{kind:"positional",name:"command",required:!0,constraint:"The shell command, run by bash from the current directory with this process\u2019s environment. The same refusals apply as for a bash tool call, judged on this command."}],output:[{name:"job_id",type:"string",required:!0,constraint:"The job object\u2019s id; read or watch it with `crtr canvas`, stop it with `node bash kill`."},{name:"log_path",type:"string",required:!0,constraint:"File receiving the command\u2019s combined stdout and stderr, capped at 10 MiB."}],outputKind:"object",effects:["Works only inside a node\u2019s bash tool call; the job belongs to the calling node.","Starts the command detached from this call, so it keeps running after the call and the turn end. A `timeout` around this leaf bounds only the launch, not the job.","Records the job as a canvas object; when the command exits, its watchers (the calling node by default) get an urgent `exited` push with the exit code and log path.","Sets no cancel deadline: the job runs to completion unless stopped with `node bash kill`.","Fails, with the job id and log path, if the job could not be recorded; the command is then running but will not message the node when it exits."]},run:d(async e=>{const a=e.command,n=w(),t=process.env.CRTR_CONTEXT_DIR;if(n===void 0||n===""||t===void 0||t==="")throw new r({error:"no_node",message:"`node bash run` starts a job for the calling node and works only inside a node\u2019s bash tool",next:"Run it from a node\u2019s bash tool call."});const s=process.cwd(),{bashCommandRefusal:i}=await import("../../core/bash-guard.js"),l=i(a,s);if(l!==null)throw new r({error:"refused",message:l,next:"Run a different command."});const o=j(t,M());I(o.dir,{recursive:!0}),f(o.cmdSh,a),f(o.jobLog,""),f(o.jobBg,"");const{launchBashJobSupervisor:c}=await import("../../core/runtime/broker/bash-job-launch.js"),{[C]:m,...k}=process.env;try{await c(n,o,t,s,k)}catch(p){throw D(o.dir,{recursive:!0,force:!0}),p}const{recordBackgroundedJob:x}=await import("../../core/runtime/broker/daemon-ops.js");try{await x(n,o.jobId,{command:a,job_dir:o.dir,log_path:o.jobLog})}catch(p){throw new r({error:"job_not_recorded",message:`job ${o.jobId} is running but could not be recorded (${p.message}); it will NOT message you when it exits. Log: ${o.jobLog}`,next:`Watch it with \`tail -f ${o.jobLog}\`, or stop it with \`crtr node bash kill ${o.jobId}\`.`},T.GENERAL)}return{job_id:o.jobId,log_path:o.jobLog}},"run"),render:d(e=>`[backgrounded as job ${e.job_id}. It will interrupt you with a message when it exits. Watch: tail -f ${e.log_path} ; stop: crtr node bash kill ${e.job_id}]`,"render")}),K=h({name:"background",description:"hand the focused node\u2019s running bash command to its background job system",whenToUse:"the tmux prefix menu invokes this when a foreground bash command should stop blocking the agent immediately but keep running as a file-backed background job",tier:"hidden",help:{name:"node bash background",summary:"hand every running foreground bash command for one node to its file-backed background job system",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node whose running bash command to background. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE. The prefix menu passes this for you."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The target node."},{name:"jobs",type:"object[]",required:!0,constraint:"Per handed-off job {job_id, elapsed_ms} \u2014 the foreground elapsed time in milliseconds at the moment of handoff."}],outputKind:"object",effects:["Signals every foreground bash command currently running for the node to return from its tool call immediately while its detached supervisor keeps the command running.","Each handed-off command retains its log under the node context jobs directory and becomes a job object; when it exits, the job\u2019s watchers (its node by default) get an urgent `exited` push carrying the exit code and log path."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash handoff"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=R(n.paths.context_dir),s=Date.now(),i=t.map(l=>({job_id:l.jobId,elapsed_ms:s-(S(l.jobId)??s)}));return{node_id:a,jobs:i}},"run"),render:d(e=>{const a=e.jobs;if(a.length===0)return`No foreground bash command is running for ${e.node_id}.`;const n=Math.max(...a.map(t=>t.elapsed_ms));return`Handed ${a.length} bash command(s) to the background job system for ${e.node_id} \xB7 ran ${_(n)} in the foreground.`},"render")}),X=h({name:"list",description:"list a node\u2019s live background bash jobs",whenToUse:"you need the ids, commands, and ages of your jobs that are still running",help:{name:"node bash list",summary:"read every still-running background bash job owned by one node",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node whose background jobs to list. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."},{kind:"flag",name:"tail",type:"int",required:!1,default:0,constraint:"Include this many trailing log lines per job (0 = none)."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The target node."},{name:"jobs",type:"object[]",required:!0,constraint:"Oldest-first {job_id, command, purpose, started_at, elapsed_ms, deadline_at, log_path, pgid, log_tail} for every live background job; purpose is a human-readable label or null; deadline_at is the auto-cancel time or null for a job that runs to completion."}],outputKind:"object",effects:["Reads the node context jobs directory. Changes nothing."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash job list"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=Date.now(),s=Math.max(0,Math.trunc(Number(e.tail??0))),i=n.paths.context_dir,l=E(i).map(o=>({job_id:o.jobId,command:o.command,purpose:o.purpose,started_at:new Date(o.startedAtMs).toISOString(),elapsed_ms:t-o.startedAtMs,deadline_at:o.deadlineAtMs===null?null:new Date(o.deadlineAtMs).toISOString(),log_path:o.logPath,pgid:B(i,o.jobId)??null,log_tail:L(o.logPath,s)}));return{node_id:n.node_id,jobs:l}},"run"),render:d(e=>{const a=e.jobs;return a.length===0?`No background bash jobs for ${e.node_id}.`:a.map(n=>`${n.job_id} \xB7 ${_(n.elapsed_ms)} \xB7 ${q(n.command)}${n.purpose===null?"":`
|
|
2
2
|
purpose: ${n.purpose}`}${n.deadline_at===null?"":`
|
|
3
3
|
cancels at: ${n.deadline_at}`}
|
|
4
4
|
log: ${n.log_path}`).join(`
|
|
5
|
-
`)},"render")}),
|
|
5
|
+
`)},"render")}),G=h({name:"kill",description:"stop one of a node\u2019s background bash jobs",whenToUse:"a job you started is no longer wanted or is stuck, and it should stop now",help:{name:"node bash kill",summary:"best-effort stop a background bash job and tell the owning node it was canceled",params:[{kind:"positional",name:"job",required:!0,constraint:"Job id, as reported by node bash list."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The owning node."},{name:"signaled",type:"boolean",required:!0,constraint:"True when at least one observed supervisor-tree process received SIGTERM."}],outputKind:"object",effects:["Snapshots the supervisor\u2019s observable process tree, sends SIGTERM then SIGKILL to those processes, and cannot stop a descendant that detached and reparented before the snapshot.","Records the job\u2019s exit so it stops showing as live, and ends its job object as killed; the job\u2019s watchers (its node by default) get an urgent `killed` push."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash job"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=e.job;if(!y(t))throw new r({error:"invalid_job_id",message:`invalid background job id: ${t}`,next:"Use a job id reported by `crtr node bash list`."});const s=await A().stopBashJob(n.node_id,t);return{node_id:n.node_id,signaled:s.signaled}},"run"),render:d(e=>`Canceled a background bash job for ${e.node_id}${e.signaled===!0?"":" (no observed job process was still running)"}.`,"render")}),W=h({name:"extend",description:"claim an auto-backgrounded bash job so the daemon does not cancel it at its deadline",whenToUse:"a command that was moved to a job automatically is worth keeping past its cancel deadline \u2014 give it more time with --for, or let it run to completion with --keep",help:{name:"node bash extend",summary:"move or clear the auto-cancel deadline on one of a node\u2019s live background bash jobs",params:[{kind:"positional",name:"job",required:!0,constraint:"Job id, as reported by node bash list or the auto-background notice."},{kind:"flag",name:"for",type:"string",required:!1,constraint:"New window from now before the job is canceled, as a duration (e.g. 90s, 10m, 1h30m). Mutually exclusive with --keep; give exactly one."},{kind:"flag",name:"keep",type:"bool",required:!1,constraint:"Remove the deadline entirely so the job runs to completion. Mutually exclusive with --for; give exactly one."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The owning node."},{name:"deadline_at",type:"string",required:!1,constraint:"The job\u2019s new auto-cancel time (ISO), or absent when --keep cleared the deadline so the job runs to completion."}],outputKind:"object",effects:["Rewrites or removes the job\u2019s deadline file. The daemon\u2019s deadline lane reads it on its next scan. Signals nothing."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash job"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=e.job;if(!y(t))throw new r({error:"invalid_job_id",message:`invalid background job id: ${t}`,next:"Use a job id reported by `crtr node bash list`."});const s=e.for,i=e.keep===!0;if(i===(s!==void 0&&s!==""))throw new r({error:"invalid_args",message:"give exactly one of --for <duration> or --keep",next:"Extend by a window with --for 10m, or drop the deadline with --keep."});const l=n.paths.context_dir,o=j(l,t);if(!g(o.dir))throw new r({error:"not_found",message:`no background job ${t} for ${n.node_id}`,next:"List live jobs with `crtr node bash list`."});if(!g(o.jobBg)||g(o.jobExit))throw new r({error:"not_running",message:`background job ${t} for ${n.node_id} is no longer running`,next:`It finished or was stopped, so there is no deadline to change. Log: ${o.jobLog}`});if(i)return N(o),{node_id:n.node_id};const c=U(s);if(c===null||c<=0)throw new r({error:"invalid_duration",message:`not a duration: ${s}`,next:"Use a value like 90s, 10m, or 1h30m."});const m=Date.now()+c;return J(o,m),{node_id:n.node_id,deadline_at:new Date(m).toISOString()}},"run"),render:d(e=>e.deadline_at===void 0?`Cleared the deadline \u2014 the background job will run to completion for ${e.node_id}.`:`Background job for ${e.node_id} will be canceled at ${e.deadline_at} unless extended again.`,"render")}),ae=$({name:"bash",description:"start, list, stop, and extend your background bash jobs",whenToUse:"a command needs longer than one tool call \u2014 start it as a job and be woken with its exit code instead of using `&`, `nohup`, or a cron that polls a log \u2014 or you need to list, stop, or extend the deadline of a job you already have",help:{name:"node bash",summary:"start, list, stop, and extend background bash jobs owned by a node",model:"Every bash call runs under a detached supervisor with its output in a log file, so a command that outlives its call keeps running as a job: a canvas object its node owns, readable and watchable with `crtr canvas`. `run` starts a job on purpose and it is never auto-canceled. A foreground command still running after 5 minutes is moved to a job automatically, with a cancel deadline unless `extend` claims it."},children:[O,K,X,G,W]});export{ae as nodeBash};
|
package/dist/commands/node.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var t=Object.defineProperty;var e=(o
|
|
1
|
+
var t=Object.defineProperty;var e=(n,o)=>t(n,"name",{value:o,configurable:!0});import{defineBranch as a}from"../core/command.js";import{nodeNew as i}from"./node/create.js";import{nodeInspect as r,nodeFork as s}from"./node/inspect.js";import{nodeLifecycle as d,nodeConfig as c,nodePromote as h,nodeYield as m}from"./node/lifecycle.js";import{nodeMessage as p}from"./node/message.js";import{nodeWaitBranch as l}from"./node/wait.js";import{nodeSubscription as f}from"./node/subscription.js";import{nodeBash as g}from"./node/bash.js";import{pyramidSlash as u}from"./node/pyramid.js";import{nodeWorktreeBranch as w}from"./node-worktree.js";import{nodeOutcome as y}from"./node/outcome.js";function A(){return a({name:"node",slash:u,rootEntry:{concept:"a unit of the agent runtime \u2014 an agent on the canvas with its own context dir and pi vehicle. Spawning nodes is how work gets done here, not an optimization you reach for late",desc:"spawn, inspect, and navigate nodes on the canvas",useWhen:"delegating a genuinely independent subtask, or walking the subscription graph. Base nodes work hands-on; orchestrators fan out. Give each child a bounded outcome distinct from your whole assignment so delegation moves work toward completion instead of recursively passing it down. Once delegated, do not duplicate the child\u2019s work; its finish wakes you. Use the help surface for the exact node contract before invoking it. For a long-running shell command, `crtr node bash -h`."},help:{name:"node",summary:"create, inspect, message, and change canvas nodes",model:"A node is one agent on the canvas. Immediate actions, durable waits, subscriptions, lifecycle changes, and scheduled crons are separate intent families; choose the family by when the effect happens and what persistent state it changes."},children:[i,s,p,l,f,y,r,d,w,c,h,m,g]})}e(A,"registerNode");export{A as registerNode};
|
package/dist/core/bash-guard.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
var $=Object.defineProperty;var a=(n,t)=>$(n,"name",{value:t,configurable:!0});import{homedir as
|
|
1
|
+
var $=Object.defineProperty;var a=(n,t)=>$(n,"name",{value:t,configurable:!0});import{homedir as k,userInfo as I}from"node:os";import{isAbsolute as R,resolve as x,sep as v}from"node:path";import{commandSegments as L,stripHeredocs as O}from"./shell-segments.js";const h=10,W={s:1,m:60,h:3600,d:86400};function N(n){const t=/^\s*sleep(\s+[^;&|\n]+)/.exec(n);if(!t)return null;let e=0;for(const s of t[1].trim().split(/\s+/)){const r=/^([0-9]*\.?[0-9]+)([smhd]?)$/.exec(s);if(!r)return null;e+=Number(r[1])*W[r[2]||"s"]}return e}a(N,"leadingSleepSeconds");function z(n){const t=N(n);return t===null||t<=h?null:`Refused: this command starts by sleeping ${t}s, over the ${h}s inline limit.
|
|
2
2
|
Holding a live window on a clock is never how you wait here. Instead:
|
|
3
3
|
- schedule the wake and end your turn: crtr cron -h (one-shot at a time, or recurring)
|
|
4
4
|
- race an inbox message against a deadline: crtr node wait deadline -h
|
|
5
|
+
- waiting on a command: crtr node bash run '<command>' starts it as a job and wakes you with its exit code
|
|
5
6
|
- waiting on a child, a reply from the user, or any spine event: just stop \u2014 the runtime wakes you.
|
|
6
|
-
If you truly need a short pause inline, sleep ${h}s or less.`}a(z,"longSleepRefusal");const D=["/","/Applications","/bin","/dev","/etc","/home","/Library","/opt","/private","/private/etc","/private/tmp","/private/var","/sbin","/System","/tmp","/usr","/Users","/var","/Volumes"];function M(n,t,e){return D.includes(n)?"a system directory":n===t?"your home directory":n===`${t}/.crouter`?"the canvas home \u2014 every node\u2019s conversation and state":e===n||!e.startsWith(n.endsWith(v)?n:n+v)?null:`a directory containing your working directory (${e})`}a(M,"protectedReason");function T(n,t){const e=
|
|
7
|
+
If you truly need a short pause inline, sleep ${h}s or less.`}a(z,"longSleepRefusal");const D=["/","/Applications","/bin","/dev","/etc","/home","/Library","/opt","/private","/private/etc","/private/tmp","/private/var","/sbin","/System","/tmp","/usr","/Users","/var","/Volumes"];function M(n,t,e){return D.includes(n)?"a system directory":n===t?"your home directory":n===`${t}/.crouter`?"the canvas home \u2014 every node\u2019s conversation and state":e===n||!e.startsWith(n.endsWith(v)?n:n+v)?null:`a directory containing your working directory (${e})`}a(M,"protectedReason");function T(n,t){const e=I().username;return new Map([["HOME",n],["PWD",t],["USER",e],["LOGNAME",e]])}a(T,"initialVariables");function y(n,t){let e=n;if(e==="~"||e.startsWith("~/")){const s=t.get("HOME");if(s===void 0)return null;e=s+e.slice(1)}else if(e.startsWith("~"))return null;return e=e.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,(s,r,i)=>t.get(r??i)??s),/[$`*?[]/.test(e)?null:e}a(y,"expandText");function w(n,t,e){const s=y(n.replace(/\/+\*?\.?$/,"")||"/",t);return s===null?null:R(s)?x(s):e===null?null:x(e,s)}a(w,"resolveTarget");function d(n,t){for(const e of t){const s=e.indexOf("=");if(s<=0)continue;const r=e.slice(0,s);if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(r))continue;const i=y(e.slice(s+1),n);i===null?n.delete(r):n.set(r,i)}}a(d,"applyAssignments");const H=/^[0-9]*(&?>>?|<<?|>&|<&)/;function m(n){const t=[];let e=!1,s=!1;for(const r of n.slice(1)){if(s){s=!1;continue}const i=H.exec(r.text);if(i!==null&&!r.opaque){s=r.text===i[0];continue}if(!e&&r.text==="--"){e=!0;continue}!e&&r.text.startsWith("-")&&r.text!=="-"||t.push(r.text)}return t}a(m,"operands");function P(n){const t=new Set,e=new Set;for(const s of n.slice(1)){if(s.text==="--")break;if(s.text.startsWith("--"))e.add(s.text.slice(2));else if(s.text.startsWith("-")&&s.text.length>1)for(const r of s.text.slice(1))t.add(r)}return{letters:t,long:e}}a(P,"flagLetters");function E(n){const{letters:t,long:e}=P(n);return t.has("r")||t.has("R")||e.has("recursive")}a(E,"isRecursive");const C=/^\/dev\/r?(disk|sd[a-z]|nvme|hd[a-z]|vd[a-z]|mmcblk|md)/;function q(n){const t=[];let e=1;for(;e<n.length;){const s=n[e].text;if(s==="--"||/^-[HLPEdsx]$/.test(s))e+=1;else if(s==="-D"||s==="-O")e+=2;else if(s==="-f")e+1<n.length&&t.push(n[e+1].text),e+=2;else break}for(;e<n.length&&!n[e].text.startsWith("-");e+=1)t.push(n[e].text);return t}a(q,"findPaths");const Z=new Set(["sudo","doas","command","env","nice","nohup","time","stdbuf","xargs","exec","builtin"]);function V(n){let t=n;for(;t.length>0&&!t[0].opaque&&Z.has(t[0].text);){let e=1;for(;e<t.length&&t[e].text.startsWith("-");){const s=/^-[nu]$/.test(t[e].text);e+=s?2:1}for(;e<t.length&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(t[e].text);)e+=1;if(e>=t.length)return[];t=t.slice(e)}return t}a(V,"stripCommandPrefixes");function U(n){const t={targets:[],named:null},e=n[0];if(e===void 0||e.opaque)return t;const s=e.text.slice(e.text.lastIndexOf("/")+1),r=m(n);switch(s){case"rm":return E(n)?{targets:r,named:null}:t;case"mv":return r.length>=2?{targets:r.slice(0,-1),named:null}:t;case"chmod":case"chown":case"chgrp":return E(n)&&r.length>=2?{targets:r.slice(1),named:null}:t;case"find":return n.some(o=>!o.opaque&&(o.text==="-delete"||o.text==="-exec"||o.text==="-execdir"))?{targets:q(n),named:null}:t;case"dd":{const i=n.find(l=>l.text.startsWith("of="));if(i===void 0)return t;const o=i.text.slice(3);return C.test(o)?{targets:[],named:o}:o.startsWith("/dev/")?t:{targets:[o],named:null}}default:if(/^(mkfs|newfs)([.-].+)?$/.test(s))return{targets:[],named:s};if(s==="diskutil"){const i=r[0];if(i!==void 0&&/^(erase|reformat|partition|zeroDisk|randomDisk|secureErase)/i.test(i))return{targets:[],named:`diskutil ${i}`}}return t}}a(U,"destructiveTargets");function j(n,t,e){const s=T(e,t);let r=[t];for(const i of L(O(n))){const o=V(i.tokens),l=o[0];if(l!==void 0){const u=new Map(s);d(u,i.assignments);const c=r[r.length-1];c!==void 0&&u.set("PWD",c);const{targets:S,named:p}=U(o);if(p!==null)return b(i.text,p,"a whole disk or volume \u2014 never a task boundary");for(const A of S)for(const _ of r.length===0?[null]:r){const f=w(A,u,_);if(f===null)continue;const g=M(f,e,t);if(g!==null)return b(i.text,f,g)}}if(!i.background){if(l===void 0)d(s,i.assignments);else if(!l.opaque&&B.has(l.text))d(s,i.assignments),d(s,m(o));else if(!l.opaque&&l.text==="cd"){const u=m(o)[0],c=u===void 0?s.get("HOME")??null:w(u,s,r[r.length-1]??null);r=c===null?[]:[...r,c]}}}return null}a(j,"destructiveCommandRefusal");const B=new Set(["export","declare","typeset","readonly"]);function b(n,t,e){return`Refused: \`${n}\` targets ${t}, which is ${e}.
|
|
7
8
|
Nothing an agent is asked to do requires destroying that path, and it cannot be undone.
|
|
8
9
|
If you meant somewhere else, name that path explicitly and run the command again. Two traps to check first:
|
|
9
10
|
- a \`VAR=value cmd\` prefix applies ONLY to that one command, so a later \`$VAR\` in the same line is the SHELL's value \u2014 this is exactly how a node deleted a home directory.
|
|
10
11
|
- \`~\` and \`$HOME\` are the real home directory, never a scratch dir; build scratch paths with mktemp -d and hold them in a variable of your own.
|
|
11
|
-
If you believe this path genuinely must be destroyed, ask the user \u2014 do not work around this refusal.`}a(b,"refusalMessage");function K(n,t,e=
|
|
12
|
+
If you believe this path genuinely must be destroyed, ask the user \u2014 do not work around this refusal.`}a(b,"refusalMessage");function K(n,t,e=k()){return z(n)??j(n,t,e)}a(K,"bashCommandRefusal");export{K as bashCommandRefusal,N as leadingSleepSeconds};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
var Z=Object.defineProperty;var r=(e,n)=>Z(e,"name",{value:n,configurable:!0});import{prepared as d}from"../sqlite-statements.js";import{envNodeId as
|
|
1
|
+
var Z=Object.defineProperty;var r=(e,n)=>Z(e,"name",{value:n,configurable:!0});import{prepared as d}from"../sqlite-statements.js";import{envNodeId as x}from"../../shared/env.js";import{rmdirSync as ee,rmSync as ne,statSync as te}from"node:fs";import{dirname as oe,join as j,resolve as re}from"node:path";import{removeTreeBeneath as W}from"../spaces/open-beneath.js";import{emitEvent as ie}from"../events/emit.js";import{openDb as a,prepared as b,withCanvasWrite as p}from"./db.js";import{conversationStore as se,ConversationNotFoundError as de}from"../conversation-store/index.js";import{hasUnseenWakeMail as ae}from"./deliveries.js";import{isAppRun as I}from"./app-run.js";import{registerOutcomeAction as F}from"./outcome-actions.js";import{publish as le}from"../graph/events.js";import{nodeObject as ue}from"../graph/objects.js";import{findNodeBySessionFileSql as _e}from"./sessions.js";import{hydrateFault as ce}from"./recovery.js";import{hasUnreconciledWorktree as pe,openWorktree as fe,readWorktree as Ee,worktreeFromRow as me}from"./worktrees.js";import{isPidAlive as Ne,isPidPresent as H,recordedPidLiveness as Re}from"./pid.js";import{ensureHome as ye,jobDir as Se,nodeDir as Oe,nodeRunDir as C}from"./paths.js";import{appForNode as we,ensureNodeDirs as Te,nodeAgentDir as he}from"./node-agent-paths.js";import{appSpace as U}from"../layout.js";import{createObject as Le,getById as M,ownerForGrantee as w,setObjectKind as ge,setObjectName as De}from"../graph/objects.js";import{deleteNodeWithOwned as ve}from"../graph/deletion.js";function m(e,n){if(e==null)return null;const t=JSON.stringify(e);if(t===void 0)throw new TypeError(`${n} must be JSON-serializable`);return t}r(m,"jsonValue");function rn(e,n){if(d(e,"SELECT deleting_at FROM nodes WHERE node_id = ?").get(n)?.deleting_at)throw new Error("run is deleting")}r(rn,"assertRunNotDeleting");function ke(e,n){const t=w(n,e.grantee??"app:terminal");Le(n,{id:e.node_id,type:"node",kind:e.kind,owner:t.rowid,privacy:"public",name:e.name,preview:""}),d(n,`INSERT INTO nodes (node_id, description, title, icon, cycles, mode, lifecycle, cwd, host_kind, profile_id, parent, spawned_by, creator, fork_from, fork_source_file, created, passive_default, cycle_pending, model_override, persona_ack_json, review_binding_json, launch_config_json, scopes_json, isolated, status, intent, final_report, finalized_at, deadline_at, "window", tmux_session, pane, sub, grant_id, client_id, metadata_json, run_id, event_cursor, usage_json, deleting_at) VALUES (${Array(40).fill("?").join(", ")})`).run(e.node_id,e.description??null,e.title??null,e.icon??null,e.cycles??null,e.mode,e.lifecycle,e.cwd,e.host_kind??null,e.profile_id??null,e.parent??null,e.spawned_by??null,e.creator??null,e.fork_from??null,e.fork_source_file??null,e.created,e.passive_default===!0?1:0,e.cycle_pending===!0?1:0,e.model_override??null,m(e.persona_ack,"persona_ack"),m(e.review_binding,"review_binding"),m(e.launch,"launch"),m(e.scopes,"scopes"),e.isolated===!0?1:0,e.status??"active",e.intent??null,e.final_report??null,e.finalized_at??null,e.deadline_at??null,e.window??null,e.tmux_session??null,e.pane??null,e.sub??null,e.grant_id??null,e.client_id??null,e.metadata_json??null,e.run_id??null,e.event_cursor??null,e.usage_json??null,e.deleting_at??null)}r(ke,"seedRow");function P(e){if(e.fault_id==null)return null;const n=Object.entries(e).filter(([t])=>t.startsWith("fault_")).map(([t,o])=>[t==="fault_id"?t:t.slice(6),o]);return ce(Object.fromEntries(n))}r(P,"faultFrom");function E(e){const n={node_id:e.node_id,name:e.name,description:e.description??void 0,cycles:e.cycles??void 0,kind:e.kind,mode:e.mode,lifecycle:e.lifecycle,status:e.status,cwd:e.cwd,host_kind:e.host_kind??null,profile_id:e.profile_id??null,scopes:e.scopes_json==null?null:JSON.parse(e.scopes_json),isolated:e.isolated===1,grantee:e.grantee,sub:e.sub??null,grant_id:e.grant_id??null,client_id:e.client_id??null,metadata_json:e.metadata_json??null,run_id:e.run_id??null,event_cursor:e.event_cursor??null,last_activity_at:e.last_activity_at??null,usage_json:e.usage_json??null,last_turn_error_json:e.last_turn_error_json??null,deleting_at:e.deleting_at??null,parent:e.parent??null,created:e.created,intent:e.intent??null,frozen_at:e.frozen_at??null,respawn_failures:e.respawn_failures??0,respawn_not_before:e.respawn_not_before??null,launched_at:e.launched_at??null,last_settled_at:e.last_settled_at??null,terminal_reason:e.terminal_reason??null,final_report:e.final_report??null,finalized_at:e.finalized_at??null,outcome_kind:e.outcome_kind??null,outcome_reason:e.outcome_reason??null,outcome_at:e.outcome_at??null,outcome_revision:e.outcome_revision??0,outcome_detail_json:e.outcome_detail_json??null,deadline_at:e.deadline_at??null,telemetry_tokens_in:e.telemetry_tokens_in??null,telemetry_context_tokens:e.telemetry_context_tokens??null,telemetry_last_activity:e.telemetry_last_activity??null,telemetry_updated_at:e.telemetry_updated_at??null,pi_pid:e.pi_pid??null,pi_pid_identity:e.pi_pid_identity??null,window:e.window??null,tmux_session:e.tmux_session??null,pane:e.pane??null,fault:null,streaming:!1},t=H(n.pi_pid),o=P(e);return n.fault=o?.link==="pi\u2192provider"&&o.retry.by==="daemon"&&o.retry.disposition==="auto"&&!t?null:o,n.streaming=e.turn_open_at!==null&&e.turn_closed_at===null&&t,n}r(E,"rowFrom");function q(e,n=Ee(e.node_id)){return{...E(e),title:e.title??void 0,icon:e.icon??void 0,spawned_by:e.spawned_by??null,creator:e.creator??null,fork_from:e.fork_from??null,fork_source_file:e.fork_source_file??null,passive_default:e.passive_default===1,cycle_pending:e.cycle_pending===1,model_override:e.model_override??null,scopes:e.scopes_json==null?null:JSON.parse(e.scopes_json),persona_ack:e.persona_ack_json==null?void 0:JSON.parse(e.persona_ack_json),review_binding:e.review_binding_json==null?void 0:JSON.parse(e.review_binding_json),launch:e.launch_config_json==null?void 0:JSON.parse(e.launch_config_json),pi_session_id:e.pi_session_id??null,pi_session_file:e.pi_session_file??null,...n===null?{}:{managed_worktree:n}}}r(q,"metaFrom");function sn(e,n={}){return ye(),p(t=>{ke(e,t),e.managed_worktree!==void 0&&e.managed_worktree!==null&&fe(e.node_id,e.managed_worktree,t),n.outcomeDelivery!==void 0&&F(t,e.node_id,n.outcomeDelivery),n.withinCreate?.(t,e)}),Te(e.node_id),y(e.node_id)}r(sn,"createNode");function y(e){const n=b(a(),"SELECT * FROM node_rows WHERE node_id = ?").get(e);return n===void 0?null:q(n)}r(y,"getNode");const Ae=1440*60*1e3;function $(e){return{requestKey:e.request_key,nodeId:e.node_id,state:e.state}}r($,"createRequestRecord");function dn(e){const n=a().prepare("SELECT request_key, node_id, state FROM create_requests WHERE request_key = ?").get(e);return n===void 0?null:$(n)}r(dn,"findCreateRequest");function an(e){return p(n=>{const t=Date.now();n.prepare("DELETE FROM create_requests WHERE expires_at <= ?").run(t);const o=n.prepare("SELECT request_key, node_id, state FROM create_requests WHERE request_key = ?").get(e);return o!==void 0?$(o):(n.prepare("INSERT INTO create_requests (request_key, node_id, expires_at, state) VALUES (?, NULL, ?, 'reserved')").run(e,t+Ae),{requestKey:e,nodeId:null,state:"reserved"})})}r(an,"reserveCreateRequest");function ln(e=Date.now()){return p(n=>Number(n.prepare("DELETE FROM create_requests WHERE expires_at <= ?").run(e).changes))}r(ln,"pruneExpiredCreateRequests");function un(e,n){p(t=>{t.prepare("UPDATE create_requests SET node_id = ?, state = 'complete' WHERE request_key = ?").run(n,e)})}r(un,"recordCreateRequestNode");function _n(e,n){p(t=>{t.prepare("UPDATE create_requests SET node_id = ?, state = 'reserved' WHERE request_key = ?").run(n,e)})}r(_n,"reserveCreateRequestNode");function cn(e,n){p(t=>{t.prepare("UPDATE create_requests SET state = 'complete' WHERE request_key = ? AND node_id = ?").run(e,n)})}r(cn,"completeCreateRequest");function pn(e,n){p(t=>{if(n===null){t.prepare("DELETE FROM create_requests WHERE request_key = ? AND node_id IS NULL").run(e);return}t.prepare("DELETE FROM create_requests WHERE request_key = ? AND node_id = ?").run(e,n)})}r(pn,"abandonCreateRequest");function fn(e){return re(e)}r(fn,"canonicalSessionFile");function En(e){const n=_e(e);return n===null?null:y(n)}r(En,"findNodeBySessionFile");function mn(e){const n=d(a(),"SELECT * FROM node_rows WHERE node_id = ?").get(e);return n?E(n):null}r(mn,"getRow");function Nn(e){const n=d(a(),"SELECT * FROM node_rows WHERE pane = ?").get(e);return n?E(n):null}r(Nn,"getRowByPane");function Rn(e){return d(a(),`SELECT * FROM node_rows
|
|
2
2
|
WHERE deadline_at IS NOT NULL AND outcome_at IS NULL
|
|
3
3
|
AND deadline_at <= ? AND status IN ('active','idle') AND kind != 'human'
|
|
4
4
|
ORDER BY deadline_at, created, node_id`).all(e).map(E)}r(Rn,"dueNodeDeadlines");function yn(e,n){return Number(d(a(),"UPDATE nodes SET deadline_at = ? WHERE node_id = ?").run(n,e).changes)===1}r(yn,"setNodeDeadline");function Sn(e,n){return!Number.isFinite(n)||n<=0?0:p(t=>{const o=d(t,`SELECT node_id, deadline_at FROM nodes
|
|
5
|
-
WHERE profile_id = ? AND deadline_at IS NOT NULL AND status IN ('active', 'idle')`).all(e),i=d(t,"UPDATE nodes SET deadline_at = ? WHERE node_id = ? AND deadline_at = ?");let s=0;for(const l of o){const
|
|
6
|
-
WHERE o.type = 'node' AND i.scheme = 'local-user' AND i.external_id = 'user:local'`,s=e.prepare(`${o} LIMIT 1`).get()!==void 0?Number(e.prepare(`UPDATE objects SET owner = ? WHERE rowid IN (${o})`).run(
|
|
5
|
+
WHERE profile_id = ? AND deadline_at IS NOT NULL AND status IN ('active', 'idle')`).all(e),i=d(t,"UPDATE nodes SET deadline_at = ? WHERE node_id = ? AND deadline_at = ?");let s=0;for(const l of o){const _=Date.parse(l.deadline_at);Number.isNaN(_)||(s+=Number(i.run(new Date(_+n).toISOString(),l.node_id,l.deadline_at).changes))}return s})}r(Sn,"shiftProfileNodeDeadlines");function On(e,n,t){const o=`SELECT o.rowid FROM objects o JOIN identities i ON i.object = o.owner JOIN nodes n ON n.node_id = o.id
|
|
6
|
+
WHERE o.type = 'node' AND i.scheme = 'local-user' AND i.external_id = 'user:local'`,s=e.prepare(`${o} LIMIT 1`).get()!==void 0?Number(e.prepare(`UPDATE objects SET owner = ? WHERE rowid IN (${o})`).run(w(e,"app:terminal").rowid).changes):0,l=`SELECT o.rowid FROM objects o JOIN peer_stubs s ON s.object = o.rowid JOIN identities i ON i.object = o.owner
|
|
7
|
+
WHERE o.type = 'node' AND o.reception = 2 AND json_extract(s.envelope_json, '$.app') IS NULL
|
|
8
|
+
AND i.scheme = 'app' AND i.external_id = 'app:terminal'`,_=e.prepare(`${l} LIMIT 1`).get()!==void 0?Number(e.prepare(`UPDATE objects SET owner = ? WHERE rowid IN (${l})`).run(w(e,"user:local").rowid).changes):0;return Number(d(e,"UPDATE nodes SET sub = ? WHERE sub = 'user:local'").run(n).changes)+s+_+Number(d(e,`UPDATE nodes SET sub = ? WHERE sub IS NULL AND node_id IN (
|
|
7
9
|
SELECT o.id FROM objects o JOIN identities i ON i.object = o.owner
|
|
8
|
-
WHERE o.type = 'node' AND i.scheme = 'app' AND i.external_id = 'app:terminal')`).run(n).changes)+Number(d(e,"UPDATE nodes SET grant_id = NULL WHERE grant_id = ?").run(t).changes)}r(On,"rewriteLocalNodeIdentity");function
|
|
10
|
+
WHERE o.type = 'node' AND i.scheme = 'app' AND i.external_id = 'app:terminal')`).run(n).changes)+Number(d(e,"UPDATE nodes SET grant_id = NULL WHERE grant_id = ?").run(t).changes)}r(On,"rewriteLocalNodeIdentity");function T(e,n){if(e==null)return null;if(typeof e=="string"||typeof e=="number"||typeof e=="bigint"||e instanceof Uint8Array)return e;throw new TypeError(`${n} must be a SQLite scalar`)}r(T,"sqlInput");function wn(e,n){const t=[],o=r((s,l=s)=>{Object.prototype.hasOwnProperty.call(n,s)&&t.push([l,T(n[s],s)])},"add");if(o("description"),o("title"),o("icon"),o("cycles"),o("mode"),o("lifecycle"),o("cwd"),o("host_kind"),o("profile_id"),o("parent"),o("spawned_by"),o("creator"),o("fork_from"),o("fork_source_file"),o("model_override"),Object.prototype.hasOwnProperty.call(n,"passive_default")&&t.push(["passive_default",n.passive_default===!0?1:0]),Object.prototype.hasOwnProperty.call(n,"cycle_pending")&&t.push(["cycle_pending",n.cycle_pending===!0?1:0]),Object.prototype.hasOwnProperty.call(n,"persona_ack")&&t.push(["persona_ack_json",m(n.persona_ack,"persona_ack")]),Object.prototype.hasOwnProperty.call(n,"review_binding")&&t.push(["review_binding_json",m(n.review_binding,"review_binding")]),Object.prototype.hasOwnProperty.call(n,"isolated"))throw new TypeError("isolated cannot change after node creation");Object.prototype.hasOwnProperty.call(n,"scopes")&&t.push(["scopes_json",m(n.scopes,"scopes")]),Object.prototype.hasOwnProperty.call(n,"launch")&&t.push(["launch_config_json",m(n.launch,"launch")]);const i=r(s=>Object.prototype.hasOwnProperty.call(n,s),"has");return p(s=>{const l=M(s,e);if(l===null||l.type!=="node")throw new Error(`unknown node: ${e}`);if(i("name")&&De(s,l,T(n.name,"name")),i("kind")&&ge(s,l,T(n.kind,"kind")),t.length===0)return;if(s.prepare(`UPDATE nodes SET ${t.map(([u])=>`${u} = ?`).join(", ")} WHERE node_id = ?`).run(...t.map(([,u])=>u),e).changes!==1)throw new Error(`unknown node: ${e}`)}),y(e)}r(wn,"updateNode");function Tn(e,n){d(a(),"UPDATE nodes SET status = ? WHERE node_id = ?").run(n,e)}r(Tn,"setStatus");function hn(e,n){d(a(),"UPDATE nodes SET intent = ? WHERE node_id = ?").run(n??null,e)}r(hn,"setIntent");function Ln(e,n,t={}){if(t.outcome===void 0){const i=[],s=[];Object.prototype.hasOwnProperty.call(n,"status")&&(i.push("status = ?"),s.push(n.status??null)),Object.prototype.hasOwnProperty.call(n,"intent")&&(i.push("intent = ?"),s.push(n.intent??null)),Object.prototype.hasOwnProperty.call(n,"terminal_reason")&&(i.push("terminal_reason = ?"),s.push(n.terminal_reason??null));const l=["node_id = ?"];s.push(e),t.fromStatuses!==void 0&&(l.push(`status IN (${t.fromStatuses.map(()=>"?").join(", ")})`),s.push(...t.fromStatuses)),t.requireNoFinalReport===!0&&l.push("final_report IS NULL");const _=a().prepare(`UPDATE nodes SET ${i.join(", ")} WHERE ${l.join(" AND ")}`).run(...s);return Number(_.changes)}const o=t.outcome;return p(i=>{const s=[],l=[];Object.prototype.hasOwnProperty.call(n,"status")&&(s.push("status = ?"),l.push(n.status??null)),Object.prototype.hasOwnProperty.call(n,"intent")&&(s.push("intent = ?"),l.push(n.intent??null)),Object.prototype.hasOwnProperty.call(n,"terminal_reason")&&(s.push("terminal_reason = ?"),l.push(n.terminal_reason??null));const _=r((R,c)=>o.supersede===!0?"?":`CASE WHEN outcome_at IS NULL THEN ? ELSE ${c} END`,"outcomeValue");s.push(`outcome_kind = ${_(o.kind,"outcome_kind")}`,`outcome_reason = ${_(o.reason,"outcome_reason")}`,`outcome_detail_json = ${_(o.detailJson,"outcome_detail_json")}`,`outcome_revision = ${o.supersede===!0?"outcome_revision + 1":"CASE WHEN outcome_at IS NULL THEN outcome_revision + 1 ELSE outcome_revision END"}`,`outcome_at = ${_(o.at,"outcome_at")}`),l.push(o.kind,o.reason,o.detailJson,o.at);const u=["node_id = ?"];l.push(e),t.fromStatuses!==void 0&&(u.push(`status IN (${t.fromStatuses.map(()=>"?").join(", ")})`),l.push(...t.fromStatuses)),t.requireNoFinalReport===!0&&u.push("final_report IS NULL");const f=i.prepare(`UPDATE nodes SET ${s.join(", ")} WHERE ${u.join(" AND ")}`).run(...l);if(Number(f.changes)===0)return 0;const O=d(i,"SELECT outcome_at FROM nodes WHERE node_id = ?").get(e);return(o.supersede===!0||O.outcome_at===o.at)&&o.documentJson!==null&&o.documentJson!==void 0&&le(i,{event:"outcome",object:ue(i,e),actor:null,attrs:{revision:Number(d(i,"SELECT outcome_revision FROM nodes WHERE node_id = ?").get(e).outcome_revision)},messageBody:JSON.parse(o.documentJson)}),Number(f.changes)})}r(Ln,"transitionStatusIntent");function gn(e){return d(a(),"SELECT final_report FROM nodes WHERE node_id = ?").get(e)?.final_report??null}r(gn,"getFinalReport");function Dn(e,n){return d(a(),"UPDATE nodes SET final_report = NULL, finalized_at = NULL WHERE node_id = ? AND final_report = ?").run(e,n).changes>0}r(Dn,"clearFinalReportIfMatches");function vn(e,n){d(a(),'UPDATE nodes SET tmux_session = ?, "window" = ?, pane = ? WHERE node_id = ?').run(n.tmux_session??null,n.window??null,n.pane??null,e)}r(vn,"setPresence");function kn(e,n){d(a(),"UPDATE nodes SET launched_at = ?, frozen_at = NULL, respawn_not_before = NULL WHERE node_id = ?").run(n,e)}r(kn,"recordLaunch");function An(e,n){d(a(),"UPDATE nodes SET last_settled_at = ? WHERE node_id = ?").run(n,e)}r(An,"recordSettledTurn");function xn(e,n){d(a(),"UPDATE nodes SET frozen_at = ? WHERE node_id = ?").run(n,e)}r(xn,"setFrozen");function jn(e){d(a(),"UPDATE nodes SET frozen_at = NULL WHERE node_id = ?").run(e)}r(jn,"clearFrozen");function Wn(e,n){d(a(),"UPDATE nodes SET respawn_failures = ?, respawn_not_before = ? WHERE node_id = ?").run(n.failures,n.notBefore,e)}r(Wn,"recordRespawnAttempt");const N="node_id NOT IN (SELECT node_id FROM warm_pool)",z=["state","cleanup_state","path","branch","repo_root","base_ref","base_sha","created_at","closed_at","next_attempt_at","attempts","quarantined","sweep_reason","sweep_detail","sweep_last_at","sweep_observed_json","abandoned_json","land_intent_json","disposal_json"];function xe(e){return e.worktree_state===null?null:me(Object.fromEntries(z.map(n=>[n,e[`worktree_${n}`]])))}r(xe,"worktreeFromProjection");function bn(e){const n=z.map(o=>`w.${o} AS worktree_${o}`).join(", ");return a().prepare(`SELECT n.*, ${n} FROM node_rows n LEFT JOIN worktrees w ON w.node_id = n.node_id WHERE n.profile_id = ? ORDER BY n.created`).all(e).map(o=>q(o,xe(o)))}r(bn,"listNodesByProfile");function In(){const e=a();return b(e,`SELECT
|
|
9
11
|
node_id, name, description, kind, mode, lifecycle, status, cwd, profile_id,
|
|
10
12
|
grantee, parent, created, frozen_at, terminal_reason, final_report,
|
|
11
13
|
telemetry_tokens_in, telemetry_updated_at, pi_pid, turn_open_at, turn_closed_at,
|
|
@@ -13,13 +15,13 @@ var Z=Object.defineProperty;var r=(e,n)=>Z(e,"name",{value:n,configurable:!0});i
|
|
|
13
15
|
fault_operation_id, fault_anchor_entry_id, fault_retry_disposition,
|
|
14
16
|
fault_retry_by, fault_retry_attempt, fault_retry_max, fault_retry_next_at,
|
|
15
17
|
fault_provider_error_json
|
|
16
|
-
FROM node_rows WHERE ${N} ORDER BY created`).all().map(E)}r(
|
|
18
|
+
FROM node_rows WHERE ${N} ORDER BY created`).all().map(E)}r(In,"browseNodes");function Fn(e,n){d(a(),`UPDATE nodes
|
|
17
19
|
SET telemetry_tokens_in = ?,
|
|
18
20
|
telemetry_context_tokens = COALESCE(?, telemetry_context_tokens),
|
|
19
21
|
telemetry_last_activity = COALESCE(?, telemetry_last_activity),
|
|
20
22
|
telemetry_updated_at = ?
|
|
21
23
|
WHERE node_id = ?
|
|
22
|
-
AND (telemetry_updated_at IS NULL OR telemetry_updated_at < ?)`).run(n.tokensIn,n.contextTokens,n.lastActivity,n.updatedAt,e,n.updatedAt)}r(
|
|
24
|
+
AND (telemetry_updated_at IS NULL OR telemetry_updated_at < ?)`).run(n.tokensIn,n.contextTokens,n.lastActivity,n.updatedAt,e,n.updatedAt)}r(Fn,"updateNodeTelemetry");function Hn(){return d(a(),`SELECT
|
|
23
25
|
node_id, name, description, cycles, kind, status, frozen_at,
|
|
24
26
|
telemetry_context_tokens, telemetry_last_activity,
|
|
25
27
|
pi_pid, turn_open_at, turn_closed_at,
|
|
@@ -27,7 +29,7 @@ var Z=Object.defineProperty;var r=(e,n)=>Z(e,"name",{value:n,configurable:!0});i
|
|
|
27
29
|
fault_operation_id, fault_anchor_entry_id, fault_retry_disposition,
|
|
28
30
|
fault_retry_by, fault_retry_attempt, fault_retry_max, fault_retry_next_at,
|
|
29
31
|
fault_provider_error_json
|
|
30
|
-
FROM node_rows WHERE ${N} ORDER BY created`).all().map(n=>{const t=n.pi_pid??null,o=H(t),i=P(n);return{node_id:n.node_id,name:n.name,description:n.description??null,cycles:n.cycles??null,kind:n.kind,status:n.status,frozen_at:n.frozen_at??null,pi_pid:t,telemetry_context_tokens:n.telemetry_context_tokens??null,telemetry_last_activity:n.telemetry_last_activity??null,fault:i?.link==="pi\u2192provider"&&i.retry.by==="daemon"&&i.retry.disposition==="auto"&&!o?null:i,streaming:n.turn_open_at!==null&&n.turn_closed_at===null&&o}})}r(
|
|
32
|
+
FROM node_rows WHERE ${N} ORDER BY created`).all().map(n=>{const t=n.pi_pid??null,o=H(t),i=P(n);return{node_id:n.node_id,name:n.name,description:n.description??null,cycles:n.cycles??null,kind:n.kind,status:n.status,frozen_at:n.frozen_at??null,pi_pid:t,telemetry_context_tokens:n.telemetry_context_tokens??null,telemetry_last_activity:n.telemetry_last_activity??null,fault:i?.link==="pi\u2192provider"&&i.retry.by==="daemon"&&i.retry.disposition==="auto"&&!o?null:i,streaming:n.turn_open_at!==null&&n.turn_closed_at===null&&o}})}r(Hn,"graphNodes");function je(e){const n=[N],t=[];if(e?.status!==void 0){const i=Array.isArray(e.status)?e.status:[e.status];n.push(`status IN (${i.map(()=>"?").join(",")})`),t.push(...i)}return e?.withRecordedPid===!0&&n.push("pi_pid IS NOT NULL"),e?.withRecordedPidOrFrozen===!0&&n.push("(pi_pid IS NOT NULL OR frozen_at IS NOT NULL)"),a().prepare(`SELECT * FROM node_rows WHERE ${n.join(" AND ")} ORDER BY created`).all(...t).map(E)}r(je,"listNodes");const We=`SELECT
|
|
31
33
|
node_id, created, launched_at, last_settled_at, finalized_at,
|
|
32
34
|
outcome_at, last_activity_at, pane, "window", tmux_session
|
|
33
35
|
FROM nodes INDEXED BY idx_nodes_terminal_placement_created
|
|
@@ -48,16 +50,16 @@ var Z=Object.defineProperty;var r=(e,n)=>Z(e,"name",{value:n,configurable:!0});i
|
|
|
48
50
|
AND v.node_id NOT IN (SELECT node_id FROM warm_pool)
|
|
49
51
|
AND v.kind != 'human'
|
|
50
52
|
AND (v.status IN ('active', 'idle') OR (v.status IN ('done', 'canceled', 'dead') AND (v.pi_pid IS NOT NULL OR v.frozen_at IS NOT NULL)))
|
|
51
|
-
ORDER BY v.created`;function Un(){return d(a(),be).all().map(E)}r(Un,"listNodesForSupervision");function Mn(){return d(a(),`SELECT node_id, name, created, grantee FROM node_rows WHERE ${N} ORDER BY created`).all()}r(Mn,"listNodeIdentities");function Pn(e,n){p(t=>{d(t,"INSERT OR REPLACE INTO warm_pool (node_id, recipe_key, created) VALUES (?, ?, ?)").run(e,n,new Date().toISOString())})}r(Pn,"registerWarmSpare");function qn(e){return p(n=>d(n,"DELETE FROM warm_pool WHERE node_id = ?").run(e).changes>0)}r(qn,"unregisterWarmSpare");function
|
|
53
|
+
ORDER BY v.created`;function Un(){return d(a(),be).all().map(E)}r(Un,"listNodesForSupervision");function Mn(){return d(a(),`SELECT node_id, name, created, grantee FROM node_rows WHERE ${N} ORDER BY created`).all()}r(Mn,"listNodeIdentities");function Pn(e,n){p(t=>{d(t,"INSERT OR REPLACE INTO warm_pool (node_id, recipe_key, created) VALUES (?, ?, ?)").run(e,n,new Date().toISOString())})}r(Pn,"registerWarmSpare");function qn(e){return p(n=>d(n,"DELETE FROM warm_pool WHERE node_id = ?").run(e).changes>0)}r(qn,"unregisterWarmSpare");function h(e,n){const t=`SELECT w.node_id AS node_id, w.recipe_key AS recipe_key, w.created AS created,
|
|
52
54
|
n.status AS status, n.pi_pid AS pi_pid, n.pi_pid_identity AS pi_pid_identity
|
|
53
|
-
FROM warm_pool w JOIN node_rows n ON n.node_id = w.node_id`+(n===void 0?"":" WHERE w.recipe_key = ?")+" ORDER BY w.created";return(n===void 0?e.prepare(t).all():e.prepare(t).all(n)).map(i=>({node_id:i.node_id,recipe_key:i.recipe_key,created:i.created,status:i.status,pi_pid:i.pi_pid??null,pi_pid_identity:i.pi_pid_identity??null}))}r(
|
|
55
|
+
FROM warm_pool w JOIN node_rows n ON n.node_id = w.node_id`+(n===void 0?"":" WHERE w.recipe_key = ?")+" ORDER BY w.created";return(n===void 0?e.prepare(t).all():e.prepare(t).all(n)).map(i=>({node_id:i.node_id,recipe_key:i.recipe_key,created:i.created,status:i.status,pi_pid:i.pi_pid??null,pi_pid_identity:i.pi_pid_identity??null}))}r(h,"warmSpareRowsFor");function $n(){return h(a())}r($n,"listWarmSpares");function zn(e){return d(a(),"SELECT 1 FROM warm_pool WHERE node_id = ?").get(e)!==void 0}r(zn,"isWarmSpare");function B(e){return e.status!=="active"&&e.status!=="idle"?!1:Re(e.pi_pid,e.pi_pid_identity)==="alive"}r(B,"spareIsClaimable");function Bn(e,n,t={}){return p(o=>{const i=h(o,e).find(B);return i===void 0||t.requestKey!==void 0&&o.prepare("UPDATE create_requests SET node_id = ?, state = 'warm_claimed' WHERE request_key = ? AND node_id IS NULL").run(i.node_id,t.requestKey).changes===0||d(o,"DELETE FROM warm_pool WHERE node_id = ?").run(i.node_id).changes===0?null:(d(o,"UPDATE nodes SET created = ?, deadline_at = ? WHERE node_id = ?").run(n,t.deadlineAt??null,i.node_id),t.outcomeDelivery!==void 0&&F(o,i.node_id,t.outcomeDelivery),i.node_id)})}r(Bn,"claimWarmSpare");function Jn(e){return h(a(),e).filter(B).length}r(Jn,"warmSpareCount");function Yn(e){return d(a(),"SELECT node_id FROM nodes WHERE parent = ? ORDER BY created").all(e).map(n=>n.node_id)}r(Yn,"childrenOf");function Vn(){return a().prepare(`SELECT node_id, name, kind, mode, lifecycle, status, cwd, host_kind, parent, created
|
|
54
56
|
FROM node_rows WHERE ${N} ORDER BY created`).all().map(e=>({node_id:e.node_id,name:e.name,kind:e.kind,mode:e.mode,lifecycle:e.lifecycle,status:e.status,cwd:e.cwd,host_kind:e.host_kind??null,parent:e.parent??null,created:e.created}))}r(Vn,"rosterNodes");function Xn(e){return a().prepare(`SELECT * FROM node_rows
|
|
55
57
|
WHERE parent IS NULL AND kind != 'human'
|
|
56
58
|
AND status IN ('active','idle') AND cwd = ?
|
|
57
59
|
AND ${N}
|
|
58
|
-
ORDER BY created DESC`).all(e).map(E)}r(Xn,"listResumableRoots");export*from"./tree.js";function Gn(e,n,t){return p(o=>{const i=d(o,"SELECT status, final_report, frozen_at FROM nodes WHERE node_id = ?").get(e);return i===void 0?{kind:"missing"}:n(i)?{kind:"terminal",status:i.status,final_report:i.final_report}:{kind:"ran",result:t()}})}r(Gn,"withFreshTerminalGuard");function S(e){return pe(e)}r(S,"hasUnreconciledManagedWorktree");function J(e,n={}){return!["done","dead","canceled"].includes(e.status)||e.pi_pid!==null||e.frozen_at!==null||S(e.node_id)||e.node_id===
|
|
59
|
-
AND NOT EXISTS (SELECT 1 FROM sessions s WHERE s.node_id = n.node_id) LIMIT 1`);for(const i of e)if(!(t.get(i)!==void 0||o.get(i)!==void 0))try{se().delete(i)}catch(s){if(!(s instanceof de))throw s}}r(
|
|
60
|
+
ORDER BY created DESC`).all(e).map(E)}r(Xn,"listResumableRoots");export*from"./tree.js";function Gn(e,n,t){return p(o=>{const i=d(o,"SELECT status, final_report, frozen_at FROM nodes WHERE node_id = ?").get(e);return i===void 0?{kind:"missing"}:n(i)?{kind:"terminal",status:i.status,final_report:i.final_report}:{kind:"ran",result:t()}})}r(Gn,"withFreshTerminalGuard");function S(e){return pe(e)}r(S,"hasUnreconciledManagedWorktree");function J(e,n={}){return!["done","dead","canceled"].includes(e.status)||e.pi_pid!==null||e.frozen_at!==null||S(e.node_id)||e.node_id===x()||I(e)?!1:n.pastTtl===!0||!["crashed","stranded",null].includes(e.terminal_reason??null)}r(J,"isCollectible");function L(e){if(e.length===0)return[];const n=e.map(()=>"?").join(",");return a().prepare(`SELECT DISTINCT session_id FROM sessions WHERE node_id IN (${n}) AND session_id IS NOT NULL`).all(...e).map(({session_id:o})=>o)}r(L,"nodeConversationIds");function g(e){const n=a(),t=d(n,"SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1"),o=d(n,`SELECT 1 FROM nodes n WHERE n.fork_source_file LIKE '%\\_' || ? || '.jsonl' ESCAPE '\\'
|
|
61
|
+
AND NOT EXISTS (SELECT 1 FROM sessions s WHERE s.node_id = n.node_id) LIMIT 1`);for(const i of e)if(!(t.get(i)!==void 0||o.get(i)!==void 0))try{se().delete(i)}catch(s){if(!(s instanceof de))throw s}}r(g,"deleteNodeConversations");function D(e,n){const t=M(e,n);return t===null||t.deleted_at!==null?!1:(ve(e,t),!0)}r(D,"deleteNodeObject");function v(e){return{nodeId:e,app:y(e)===null?null:we(e)}}r(v,"nodeFiles");function Y({nodeId:e,app:n}){n!==null&&W(U(n).nodes,he(e,n)),W(oe(C(e)),C(e))}r(Y,"removeNodeAppTrees");function V({nodeId:e}){return[Oe(e)]}r(V,"nodeOwnedDirs");function X({nodeId:e,app:n}){if(n!==null)try{ee(j(U(n).conversations,e))}catch(t){if(!["ENOENT","ENOTEMPTY"].includes(t.code??""))throw t}}r(X,"removeNodeConversationDir");function k(e){Y(e);for(const n of V(e))ne(n,{recursive:!0,force:!0});X(e)}r(k,"removeNodeFiles");function Qn(e){if(S(e))return{deleted:!1};const n=v(e),t=L([e]),o=p(i=>(D(i,e),{deleted:!0}));return g(t),k(n),o}r(Qn,"deleteNode");function Kn(e){const n=[...new Set(e)];if(n.length===0)return[];const t=n.filter(S);if(t.length>0)throw new Error(`refusing to delete nodes with unreconciled managed worktrees: ${t.join(", ")}`);const o=n.map(v),i=L(n),s=n.map(()=>"?").join(","),_=a().prepare(`SELECT node_id FROM nodes WHERE node_id IN (${s}) ORDER BY created`).all(...n).map(({node_id:u})=>u).filter(u=>p(f=>D(f,u)));g(i);for(const u of _){const f=o[n.indexOf(u)];A(f,()=>k(f))}return _}r(Kn,"deleteNodes");function Zn(e){const n=e.dryRun??!1,t=e.includeStale??!1,o=Date.now()-e.ttlDays*864e5,i=new Date(o).toISOString(),s=x()??"",l=a(),_=r(c=>K(c)<o,"quiet"),u=r(c=>({node_id:c.node_id,status:c.status,created:c.created}),"toPruned"),f=d(l,`SELECT * FROM node_rows
|
|
60
62
|
WHERE status IN ('dead', 'done', 'canceled') AND created < ?
|
|
61
|
-
ORDER BY created`).all(i).map(E).filter(
|
|
63
|
+
ORDER BY created`).all(i).map(E).filter(c=>J(c,{pastTtl:!0})&&_(c)&&!e.held(c.node_id)).map(u),O=t?d(l,`SELECT * FROM node_rows
|
|
62
64
|
WHERE status IN ('active', 'idle') AND created < ?
|
|
63
|
-
ORDER BY created`).all(i).map(E).filter(
|
|
65
|
+
ORDER BY created`).all(i).map(E).filter(c=>c.node_id!==s&&!Ne(c.pi_pid??null)&&(c.frozen_at??null)===null&&(c.respawn_not_before??null)===null&&c.intent!=="idle-release"&&!I(c)&&!ae(c.node_id)&&!S(c.node_id)&&_(c)&&!e.held(c.node_id)).map(u):[],R=G([...f,...O]);return n||R.length===0?{pruned:R,dryRun:n}:(Q(R,e.removeDirs),{pruned:R,dryRun:n})}r(Zn,"pruneNodes");function G(e){const n=a(),t=d(n,"SELECT node_id FROM nodes WHERE run_id = ? AND node_id != ?"),o=new Set(e.map(i=>i.node_id));return e.filter(i=>t.all(i.node_id,i.node_id).every(s=>o.has(s.node_id)))}r(G,"retainRunRootsWithMembers");function Q(e,n){if(e.length===0)return;const t=e.map(i=>v(i.node_id)),o=L(e.map(i=>i.node_id));for(const i of e)p(s=>D(s,i.node_id));if(g(o),n!==void 0){for(const i of t)A(i,()=>{Y(i),X(i)});n(t.flatMap(V));return}for(const i of t)A(i,()=>k(i))}r(Q,"deletePrunedNodes");function A(e,n){try{n()}catch(t){ie({level:"error",event:"canvas.node_files.remove_failed",error:t,fields:{node_id:e.nodeId}})}}r(A,"eachNodeBestEffort");function K(e){let n=0;for(const t of[e.created,e.launched_at,e.last_settled_at,e.finalized_at,e.outcome_at,e.last_activity_at]){if(t==null)continue;const o=Date.parse(t);Number.isFinite(o)&&o>n&&(n=o)}try{const t=te(j(Se(e.node_id),"telemetry.json"),{throwIfNoEntry:!1});t!==void 0&&t.mtimeMs>n&&(n=t.mtimeMs)}catch{}return n}r(K,"lastActivityMs");function et(e){const n=e.dryRun??!1,t=je(),o=t.filter(u=>J(u)&&!e.held(u.node_id)),i=t.length-o.length,s=Math.max(0,e.maxNodes-i),l=o.map(u=>({row:u,recency:K(u)})).sort((u,f)=>f.recency-u.recency).slice(s).map(({row:u})=>({node_id:u.node_id,status:u.status,created:u.created})),_=G(l);return n||_.length===0?{pruned:_,dryRun:n}:(Q(_),{pruned:_,dryRun:n})}r(et,"pruneToLimit");export{pn as abandonCreateRequest,rn as assertRunNotDeleting,In as browseNodes,fn as canonicalSessionFile,Yn as childrenOf,Bn as claimWarmSpare,Dn as clearFinalReportIfMatches,jn as clearFrozen,cn as completeCreateRequest,sn as createNode,Qn as deleteNode,Kn as deleteNodes,Rn as dueNodeDeadlines,dn as findCreateRequest,En as findNodeBySessionFile,gn as getFinalReport,y as getNode,mn as getRow,Nn as getRowByPane,Hn as graphNodes,J as isCollectible,zn as isWarmSpare,K as lastActivityMs,Mn as listNodeIdentities,je as listNodes,bn as listNodesByProfile,Un as listNodesForSupervision,Xn as listResumableRoots,Cn as listTerminalPlacementResidue,$n as listWarmSpares,v as nodeFiles,be as nodesForSupervisionSql,ln as pruneExpiredCreateRequests,Zn as pruneNodes,et as pruneToLimit,un as recordCreateRequestNode,kn as recordLaunch,Wn as recordRespawnAttempt,An as recordSettledTurn,Pn as registerWarmSpare,k as removeNodeFiles,an as reserveCreateRequest,_n as reserveCreateRequestNode,On as rewriteLocalNodeIdentity,Vn as rosterNodes,xn as setFrozen,hn as setIntent,yn as setNodeDeadline,vn as setPresence,Tn as setStatus,Sn as shiftProfileNodeDeadlines,We as terminalPlacementResidueSql,Ln as transitionStatusIntent,qn as unregisterWarmSpare,wn as updateNode,Fn as updateNodeTelemetry,Jn as warmSpareCount,Gn as withFreshTerminalGuard};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type BashJobPaths } from '../../bash-jobs.js';
|
|
2
|
+
/** True when jobs are started by the daemon's launcher rather than this process. */
|
|
3
|
+
export declare const BASH_JOBS_USE_LAUNCHER: boolean;
|
|
4
|
+
/** Start the supervisor for a job whose `cmd.sh` and `job.log` are already written.
|
|
5
|
+
* Resolves with the supervisor's process group and its launch-time identity, which
|
|
6
|
+
* lets a later cancellation reject a stale pid; it is not a containment guarantee. */
|
|
7
|
+
export declare function launchBashJobSupervisor(nodeId: string, paths: BashJobPaths, contextDir: string, cwd: string, env: NodeJS.ProcessEnv): Promise<{
|
|
8
|
+
pgid: number;
|
|
9
|
+
identity: string;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var u=Object.defineProperty;var m=(e,o)=>u(e,"name",{value:o,configurable:!0});import{spawn as S}from"node:child_process";import{writeFileSync as a}from"node:fs";import{BASH_JOB_SUPERVISOR_SCRIPT as l}from"../../bash-job-supervisor.js";import{readBashNiceness as h}from"../../bash-jobs.js";import{capturePidIdentities as j}from"../../canvas/pid.js";import{freshChildNodeExecPath as g}from"../branded-host.js";import{resolveAdmittedHostCommand as B}from"../spawn-env.js";import{startBrokerBashJob as I}from"./daemon-ops.js";const P=process.platform==="linux";async function J(e,o,f,n,d){const b=await g();if(P){const{pid:t}=await I(e,{job_id:o.jobId,cwd:n,env:d});return{pgid:t,identity:`launcher:${t}`}}const c=B("bash",d,n),r=S(c,["-c",l,"--",o.cmdSh,o.jobLog,o.jobExit,o.jobRun,o.jobDone,c,String(h(f)),b],{cwd:n,detached:!0,stdio:"ignore",env:d}),i=await new Promise((t,p)=>{r.once("error",p),r.pid!==void 0&&t(r.pid)});r.unref();const s=j([i])?.get(i);if(s===void 0){try{process.kill(-i,"SIGKILL")}catch{}throw new Error(`bash job ${o.jobId} started without a process identity`)}return a(o.jobPgid,String(i)),a(o.jobPgidIdentity,s),{pgid:i,identity:s}}m(J,"launchBashJobSupervisor");export{P as BASH_JOBS_USE_LAUNCHER,J as launchBashJobSupervisor};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var B=Object.defineProperty;var h=(e,o)=>B(e,"name",{value:o,configurable:!0});import{killBashJobObject as E,recordBashJobObject as J}from"../../../core/spaces/stop.js";import{readerFor as P}from"../reader.js";import{join as $}from"node:path";import{createFileBeneath as j,existsBeneath as m,unlinkBeneath as A}from"../../../core/spaces/open-beneath.js";import{launchAppProcess as q,appProcessSandboxed as O}from"../../../core/spaces/process.js";import{BASH_JOB_SUPERVISOR_SCRIPT as N}from"../../../core/bash-job-supervisor.js";import{resolveAdmittedHostCommand as L}from"../../../core/runtime/spawn-env.js";import{requireCurrentExecution as y}from"./broker-recovery.js";import{isSafeNodeId as T}from"../../../core/canvas/paths.js";import{contextDir as v}from"../../../core/canvas/node-agent-paths.js";import{getNode as g}from"../../../core/canvas/canvas.js";import{isBrokerLive as D}from"../../../core/runtime/model-swap.js";import{assertNotFinalized as R}from"../../../core/runtime/reopen.js";import{reviveNode as C}from"../../../core/runtime/revive.js";import{activeBackgroundBashJobs as z,bashJobPaths as k,isBashJobProcessGroupAlive as F,readJobPgid as M,readJobPgidIdentity as G,isSafeBashJobId as I,readBashNiceness as H,stopBackgroundBashJob as Z}from"../../../core/bash-jobs.js";import{ApiError as i}from"../../../api/index.js";import{notFound as U}from"../../../core/errors.js";import{freshChildNodeExecPath as V}from"../../../core/runtime/branded-host.js";function f(e){const o=e.params.id;if(o===void 0||!T(o))throw new i(400,"invalid_node_id",`invalid node id: ${JSON.stringify(o)}`);if(g(o)===null)throw U(`unknown node: ${o}`,{received:o});const d=e.params.jobId;if(d!==void 0){if(!I(d))throw new i(400,"invalid_job_id",`invalid background job id: ${JSON.stringify(d)}`);return{nodeId:o,jobId:d,dir:v(o)}}return{nodeId:o,dir:v(o)}}h(f,"requireTarget");async function X(e){const{nodeId:o,dir:t}=f(e);if(e.principal.class!=="broker"||e.principal.node!==o)throw new i(403,"permission_error","only this node\u2019s broker may start a job");const d=e.body;if(!d||typeof d!="object"||Array.isArray(d))throw new i(400,"invalid_request","job.start requires a JSON body");const a=d;y(o,a.expected_execution_id);const r=a.job_id,s=a.cwd,p=a.env;if(typeof r!="string"||!I(r)||typeof s!="string"||!p||typeof p!="object"||Array.isArray(p)||Object.entries(p).some(([c,b])=>!/^[A-Za-z_][A-Za-z0-9_]*$/.test(c)||typeof b!="string"))throw new i(400,"invalid_request","invalid job id, cwd or environment");if(!O())throw new i(400,"invalid_request","job.start is only used with the root launcher");const n=k(t,r);if(!(m(n.root,n.jobRun)||m(n.root,n.jobBg))||![n.cmdSh,n.jobLog].every(c=>m(n.root,c)))throw new i(404,"job_not_found","job was not prepared by this node");const w=$(n.dir,"job.starting");try{j(n.root,w,"")}catch(c){throw c.code==="EEXIST"?new i(409,"job_already_started","job was already started"):c}let l;try{const c=p,b=L("bash",c,s),_=g(o),S=await V();return l=await q({nodeId:o,app:_.grantee??"app:terminal",isolated:_.isolated,cwd:s,env:c,capture:!1,command:[b,"-c",N,"--",n.cmdSh,n.jobLog,n.jobExit,n.jobRun,n.jobDone,b,String(H(t)),S]}),j(n.root,n.jobPgid,String(l.pid),384),j(n.root,n.jobPgidIdentity,`launcher:${l.pid}`,384),{status:201,body:{pid:l.pid}}}catch(c){if(l!==void 0)try{await l.stop()}catch(b){throw new AggregateError([c,b],`job ${r} launched but could not be recorded or stopped`)}try{A(n.root,w)}catch(b){if(b.code!=="ENOENT")throw b}throw c}}h(X,"handleStart");function x(e){const{nodeId:o,dir:t}=f(e),d=Date.now();return{status:200,body:z(t).map(r=>{const s=M(t,r.jobId)??null;return{job_id:r.jobId,command:r.command,purpose:r.purpose,started_at:new Date(r.startedAtMs).toISOString(),elapsed_ms:Math.max(0,d-r.startedAtMs),pgid:s,pgid_alive:s!==null&&F(s,G(t,r.jobId))}})}}h(x,"handleList");async function K(e){const{nodeId:o,jobId:t,dir:d}=f(e);if(t===void 0)throw new i(400,"invalid_request","a job id is required");const a=e.query.get("foreground")==="1";if(a){if(e.principal.class!=="broker"||e.principal.node!==o)throw new i(403,"permission_error","only this node\u2019s broker may abort its foreground job");y(o,e.body?.expected_execution_id)}const r=await Z(d,t,a,o);if(r.kind==="missing")throw new i(404,"job_not_found",`no background job ${t} for ${o}`);if(r.kind==="retired")throw new i(404,"job_not_found",`background job ${t} for ${o} is no longer running`,{log_path:r.logPath});if(r.kind==="not-stoppable")throw new i(409,"job_not_stoppable",`job ${t} recorded no trusted process identity`,{log_path:r.logPath});if(a)return{status:200,body:{node_id:o,job_id:t,signaled:r.signaled}};R(o),E(t,n=>{const u=P(n,e);return u.class==="person"?u.person:u.class==="app"?u.app:u.node});const s=g(o);if(s!==null&&!D(s))try{await C(o,{resume:!0,capacity:"freeze"})}catch{}return{status:200,body:{node_id:o,job_id:t,signaled:r.signaled}}}h(K,"handleStop");function Q(e){const{nodeId:o,jobId:t,dir:d}=f(e);if(t===void 0)throw new i(400,"invalid_request","a job id is required");if(e.principal.class!=="broker"||e.principal.node!==o)throw new i(403,"permission_error","only this node\u2019s broker may record its job");const a=e.body,r=k(d,t);if(typeof a?.command!="string"||a.job_dir!==r.dir||a.log_path!==r.jobLog)throw new i(400,"invalid_request",`job.background requires command, job_dir ${r.dir} and log_path ${r.jobLog}`);if(!m(r.root,r.jobBg))throw new i(404,"job_not_found",`job ${t} of ${o} is not backgrounded`);const s=a.command;return{status:201,body:{id:J(o,t,{command:s,jobDir:r.dir,logPath:r.jobLog}).id}}}h(Q,"handleBackground");const go=[{method:"POST",pattern:"/v1/nodes/:id/jobs/:jobId/background",handler:Q},{method:"POST",pattern:"/v1/nodes/:id/jobs/start",handler:X},{method:"GET",pattern:"/v1/nodes/:id/jobs",handler:x},{method:"DELETE",pattern:"/v1/nodes/:id/jobs/:jobId",handler:K}];export{go as bashJobRoutes};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)}}
|
|
3
|
-
`}}
|
|
4
|
-
[crtr could not record job ${e.jobId} (${
|
|
5
|
-
[backgrounded as job ${e.jobId} after ${
|
|
6
|
-
`),
|
|
7
|
-
[${i} earlier output line${i===1?"":"s"} are in the job log only]`;return{content:[{type:"text",text:`${s}${
|
|
1
|
+
var ae=Object.defineProperty;var o=(t,n)=>ae(t,"name",{value:n,configurable:!0});import{envBashAutoBgDeadlineMs as ce,envBashValveMs as ue,envNodeId as de}from"../shared/env.js";import{closeSync as le,existsSync as j,mkdirSync as z,openSync as fe,readFileSync as K,readSync as he,rmSync as W,statSync as q,writeFileSync as S}from"node:fs";import{homedir as be}from"node:os";import{dirname as me}from"node:path";import{performance as J}from"node:perf_hooks";import{backgroundBashJob as X,bashJobPaths as pe,cancelBashJobProcessTree as ge,countJobLogLines as we,formatBashElapsed as Y,newBashJobId as ye,normalizeBashJobPurpose as je,tailJobLog as ve,writeJobDeadlineMs as Ee}from"../core/bash-jobs.js";import{MAX_BACKGROUND_BASH_LOG_BYTES as ct,BACKGROUND_BASH_LOG_TRUNCATION_MARKER as ut}from"../core/bash-job-supervisor.js";import{recordBackgroundedJob as _e,stopBrokerBashJob as Se}from"../core/runtime/broker/daemon-ops.js";import{BASH_JOBS_USE_LAUNCHER as Be,launchBashJobSupervisor as Ie}from"../core/runtime/broker/bash-job-launch.js";import{bashCommandRefusal as Le}from"../core/bash-guard.js";import{readConfig as ke}from"../core/config.js";import{PREVIEW_RESULT_PATH_ENV as Ae,previewResultPath as Oe}from"../core/preview-result-path.js";import{Type as B}from"typebox";import{createBashToolDefinition as Q}from"@earendil-works/pi-coding-agent";const xe=200;function Re(){return ue()}o(Re,"resolveValveMs");function Te(t){return pe(t,ye())}o(Te,"makeJobPaths");const I=new Set;let Z=!1;function ee(){for(const t of[...I])try{X(t),I.delete(t)}catch(n){process.stderr.write(`crtr: failed to background bash job ${t.jobId} during broker termination: ${n.message}
|
|
2
|
+
`)}}o(ee,"backgroundLiveJobsForTermination");function $e(){Z||(Z=!0,process.prependListener("SIGTERM",ee),process.platform!=="win32"&&process.prependListener("SIGHUP",ee))}o($e,"installTerminationHandoff");const Je=64*1024;function N(t,n,i){let s;try{s=q(t)}catch{return n}if(s.size<=n)return n;const c=fe(t,"r");try{for(;n<s.size;){const b=Math.min(Je,s.size-n),m=Buffer.allocUnsafe(b),a=he(c,m,0,b,n);if(a===0)break;i(m.subarray(0,a)),n+=a}return n}finally{le(c)}}o(N,"tailOnce");function v(t){try{W(t,{recursive:!0,force:!0})}catch{}}o(v,"cleanupJobDir");function Ne(t){try{const n=K(t,"utf8").trim(),i=Number(n);return Number.isFinite(i)?i:null}catch{return null}}o(Ne,"readExitCode");function Ce(t){try{if(q(t).isDirectory())return{cwd:t}}catch{}const n=be();return{cwd:n,warning:`[working directory no longer exists: ${t}; running from ${n}]
|
|
3
|
+
`}}o(Ce,"resolveExecutionCwd");function te(t,n,i=()=>null,s,c=()=>{}){return{exec:o(async(b,m,{onData:a,signal:f,timeout:p,env:L})=>new Promise((E,d)=>{const w=i();if(f?.aborted){d(new Error("aborted"));return}const k=Le(b,m);if(k!==null){d(new Error(k));return}const e=Te(n);z(e.dir,{recursive:!0}),S(e.cmdSh,b),S(e.jobLog,""),S(e.jobRun,""),w!==null&&S(e.jobPurpose,w);let l=0,g=!1,_,A,O,C=!1;const ne=Be,M=J.now(),oe=Re();let y;const re=o(()=>{y&&clearInterval(y),f&&f.removeEventListener("abort",H),I.delete(e)},"teardown"),h=o(r=>{g||(g=!0,re(),r())},"finish");let D=!1;function x(r=!1){g||D||(D=!0,y&&clearInterval(y),_e(t,e.jobId,{command:K(e.cmdSh,"utf8"),job_dir:e.dir,log_path:e.jobLog}).then(()=>P(r,""),u=>P(r,`
|
|
4
|
+
[crtr could not record job ${e.jobId} (${u.message}); it will NOT message you when it exits. Watch its log.]`)))}o(x,"finishBackground");function P(r,u){const se=Y(J.now()-M);let F="";if(r){const V=ce();Ee(e,Date.now()+V),F=`It will be CANCELED in ${Y(V)} unless you claim it: crtr node bash extend ${e.jobId} --for <duration> (or --keep to run to completion). `}const G=`
|
|
5
|
+
[backgrounded as job ${e.jobId} after ${se} in the foreground${r?" (auto)":""}. `+F+(u===""?"It will interrupt you with a message when it exits. ":"")+`Watch: tail -f ${e.jobLog} ; stop: crtr node bash kill ${e.jobId}]`;a(Buffer.from(G+u)),c({jobId:e.jobId,logPath:e.jobLog,notice:G+u}),h(()=>E({exitCode:0}))}o(P,"handOff");function U(){l=N(e.jobLog,l,a);const r=Ne(e.jobExit);h(()=>E({exitCode:r})),v(e.dir)}o(U,"finishForeground");function ie(r=!1){const u=X(e);if(u==="backgrounded"||u==="already-backgrounded"){x(r);return}if(u==="finished"){U();return}h(()=>d(new Error(`bash job ${e.jobId} disappeared before it could be backgrounded`))),v(e.dir)}o(ie,"requestBackground");function R(r){if(!(g||C)){if(l=N(e.jobLog,l,a),j(e.jobBg)){x();return}if(!j(e.jobExit)){if(_===void 0){O??=r;return}if(ne){C=!0,Se(t,e.jobId).then(()=>{h(()=>d(r)),v(e.dir)},u=>h(()=>d(u)));return}_!==void 0&&A!==void 0&&ge(_,A),h(()=>d(r)),v(e.dir)}}}o(R,"requestStop");function H(){R(new Error("aborted"))}o(H,"onAbort"),f&&f.addEventListener("abort",H,{once:!0});const T=Ce(m);T.warning&&a(Buffer.from(T.warning));let $=L??process.env;s!==void 0&&(z(me(s),{recursive:!0}),W(s,{force:!0}),$={...$,[Ae]:s}),Ie(t,e,n,T.cwd,$).then(r=>{_=r.pgid,A=r.identity,I.add(e),O!==void 0&&R(O)},r=>{h(()=>d(r)),v(e.dir)}),y=setInterval(()=>{if(l=N(e.jobLog,l,a),j(e.jobDone)){U();return}if(j(e.jobBg)){x();return}if(j(e.jobExit))return;const r=J.now()-M;if(p!==void 0&&p>0){r>=p*1e3&&R(new Error(`timeout:${p}`));return}r>=oe&&ie(!0)},xe)}),"exec")}}o(te,"createValveOperations");const Me=["One short clause naming what this step is FOR, in the words of the person you",'are working for \u2014 "Reading the Acme thread", not "Fetching emails" and never','"Running GMAIL_FETCH_EMAILS". A person reads this while the command runs; it',"is the only thing they see of this step. Under about eight words. Omit it for","steps that only touch your own scratch work."].join(" "),De=3;function Pe(t){const n=ve(t.logPath,De),i=Math.max(0,we(t.logPath)-n.length),s=n.length===0?"(no output yet)":n.join(`
|
|
6
|
+
`),c=i===0?"":`
|
|
7
|
+
[${i} earlier output line${i===1?"":"s"} are in the job log only]`;return{content:[{type:"text",text:`${s}${c}${t.notice}`}],details:void 0}}o(Pe,"handoffResult");function Ue(t){return{...t,parameters:B.Object({purpose:B.Optional(B.String({description:Me})),...t.parameters.properties})}}o(Ue,"withPurpose");const He=["A command that needs longer than one call \u2014 a test lane, a build, a soak \u2014 is started with","`crtr node bash run '<command>'`, which returns at once and wakes you with the exit code; the job is a canvas","object others can watch. Do not use `&`, `nohup`, `setsid`, or a cron that polls a log. A foreground command","past 5 minutes is moved to a job automatically; a `timeout` kills it and everything it started."].join(" ");function Fe(t){const{command:n,...i}=t.parameters.properties;return{...t,parameters:B.Object({command:{...n,description:`${n.description??""} ${He}`.trim()},...i})}}o(Fe,"withJobGuidance");function Ge(t,n,i,s){const c=Fe(Q(i,{operations:te(t,n)}));return{...s?Ue(c):c,execute:o(async(a,f,p,L,E)=>{const d=s?je(f.purpose):null;let w=null;const e=await Q(i,{operations:te(t,n,()=>d,Oe(a),g=>{w=g})}).execute(a,f,p,L,E),l=w;return l===null?e:Pe(l)},"execute")}}o(Ge,"createValveToolDefinition");function Ve(t){const n=de();if(n===void 0||n.trim()==="")return;const i=process.env.CRTR_CONTEXT_DIR;i===void 0||i.trim()===""||($e(),t.on("session_start",(s,c)=>{t.registerTool(Ge(n,i,c.cwd,ke("user").bash_tool_purpose))}))}o(Ve,"default");export{ut as BACKGROUND_BASH_LOG_TRUNCATION_MARKER,ct as MAX_BACKGROUND_BASH_LOG_BYTES,te as createValveOperations,Ge as createValveToolDefinition,Ve as default};
|
|
@@ -7,6 +7,12 @@ description: Behavior changes in @north-light/sdk that an app must act on, newes
|
|
|
7
7
|
|
|
8
8
|
Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
|
|
9
9
|
|
|
10
|
+
## 0.3.527
|
|
11
|
+
|
|
12
|
+
`POST /v1/nodes/:id/jobs/start` accepts a job prepared as already backgrounded. Additive: no request or response an existing app sends or reads changes.
|
|
13
|
+
|
|
14
|
+
- The route used to refuse a job directory without `job.run` (404 `job_not_found`). It now also accepts one with `job.bg`, which `crtr node bash run` writes before launching. A job directory with neither marker is still refused.
|
|
15
|
+
|
|
10
16
|
## 0.3.521
|
|
11
17
|
|
|
12
18
|
`GET /v1/status` names the runtime generation the daemon is serving. Additive: no request or response an existing app sends or reads changes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.528",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
"license": "UNLICENSED",
|
|
141
141
|
"dependencies": {
|
|
142
|
-
"@north-light/identity": "0.3.
|
|
142
|
+
"@north-light/identity": "0.3.528",
|
|
143
143
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
144
144
|
"@earendil-works/pi-ai": "1.1.0",
|
|
145
145
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.528",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.528",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@milkdown/preset-gfm": "^7.22.3",
|
|
25
25
|
"@milkdown/prose": "^7.22.3",
|
|
26
26
|
"@milkdown/utils": "^7.22.3",
|
|
27
|
-
"@north-light/identity": "0.3.
|
|
27
|
+
"@north-light/identity": "0.3.528",
|
|
28
28
|
"cron-parser": "^5.6.0",
|
|
29
29
|
"esbuild": "^0.27.7",
|
|
30
30
|
"jose": "^6.2.12",
|
|
@@ -12468,17 +12468,17 @@
|
|
|
12468
12468
|
},
|
|
12469
12469
|
"packages/crouter-api": {
|
|
12470
12470
|
"name": "@north-light/api",
|
|
12471
|
-
"version": "0.3.
|
|
12471
|
+
"version": "0.3.528",
|
|
12472
12472
|
"license": "UNLICENSED"
|
|
12473
12473
|
},
|
|
12474
12474
|
"packages/crouter-env-docker": {
|
|
12475
12475
|
"name": "@north-light/env-docker",
|
|
12476
|
-
"version": "0.3.
|
|
12476
|
+
"version": "0.3.528",
|
|
12477
12477
|
"license": "UNLICENSED"
|
|
12478
12478
|
},
|
|
12479
12479
|
"packages/crouter-identity": {
|
|
12480
12480
|
"name": "@north-light/identity",
|
|
12481
|
-
"version": "0.3.
|
|
12481
|
+
"version": "0.3.528",
|
|
12482
12482
|
"license": "UNLICENSED",
|
|
12483
12483
|
"dependencies": {
|
|
12484
12484
|
"jose": "^6.2.1"
|
|
@@ -12486,20 +12486,20 @@
|
|
|
12486
12486
|
},
|
|
12487
12487
|
"packages/crouter-plugin": {
|
|
12488
12488
|
"name": "@north-light/plugin",
|
|
12489
|
-
"version": "0.3.
|
|
12489
|
+
"version": "0.3.528",
|
|
12490
12490
|
"license": "UNLICENSED",
|
|
12491
12491
|
"dependencies": {
|
|
12492
|
-
"@north-light/api": "0.3.
|
|
12493
|
-
"@north-light/identity": "0.3.
|
|
12492
|
+
"@north-light/api": "0.3.528",
|
|
12493
|
+
"@north-light/identity": "0.3.528"
|
|
12494
12494
|
}
|
|
12495
12495
|
},
|
|
12496
12496
|
"packages/crouter-sdk": {
|
|
12497
12497
|
"name": "@north-light/sdk",
|
|
12498
|
-
"version": "0.3.
|
|
12498
|
+
"version": "0.3.528",
|
|
12499
12499
|
"license": "UNLICENSED",
|
|
12500
12500
|
"dependencies": {
|
|
12501
|
-
"@north-light/api": "0.3.
|
|
12502
|
-
"@north-light/identity": "0.3.
|
|
12501
|
+
"@north-light/api": "0.3.528",
|
|
12502
|
+
"@north-light/identity": "0.3.528",
|
|
12503
12503
|
"jose": "^6.2.1"
|
|
12504
12504
|
},
|
|
12505
12505
|
"bin": {
|