@north-light/cli 0.3.493 → 0.3.495

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
2
- export declare const CONTRACT_LEVEL = "1.14";
2
+ export declare const CONTRACT_LEVEL = "1.15";
3
3
  /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
4
4
  export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
5
5
  /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
@@ -1 +1 @@
1
- var c=Object.defineProperty;var e=(r,t)=>c(r,"name",{value:t,configurable:!0});const f="1.14",i="Crouter-Required-Contract",p="Crouter-Contract";function l(r){const t=/^(\d+)\.(\d+)$/.exec(r.trim());return t===null?null:{major:Number(t[1]),minor:Number(t[2])}}e(l,"parseContractLevel");function a(r){return r==null?[]:r.split(",").map(t=>l(t)).filter(t=>t!==null)}e(a,"parseServedContracts");function s(r,t){const n=l(r);return n===null?!1:(typeof t=="string"||t===null||t===void 0?a(t):t).some(u=>u.major===n.major&&u.minor>=n.minor)}e(s,"contractSatisfies");function C(r,t,n={}){if(r===void 0)return null;if(l(r)===null)return{status:400,code:"invalid_request",param:i,message:`${i} must be a contract level like 1.0.`};if(s(r,t))return null;const o=r.trim();return{status:426,code:"runtime_version_unsupported",message:t===null||a(t).length===0?`The runtime did not name its contract level; the caller requires contract ${o} or later. Roll the runtime forward.`:`The runtime serves contract ${t}; the caller requires contract ${o} or later within the same major. Roll the runtime forward, or pin an SDK whose crouterContract the runtime serves.`,details:{sdk_version:(n.legacyMinimum??"").trim(),runtime_version:n.runtimeVersion??null,required_contract:o,runtime_contract:t}}}e(C,"contractRefusal");export{p as CONTRACT_HEADER,f as CONTRACT_LEVEL,i as REQUIRED_CONTRACT_HEADER,C as contractRefusal,s as contractSatisfies,l as parseContractLevel,a as parseServedContracts};
1
+ var c=Object.defineProperty;var e=(r,t)=>c(r,"name",{value:t,configurable:!0});const f="1.15",i="Crouter-Required-Contract",p="Crouter-Contract";function l(r){const t=/^(\d+)\.(\d+)$/.exec(r.trim());return t===null?null:{major:Number(t[1]),minor:Number(t[2])}}e(l,"parseContractLevel");function a(r){return r==null?[]:r.split(",").map(t=>l(t)).filter(t=>t!==null)}e(a,"parseServedContracts");function s(r,t){const n=l(r);return n===null?!1:(typeof t=="string"||t===null||t===void 0?a(t):t).some(u=>u.major===n.major&&u.minor>=n.minor)}e(s,"contractSatisfies");function C(r,t,n={}){if(r===void 0)return null;if(l(r)===null)return{status:400,code:"invalid_request",param:i,message:`${i} must be a contract level like 1.0.`};if(s(r,t))return null;const o=r.trim();return{status:426,code:"runtime_version_unsupported",message:t===null||a(t).length===0?`The runtime did not name its contract level; the caller requires contract ${o} or later. Roll the runtime forward.`:`The runtime serves contract ${t}; the caller requires contract ${o} or later within the same major. Roll the runtime forward, or pin an SDK whose crouterContract the runtime serves.`,details:{sdk_version:(n.legacyMinimum??"").trim(),runtime_version:n.runtimeVersion??null,required_contract:o,runtime_contract:t}}}e(C,"contractRefusal");export{p as CONTRACT_HEADER,f as CONTRACT_LEVEL,i as REQUIRED_CONTRACT_HEADER,C as contractRefusal,s as contractSatisfies,l as parseContractLevel,a as parseServedContracts};
@@ -5,9 +5,21 @@ export interface AppLimitsDTO {
5
5
  disk_bytes: number | null;
6
6
  memory_source: 'default' | 'override';
7
7
  disk_source: 'default' | 'override';
8
- /** Bytes the app's project quota counts now, or null when disk quota is off. */
8
+ /**
9
+ * Bytes under the app's folder: the project quota's count when disk quota is on, otherwise a
10
+ * directory walk (up to a minute old). Null when the host has no launcher, the walk has not finished or failed, or the app has no folder yet.
11
+ */
9
12
  disk_used_bytes: number | null;
10
13
  }
14
+ /** The runtime's data volume and what fills it. Absent when the host has no launcher. */
15
+ export interface DataDiskDTO {
16
+ total_bytes: number;
17
+ used_bytes: number;
18
+ /** Bytes under the person's /user folder, or null when it could not be measured. */
19
+ user_bytes: number | null;
20
+ /** Bytes under the runtime directory (canvas, logs, limits state), or null when it could not be measured. */
21
+ runtime_bytes: number | null;
22
+ }
11
23
  export interface LimitsDTO {
12
24
  /** False when this host cannot enforce the limit; `reason` says why. Caps are still recorded. */
13
25
  memory_enforced: boolean;
@@ -15,6 +27,8 @@ export interface LimitsDTO {
15
27
  disk_enforced: boolean;
16
28
  disk_reason: string | null;
17
29
  apps: AppLimitsDTO[];
30
+ /** Present when the host measures its data volume. */
31
+ data_disk?: DataDiskDTO;
18
32
  }
19
33
  /** `PUT /v1/limits/apps/{app}`: omitted fields stay as they were. A value is a byte count or `unlimited`. */
20
34
  export interface SetAppLimitsRequest {
@@ -9,7 +9,15 @@ 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.493
12
+ ## 0.3.494
13
+
14
+ `GET /v1/limits` reports how much of the data disk each app uses on a host without disk quota, and the disk as a whole. Additive: no request or response an existing app sends or reads changes.
15
+
16
+ ### `disk_used_bytes` and `data_disk` on `LimitsDTO` (additive)
17
+
18
+ - `AppLimitsDTO.disk_used_bytes` was `null` whenever the host could not enforce project quota (every hosted runtime on Blaxel). It now holds the bytes under the app's folder, measured by a directory walk at most a minute old. It stays `null` when the host has no launcher, the first walk has not finished (a read waits at most 5 s for it), or the walk failed.
19
+ - `LimitsDTO.data_disk` is new and optional: `{total_bytes, used_bytes, user_bytes, runtime_bytes}` for the data volume. `crtr sys limits show` prints it.
20
+
13
21
 
14
22
  An app can act on a person's hosted runtime with no sign-in of theirs, and `@north-light/plugin` can declare canvas object kinds. Everything below is additive: no request or response an existing app sends or reads changes.
15
23
 
@@ -1,2 +1,2 @@
1
- var G=Object.defineProperty;var s=(e,n)=>G(e,"name",{value:n,configurable:!0});import{envNodeId as V}from"../../shared/env.js";import{defineLeaf as X}from"../../core/command.js";import{InputError as o}from"../../core/io.js";import{requireScope as Q}from"../../core/errors.js";import{mark as N}from"../../core/timing.js";import{OUTPUT_SCHEMA_CONSTRAINTS as Z,OUTPUT_SCHEMA_TRANSPORT as ee,parseOutputSchemaValue as te}from"../../core/runtime/structured-output.js";import{MODEL_SPEC_FORMS as oe,MODEL_SPEC_NEXT as ne}from"../../core/runtime/model-selection.js";import{parseDurationMs as re}from"../../core/wake.js";import{kindsStateBlock as ie,profilesStateBlock as ae,resolveProfileViaDaemon as se}from"../profile/daemon-profiles.js";import{cliClient as H,getNodeOrNull as de,rethrowAsCliError as $}from"../api-client.js";import{inTmux as z}from"../../core/runtime/placement-tmux.js";import{openSpawnViewer as ce}from"../surface/node/placement.js";import{randomUUID as le}from"node:crypto";import{resolve as ue}from"node:path";import{isDaemonTransportApiError as W}from"../../api/errors.js";import{ExitCode as he}from"../../types.js";const fe=1e5,B='--output-schema writes the accepted result to the node\u2019s context/result.json and pushes it as the node\u2019s final report; a node that cannot honestly satisfy the schema declines with `crtr push result --decline "<reason>" --code <token>`, which finishes it with a failure outcome (reason `declined`) whose `declined` field carries { reason, code, retryable } and no result. Name the code tokens you accept in the prompt; crouter carries the code verbatim and you validate it.',K=["A resident is told never to push a final, so the spawner gets no report wake when the work is done.","If it stops mid-task, nothing prompts it to continue.","After 15 unattended minutes it is concluded with only a deferred update, which does not wake the spawner."],J="To re-task a terminal child after its final, use `crtr node message send --reopen`; to make an existing node resident later, use `crtr node config --lifecycle resident`.",T="Do not wait, poll, or duplicate this child's assignment in your own context \u2014 there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn \u2014 the wake brings you back.";async function me(e){if(e===void 0||e==="")return T;const n=await de(e);if(n===null||n.mode!=="orchestrator")return T;const t=n.context_tokens??null;return t===null||t<fe?T:`Child spawned \u2014 you're auto-subscribed, so its finish wakes you on its own; never wait, poll, or duplicate the child's assignment in your own context. But you're an orchestrator already carrying ~${Math.round(t/1e3)}k of context: rather than hold this window open for the result, bring your document \`roadmap\` current (\`crtr doc edit roadmap\`) and \`crtr node yield\` now. Yielding ends this turn, and the child's completion then revives you fresh against your roadmap \u2014 let that clean revive absorb the result instead of this bloated context.`}s(me,"childFollowUp");function pe(e,n){throw W(e)||$(e),new o({error:"node_create_effect_unknown",message:`Node creation did not receive an acknowledgement after automatic recovery with request key ${n}; its effect is unknown.`,next:"Do not create a replacement node until the daemon is reachable and this request can be recovered."},he.NETWORK)}s(pe,"createTransportFailure");function we(){return[{kind:"stdin",name:"prompt",required:!1,constraint:"First user message."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Persona kind. Defaults to the profile's default kind; `general` when the profile has none. The <kinds> list below names every top-level installable kind."},{kind:"flag",name:"mode",type:"enum",choices:["base","orchestrator"],required:!1,default:"base",constraint:"Persona mode. base for one hands-on unit; orchestrator when the unit itself needs decomposition across children. When you hold several parallel units of one kind, spawn ONE orchestrator child of that kind to own the fan-out rather than spawning the units yourself \u2014 unless you are already an orchestrator of that kind, whose job is exactly that fan-out."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Pin the node to this directory. Without --worktree, defaults to the spawner\u2019s directory for a managed child and the invocation directory for a root. With --worktree, selects the repository used for creation, defaulting to the invocation directory; the child runs in the new checkout."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Display name."},{kind:"flag",name:"parent",type:"string",required:!1,constraint:"Parent node id. Defaults to the calling node."},{kind:"flag",name:"node-id",type:"string",required:!1,constraint:"Exact id for the new node; lowercase letters, digits, and hyphens only, at most 128 characters. Rejected if already present."},{kind:"flag",name:"root",type:"bool",required:!1,constraint:"Spawn an independent root with no parent or subscription. Mutually exclusive with --worktree."},{kind:"flag",name:"resident",type:"bool",required:!1,constraint:"Birth the node as resident, a conversation partner for a person rather than a worker. Omit unless the user explicitly asks for a node they can talk with back and forth.",focusedHelp:{whenToUse:"Pass it ONLY when the user explicitly asks for a node they can talk with back and forth. Never infer it from task size, from expecting the user to look at the node, or from wanting to follow up later. "+J,value:"Boolean. Roots default to resident, so with --root it changes nothing. Managed children default to terminal; for a managed child the command first refuses, explains the cost, and says how to confirm.",effects:[...K,"It ends by going dormant or by being closed."]}},{kind:"flag",name:"terminal",type:"bool",required:!1,constraint:"Birth the node as terminal: it finalizes when its work is done and leaves nothing wakeable behind. Only meaningful with --root, whose default is resident; managed children are already terminal. Pass it for bounded programmatic work no one will open a viewer on. Mutually exclusive with --resident."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,hidden:!0,constraint:"Confirm a resident managed child (--resident without --root) after the user explicitly asked for one."},{kind:"flag",name:"worktree",type:"string",required:!1,constraint:"Existing local base branch for this child\u2019s managed worktree. When invoking from a managed worktree, also pass --cwd pointing at the repository\u2019s primary checkout; keep --worktree set to the desired parent branch. Mutually exclusive with --root."},{kind:"flag",name:"fork-from",type:"string",required:!1,constraint:"Fork the new node from an existing node id, absolute session path, or partial pi session uuid."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Durable model override. Omit unless the task or governing guidance requires a specific model.",focusedHelp:{whenToUse:"Use only when the user, task, or governing agent guidance already requires a specific provider, model, capability tier, or family. Omit it for routine delegation: the selected persona kind and profile own normal model selection, and review personas may enforce a quality floor.",value:`Accepted forms: ${oe}. Concrete provider/id requests must be registered. Portable provider/tier, bare-tier, and family-alias requests resolve through the configured model ladders.`,effects:["The resolved concrete model is stored on the node and in its launch recipe.","A concrete provider/id runs exactly that model on every launch and revive. A portable provider/tier, bare tier, or family alias keeps its tier: when that model's provider is rate-limited or rejects its credential, the node moves to the next model in its route: another provider's model at the same tier first, then the configured fallback tier (strong by default).","A persona quality floor may raise a weaker request before its resolved model is stored."]}},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile override. Use only when the task needs different project purview, owned documents, or environment; otherwise omit for normal inheritance or selection.",focusedHelp:{whenToUse:"Use only when the work must run under a different profile because it needs that profile\u2019s project purview, owned documents, stored environment, or defaults. Do not use it merely to change the working directory, persona kind, model, or graph ownership; those have separate parameters.",value:"Pass a profile id or name from the <profiles> list below; a close-enough name resolves to the nearest profile and the choice is reported on stderr. Omit it inside a node to inherit the caller\u2019s profile; outside a node, omission runs normal cwd-based profile selection.",effects:["The selected profile id is stored on the node and supplies its profile-scoped config, documents, environment, and injected bearings.","It does not change the parent or subscription; --cwd separately pins the working directory."]}},{kind:"flag",name:"situational-context",type:"string",required:!1,constraint:"Hidden ambient context appended to injected bearings, never visible chat."},{kind:"flag",name:"no-kickoff",type:"bool",required:!1,hidden:!0,constraint:"With --root, start the broker and session idle without a first message or model turn. Mutually exclusive with a prompt and --mode orchestrator."},{kind:"flag",name:"scopes",type:"string",required:!1,constraint:"Comma-separated scope allow-list for the node and every descendant (they inherit it and cannot widen it), e.g. `crtr:llm,crtr:act,device:shell,device:messages_read`. A provider-group call (`crtr device messages send` needs `device:messages_send`) is refused with scope_missing before any request leaves the machine when the group is absent. Must be a subset of what you hold; `crtr:act` requires `crtr:llm`. Enforced on the broker route (the node's own `crtr` calls); on an unsandboxed host a same-UID process can bypass it by calling the daemon socket directly (crouter#146), so it guards against a mistaken call, not a hostile node. Keep the list short: it travels in a request header on every device call (crouter#147)."},{kind:"flag",name:"deadline",type:"string",required:!1,constraint:"Positive, representable wall-clock duration from spawn. Grammar: one or more positive integer-unit parts; units: s, m, h, d; adjacent parts are summed. Omit for no deadline."},{kind:"flag",name:"outcome-action",type:"string",required:!1,constraint:"Declared human-action name that receives the node outcome. Grammar: 1\u2013128 ASCII letters, digits, dots, underscores, or hyphens; starts with a letter or digit. Omit when no external delivery target is needed."},{kind:"flag",name:"outcome-payload",type:"string",required:!1,constraint:"JSON value, serialized at most 64 KiB, frozen and echoed to --outcome-action. Requires --outcome-action."},{kind:"flag",name:"output-schema",type:"string",required:!1,constraint:"Replace prose completion with a schema-constrained final result. Use only when its consumer requires fixed fields.",focusedHelp:{whenToUse:"Use only when the node\u2019s consumer needs a machine-checkable final object with fixed fields. Omit it for ordinary delegation or when a prose report is enough; structured output replaces the completion protocol, and an over-constrained schema can force retries.",value:`Pass the JSON Schema the result must satisfy. ${ee} ${Z}`,effects:["The node must answer with `crtr push result` and cannot finish until the result is accepted; a plain `crtr push final` is blocked while the request is pending.",B,"The managed parent wakes through its existing subscription when the final report arrives; an independent root has no subscriber to wake.","An accepted result, or a decline, removes the request and terminates the node."]}}]}s(we,"nodeNewParams");function ke(){return[{name:"node_id",type:"string",required:!0,constraint:"New node id."},{name:"name",type:"string",required:!0,constraint:"Display name."},{name:"frozen_at",type:"string",required:!1,constraint:"Present when a managed child exists but capacity deferred its broker start."},...z()?[{name:"window",type:"string",required:!1,constraint:"tmux window id when a viewer opens."},{name:"session",type:"string",required:!1,constraint:"tmux session when a viewer opens."}]:[],{name:"follow_up",type:"string",required:!0,constraint:"Managed child: continue unrelated work or stop; its final report wakes you. Root: hand it off; no report wakes you."}]}s(ke,"nodeNewOutput");function ye(){return["Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.","A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.","--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch.",B,"--deadline persists a wall-clock deadline from spawn; expiry cancels the node and records a failure outcome with reason deadline_exceeded.","--outcome-action registers one resolved local human-action target. When the node settles, crouter retries delivery of its outcome document until the target accepts it or permanently rejects it; --outcome-payload is frozen and echoed in that document.",...z()?["--root opens a viewer for the new node in the current tmux session."]:[]]}s(ye,"nodeNewEffects");async function ge(e){Q("crtr:act","crtr node new");const n=e.prompt??"",t=e.noKickoff===!0;if(!t&&n.trim()==="")throw new o({error:"empty_prompt",message:"a prompt is required (stdin or positional)",next:"Pipe a task on stdin or pass it as an argument, or pass --no-kickoff to spawn an idle node with none."});if(t&&n.trim()!=="")throw new o({error:"no_kickoff_with_prompt",message:"--no-kickoff cannot be combined with a prompt",field:"no-kickoff",next:"Drop --no-kickoff, or drop the prompt."});const l=e.kind,O=e.mode??"base",u=e.cwd?.trim(),h=u!==void 0&&u!==""?ue(u):void 0,S=e.name,Y=e.parent,i=e.root===!0,f=e.resident===!0,P=e.terminal===!0;if(f&&P)throw new o({error:"lifecycle_conflict",message:"--resident and --terminal cannot both be passed",field:"terminal",next:"Pass the one birth lifecycle you want, or neither to take the default (resident for --root, terminal for a managed child)."});if(f&&!i&&e.force!==!0)throw new o({error:"resident_child_requires_force",message:`--resident on a managed child makes it a conversation partner for a person, not a worker that reports back. ${K.join(" ")}`,field:"resident",next:`If the user did not explicitly ask for a node they can talk with back and forth, drop --resident; a terminal child's final report wakes you. ${J} If the user did explicitly ask for one, re-run with --force.`});const m=e.worktree;if(m!==void 0&&m.trim()==="")throw new o({error:"worktree_base_required",message:"--worktree requires a base branch",field:"worktree",next:"Pass an existing local branch that landing can fast-forward."});const p=m?.trim(),E=e.forkFrom,w=e.model?.trim();if(w==="")throw new o({error:"empty_spec",message:"a model spec is required",field:"model",next:`Run \`crtr node new --model -h\` and read the focused contract. ${ne}`});const D=e.profile,C=D===void 0?void 0:(await se(D)).id,k=e.situationalContext?.trim(),A=k!==void 0&&k!==""?k:void 0,U=te(e.outputSchema),I=U!==null?JSON.stringify(U):null,d=e.deadline;if(d!==void 0){const r=re(d),q=r===null?Number.NaN:new Date(Date.now()+r).getTime();if(r===null||!Number.isFinite(r)||r<=0||!Number.isFinite(q))throw new o({error:"invalid_deadline",message:"--deadline must be a positive representable duration",field:"deadline",next:"Pass positive integer-unit parts using s, m, h, or d."})}const y=e.outcomeAction,g=e.outcomePayload;if(g!==void 0&&y===void 0)throw new o({error:"outcome_payload_requires_action",message:"--outcome-payload requires --outcome-action",field:"outcome-payload",next:"Pass the declared delivery action with --outcome-action."});let v;if(g!==void 0)try{v=JSON.parse(g)}catch{throw new o({error:"invalid_outcome_payload",message:"--outcome-payload must be valid JSON",field:"outcome-payload",next:"Pass one valid JSON value."})}const R=e.scopes,c=R===void 0?void 0:R.split(",").map(r=>r.trim()).filter(r=>r!=="");if(c!==void 0&&c.length===0)throw new o({error:"empty_scopes",message:"--scopes names no scope",field:"scopes",next:"Pass a comma-separated list such as crtr:llm,crtr:act, or omit --scopes to inherit."});const b=e.nodeId?.trim();if(i&&p!==void 0)throw new o({error:"worktree_root_conflict",message:"--worktree is mutually exclusive with --root",field:"worktree",next:"Drop either --root or --worktree."});if(t&&!i)throw new o({error:"no_kickoff_requires_root",message:"--no-kickoff is only valid with --root",field:"no-kickoff",next:"Add --root, or drop --no-kickoff and provide a prompt."});if(t&&O==="orchestrator")throw new o({error:"no_kickoff_orchestrator_unsupported",message:"--no-kickoff cannot be combined with --mode orchestrator",field:"no-kickoff",next:"Drop --no-kickoff and provide a prompt, or spawn in base mode."});const _=V(),M=Y??_;N("node_new.create_start",{root:i});const j=le(),L={...l!==void 0?{kind:l}:{},mode:O,cwd:process.cwd(),...h!==void 0&&h!==""?{pin_cwd:h}:{},...S!==void 0?{name:S}:{},prompt:n,...i?{root:!0}:{parent:M??null},..._!==void 0?{creator:_}:{},...f?{root_lifecycle:"resident"}:{},...P?{root_lifecycle:"terminal"}:{},...p!==void 0?{worktree:p}:{},...E!==void 0?{fork_from:E}:{},...w!==void 0?{model:w}:{},...C!==void 0?{profile:C}:{},...A!==void 0?{situational_context:A}:{},...t?{no_kickoff:!0}:{},...I!==null?{output_schema:I}:{},...c!==void 0?{scopes:c}:{},...d!==void 0?{deadline:d}:{},...y!==void 0?{outcome_delivery:{action:y,...v===void 0?{}:{payload:v}}}:{},...b!==void 0&&b!==""?{node_id:b}:{},...i?{prefer_warm:!0}:{},request_key:j};let a;try{a=await H().createNode(L)}catch(r){W(r)||$(r);try{a=await H().createNode(L)}catch(q){pe(q,j)}}N("node_new.node_created",{node:a.node_id});let x={};i&&(x=ce(a.node_id,a.cwd,a.name)),i&&N("node_new.viewer_window_opened");const F=!i&&a.frozen_at!==null;return{node_id:a.node_id,name:a.name,...F?{frozen_at:a.frozen_at}:{},window:x.window,session:x.session,follow_up:i?t?"Independent root spawned idle. No kickoff turn ran; hand it off or attach and prompt it. No report wakes you.":"Independent root spawned. You are not subscribed, so its finish does not wake you; hand it off and move on.":F?"Child row exists, but no broker started because capacity is full. It is frozen and will launch when a broker slot frees; its final report wakes you then.":await me(M)}}s(ge,"runNodeCreation");const je=X({name:"new",description:"create and start a node immediately",whenToUse:"a unit of work is genuinely separable from what you are doing: a bounded subtask you would otherwise grind out inline, several independent units worth running in parallel (usually via one orchestrator child of the matching kind rather than a flat fan-out from you), a scouting pass over unfamiliar code, a change that wants its own git worktree, or an answer you need in a fixed shape rather than as prose",tier:"important",help:{name:"node new",summary:"create a node immediately and start its detached broker",guide:"The prompt is the child\u2019s entire brief: it wakes with this prompt, its persona, and ambient bearings \u2014 none of your conversation carries over. State the outcome and constraints. Pass an implementation plan only when it is already settled or necessary to preserve an approved decision; otherwise, let the child investigate and choose how. Give the reason along with the request: the larger goal this serves and what the child's output enables, so the child resolves ambiguity toward your intent instead of guessing. Point at every document the task depends on (a spec, an explore map, an earlier child's findings) as `[[<node-id>/<name>]]`, or `[[name]]` for a repo, profile or user document; the child reads each with `crtr canvas read` and receives nothing the prompt does not point at. Name every file or directory as an absolute host path, never `~`-relative: the child's `~` is its own HOME, not yours, and a hook cwd outside its mounts (e.g. `/tmp`) is refused. When the report must carry something specific\u2014findings inline, or pointers to documents it wrote\u2014say so in the prompt.",params:we(),output:ke(),outputKind:"object",effects:ye()},helpState:{leaf:ie,flags:{profile:ae}},run:s(e=>ge(e),"run"),render:s(e=>{const n=e.window?` in window ${e.window}`:"",t=[`Spawned "${e.name}" (${e.node_id})${n}.`];return e.session&&t.push(`- session: ${e.session}`),t.push("",String(e.follow_up)),t.join(`
1
+ var G=Object.defineProperty;var s=(e,n)=>G(e,"name",{value:n,configurable:!0});import{envNodeId as V}from"../../shared/env.js";import{defineLeaf as X}from"../../core/command.js";import{InputError as o}from"../../core/io.js";import{requireScope as Q}from"../../core/errors.js";import{mark as N}from"../../core/timing.js";import{OUTPUT_SCHEMA_CONSTRAINTS as Z,OUTPUT_SCHEMA_TRANSPORT as ee,parseOutputSchemaValue as te}from"../../core/runtime/structured-output.js";import{MODEL_SPEC_FORMS as oe,MODEL_SPEC_NEXT as ne}from"../../core/runtime/model-selection.js";import{parseDurationMs as re}from"../../core/wake.js";import{kindsStateBlock as ie,profilesStateBlock as ae,resolveProfileViaDaemon as se}from"../profile/daemon-profiles.js";import{cliClient as H,getNodeOrNull as de,rethrowAsCliError as $}from"../api-client.js";import{inTmux as z}from"../../core/runtime/placement-tmux.js";import{openSpawnViewer as ce}from"../surface/node/placement.js";import{randomUUID as le}from"node:crypto";import{resolve as ue}from"node:path";import{isDaemonTransportApiError as W}from"../../api/errors.js";import{ExitCode as he}from"../../types.js";const fe=1e5,B='--output-schema writes the accepted result to the node\u2019s context/result.json and pushes it as the node\u2019s final report; a node that cannot honestly satisfy the schema declines with `crtr push result --decline "<reason>" --code <token>`, which finishes it with a failure outcome (reason `declined`) whose `declined` field carries { reason, code, retryable } and no result. Name the code tokens you accept in the prompt; crouter carries the code verbatim and you validate it.',K=["A resident is told never to push a final, so the spawner gets no report wake when the work is done.","If it stops mid-task, nothing prompts it to continue.","After 15 unattended minutes it is concluded with only a deferred update, which does not wake the spawner."],J="To re-task a terminal child after its final, use `crtr node message send --reopen`; to make an existing node resident later, use `crtr node config --lifecycle resident`.",T="Do not wait, poll, or duplicate this child's assignment in your own context \u2014 there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn \u2014 the wake brings you back.";async function me(e){if(e===void 0||e==="")return T;const n=await de(e);if(n===null||n.mode!=="orchestrator")return T;const t=n.context_tokens??null;return t===null||t<fe?T:`Child spawned \u2014 you're auto-subscribed, so its finish wakes you on its own; never wait, poll, or duplicate the child's assignment in your own context. But you're an orchestrator already carrying ~${Math.round(t/1e3)}k of context: rather than hold this window open for the result, bring your document \`roadmap\` current (\`crtr doc edit roadmap\`) and \`crtr node yield\` now. Yielding ends this turn, and the child's completion then revives you fresh against your roadmap \u2014 let that clean revive absorb the result instead of this bloated context.`}s(me,"childFollowUp");function pe(e,n){throw W(e)||$(e),new o({error:"node_create_effect_unknown",message:`Node creation did not receive an acknowledgement after automatic recovery with request key ${n}; its effect is unknown.`,next:"Do not create a replacement node until the daemon is reachable and this request can be recovered."},he.NETWORK)}s(pe,"createTransportFailure");function we(){return[{kind:"stdin",name:"prompt",required:!1,constraint:"First user message."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Persona kind. Defaults to the profile's default kind; `general` when the profile has none. The <kinds> list below names every top-level installable kind."},{kind:"flag",name:"mode",type:"enum",choices:["base","orchestrator"],required:!1,default:"base",constraint:"Persona mode. base for one hands-on unit; orchestrator when the unit itself needs decomposition across children. When you hold several parallel units of one kind, spawn ONE orchestrator child of that kind to own the fan-out rather than spawning the units yourself \u2014 unless you are already an orchestrator of that kind, whose job is exactly that fan-out."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Pin the node to this directory. Without --worktree, defaults to the spawner\u2019s directory for a managed child and the invocation directory for a root. With --worktree, selects the repository used for creation, defaulting to the invocation directory; the child runs in the new checkout."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Display name."},{kind:"flag",name:"parent",type:"string",required:!1,constraint:"Parent node id. Defaults to the calling node."},{kind:"flag",name:"node-id",type:"string",required:!1,constraint:"Exact id for the new node; lowercase letters, digits, and hyphens only, at most 128 characters. Rejected if already present."},{kind:"flag",name:"root",type:"bool",required:!1,constraint:"Spawn an independent root with no parent or subscription. Mutually exclusive with --worktree."},{kind:"flag",name:"resident",type:"bool",required:!1,constraint:"Birth the node as resident, a conversation partner for a person rather than a worker. Omit unless the user explicitly asks for a node they can talk with back and forth.",focusedHelp:{whenToUse:"Pass it ONLY when the user explicitly asks for a node they can talk with back and forth. Never infer it from task size, from expecting the user to look at the node, or from wanting to follow up later. "+J,value:"Boolean. Roots default to resident, so with --root it changes nothing. Managed children default to terminal; for a managed child the command first refuses, explains the cost, and says how to confirm.",effects:[...K,"It ends by going dormant or by being closed."]}},{kind:"flag",name:"terminal",type:"bool",required:!1,constraint:"Birth the node as terminal: it finalizes when its work is done and leaves nothing wakeable behind. Only meaningful with --root, whose default is resident; managed children are already terminal. Pass it for bounded programmatic work no one will open a viewer on. Mutually exclusive with --resident."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,hidden:!0,constraint:"Confirm a resident managed child (--resident without --root) after the user explicitly asked for one."},{kind:"flag",name:"worktree",type:"string",required:!1,constraint:"Existing local base branch for this child\u2019s managed worktree. When invoking from a managed worktree, also pass --cwd pointing at the repository\u2019s primary checkout; keep --worktree set to the desired parent branch. Mutually exclusive with --root."},{kind:"flag",name:"fork-from",type:"string",required:!1,constraint:"Fork the new node from an existing node id, absolute session path, or partial pi session uuid."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Durable model override. Omit unless the task or governing guidance requires a specific model.",focusedHelp:{whenToUse:"Use only when the user, task, or governing agent guidance already requires a specific provider, model, capability tier, or family. Omit it for routine delegation: the selected persona kind and profile own normal model selection, and review personas may enforce a quality floor.",value:`Accepted forms: ${oe}. Concrete provider/id requests must be registered. Portable provider/tier, bare-tier, and family-alias requests resolve through the configured model ladders.`,effects:["The resolved concrete model is stored on the node and in its launch recipe.","A concrete provider/id runs exactly that model on every launch and revive. A portable provider/tier, bare tier, or family alias keeps its tier: when that model's provider is rate-limited or rejects its credential, the node moves to the next model in its route: another provider's model at the same tier first, then the configured fallback tier (strong by default).","A persona quality floor may raise a weaker request before its resolved model is stored."]}},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile override. Use only when the task needs different project purview, owned documents, or environment; otherwise omit for normal inheritance or selection.",focusedHelp:{whenToUse:"Use only when the work must run under a different profile because it needs that profile\u2019s project purview, owned documents, stored environment, or defaults. Do not use it merely to change the working directory, persona kind, model, or graph ownership; those have separate parameters.",value:"Pass a profile id or name from the <profiles> list below; a close-enough name resolves to the nearest profile and the choice is reported on stderr. Omit it inside a node to inherit the caller\u2019s profile; outside a node, omission runs normal cwd-based profile selection.",effects:["The selected profile id is stored on the node and supplies its profile-scoped config, documents, environment, and injected bearings.","It does not change the parent or subscription; --cwd separately pins the working directory."]}},{kind:"flag",name:"situational-context",type:"string",required:!1,constraint:"Hidden ambient context appended to injected bearings, never visible chat."},{kind:"flag",name:"no-kickoff",type:"bool",required:!1,hidden:!0,constraint:"With --root, start the broker and session idle without a first message or model turn. Mutually exclusive with a prompt and --mode orchestrator."},{kind:"flag",name:"scopes",type:"string",required:!1,constraint:"Comma-separated scope allow-list for the node and every descendant (they inherit it and cannot widen it), e.g. `crtr:llm,crtr:act,device:shell,device:messages_read`. A provider-group call (`crtr device messages send` needs `device:messages_send`) is refused with scope_missing before any request leaves the machine when the group is absent. Must be a subset of what you hold; `crtr:act` requires `crtr:llm`. Enforced on the broker route (the node's own `crtr` calls); on an unsandboxed host a same-UID process can bypass it by calling the daemon socket directly (crouter#146), so it guards against a mistaken call, not a hostile node. Keep the list short: it travels in a request header on every device call (crouter#147)."},{kind:"flag",name:"deadline",type:"string",required:!1,constraint:"Positive, representable wall-clock duration from spawn. Grammar: one or more positive integer-unit parts; units: s, m, h, d; adjacent parts are summed. Omit for no deadline."},{kind:"flag",name:"outcome-action",type:"string",required:!1,constraint:"Declared human-action name that receives the node outcome. Grammar: 1\u2013128 ASCII letters, digits, dots, underscores, or hyphens; starts with a letter or digit. Omit when no external delivery target is needed."},{kind:"flag",name:"outcome-payload",type:"string",required:!1,constraint:"JSON value, serialized at most 64 KiB, frozen and echoed to --outcome-action. Requires --outcome-action."},{kind:"flag",name:"output-schema",type:"string",required:!1,constraint:"Replace prose completion with a schema-constrained final result. Use only when its consumer requires fixed fields.",focusedHelp:{whenToUse:"Use only when the node\u2019s consumer needs a machine-checkable final object with fixed fields. Omit it for ordinary delegation or when a prose report is enough; structured output replaces the completion protocol, and an over-constrained schema can force retries.",value:`Pass the JSON Schema the result must satisfy. ${ee} ${Z}`,effects:["The node must answer with `crtr push result` and cannot finish until the result is accepted; a plain `crtr push final` is blocked while the request is pending.",B,"The managed parent wakes through its existing subscription when the final report arrives; an independent root has no subscriber to wake.","An accepted result, or a decline, removes the request and terminates the node."]}}]}s(we,"nodeNewParams");function ye(){return[{name:"node_id",type:"string",required:!0,constraint:"New node id."},{name:"name",type:"string",required:!0,constraint:"Display name."},{name:"frozen_at",type:"string",required:!1,constraint:"Present when a managed child exists but capacity deferred its broker start."},...z()?[{name:"window",type:"string",required:!1,constraint:"tmux window id when a viewer opens."},{name:"session",type:"string",required:!1,constraint:"tmux session when a viewer opens."}]:[],{name:"follow_up",type:"string",required:!0,constraint:"Managed child: continue unrelated work or stop; its final report wakes you. Root: hand it off; no report wakes you."}]}s(ye,"nodeNewOutput");function ke(){return["Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.","A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.","--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch.",B,"--deadline persists a wall-clock deadline from spawn; expiry cancels the node and records a failure outcome with reason deadline_exceeded.","--outcome-action registers one resolved local human-action target. When the node settles, crouter retries delivery of its outcome document until the target accepts it or permanently rejects it; --outcome-payload is frozen and echoed in that document.",...z()?["--root opens a viewer for the new node in the current tmux session."]:[]]}s(ke,"nodeNewEffects");async function ge(e){Q("crtr:act","crtr node new");const n=e.prompt??"",t=e.noKickoff===!0;if(!t&&n.trim()==="")throw new o({error:"empty_prompt",message:"a prompt is required (stdin or positional)",next:"Pipe a task on stdin or pass it as an argument, or pass --no-kickoff to spawn an idle node with none."});if(t&&n.trim()!=="")throw new o({error:"no_kickoff_with_prompt",message:"--no-kickoff cannot be combined with a prompt",field:"no-kickoff",next:"Drop --no-kickoff, or drop the prompt."});const l=e.kind,O=e.mode??"base",u=e.cwd?.trim(),h=u!==void 0&&u!==""?ue(u):void 0,S=e.name,Y=e.parent,i=e.root===!0,f=e.resident===!0,P=e.terminal===!0;if(f&&P)throw new o({error:"lifecycle_conflict",message:"--resident and --terminal cannot both be passed",field:"terminal",next:"Pass the one birth lifecycle you want, or neither to take the default (resident for --root, terminal for a managed child)."});if(f&&!i&&e.force!==!0)throw new o({error:"resident_child_requires_force",message:`--resident on a managed child makes it a conversation partner for a person, not a worker that reports back. ${K.join(" ")}`,field:"resident",next:`If the user did not explicitly ask for a node they can talk with back and forth, drop --resident; a terminal child's final report wakes you. ${J} If the user did explicitly ask for one, re-run with --force.`});const m=e.worktree;if(m!==void 0&&m.trim()==="")throw new o({error:"worktree_base_required",message:"--worktree requires a base branch",field:"worktree",next:"Pass an existing local branch that landing can fast-forward."});const p=m?.trim(),E=e.forkFrom,w=e.model?.trim();if(w==="")throw new o({error:"empty_spec",message:"a model spec is required",field:"model",next:`Run \`crtr node new --model -h\` and read the focused contract. ${ne}`});const D=e.profile,C=D===void 0?void 0:(await se(D)).id,y=e.situationalContext?.trim(),A=y!==void 0&&y!==""?y:void 0,U=te(e.outputSchema),I=U!==null?JSON.stringify(U):null,d=e.deadline;if(d!==void 0){const r=re(d),q=r===null?Number.NaN:new Date(Date.now()+r).getTime();if(r===null||!Number.isFinite(r)||r<=0||!Number.isFinite(q))throw new o({error:"invalid_deadline",message:"--deadline must be a positive representable duration",field:"deadline",next:"Pass positive integer-unit parts using s, m, h, or d."})}const k=e.outcomeAction,g=e.outcomePayload;if(g!==void 0&&k===void 0)throw new o({error:"outcome_payload_requires_action",message:"--outcome-payload requires --outcome-action",field:"outcome-payload",next:"Pass the declared delivery action with --outcome-action."});let v;if(g!==void 0)try{v=JSON.parse(g)}catch{throw new o({error:"invalid_outcome_payload",message:"--outcome-payload must be valid JSON",field:"outcome-payload",next:"Pass one valid JSON value."})}const R=e.scopes,c=R===void 0?void 0:R.split(",").map(r=>r.trim()).filter(r=>r!=="");if(c!==void 0&&c.length===0)throw new o({error:"empty_scopes",message:"--scopes names no scope",field:"scopes",next:"Pass a comma-separated list such as crtr:llm,crtr:act, or omit --scopes to inherit."});const b=e.nodeId?.trim();if(i&&p!==void 0)throw new o({error:"worktree_root_conflict",message:"--worktree is mutually exclusive with --root",field:"worktree",next:"Drop either --root or --worktree."});if(t&&!i)throw new o({error:"no_kickoff_requires_root",message:"--no-kickoff is only valid with --root",field:"no-kickoff",next:"Add --root, or drop --no-kickoff and provide a prompt."});if(t&&O==="orchestrator")throw new o({error:"no_kickoff_orchestrator_unsupported",message:"--no-kickoff cannot be combined with --mode orchestrator",field:"no-kickoff",next:"Drop --no-kickoff and provide a prompt, or spawn in base mode."});const _=V(),M=Y??_;N("node_new.create_start",{root:i});const j=le(),L={...l!==void 0?{kind:l}:{},mode:O,cwd:process.cwd(),...h!==void 0&&h!==""?{pin_cwd:h}:{},...S!==void 0?{name:S}:{},prompt:n,...i?{root:!0}:{parent:M??null},..._!==void 0?{creator:_}:{},...f?{root_lifecycle:"resident"}:{},...P?{root_lifecycle:"terminal"}:{},...p!==void 0?{worktree:p}:{},...E!==void 0?{fork_from:E}:{},...w!==void 0?{model:w}:{},...C!==void 0?{profile:C}:{},...A!==void 0?{situational_context:A}:{},...t?{no_kickoff:!0}:{},...I!==null?{output_schema:I}:{},...c!==void 0?{scopes:c}:{},...d!==void 0?{deadline:d}:{},...k!==void 0?{outcome_delivery:{action:k,...v===void 0?{}:{payload:v}}}:{},...b!==void 0&&b!==""?{node_id:b}:{},...i?{prefer_warm:!0}:{},request_key:j};let a;try{a=await H().createNode(L)}catch(r){W(r)||$(r);try{a=await H().createNode(L)}catch(q){pe(q,j)}}N("node_new.node_created",{node:a.node_id});let x={};i&&(x=ce(a.node_id,a.cwd,a.name)),i&&N("node_new.viewer_window_opened");const F=!i&&a.frozen_at!==null;return{node_id:a.node_id,name:a.name,...F?{frozen_at:a.frozen_at}:{},window:x.window,session:x.session,follow_up:i?t?"Independent root spawned idle. No kickoff turn ran; hand it off or attach and prompt it. No report wakes you.":"Independent root spawned. You are not subscribed, so its finish does not wake you; hand it off and move on.":F?"Child row exists, but no broker started because capacity (broker slots or the app's memory) is full. It is frozen and will launch when capacity frees; its final report wakes you then.":await me(M)}}s(ge,"runNodeCreation");const je=X({name:"new",description:"create and start a node immediately",whenToUse:"a unit of work is genuinely separable from what you are doing: a bounded subtask you would otherwise grind out inline, several independent units worth running in parallel (usually via one orchestrator child of the matching kind rather than a flat fan-out from you), a scouting pass over unfamiliar code, a change that wants its own git worktree, or an answer you need in a fixed shape rather than as prose",tier:"important",help:{name:"node new",summary:"create a node immediately and start its detached broker",guide:"The prompt is the child\u2019s entire brief: it wakes with this prompt, its persona, and ambient bearings \u2014 none of your conversation carries over. State the outcome and constraints. Pass an implementation plan only when it is already settled or necessary to preserve an approved decision; otherwise, let the child investigate and choose how. Give the reason along with the request: the larger goal this serves and what the child's output enables, so the child resolves ambiguity toward your intent instead of guessing. Point at every document the task depends on (a spec, an explore map, an earlier child's findings) as `[[<node-id>/<name>]]`, or `[[name]]` for a repo, profile or user document; the child reads each with `crtr canvas read` and receives nothing the prompt does not point at. Name every file or directory as an absolute host path, never `~`-relative: the child's `~` is its own HOME, not yours, and a hook cwd outside its mounts (e.g. `/tmp`) is refused. When the report must carry something specific\u2014findings inline, or pointers to documents it wrote\u2014say so in the prompt.",params:we(),output:ye(),outputKind:"object",effects:ke()},helpState:{leaf:ie,flags:{profile:ae}},run:s(e=>ge(e),"run"),render:s(e=>{const n=e.window?` in window ${e.window}`:"",t=[`Spawned "${e.name}" (${e.node_id})${n}.`];return e.session&&t.push(`- session: ${e.session}`),t.push("",String(e.follow_up)),t.join(`
2
2
  `)},"render")});export{me as childFollowUp,je as nodeNew};
@@ -1,3 +1,3 @@
1
- var l=Object.defineProperty;var n=(e,r)=>l(e,"name",{value:r,configurable:!0});import{defineBranch as y,defineLeaf as o}from"../../core/command.js";import{InputError as m}from"../../core/io.js";import{LimitInputError as f,parseSize as k}from"../../launcher/limits/settings.js";import{cliClient as i}from"../api-client.js";const d="A size: a byte count, a number with KiB, MiB, GiB or TiB, or `unlimited`.",p="An app id such as `app:terminal`.";function c(e,r){const t=e[r];if(t!==void 0)try{return k(String(t))}catch(s){throw s instanceof f?new m({error:"invalid_size",message:`${s.message}; received ${String(t)}`,field:r,next:"Pass a size such as 2GiB, 512MiB, 1073741824 or unlimited."}):s}}n(c,"size");const u=n(e=>e===null?"none":`${(e/1024**3).toFixed(e<1024**3?3:1)} GiB`,"cap");function a(e){const r=e.disk_used_bytes===null?"":`, disk used ${(e.disk_used_bytes/1048576).toFixed(0)} MiB`;return`- \`${e.app}\` \u2014 memory ${u(e.memory_bytes)} (${e.memory_source}), disk ${u(e.disk_bytes)} (${e.disk_source})${r}`}n(a,"line");const h={name:"limits",type:"AppLimitsDTO",required:!0,constraint:"Record: {app, memory_bytes, disk_bytes, memory_source, disk_source, disk_used_bytes}. A null cap means none."},w=o({name:"show",description:"show each app's memory and disk caps and whether this host enforces them",whenToUse:"checking what an app may use, or why a run was stopped for memory",help:{name:"sys limits show",summary:"list effective per-app memory and disk caps, marking the person-set overrides",params:[],output:[{name:"memory_enforced",type:"boolean",required:!0,constraint:"False when this host cannot write cgroup memory limits; reason in memory_reason."},{name:"memory_reason",type:"string",required:!1,constraint:"Why memory limits are not enforced, or null."},{name:"disk_enforced",type:"boolean",required:!0,constraint:"False when the apps volume has no project quota; reason in disk_reason."},{name:"disk_reason",type:"string",required:!1,constraint:"Why disk limits are not enforced, or null."},{name:"apps",type:"AppLimitsDTO[]",required:!0,constraint:"One record per known app, by id."}],outputKind:"object",effects:["None. Read-only."]},run:n(async()=>({...await i().showLimits()}),"run"),render:n(e=>{const r=e;return[r.memory_enforced?"Memory limits: enforced.":`Memory limits: not enforced (${r.memory_reason}).`,r.disk_enforced?"Disk limits: enforced.":`Disk limits: not enforced (${r.disk_reason}).`,...r.apps.map(a)].join(`
2
- `)},"render")}),g=o({name:"set",description:"set an app's memory and/or disk cap",whenToUse:"raising or lowering what one app may use, or removing its cap with `unlimited`",help:{name:"sys limits set",summary:"store a person-set memory and/or disk cap for one app and apply it now",params:[{kind:"flag",name:"app",type:"string",required:!0,constraint:p},{kind:"flag",name:"memory",type:"string",required:!1,constraint:`${d} Total resident memory of all the app's running processes; at least 64 MiB.`},{kind:"flag",name:"disk",type:"string",required:!1,constraint:`${d} Bytes under the app's folder, cache included; at least 16 MiB.`}],output:[{name:"app",type:"string",required:!0,constraint:"The app changed."},{name:"applied",type:"boolean",required:!0,constraint:"True when the launcher wrote the caps to the host now; false when this host has no launcher, so the caps are only recorded."},h],outputKind:"object",effects:["Writes limits.json in the runtime directory, which no sandbox mounts.","The launcher rewrites the app's cgroup memory.max and project quota at once; running processes keep running, and a process over a lowered memory cap is reclaimed or OOM-killed by the kernel."]},run:n(async e=>{const r=c(e,"memory"),t=c(e,"disk");if(r===void 0&&t===void 0)throw new m({error:"nothing_to_set",message:"received neither --memory nor --disk",next:"Pass --memory <size>, --disk <size>, or both."});return await i().setAppLimits(String(e.app),{...r===void 0?{}:{memory_bytes:r},...t===void 0?{}:{disk_bytes:t}})},"run"),render:n(e=>`${a(e.limits)}${e.applied?"":`
3
- Recorded only: this host has no launcher to enforce it.`}`,"render")}),_=o({name:"remove",description:"remove a person-set cap so the app returns to its default",whenToUse:"undoing a `sys limits set` override",help:{name:"sys limits remove",summary:"drop an app's memory and/or disk override; with neither flag, both",params:[{kind:"flag",name:"app",type:"string",required:!0,constraint:p},{kind:"flag",name:"memory",type:"bool",required:!1,constraint:"Remove the memory override."},{kind:"flag",name:"disk",type:"bool",required:!1,constraint:"Remove the disk override."}],output:[{name:"app",type:"string",required:!0,constraint:"The app changed."},{name:"applied",type:"boolean",required:!0,constraint:"True when the launcher wrote the default caps now."},h],outputKind:"object",effects:["Edits limits.json and has the launcher apply the default caps at once."]},run:n(async e=>{const r=[...e.memory===!0?["memory"]:[],...e.disk===!0?["disk"]:[]];return await i().removeAppLimits(String(e.app),r.length===0?["memory","disk"]:r)},"run"),render:n(e=>a(e.limits),"render")}),B=y({name:"limits",description:"per-app memory and disk caps on this runtime",whenToUse:"a person needs to see, raise, lower or remove what one app may use of the hosted runtime's memory and disk",help:{name:"sys limits",summary:"view and change per-app memory and disk caps",model:"Every app has a default memory and disk cap; the terminal app has none. A person-set override replaces the default for one resource of one app and applies immediately. An app cannot change its own caps. A host without cgroup memory control or disk project quota records the caps and reports the limit as not enforced."},children:[w,g,_]});export{B as sysLimitsBranch};
1
+ var y=Object.defineProperty;var t=(e,r)=>y(e,"name",{value:r,configurable:!0});import{defineBranch as f,defineLeaf as i}from"../../core/command.js";import{InputError as d}from"../../core/io.js";import{LimitInputError as k,parseSize as w}from"../../launcher/limits/settings.js";import{cliClient as a}from"../api-client.js";const p="A size: a byte count, a number with KiB, MiB, GiB or TiB, or `unlimited`.",c="An app id such as `app:terminal`.";function u(e,r){const s=e[r];if(s!==void 0)try{return w(String(s))}catch(n){throw n instanceof k?new d({error:"invalid_size",message:`${n.message}; received ${String(s)}`,field:r,next:"Pass a size such as 2GiB, 512MiB, 1073741824 or unlimited."}):n}}t(u,"size");const h=t(e=>e===null?"none":`${(e/1024**3).toFixed(e<1024**3?3:1)} GiB`,"cap"),o=t(e=>e>=1024**3?`${(e/1024**3).toFixed(2)} GiB`:`${(e/1024**2).toFixed(0)} MiB`,"mib");function _(e){const r=t((s,n)=>n===null?"":`, ${s} ${o(n)}`,"part");return`Data disk: ${o(e.used_bytes)} used of ${o(e.total_bytes)} (${(100*e.used_bytes/Math.max(e.total_bytes,1)).toFixed(0)}%)${r("/user",e.user_bytes)}${r("/runtime",e.runtime_bytes)}.`}t(_,"diskLine");function m(e){const r=e.disk_used_bytes===null?"":`, disk used ${o(e.disk_used_bytes)}`;return`- \`${e.app}\` \u2014 memory ${h(e.memory_bytes)} (${e.memory_source}), disk ${h(e.disk_bytes)} (${e.disk_source})${r}`}t(m,"line");const l={name:"limits",type:"AppLimitsDTO",required:!0,constraint:"Record: {app, memory_bytes, disk_bytes, memory_source, disk_source, disk_used_bytes}. A null cap means none."},g=i({name:"show",description:"show each app's memory and disk caps and whether this host enforces them",whenToUse:"checking what an app may use, or why a run was stopped for memory",help:{name:"sys limits show",summary:"list effective per-app memory and disk caps, marking the person-set overrides",params:[],output:[{name:"memory_enforced",type:"boolean",required:!0,constraint:"False when this host cannot write cgroup memory limits; reason in memory_reason."},{name:"memory_reason",type:"string",required:!1,constraint:"Why memory limits are not enforced, or null."},{name:"disk_enforced",type:"boolean",required:!0,constraint:"False when the apps volume has no project quota; reason in disk_reason."},{name:"disk_reason",type:"string",required:!1,constraint:"Why disk limits are not enforced, or null."},{name:"apps",type:"AppLimitsDTO[]",required:!0,constraint:"One record per known app, by id; disk_used_bytes is measured by walking the app's folder when the host has no project quota."},{name:"data_disk",type:"DataDiskDTO",required:!1,constraint:"Record: {total_bytes, used_bytes, user_bytes, runtime_bytes} for the data volume; absent on a host without a launcher."}],outputKind:"object",effects:["None. Read-only."]},run:t(async()=>({...await a().showLimits()}),"run"),render:t(e=>{const r=e;return[r.memory_enforced?"Memory limits: enforced.":`Memory limits: not enforced (${r.memory_reason}).`,r.disk_enforced?"Disk limits: enforced.":`Disk limits: not enforced (${r.disk_reason}).`,...r.data_disk?[_(r.data_disk)]:[],...r.apps.map(m)].join(`
2
+ `)},"render")}),b=i({name:"set",description:"set an app's memory and/or disk cap",whenToUse:"raising or lowering what one app may use, or removing its cap with `unlimited`",help:{name:"sys limits set",summary:"store a person-set memory and/or disk cap for one app and apply it now",params:[{kind:"flag",name:"app",type:"string",required:!0,constraint:c},{kind:"flag",name:"memory",type:"string",required:!1,constraint:`${p} Total resident memory of all the app's running processes; at least 64 MiB.`},{kind:"flag",name:"disk",type:"string",required:!1,constraint:`${p} Bytes under the app's folder, cache included; at least 16 MiB.`}],output:[{name:"app",type:"string",required:!0,constraint:"The app changed."},{name:"applied",type:"boolean",required:!0,constraint:"True when the launcher wrote the caps to the host now; false when this host has no launcher, so the caps are only recorded."},l],outputKind:"object",effects:["Writes limits.json in the runtime directory, which no sandbox mounts.","The launcher rewrites the app's cgroup memory.max and project quota at once; running processes keep running, and a process over a lowered memory cap is reclaimed or OOM-killed by the kernel."]},run:t(async e=>{const r=u(e,"memory"),s=u(e,"disk");if(r===void 0&&s===void 0)throw new d({error:"nothing_to_set",message:"received neither --memory nor --disk",next:"Pass --memory <size>, --disk <size>, or both."});return await a().setAppLimits(String(e.app),{...r===void 0?{}:{memory_bytes:r},...s===void 0?{}:{disk_bytes:s}})},"run"),render:t(e=>`${m(e.limits)}${e.applied?"":`
3
+ Recorded only: this host has no launcher to enforce it.`}`,"render")}),$=i({name:"remove",description:"remove a person-set cap so the app returns to its default",whenToUse:"undoing a `sys limits set` override",help:{name:"sys limits remove",summary:"drop an app's memory and/or disk override; with neither flag, both",params:[{kind:"flag",name:"app",type:"string",required:!0,constraint:c},{kind:"flag",name:"memory",type:"bool",required:!1,constraint:"Remove the memory override."},{kind:"flag",name:"disk",type:"bool",required:!1,constraint:"Remove the disk override."}],output:[{name:"app",type:"string",required:!0,constraint:"The app changed."},{name:"applied",type:"boolean",required:!0,constraint:"True when the launcher wrote the default caps now."},l],outputKind:"object",effects:["Edits limits.json and has the launcher apply the default caps at once."]},run:t(async e=>{const r=[...e.memory===!0?["memory"]:[],...e.disk===!0?["disk"]:[]];return await a().removeAppLimits(String(e.app),r.length===0?["memory","disk"]:r)},"run"),render:t(e=>m(e.limits),"render")}),j=f({name:"limits",description:"per-app memory and disk caps on this runtime",whenToUse:"a person needs to see, raise, lower or remove what one app may use of the hosted runtime's memory and disk",help:{name:"sys limits",summary:"view and change per-app memory and disk caps",model:"Every app has a default memory and disk cap; the terminal app has none. A person-set override replaces the default for one resource of one app and applies immediately. An app cannot change its own caps. A host without cgroup memory control or disk project quota records the caps and reports the limit as not enforced."},children:[g,b,$]});export{j as sysLimitsBranch};
@@ -1,4 +1,3 @@
1
- var de=Object.defineProperty;var p=(t,e)=>de(t,"name",{value:e,configurable:!0});import{randomUUID as ce}from"node:crypto";import{streamSimple as fe}from"@earendil-works/pi-ai/compat";import{expandModelCandidates as Y,parseModelSpec as ue,probeRouteAvailability as me}from"../model-routes.js";import{readMergedLaunchConfig as ee}from"../config.js";import{openModelRegistry as pe}from"../runtime/model-registry.js";import{coolingUntilFor as te,isManagedProvider as E,markSubscriptionRateLimited as he,markSubscriptionSuccess as ye,readSubscriptionPool as _e,retryAfterMsFromHeaders as Z,retryAfterMsFromError as ge,refreshSubscriptionCredential as ve,subscriptionNeedsSuccessWrite as be}from"../subscription-state.js";import{classifyRateLimit as we,describeRateLimitScope as oe,rateLimitHeaders as xe}from"../rate-limit-scope.js";import{RunError as l}from"../runs/operations.js";import{recognizeProviderError as ne}from"../provider-error-recognition.js";import{isSafetyRefusal as ke}from"./refusal.js";import{assertUnderCap as je,recordModelUsage as qe}from"../usage/ledger.js";function Ae(t){return typeof t=="string"||t===null||Array.isArray(t)&&t.every(e=>e&&typeof e=="object"&&(e.type==="text"&&typeof e.text=="string"||e.type==="image_url"&&typeof e.image_url?.url=="string"&&/^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/.test(e.image_url.url)))}p(Ae,"validContent");function re(t){return Array.isArray(t)?t.map(e=>{if(e.type==="text")return e;const[,n,r]=/^data:(image\/(?:png|jpeg|gif|webp));base64,(.*)$/.exec(e.image_url.url);return{type:"image",mimeType:n,data:r}}):t??""}p(re,"imageContent");function We(t){if(!t||typeof t!="object"||Array.isArray(t))throw new l(400,"invalid_request","body must be an object");const e=t;if(!Array.isArray(e.messages)||!e.messages.every(n=>n&&typeof n=="object"&&["system","user","assistant","tool"].includes(n.role)&&Ae(n.content)))throw new l(400,"invalid_request","messages must be chat messages","messages");if(e.messages.some(n=>["system","assistant"].includes(n.role)&&typeof n.content!="string"&&n.content!==null))throw new l(400,"invalid_request","system and assistant content must be text","messages");if(e.model!==void 0&&typeof e.model!="string")throw new l(400,"invalid_request","model must be a string","model");if(e.model_policy!==void 0&&!["exact","substitute"].includes(e.model_policy))throw new l(400,"invalid_request","invalid model_policy","model_policy");if(e.payer!==void 0&&!["user","app"].includes(e.payer))throw new l(400,"invalid_request","invalid payer","payer");if(e.reasoning_effort!==void 0&&!["minimal","low","medium","high","xhigh","max"].includes(e.reasoning_effort))throw new l(400,"invalid_request","invalid reasoning_effort","reasoning_effort");if(e.stream!==void 0&&typeof e.stream!="boolean")throw new l(400,"invalid_request","stream must be boolean","stream");if(e.tools!==void 0&&(!Array.isArray(e.tools)||!e.tools.every(n=>n?.type==="function"&&typeof n.function?.name=="string"&&n.function.name.length>0&&(n.function.description===void 0||typeof n.function.description=="string")&&(n.function.parameters===void 0||n.function.parameters!==null&&typeof n.function.parameters=="object"&&!Array.isArray(n.function.parameters)&&n.function.parameters.type==="object"))))throw new l(400,"invalid_request","invalid tools","tools");if(e.tool_choice!==void 0&&e.tool_choice!=="auto"&&e.tool_choice!=="none"&&e.tool_choice!=="required"&&(typeof e.tool_choice!="object"||e.tool_choice===null||e.tool_choice.type!=="function"||typeof e.tool_choice.function?.name!="string"))throw new l(400,"invalid_request","invalid tool_choice","tool_choice");if(e.messages.some(n=>n.role==="assistant"&&n.tool_calls!==void 0&&(!Array.isArray(n.tool_calls)||!n.tool_calls.every(r=>r&&typeof r.id=="string"&&typeof r.function?.name=="string"&&typeof r.function.arguments=="string"&&Me(r.function.arguments)))))throw new l(400,"invalid_request","invalid assistant tool_calls","messages");if(e.stop!==void 0&&!(typeof e.stop=="string"||Array.isArray(e.stop)&&e.stop.every(n=>typeof n=="string")))throw new l(400,"invalid_request","invalid stop","stop");if(e.seed!==void 0&&!Number.isSafeInteger(e.seed))throw new l(400,"invalid_request","invalid seed","seed");for(const[n,r]of[["temperature",e.temperature],["top_p",e.top_p],["max_tokens",e.max_tokens]])if(r!==void 0&&(typeof r!="number"||!Number.isFinite(r)||n==="temperature"&&(r<0||r>2)||n==="top_p"&&(r<0||r>1)||n==="max_tokens"&&(!Number.isSafeInteger(r)||r<1)))throw new l(400,"invalid_request",`invalid ${n}`,n);if(e.response_format!==void 0){const n=e.response_format;if(!n||typeof n!="object"||Array.isArray(n)||!["text","json_object","json_schema"].includes(n.type)||n.type==="json_schema"&&(typeof n.json_schema?.name!="string"||!n.json_schema.name||!n.json_schema.schema||typeof n.json_schema.schema!="object"||Array.isArray(n.json_schema.schema)))throw new l(400,"invalid_request","invalid response_format","response_format")}if(e.metadata!==void 0&&Buffer.byteLength(JSON.stringify(e.metadata)??"")>4096)throw new l(400,"invalid_request","metadata exceeds 4 KB","metadata");return e}p(We,"parseLlmRequest");function Me(t){try{const e=JSON.parse(t);return e!==null&&typeof e=="object"&&!Array.isArray(e)}catch{return!1}}p(Me,"isJsonObject");function $e(t,e){const n=Date.now(),r=t.messages.map(s=>s.role==="system"?{role:"system",content:s.content??"",timestamp:n}:s.role==="tool"?{role:"toolResult",toolCallId:s.tool_call_id??"",toolName:"",content:typeof s.content=="string"||s.content===null?[{type:"text",text:s.content??""}]:re(s.content),isError:!1,timestamp:n}:s.role==="assistant"?{role:"assistant",content:[...s.content?[{type:"text",text:s.content}]:[],...(s.tool_calls??[]).map(c=>({type:"toolCall",id:c.id,name:c.function.name,arguments:JSON.parse(c.function.arguments)}))],api:"openai-completions",provider:"openai",model:t.model??"",usage:{input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}},stopReason:"stop",timestamp:n}:{role:"user",content:re(s.content),timestamp:n});if(e){const s=r.filter(f=>f.role==="system").map(f=>f.content).join(`
2
- `).slice(0,16384),c=r.findIndex(f=>f.role==="user");if(s&&c>=0){const f=r[c];r[c]={...f,content:typeof f.content=="string"?`${s}
1
+ var de=Object.defineProperty;var u=(t,e)=>de(t,"name",{value:e,configurable:!0});import{randomUUID as ce}from"node:crypto";import{streamSimple as fe}from"@earendil-works/pi-ai/compat";import{expandModelCandidates as Y,parseModelSpec as ue,probeRouteAvailability as me}from"../model-routes.js";import{readMergedLaunchConfig as ee}from"../config.js";import{openModelRegistry as pe}from"../runtime/model-registry.js";import{coolingUntilFor as te,isManagedProvider as E,markSubscriptionRateLimited as he,markSubscriptionSuccess as _e,readSubscriptionPool as ye,retryAfterMsFromHeaders as Z,retryAfterMsFromError as ge,refreshSubscriptionCredential as ve,subscriptionNeedsSuccessWrite as be}from"../subscription-state.js";import{classifyRateLimit as we,describeRateLimitScope as oe,rateLimitHeaders as xe}from"../rate-limit-scope.js";import{RunError as l}from"../runs/operations.js";import{recognizeProviderError as ne}from"../provider-error-recognition.js";import{isSafetyRefusal as ke}from"./refusal.js";import{assertUnderCap as je,recordModelUsage as qe}from"../usage/ledger.js";function Ae(t){return typeof t=="string"||t===null||Array.isArray(t)&&t.every(e=>e&&typeof e=="object"&&(e.type==="text"&&typeof e.text=="string"||e.type==="image_url"&&typeof e.image_url?.url=="string"&&/^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/.test(e.image_url.url)))}u(Ae,"validContent");function re(t){return Array.isArray(t)?t.map(e=>{if(e.type==="text")return e;const[,n,r]=/^data:(image\/(?:png|jpeg|gif|webp));base64,(.*)$/.exec(e.image_url.url);return{type:"image",mimeType:n,data:r}}):t??""}u(re,"imageContent");function We(t){if(!t||typeof t!="object"||Array.isArray(t))throw new l(400,"invalid_request","body must be an object");const e=t;if(!Array.isArray(e.messages)||!e.messages.every(n=>n&&typeof n=="object"&&["system","user","assistant","tool"].includes(n.role)&&Ae(n.content)))throw new l(400,"invalid_request","messages must be chat messages","messages");if(e.messages.some(n=>["system","assistant"].includes(n.role)&&typeof n.content!="string"&&n.content!==null))throw new l(400,"invalid_request","system and assistant content must be text","messages");if(e.model!==void 0&&typeof e.model!="string")throw new l(400,"invalid_request","model must be a string","model");if(e.model_policy!==void 0&&!["exact","substitute"].includes(e.model_policy))throw new l(400,"invalid_request","invalid model_policy","model_policy");if(e.payer!==void 0&&!["user","app"].includes(e.payer))throw new l(400,"invalid_request","invalid payer","payer");if(e.reasoning_effort!==void 0&&!["minimal","low","medium","high","xhigh","max"].includes(e.reasoning_effort))throw new l(400,"invalid_request","invalid reasoning_effort","reasoning_effort");if(e.stream!==void 0&&typeof e.stream!="boolean")throw new l(400,"invalid_request","stream must be boolean","stream");if(e.tools!==void 0&&(!Array.isArray(e.tools)||!e.tools.every(n=>n?.type==="function"&&typeof n.function?.name=="string"&&n.function.name.length>0&&(n.function.description===void 0||typeof n.function.description=="string")&&(n.function.parameters===void 0||n.function.parameters!==null&&typeof n.function.parameters=="object"&&!Array.isArray(n.function.parameters)&&n.function.parameters.type==="object"))))throw new l(400,"invalid_request","invalid tools","tools");if(e.tool_choice!==void 0&&e.tool_choice!=="auto"&&e.tool_choice!=="none"&&e.tool_choice!=="required"&&(typeof e.tool_choice!="object"||e.tool_choice===null||e.tool_choice.type!=="function"||typeof e.tool_choice.function?.name!="string"))throw new l(400,"invalid_request","invalid tool_choice","tool_choice");if(e.messages.some(n=>n.role==="assistant"&&n.tool_calls!==void 0&&(!Array.isArray(n.tool_calls)||!n.tool_calls.every(r=>r&&typeof r.id=="string"&&typeof r.function?.name=="string"&&typeof r.function.arguments=="string"&&Me(r.function.arguments)))))throw new l(400,"invalid_request","invalid assistant tool_calls","messages");if(e.stop!==void 0&&!(typeof e.stop=="string"||Array.isArray(e.stop)&&e.stop.every(n=>typeof n=="string")))throw new l(400,"invalid_request","invalid stop","stop");if(e.seed!==void 0&&!Number.isSafeInteger(e.seed))throw new l(400,"invalid_request","invalid seed","seed");for(const[n,r]of[["temperature",e.temperature],["top_p",e.top_p],["max_tokens",e.max_tokens]])if(r!==void 0&&(typeof r!="number"||!Number.isFinite(r)||n==="temperature"&&(r<0||r>2)||n==="top_p"&&(r<0||r>1)||n==="max_tokens"&&(!Number.isSafeInteger(r)||r<1)))throw new l(400,"invalid_request",`invalid ${n}`,n);if(e.response_format!==void 0){const n=e.response_format;if(!n||typeof n!="object"||Array.isArray(n)||!["text","json_object","json_schema"].includes(n.type)||n.type==="json_schema"&&(typeof n.json_schema?.name!="string"||!n.json_schema.name||!n.json_schema.schema||typeof n.json_schema.schema!="object"||Array.isArray(n.json_schema.schema)))throw new l(400,"invalid_request","invalid response_format","response_format")}if(e.metadata!==void 0&&Buffer.byteLength(JSON.stringify(e.metadata)??"")>4096)throw new l(400,"invalid_request","metadata exceeds 4 KB","metadata");return e}u(We,"parseLlmRequest");function Me(t){try{const e=JSON.parse(t);return e!==null&&typeof e=="object"&&!Array.isArray(e)}catch{return!1}}u(Me,"isJsonObject");function Re(t){const e=Date.now(),n=t.messages.map(a=>a.role==="system"?{role:"system",content:a.content??"",timestamp:e}:a.role==="tool"?{role:"toolResult",toolCallId:a.tool_call_id??"",toolName:"",content:typeof a.content=="string"||a.content===null?[{type:"text",text:a.content??""}]:re(a.content),isError:!1,timestamp:e}:a.role==="assistant"?{role:"assistant",content:[...a.content?[{type:"text",text:a.content}]:[],...(a.tool_calls??[]).map(w=>({type:"toolCall",id:w.id,name:w.function.name,arguments:JSON.parse(w.function.arguments)}))],api:"openai-completions",provider:"openai",model:t.model??"",usage:{input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}},stopReason:"stop",timestamp:e}:{role:"user",content:re(a.content),timestamp:e}),r=n.findIndex(a=>a.role!=="system"),y=n.slice(0,r<0?n.length:r).map(a=>a.content),m=y.join(`
3
2
 
4
- ${f.content}`:[{type:"text",text:s},...f.content]}}s&&c<0&&r.unshift({role:"user",content:s,timestamp:n});for(let f=r.length-1;f>=0;f--)r[f]?.role==="system"&&r.splice(f,1)}const _=t.tools?.map(({function:s})=>({name:s.name,description:s.description??"",parameters:s.parameters??{type:"object",properties:{}}}));return{messages:r,..._?{tools:_}:{}}}p($e,"contextOf");function se(t,e,n,r){if(!t){const s=ee(n,r),c=Object.values({...s.modelRoutes??{},...s.legacyModelRoutes??{}}),f=c.length?[s.modelLadders.defaultProvider,...s.modelRouting?.familyOrder??[],...c.map(k=>k.family)].find(k=>c.some(I=>I.family===k)):s.modelLadders.defaultProvider??"anthropic";return{kind:"logical",family:f,strength:"strong",spec:`${f}/strong`}}const _=/^([^/]+)\/(ultra|strong|medium|light)$/.exec(t);return _?{kind:"logical",family:_[1],strength:_[2],spec:t}:{kind:e==="exact"?"exact":"concrete",spec:t}}p(se,"requestFor");function Re(t,e){const{input:n,output:r,cacheRead:_,cacheWrite:s}=t.cost;return n===0&&r===0&&_===0&&s===0?null:Number.isFinite(e)&&e>=0?e:null}p(Re,"usdEquivalent");function Ie(t,e){return t.compat?.allowedFallbackModels?.find(r=>r.provider===t.provider&&r.model===e)}p(Ie,"allowedFallback");async function He(t,e,n,r,_){if(t.payer==="app")throw new l(403,"payer_not_app","app-paid inference routes are not registered");je(_);const s=await pe(),c=se(t.model,t.model_policy,r?.cwd,r?.profileId),f=p(m=>m.routeId!=="concrete"&&m.routeId!=="exact","configured"),k=p(m=>{const a=s.find(m.providerId,m.modelId);return a!==void 0&&(!t.reasoning_effort||a.reasoning)&&(t.seed===void 0&&t.response_format===void 0||a.api==="openai-completions")&&(t.stop===void 0||["openai-completions","anthropic-messages"].includes(a.api))},"matchesFeatures"),I=Y(c.kind==="exact"?{kind:"concrete",spec:c.spec}:c,r?.cwd,r?.profileId).filter(f);let w;if(c.kind==="logical")w=t.model_policy==="exact"?I.filter(m=>m.family===c.family&&m.strength===c.strength).slice(0,1):I;else{const m=ue(c.spec),a=I.filter(u=>u.providerId===m?.providerId&&u.modelId===m.modelId&&(t.model_policy!=="exact"||u.thinkingLevel===m.thinkingLevel)&&k(u));if(t.model_policy==="exact")w=a.slice(0,1);else if(a.length)w=a;else{const u=ee(r?.cwd,r?.profileId),q=[...Object.values(u.modelRoutes??{}).flatMap(i=>Object.entries(i.models)),...[u.modelLadders.anthropic,u.modelLadders.openai].flatMap(i=>Object.entries(i))].find(([,i])=>i===c.spec)?.[0],L=se(void 0,"substitute",r?.cwd,r?.profileId);w=q&&L.kind==="logical"?Y({...L,strength:q,spec:c.spec},r?.cwd,r?.profileId).filter(i=>f(i)&&i.strength===q):[]}}if(w=w.filter(k),w.length===0)throw t.model_policy==="exact"?new l(400,"substitution_refused","no eligible model route is configured"):new l(404,"model_unavailable","no eligible model route is configured");const G=`chatcmpl-${ce()}`;let j=1/0,y,U;for(const m of w){if(!me(m,s).authenticated)continue;const a=s.find(m.providerId,m.modelId);if(!a)continue;const u=m.providerId,S=E(u)?_e(u).filter(i=>!i.authFailure):[],q=S.filter(i=>te(i,a.id)<=Date.now());if(S.length&&!q.length){const i=Math.min(...S.map(A=>te(A,a.id)));i<j&&(j=i,U=`${a.provider}/${a.id}`);continue}const L=S.length?q:[void 0];for(const i of L){const A=Date.now();let Q=i?.access;if(i&&i.expires<=Date.now()&&E(u)){const o=s.getProvider(u)?.auth.oauth;if(!o)throw new l(503,"upstream_model_unavailable",`no OAuth refresh for ${u}`,void 0,{served_model:`${a.provider}/${a.id}`});Q=(await ve(u,i.label,async(g,X)=>{const P=await o.refresh({type:"oauth",refresh:g,access:"",expires:0,...X?{clientId:X}:{}},e??new AbortController().signal),z=P.clientId;return{refresh:P.refresh,access:P.access,expires:P.expires,...typeof z=="string"&&z?{clientId:z}:{}}})).access}const ie=$e(t,!!i),M=i?void 0:await s.getApiKeyAndHeaders(a);if(M&&!M.ok)continue;const v=`${a.provider}/${a.id}`,$=t.model??v;let x=!1,V,b=3e4,R;const J=[],T=[],O=[];let d;const N=p((o,h=null)=>({id:G,object:"chat.completion.chunk",created:Math.floor(A/1e3),model:v,served_model:v,requested_model:$,substituted:$!==v&&!/^([^/]+)\/(ultra|strong|medium|light)$/.test($),choices:[{index:0,delta:o,finish_reason:h}]}),"chunk"),D=p(o=>{J.push(o),n?.(o)},"emit"),ae=fe(a,ie,{apiKey:Q??(M?.ok?M.apiKey:void 0),...M?.ok?{headers:M.headers}:{},signal:e,reasoning:t.reasoning_effort??m.thinkingLevel,...t.temperature!==void 0?{temperature:t.temperature}:{},...t.max_tokens!==void 0?{maxTokens:t.max_tokens}:{},...t.top_p!==void 0?{samplingParams:{top_p:t.top_p}}:{},...t.tool_choice==="none"?{toolChoice:"none"}:{},onPayload:p(o=>{if(!o||typeof o!="object")return;const h=t.tool_choice,g=h==="required"?a.api==="anthropic-messages"?{type:"any"}:"required":typeof h=="object"&&h!==null?a.api==="anthropic-messages"?{type:"tool",name:h.function.name}:h:void 0;return{...o,...g?{tool_choice:g}:{},...t.stop!==void 0?{[a.api==="anthropic-messages"?"stop_sequences":"stop"]:typeof t.stop=="string"?[t.stop]:t.stop}:{},...t.seed!==void 0?{seed:t.seed}:{},...t.response_format!==void 0?{response_format:t.response_format}:{}}},"onPayload"),fetch:p(async(o,h)=>{const g=await fetch(o,h);return V=g.status,g.status===429&&(x=!0,R=g.headers,b=Z(g.headers)??15e3),g},"fetch"),onResponse:p(o=>{o.status===429&&(x=!0,R=o.headers,b=Z(o.headers)??15e3)},"onResponse")});try{for await(const o of ae)o.type==="text_delta"&&(T.push(o.delta),D(N({content:o.delta}))),o.type==="thinking_delta"&&(O.push(o.delta),D(N({reasoning_content:o.delta}))),o.type==="done"&&(d=o.message),o.type==="error"&&(d=o.error)}catch(o){const h=ge(o);R===void 0&&o&&typeof o=="object"&&"headers"in o&&Object.keys(xe(o.headers)).length&&(R=o.headers);const g=ne({status:o&&typeof o=="object"&&"status"in o?o.status:void 0,message:o instanceof Error?o.message:String(o)});if(!x&&g.kind!=="rate-limit")throw J.length?new l(503,"upstream_model_unavailable",o instanceof Error?o.message:String(o),void 0,{served_model:v}):o;x=!0,b=h??b}if(!x&&d?.stopReason==="error"&&ne({message:d.errorMessage}).kind==="rate-limit"&&(x=!0,b=15e3),x){const o=we(R,a.id);if(Z(R)===void 0&&o.resetAt!==void 0&&(b=Math.max(b,o.resetAt-Date.now())),y={provider:u,model:a.id,...i?{account:i.label}:{},observation:o},console.error(`[llm] 429 from ${v}${i?` account "${i.label}"`:""}: scope=${oe(o.scope)} retry_after_ms=${b} headers=${JSON.stringify(o.headers)}`),i&&E(u)&&await he(u,i.label,b,A,Date.now(),o.scope),Date.now()+b<j&&(j=Date.now()+b,U=v),T.length||O.length)throw new l(503,"service_unavailable","vendor rate limit after output began",void 0,{served_model:v});continue}const F=ke(d);if(V===400&&t.tool_choice!==void 0&&t.tool_choice!=="auto"&&t.tool_choice!=="none"&&/\btool[_ ]choice\b/i.test(d?.errorMessage??"")&&/not supported|does not support|unsupported|cannot|must be auto/i.test(d?.errorMessage??""))throw new l(400,"invalid_request",d.errorMessage,"tool_choice",{served_model:v});if(!d||d.stopReason==="error"&&!F||d.stopReason==="aborted")throw new l(503,"upstream_model_unavailable",d?.errorMessage??"model request failed",void 0,{served_model:v});i&&E(u)&&be(i,a.id)&&await ye(u,i.label,A,a.id);const W=d.content.filter(o=>o.type==="toolCall").map(o=>({id:o.id,type:"function",function:{name:o.name,arguments:JSON.stringify(o.arguments)}})),H=F?"content_filter":d.stopReason==="toolUse"?"tool_calls":d.stopReason==="length"?"length":"stop",K=d.responseModel?Ie(a,d.responseModel):void 0,C=K?`${a.provider}/${K.model}`:v,B={prompt_tokens:d.usage.input,completion_tokens:d.usage.output,total_tokens:d.usage.totalTokens,cache_read_tokens:d.usage.cacheRead,cache_write_tokens:d.usage.cacheWrite,usd_equivalent:Re(K??a,d.usage.cost.total),served_model:C};_&&qe(_.grantee,"user",C,d.usage,B.usd_equivalent);const le={id:G,object:"chat.completion",created:Math.floor(A/1e3),model:C,served_model:C,requested_model:$,substituted:$!==C&&!/^([^/]+)\/(ultra|strong|medium|light)$/.test($),choices:[{index:0,message:{role:"assistant",content:T.join("")||null,...O.length?{reasoning_content:O.join("")}:{},...W.length?{tool_calls:W}:{},...F?{refusal:d.errorMessage??"Content filtered"}:{}},finish_reason:H}],usage:B,...t.metadata!==void 0?{metadata:t.metadata}:{}};for(const[o,h]of W.entries())D(N({tool_calls:[{index:o,...h}]}));return D({...N({},H),...F?{choices:[{index:0,delta:{},finish_reason:H,message:{refusal:d.errorMessage??"Content filtered"}}]}:{},usage:B}),{completion:le,chunks:J}}}throw j!==1/0?new l(429,"all_routes_exhausted","all eligible model routes are rate-limited",void 0,{retry_after_s:Math.max(1,Math.ceil((j-Date.now())/1e3)),served_model:y?`${y.provider}/${y.model}`:U,...y?{rate_limit:{provider:y.provider,model:y.model,...y.account?{account:y.account}:{},scope:oe(y.observation.scope),...y.observation.claim?{claim:y.observation.claim}:{},headers:y.observation.headers}}:{}},void 0,!0):t.model_policy==="exact"?new l(400,"substitution_refused","the requested model is unavailable"):new l(404,"model_unavailable","no usable model route is available")}p(He,"runLlm");export{We as parseLlmRequest,He as runLlm};
3
+ `),p=t.tools?.map(({function:a})=>({name:a.name,description:a.description??"",parameters:a.parameters??{type:"object",properties:{}}}));return{...m?{systemPrompt:m}:{},messages:n.slice(y.length),...p?{tools:p}:{}}}u(Re,"contextOf");function se(t,e,n,r){if(!t){const m=ee(n,r),p=Object.values({...m.modelRoutes??{},...m.legacyModelRoutes??{}}),a=p.length?[m.modelLadders.defaultProvider,...m.modelRouting?.familyOrder??[],...p.map(w=>w.family)].find(w=>p.some(I=>I.family===w)):m.modelLadders.defaultProvider??"anthropic";return{kind:"logical",family:a,strength:"strong",spec:`${a}/strong`}}const y=/^([^/]+)\/(ultra|strong|medium|light)$/.exec(t);return y?{kind:"logical",family:y[1],strength:y[2],spec:t}:{kind:e==="exact"?"exact":"concrete",spec:t}}u(se,"requestFor");function $e(t,e){const{input:n,output:r,cacheRead:y,cacheWrite:m}=t.cost;return n===0&&r===0&&y===0&&m===0?null:Number.isFinite(e)&&e>=0?e:null}u($e,"usdEquivalent");function Ie(t,e){return t.compat?.allowedFallbackModels?.find(r=>r.provider===t.provider&&r.model===e)}u(Ie,"allowedFallback");async function He(t,e,n,r,y){if(t.payer==="app")throw new l(403,"payer_not_app","app-paid inference routes are not registered");je(y);const m=await pe(),p=se(t.model,t.model_policy,r?.cwd,r?.profileId),a=u(f=>f.routeId!=="concrete"&&f.routeId!=="exact","configured"),w=u(f=>{const s=m.find(f.providerId,f.modelId);return s!==void 0&&(!t.reasoning_effort||s.reasoning)&&(t.seed===void 0&&t.response_format===void 0||s.api==="openai-completions")&&(t.stop===void 0||["openai-completions","anthropic-messages"].includes(s.api))},"matchesFeatures"),I=Y(p.kind==="exact"?{kind:"concrete",spec:p.spec}:p,r?.cwd,r?.profileId).filter(a);let x;if(p.kind==="logical")x=t.model_policy==="exact"?I.filter(f=>f.family===p.family&&f.strength===p.strength).slice(0,1):I;else{const f=ue(p.spec),s=I.filter(c=>c.providerId===f?.providerId&&c.modelId===f.modelId&&(t.model_policy!=="exact"||c.thinkingLevel===f.thinkingLevel)&&w(c));if(t.model_policy==="exact")x=s.slice(0,1);else if(s.length)x=s;else{const c=ee(r?.cwd,r?.profileId),q=[...Object.values(c.modelRoutes??{}).flatMap(i=>Object.entries(i.models)),...[c.modelLadders.anthropic,c.modelLadders.openai].flatMap(i=>Object.entries(i))].find(([,i])=>i===p.spec)?.[0],L=se(void 0,"substitute",r?.cwd,r?.profileId);x=q&&L.kind==="logical"?Y({...L,strength:q,spec:p.spec},r?.cwd,r?.profileId).filter(i=>a(i)&&i.strength===q):[]}}if(x=x.filter(w),x.length===0)throw t.model_policy==="exact"?new l(400,"substitution_refused","no eligible model route is configured"):new l(404,"model_unavailable","no eligible model route is configured");const G=`chatcmpl-${ce()}`;let j=1/0,_,J;for(const f of x){if(!me(f,m).authenticated)continue;const s=m.find(f.providerId,f.modelId);if(!s)continue;const c=f.providerId,S=E(c)?ye(c).filter(i=>!i.authFailure):[],q=S.filter(i=>te(i,s.id)<=Date.now());if(S.length&&!q.length){const i=Math.min(...S.map(A=>te(A,s.id)));i<j&&(j=i,J=`${s.provider}/${s.id}`);continue}const L=S.length?q:[void 0];for(const i of L){const A=Date.now();let Q=i?.access;if(i&&i.expires<=Date.now()&&E(c)){const o=m.getProvider(c)?.auth.oauth;if(!o)throw new l(503,"upstream_model_unavailable",`no OAuth refresh for ${c}`,void 0,{served_model:`${s.provider}/${s.id}`});Q=(await ve(c,i.label,async(g,X)=>{const P=await o.refresh({type:"oauth",refresh:g,access:"",expires:0,...X?{clientId:X}:{}},e??new AbortController().signal),B=P.clientId;return{refresh:P.refresh,access:P.access,expires:P.expires,...typeof B=="string"&&B?{clientId:B}:{}}})).access}const ie=Re(t),M=i?void 0:await m.getApiKeyAndHeaders(s);if(M&&!M.ok)continue;const v=`${s.provider}/${s.id}`,R=t.model??v;let k=!1,V,b=3e4,$;const U=[],T=[],O=[];let d;const N=u((o,h=null)=>({id:G,object:"chat.completion.chunk",created:Math.floor(A/1e3),model:v,served_model:v,requested_model:R,substituted:R!==v&&!/^([^/]+)\/(ultra|strong|medium|light)$/.test(R),choices:[{index:0,delta:o,finish_reason:h}]}),"chunk"),D=u(o=>{U.push(o),n?.(o)},"emit"),ae=fe(s,ie,{apiKey:Q??(M?.ok?M.apiKey:void 0),...M?.ok?{headers:M.headers}:{},signal:e,reasoning:t.reasoning_effort??f.thinkingLevel,...t.temperature!==void 0?{temperature:t.temperature}:{},...t.max_tokens!==void 0?{maxTokens:t.max_tokens}:{},...t.top_p!==void 0?{samplingParams:{top_p:t.top_p}}:{},...t.tool_choice==="none"?{toolChoice:"none"}:{},onPayload:u(o=>{if(!o||typeof o!="object")return;const h=t.tool_choice,g=h==="required"?s.api==="anthropic-messages"?{type:"any"}:"required":typeof h=="object"&&h!==null?s.api==="anthropic-messages"?{type:"tool",name:h.function.name}:h:void 0;return{...o,...g?{tool_choice:g}:{},...t.stop!==void 0?{[s.api==="anthropic-messages"?"stop_sequences":"stop"]:typeof t.stop=="string"?[t.stop]:t.stop}:{},...t.seed!==void 0?{seed:t.seed}:{},...t.response_format!==void 0?{response_format:t.response_format}:{}}},"onPayload"),fetch:u(async(o,h)=>{const g=await fetch(o,h);return V=g.status,g.status===429&&(k=!0,$=g.headers,b=Z(g.headers)??15e3),g},"fetch"),onResponse:u(o=>{o.status===429&&(k=!0,$=o.headers,b=Z(o.headers)??15e3)},"onResponse")});try{for await(const o of ae)o.type==="text_delta"&&(T.push(o.delta),D(N({content:o.delta}))),o.type==="thinking_delta"&&(O.push(o.delta),D(N({reasoning_content:o.delta}))),o.type==="done"&&(d=o.message),o.type==="error"&&(d=o.error)}catch(o){const h=ge(o);$===void 0&&o&&typeof o=="object"&&"headers"in o&&Object.keys(xe(o.headers)).length&&($=o.headers);const g=ne({status:o&&typeof o=="object"&&"status"in o?o.status:void 0,message:o instanceof Error?o.message:String(o)});if(!k&&g.kind!=="rate-limit")throw U.length?new l(503,"upstream_model_unavailable",o instanceof Error?o.message:String(o),void 0,{served_model:v}):o;k=!0,b=h??b}if(!k&&d?.stopReason==="error"&&ne({message:d.errorMessage}).kind==="rate-limit"&&(k=!0,b=15e3),k){const o=we($,s.id);if(Z($)===void 0&&o.resetAt!==void 0&&(b=Math.max(b,o.resetAt-Date.now())),_={provider:c,model:s.id,...i?{account:i.label}:{},observation:o},console.error(`[llm] 429 from ${v}${i?` account "${i.label}"`:""}: scope=${oe(o.scope)} retry_after_ms=${b} headers=${JSON.stringify(o.headers)}`),i&&E(c)&&await he(c,i.label,b,A,Date.now(),o.scope),Date.now()+b<j&&(j=Date.now()+b,J=v),T.length||O.length)throw new l(503,"service_unavailable","vendor rate limit after output began",void 0,{served_model:v});continue}const F=ke(d);if(V===400&&t.tool_choice!==void 0&&t.tool_choice!=="auto"&&t.tool_choice!=="none"&&/\btool[_ ]choice\b/i.test(d?.errorMessage??"")&&/not supported|does not support|unsupported|cannot|must be auto/i.test(d?.errorMessage??""))throw new l(400,"invalid_request",d.errorMessage,"tool_choice",{served_model:v});if(!d||d.stopReason==="error"&&!F||d.stopReason==="aborted")throw new l(503,"upstream_model_unavailable",d?.errorMessage??"model request failed",void 0,{served_model:v});i&&E(c)&&be(i,s.id)&&await _e(c,i.label,A,s.id);const W=d.content.filter(o=>o.type==="toolCall").map(o=>({id:o.id,type:"function",function:{name:o.name,arguments:JSON.stringify(o.arguments)}})),H=F?"content_filter":d.stopReason==="toolUse"?"tool_calls":d.stopReason==="length"?"length":"stop",K=d.responseModel?Ie(s,d.responseModel):void 0,C=K?`${s.provider}/${K.model}`:v,z={prompt_tokens:d.usage.input,completion_tokens:d.usage.output,total_tokens:d.usage.totalTokens,cache_read_tokens:d.usage.cacheRead,cache_write_tokens:d.usage.cacheWrite,usd_equivalent:$e(K??s,d.usage.cost.total),served_model:C};y&&qe(y.grantee,"user",C,d.usage,z.usd_equivalent);const le={id:G,object:"chat.completion",created:Math.floor(A/1e3),model:C,served_model:C,requested_model:R,substituted:R!==C&&!/^([^/]+)\/(ultra|strong|medium|light)$/.test(R),choices:[{index:0,message:{role:"assistant",content:T.join("")||null,...O.length?{reasoning_content:O.join("")}:{},...W.length?{tool_calls:W}:{},...F?{refusal:d.errorMessage??"Content filtered"}:{}},finish_reason:H}],usage:z,...t.metadata!==void 0?{metadata:t.metadata}:{}};for(const[o,h]of W.entries())D(N({tool_calls:[{index:o,...h}]}));return D({...N({},H),...F?{choices:[{index:0,delta:{},finish_reason:H,message:{refusal:d.errorMessage??"Content filtered"}}]}:{},usage:z}),{completion:le,chunks:U}}}throw j!==1/0?new l(429,"all_routes_exhausted","all eligible model routes are rate-limited",void 0,{retry_after_s:Math.max(1,Math.ceil((j-Date.now())/1e3)),served_model:_?`${_.provider}/${_.model}`:J,..._?{rate_limit:{provider:_.provider,model:_.model,..._.account?{account:_.account}:{},scope:oe(_.observation.scope),..._.observation.claim?{claim:_.observation.claim}:{},headers:_.observation.headers}}:{}},void 0,!0):t.model_policy==="exact"?new l(400,"substitution_refused","the requested model is unavailable"):new l(404,"model_unavailable","no usable model route is available")}u(He,"runLlm");export{We as parseLlmRequest,He as runLlm};
@@ -12,6 +12,18 @@ export declare function brokerThresholdsForDaemon(): {
12
12
  * An InputError so its details round-trip through crtrd's error body and reach
13
13
  * a CLI caller as the real cap text instead of an opaque 5xx. */
14
14
  export declare function brokerCapReached(cap: number, live: number): InputError;
15
+ /** The typed refusal a caller that cannot wait gets when the app's memory cannot take another broker. */
16
+ export declare function appMemoryFull(detail: {
17
+ app: string;
18
+ free_bytes: number;
19
+ needed_bytes: number;
20
+ }): InputError;
21
+ /** The typed refusal when the app's memory cap is below what one broker needs to boot; waiting will not change it. */
22
+ export declare function appMemoryTooSmall(detail: {
23
+ app: string;
24
+ max_bytes: number;
25
+ needed_bytes: number;
26
+ }): InputError;
15
27
  /** Real child-exit status, exactly as Node reports it. Codes/signals are
16
28
  * diagnostics only — never policy inputs (design D-4). */
17
29
  export interface FleetExitStatus {
@@ -1 +1 @@
1
- var i=Object.defineProperty;var o=(e,r)=>i(e,"name",{value:r,configurable:!0});import{readConfig as s}from"../config.js";import{InputError as a}from"../io.js";import{envMaxLiveBrokers as h,envWarnLiveBrokers as c}from"../../shared/env.js";function l(){const e=s("user").brokerThresholds,r=h()??e.automaticReviveCap,t=c()??e.warning;return{warning:Math.min(t,r),automaticReviveCap:r}}o(l,"brokerThresholdsForDaemon");function b(e,r){return new a({error:"broker_cap_reached",message:`no broker slot is free \u2014 ${r} live brokers at the cap of ${e}.`,next:"Finish or close a live node (`crtr canvas dashboard` shows them), or raise the cap with CRTR_MAX_LIVE_BROKERS, then retry."})}o(b,"brokerCapReached");let n;function v(e){if(n!==void 0&&n!==e)throw new Error("a fleet registry is already bound for this process");n=e}o(v,"bindFleet");function m(){if(n===void 0)throw new Error("no fleet registry is bound in this process \u2014 broker launches happen only inside crtrd (route launch verbs through the /v1 API)");return n}o(m,"boundFleet");function w(){n=void 0}o(w,"resetFleetForTests");export{v as bindFleet,m as boundFleet,b as brokerCapReached,l as brokerThresholdsForDaemon,w as resetFleetForTests};
1
+ var a=Object.defineProperty;var r=(e,o)=>a(e,"name",{value:o,configurable:!0});import{readConfig as p}from"../config.js";import{InputError as s}from"../io.js";import{envMaxLiveBrokers as m,envWarnLiveBrokers as h}from"../../shared/env.js";function b(){const e=p("user").brokerThresholds,o=m()??e.automaticReviveCap,i=h()??e.warning;return{warning:Math.min(i,o),automaticReviveCap:o}}r(b,"brokerThresholdsForDaemon");function l(e,o){return new s({error:"broker_cap_reached",message:`no broker slot is free \u2014 ${o} live brokers at the cap of ${e}.`,next:"Finish or close a live node (`crtr canvas dashboard` shows them), or raise the cap with CRTR_MAX_LIVE_BROKERS, then retry."})}r(l,"brokerCapReached");const t=r(e=>Math.round(e/1024**2),"mib");function y(e){return new s({error:"app_memory_full",message:`${e.app} has ${t(e.free_bytes)} MiB of memory free and one more broker needs ${t(e.needed_bytes)} MiB.`,next:`Retry once ${e.app}'s running nodes finish.`})}r(y,"appMemoryFull");function w(e){return new s({error:"app_memory_too_small",message:`${e.app} has a memory limit of ${t(e.max_bytes)} MiB, below the ${t(e.needed_bytes)} MiB one broker needs to start.`,next:`Raise the limit with \`crtr sys limits set --app ${e.app} --memory <size>\`, then retry.`})}r(w,"appMemoryTooSmall");let n;function _(e){if(n!==void 0&&n!==e)throw new Error("a fleet registry is already bound for this process");n=e}r(_,"bindFleet");function v(){if(n===void 0)throw new Error("no fleet registry is bound in this process \u2014 broker launches happen only inside crtrd (route launch verbs through the /v1 API)");return n}r(v,"boundFleet");function x(){n=void 0}r(x,"resetFleetForTests");export{y as appMemoryFull,w as appMemoryTooSmall,_ as bindFleet,v as boundFleet,l as brokerCapReached,b as brokerThresholdsForDaemon,x as resetFleetForTests};
@@ -1,3 +1,3 @@
1
- var be=Object.defineProperty;var u=(e,n)=>be(e,"name",{value:n,configurable:!0});import{assertGranteeRunsHere as ye}from"./unsandboxed.js";import{spawn as Pe}from"node:child_process";import{mkdirSync as le,existsSync as $,unlinkSync as ue,openSync as j,closeSync as T,writeSync as Re,chmodSync as xe,lstatSync as Oe}from"node:fs";import{ensureAppSpace as Te}from"../spaces/space.js";import{sandboxCacheEnv as De}from"../spaces/cache-env.js";import{compileLaunch as Ne,launchStillGranted as Ce}from"../spaces/mounts.js";import{grantsGeneration as fe}from"../grants/mirror.js";import{LAUNCHER_SOCKET as W}from"../../launcher/spec.js";import{launcherRequest as F}from"../../launcher/client.js";import{piAgentDir as Ae}from"./package-health.js";import{existsBeneath as z,readFileBeneath as Le,readTextBeneathIfExists as Be,setModeBeneath as y,withDirBeneath as V,writeFileBeneath as G}from"../spaces/open-beneath.js";import{dirname as X,join as g,resolve as $e,isAbsolute as je}from"node:path";import{fileURLToPath as de}from"node:url";import{connectViewSocket as Fe}from"./view-socket.js";import{appSpace as K}from"../layout.js";import{readConfig as Ge,readMergedLaunchConfig as Ke}from"../config.js";import{readRotationConfig as Me}from"../subscription-state.js";import{getNode as v}from"../canvas/index.js";import{scopesFilePath as Q,crtrHome as me,nodeRunDir as Y,nodeRunJobDir as He,jobDir as Je,viewSocketPath as qe,nodeAgentDir as Ue,contextDir as We,nodeHome as ze}from"../canvas/paths.js";import{encodeFrame as Ve}from"./broker-protocol.js";import{buildBrokerEnv as Xe}from"./spawn-env.js";import{hostExecPathFresh as Qe}from"./branded-host.js";import{isPidAlive as Z,killProcessTreePids as pe,isAnyPidAlive as Ye,captureTeardownSnapshot as Ze}from"../canvas/pid.js";import{assertMandatoryCanvasExtensions as Ie}from"./canvas-extensions.js";import{boundFleet as er,brokerCapReached as rr,brokerThresholdsForDaemon as nr}from"./fleet.js";import{emitEvent as E}from"../events/emit.js";import{bindBrokerEventSource as tr,eventSource as or}from"../events/source.js";import{envBrokerEngine as ir}from"../../shared/env.js";import{conversationIdFromPath as sr,conversationStore as cr,ConversationNotFoundError as he,storeAddress as ar}from"../conversation-store/index.js";import{takeNodeToken as lr}from"../canvas/node-tokens.js";function M(){const e=X(de(import.meta.url));return g(e,"broker-cli.js")}u(M,"resolveBrokerEntry");const ur="@earendil-works/pi-coding-agent";function fr(e,n){const t=g(Ae(),"models.json"),r=g(n,"models.json"),o=Be(X(t),t);if(o===null)return;const f=JSON.parse(o),c=u(i=>Array.isArray(i)?i.map(c):i===null||typeof i!="object"?i:Object.fromEntries(Object.entries(i).filter(([s])=>!/^(apiKey|authorization|headers|token|secret)$/i.test(s)).map(([s,_])=>[s,c(_)])),"scrub"),l=`${JSON.stringify(c(f),null,2)}
2
- `;if(t===r&&o!==l){if(JSON.stringify(f)!==JSON.stringify(c(f)))throw new Error("terminal models.json contains credentials; move them into daemon secrets");y(e,r,432);return}G(e,r,l,{atomic:!0,mode:432}),y(e,r,432)}u(fr,"writeBrokerModels");function we(e){return e.env.CRTR_BROKER_ENGINE??ir()}u(we,"resolveEngineSpec");function ge(e){return e.includes("/")||e.endsWith(".jsonl")}u(ge,"isPlainLaunchSessionPath");function dr(e,n){return je(e)?e:$e(n,e)}u(dr,"resolvePlainLaunchPath");function mr(e,n,t,r){if(t==="--session"&&!ge(r))throw new Error(`[broker] refusing to launch ${e}: launch recipe passes bare --session id '${r}' \u2014 the broker fatally rejects bare ids; pass the .jsonl path instead.`);if(!ge(r))return;const o=dr(r,n),f=sr(o);try{if(f===null)throw new he(o);cr().header(f)}catch(c){if(!(c instanceof he))throw c;const l=t==="--session"?"session file":"fork source";throw new Error(`[broker] refusing to launch ${e}: launch recipe references missing ${l} '${r}' (${o})`)}}u(mr,"preflightLaunchSessionArg");function pr(e,n,t){const r=Y(e);le(r,{recursive:!0}),process.platform==="linux"&&$(W)&&Oe(r).uid===process.getuid()&&xe(r,504);const o=g(r,"broker-launch.json"),f={user:Ge("user"),launch:Ke(t,n.env.CRTR_PROFILE_ID||null),rotation:Me()};G(r,o,JSON.stringify({...n,configSnapshot:f}),{atomic:!0}),n.env.CRTR_SCOPES_FILE!==void 0&&G(r,Q(e),JSON.stringify(v(e)?.scopes??[]),{atomic:!0});const c=Le(r,o).toString("utf8"),l=JSON.parse(c);if(!Array.isArray(l.argv))throw new Error(`[broker] refusing to launch ${e}: broker-launch.json is missing the argv array`);const i=we(n);try{const d=import.meta.resolve(i);if(d.startsWith("file:")&&!$(de(d)))throw new Error(`resolved to missing file ${d}`)}catch(d){throw new Error(`[broker] refusing to launch ${e}: cannot resolve broker engine '${i}' from ${M()} \u2014 ${d.message}`)}const s=l.argv,_=[];for(let d=0;d<s.length;d++){const P=s[d];if(P==="-e"){const D=s[++d];D!==void 0&&_.push(D);continue}if(P!=="--session"&&P!=="--fork")continue;const R=s[++d];R!==void 0&&mr(e,t,P,R)}try{Ie(_)}catch(d){throw new Error(`[broker] refusing to launch ${e}: ${d.message}`)}}u(pr,"preflightBrokerLaunch");const hr=2e3,wr=2e3,gr=150;async function H(e,n){const t=Date.now()+n;for(;Ye(e);){const r=t-Date.now();if(r<=0)return!1;await new Promise(o=>setTimeout(o,Math.min(gr,r)))}return!0}u(H,"waitForTreeExit");async function Er(e,n,t){return e.length===0?{confirmed:!0}:t&&await H(e,hr)?{confirmed:!0}:(pe(e,"SIGTERM",n),await H(e,wr)?{confirmed:!0}:(pe(e,"SIGKILL",n),await H(e,5e3)?{confirmed:!0}:{confirmed:!1,error:new Error(`broker process tree for ${e.join(", ")} remained alive after SIGKILL`)}))}u(Er,"terminateBrokerTree");function Ee(e,n){const t=v(e),r=t?.pi_pid,o=r!=null?Ze(r,t?.pi_pid_identity??null,n.excludePid):null;return o?.reused===!0&&E({level:"warn",event:"broker.teardown.pid_reuse_refused",node_id:e,fields:{recorded_pid:r,expected_identity:t?.pi_pid_identity??null}}),{tree:o?.tree??[],identities:o?.identities??null,nodeId:e,launcherPid:$(W)&&o?.tree.includes(r)?r:null}}u(Ee,"prepareBrokerTeardown");async function ke(e){if(e.launcherPid!==null){try{await F({operation:"stop",pid:e.launcherPid})}catch(n){return Z(e.launcherPid)?{confirmed:!1,error:n instanceof Error?n:new Error(String(n))}:{confirmed:!0}}return await H([e.launcherPid],5e3)?{confirmed:!0}:{confirmed:!1,error:new Error(`launcher pid ${e.launcherPid} remained alive`)}}return new Promise(n=>{let t=!1,r=!1;const o=u(i=>{r||(r=!0,n(i))},"settle"),f=u(()=>{try{ue(qe(e.nodeId))}catch{}},"cleanupSocket"),c=u(i=>{Er(e.tree,e.identities,i).then(o,s=>o({confirmed:!1,error:s instanceof Error?s:new Error(String(s))}))},"finish"),l=Fe(e.nodeId);l.once("error",()=>{t||(f(),c(!1))}),l.once("connect",()=>{t=!0;try{l.write(Ve({type:"shutdown"}))}catch{}l.end(),f(),c(!0)})})}u(ke,"runPreparedBrokerTeardown");const J=new Map;function Qr(e,n={},t){const r=J.get(e);if(r!==void 0)return t!==void 0&&r.after.push(t),r.completion;let o;const f=new Promise(i=>{o=i}),c={completion:f,resolve:o,after:t===void 0?[]:[t]};J.set(e,c);let l;try{l=Ee(e,n)}catch(i){const s={confirmed:!1,error:i instanceof Error?i:new Error(String(i))};return J.delete(e),E({level:"error",event:"broker.teardown.unconfirmed",node_id:e,error:s.error}),c.resolve(s),f}return setImmediate(()=>{J.delete(e),ke(l).then(async i=>{if(i.confirmed)for(const s of c.after)try{await s()}catch(_){E({level:"error",event:"broker.teardown.after_failed",node_id:e,error:_})}else E({level:"error",event:"broker.teardown.unconfirmed",node_id:e,error:i.error});c.resolve(i)},i=>{const s={confirmed:!1,error:i instanceof Error?i:new Error(String(i))};E({level:"error",event:"broker.teardown.unconfirmed",node_id:e,error:s.error}),c.resolve(s)})}),f}u(Qr,"requestBrokerTeardown");function ve(){return process.platform==="linux"&&$(W)}u(ve,"isolatedBrokerLauncherAvailable");const Yr={async launch(e,n,t){if(ye(v(e)?.grantee),v(e)?.isolated===!0&&!ve())throw new Error("isolated broker requires the Linux root launcher");pr(e,n,t.cwd);const r=er();if(!r.reserveSlot(e))throw rr(nr().automaticReviveCap,r.size());let o=null,f,c=!1;try{const l=Je(e);le(l,{recursive:!0}),o=j(g(l,"broker.log"),"a");const i=we(n),s={...Xe(n),CRTR_BROKER_ENGINE:i,CRTR_STORE_ADDRESS:ar()};if(i!==ur){const a=n.env.NODE_OPTIONS??process.env.NODE_OPTIONS;a!==void 0&&(s.NODE_OPTIONS=a)}or()===void 0&&tr(e);const _=v(e)?.run_id;if(_){const a=v(_);if(!a||a.deleting_at)throw new Error("run is deleting")}const d=n.env.CRTR_EXECUTION_ID;if(!d)throw new Error("broker launch has no execution id");const P=lr(d),R=ve();if(v(e)?.isolated===!0&&!R)throw new Error("isolated broker requires the Linux root launcher");if(process.platform==="linux"&&process.env.CRTR_LAUNCHER_SOCKET&&!R)throw new Error("root launcher socket is unavailable; refusing an unsandboxed broker");if(R){const a=v(e);if(!a)throw new Error(`missing node ${e}`);const w=a.grantee??"app:terminal",C=await Te(w);Object.assign(s,De(w));const m={uid:process.getuid(),gid:C.gid},p=Y(e);for(const[h,k]of[[K(w).nodes,Ue(e,w)],[K(w).nodes,We(e,w)],[p,He(e)]])V(h,k,()=>{},!0),y(h,k,1528,m);y(p,g(p,"broker-launch.json"),416,m),z(p,Q(e))&&y(p,Q(e),416,m);const re=g(p,"system-prompt.md");z(p,re)&&y(p,re,432,m);for(const h of["output-schema.json","situational-context.md"]){const k=g(p,h);z(p,k)&&y(p,k,432,m)}const ne=a.isolated===!0,S=ze(e,w,ne);ne&&(V(K(w).nodes,S,()=>{},!0),y(K(w).nodes,S,1528,m),s.HOME=S,s.PI_CODING_AGENT_DIR=g(S,".pi","agent"));const U=g(S,".pi","agent");V(S,U,()=>{},!0),fr(S,U);const te=g(U,"settings.json"),_e=g(X(M()),"../../builtin-pi-packages/pi-crtr-extensions");G(S,te,`${JSON.stringify({packages:[_e]})}
3
- `,{atomic:!0,mode:432}),y(S,te,432);const oe=g(Y(e),`token-${d}`),A=j(oe,"wx+",384);ue(oe),Re(A,P);const ie=[j(`/proc/self/fd/${A}`,"r"),j(`/proc/self/fd/${A}`,"r")];T(A);const se={nodeId:e,app:w,sub:a.sub??"user:local",grantId:a.grant_id??null,scopes:a.scopes??null,isolated:a.isolated===!0,...a.isolated===!0?{cwd:a.cwd}:{},command:[process.execPath,M(),"--canvas-home",me(),"--epoch",r.epoch(),e],env:Object.fromEntries(Object.entries({...s,...a.isolated===!0?{PWD:a.cwd}:{}}).filter(h=>h[1]!==void 0)),auditMissing:u(h=>E({level:"warn",event:"broker.mount.missing",node_id:e,fields:{source:h}}),"auditMissing")};try{let h;for(const O of ie){const L=fe(),b=Ne(se),B=await F({operation:"launch",spec:b},[O]);if(!("pid"in B))throw new Error("launcher did not return a pid");if(fe()===L||Ce(se,b)){h=B.pid;break}E({level:"info",event:"broker.launch.regranted",node_id:e,fields:{pid:B.pid}}),await F({operation:"stop",pid:B.pid,signal:"SIGKILL"}).catch(()=>{})}if(h===void 0)throw new Error("grant changed during every launch attempt");const k=h,Se=Date.now(),ce=new Promise(O=>{const L=setInterval(()=>{Z(k)||(clearInterval(L),F({operation:"exit.status",pid:k}).then(b=>"oomKilled"in b&&b.oomKilled,()=>!1).then(b=>{E({level:b?"warn":"info",event:"broker.exited",node_id:e,fields:{pid:k,uptime_ms:Date.now()-Se,oom_killed:b}}),O({code:null,signal:null,...b?{oomKilled:!0}:{}})}))},250);L.unref()});T(o),o=null;const ae={pid:k,exited:ce};return r.register(e,ae),c=!0,ce.then(O=>r.onChildExit(e,O,d)),ae}finally{for(const h of ie)T(h)}}const D=await Qe(),x=Pe(D,[M(),"--canvas-home",me(),"--epoch",r.epoch(),e],{cwd:t.cwd,detached:!0,stdio:["ignore",o,o,"pipe"],env:s});f=x;const N=x.stdio[3];if(!N||!("end"in N))throw new Error("broker token pipe did not open");N.on("error",a=>E({event:"broker.token_pipe.failed",level:"error",node_id:e,error:a})),N.end(P),T(o),o=null;const I=Date.now(),ee=new Promise(a=>{let w=!1;const C=u(m=>{w||(w=!0,a(m))},"finish");x.once("exit",(m,p)=>{E({level:m===0?"info":"warn",event:"broker.exited",node_id:e,fields:{code:m,signal:p,uptime_ms:Date.now()-I}}),C({code:m,signal:p})}),x.once("error",m=>{E({level:"error",event:"broker.launch.failed",node_id:e,error:m,fields:{uptime_ms:Date.now()-I}}),C({code:null,signal:null})})});x.unref();const q={pid:x.pid??null,exited:ee};return q.pid!==null&&(r.register(e,q),c=!0,ee.then(a=>r.onChildExit(e,a,d))),q}catch(l){if(!c&&f?.pid!=null)try{f.kill("SIGTERM")}catch{}throw l}finally{c||r.releaseReservation(e),o!==null&&T(o)}},isAlive(e){return Z((typeof e=="string"?v(e):e)?.pi_pid)},teardown(e,n={}){try{return ke(Ee(e,n))}catch(t){return Promise.resolve({confirmed:!1,error:t instanceof Error?t:new Error(String(t))})}},signal(e,n){const t=v(e)?.pi_pid;if(t!=null)try{process.kill(t,n)}catch{}}};export{Yr as headlessBrokerHost,ve as isolatedBrokerLauncherAvailable,pr as preflightBrokerLaunch,Qr as requestBrokerTeardown};
1
+ var ye=Object.defineProperty;var u=(e,t)=>ye(e,"name",{value:t,configurable:!0});import{assertGranteeRunsHere as Pe}from"./unsandboxed.js";import{spawn as Re}from"node:child_process";import{mkdirSync as ue,existsSync as j,unlinkSync as fe,openSync as F,closeSync as N,writeSync as xe,chmodSync as Oe,lstatSync as Te}from"node:fs";import{ensureAppSpace as De}from"../spaces/space.js";import{sandboxCacheEnv as Ne}from"../spaces/cache-env.js";import{compileLaunch as Ce,launchStillGranted as Ae}from"../spaces/mounts.js";import{grantsGeneration as de}from"../grants/mirror.js";import{LAUNCHER_SOCKET as z}from"../../launcher/spec.js";import{launcherRequest as M}from"../../launcher/client.js";import{piAgentDir as Le}from"./package-health.js";import{existsBeneath as V,readFileBeneath as Be,readTextBeneathIfExists as $e,setModeBeneath as P,withDirBeneath as X,writeFileBeneath as G}from"../spaces/open-beneath.js";import{dirname as Q,join as h,resolve as je,isAbsolute as Fe}from"node:path";import{fileURLToPath as me}from"node:url";import{connectViewSocket as Me}from"./view-socket.js";import{appSpace as K}from"../layout.js";import{readConfig as Ge,readMergedLaunchConfig as Ke}from"../config.js";import{readRotationConfig as He}from"../subscription-state.js";import{getNode as v}from"../canvas/index.js";import{scopesFilePath as Y,crtrHome as pe,nodeRunDir as Z,nodeRunJobDir as Je,jobDir as qe,viewSocketPath as Ue,nodeAgentDir as We,contextDir as ze,nodeHome as Ve}from"../canvas/paths.js";import{encodeFrame as Xe}from"./broker-protocol.js";import{buildBrokerEnv as Qe}from"./spawn-env.js";import{hostExecPathFresh as Ye}from"./branded-host.js";import{isPidAlive as I,killProcessTreePids as he,isAnyPidAlive as Ze,captureTeardownSnapshot as Ie}from"../canvas/pid.js";import{assertMandatoryCanvasExtensions as er}from"./canvas-extensions.js";import{appMemoryFull as rr,appMemoryTooSmall as tr,boundFleet as nr,brokerCapReached as or,brokerThresholdsForDaemon as ir}from"./fleet.js";import{admitBrokerMemory as sr}from"./memory-admission.js";import{emitEvent as g}from"../events/emit.js";import{bindBrokerEventSource as ar,eventSource as cr}from"../events/source.js";import{envBrokerEngine as lr}from"../../shared/env.js";import{conversationIdFromPath as ur,conversationStore as fr,ConversationNotFoundError as we,storeAddress as dr}from"../conversation-store/index.js";import{takeNodeToken as mr}from"../canvas/node-tokens.js";function H(){const e=Q(me(import.meta.url));return h(e,"broker-cli.js")}u(H,"resolveBrokerEntry");const pr="@earendil-works/pi-coding-agent";function hr(e,t){const n=h(Le(),"models.json"),r=h(t,"models.json"),o=$e(Q(n),n);if(o===null)return;const f=JSON.parse(o),a=u(i=>Array.isArray(i)?i.map(a):i===null||typeof i!="object"?i:Object.fromEntries(Object.entries(i).filter(([s])=>!/^(apiKey|authorization|headers|token|secret)$/i.test(s)).map(([s,_])=>[s,a(_)])),"scrub"),l=`${JSON.stringify(a(f),null,2)}
2
+ `;if(n===r&&o!==l){if(JSON.stringify(f)!==JSON.stringify(a(f)))throw new Error("terminal models.json contains credentials; move them into daemon secrets");P(e,r,432);return}G(e,r,l,{atomic:!0,mode:432}),P(e,r,432)}u(hr,"writeBrokerModels");function ge(e){return e.env.CRTR_BROKER_ENGINE??lr()}u(ge,"resolveEngineSpec");function Ee(e){return e.includes("/")||e.endsWith(".jsonl")}u(Ee,"isPlainLaunchSessionPath");function wr(e,t){return Fe(e)?e:je(t,e)}u(wr,"resolvePlainLaunchPath");function gr(e,t,n,r){if(n==="--session"&&!Ee(r))throw new Error(`[broker] refusing to launch ${e}: launch recipe passes bare --session id '${r}' \u2014 the broker fatally rejects bare ids; pass the .jsonl path instead.`);if(!Ee(r))return;const o=wr(r,t),f=ur(o);try{if(f===null)throw new we(o);fr().header(f)}catch(a){if(!(a instanceof we))throw a;const l=n==="--session"?"session file":"fork source";throw new Error(`[broker] refusing to launch ${e}: launch recipe references missing ${l} '${r}' (${o})`)}}u(gr,"preflightLaunchSessionArg");function Er(e,t,n){const r=Z(e);ue(r,{recursive:!0}),process.platform==="linux"&&j(z)&&Te(r).uid===process.getuid()&&Oe(r,504);const o=h(r,"broker-launch.json"),f={user:Ge("user"),launch:Ke(n,t.env.CRTR_PROFILE_ID||null),rotation:He()};G(r,o,JSON.stringify({...t,configSnapshot:f}),{atomic:!0}),t.env.CRTR_SCOPES_FILE!==void 0&&G(r,Y(e),JSON.stringify(v(e)?.scopes??[]),{atomic:!0});const a=Be(r,o).toString("utf8"),l=JSON.parse(a);if(!Array.isArray(l.argv))throw new Error(`[broker] refusing to launch ${e}: broker-launch.json is missing the argv array`);const i=ge(t);try{const d=import.meta.resolve(i);if(d.startsWith("file:")&&!j(me(d)))throw new Error(`resolved to missing file ${d}`)}catch(d){throw new Error(`[broker] refusing to launch ${e}: cannot resolve broker engine '${i}' from ${H()} \u2014 ${d.message}`)}const s=l.argv,_=[];for(let d=0;d<s.length;d++){const R=s[d];if(R==="-e"){const C=s[++d];C!==void 0&&_.push(C);continue}if(R!=="--session"&&R!=="--fork")continue;const O=s[++d];O!==void 0&&gr(e,n,R,O)}try{er(_)}catch(d){throw new Error(`[broker] refusing to launch ${e}: ${d.message}`)}}u(Er,"preflightBrokerLaunch");const kr=2e3,vr=2e3,_r=150;async function J(e,t){const n=Date.now()+t;for(;Ze(e);){const r=n-Date.now();if(r<=0)return!1;await new Promise(o=>setTimeout(o,Math.min(_r,r)))}return!0}u(J,"waitForTreeExit");async function Sr(e,t,n){return e.length===0?{confirmed:!0}:n&&await J(e,kr)?{confirmed:!0}:(he(e,"SIGTERM",t),await J(e,vr)?{confirmed:!0}:(he(e,"SIGKILL",t),await J(e,5e3)?{confirmed:!0}:{confirmed:!1,error:new Error(`broker process tree for ${e.join(", ")} remained alive after SIGKILL`)}))}u(Sr,"terminateBrokerTree");function ke(e,t){const n=v(e),r=n?.pi_pid,o=r!=null?Ie(r,n?.pi_pid_identity??null,t.excludePid):null;return o?.reused===!0&&g({level:"warn",event:"broker.teardown.pid_reuse_refused",node_id:e,fields:{recorded_pid:r,expected_identity:n?.pi_pid_identity??null}}),{tree:o?.tree??[],identities:o?.identities??null,nodeId:e,launcherPid:j(z)&&o?.tree.includes(r)?r:null}}u(ke,"prepareBrokerTeardown");async function ve(e){if(e.launcherPid!==null){try{await M({operation:"stop",pid:e.launcherPid})}catch(t){return I(e.launcherPid)?{confirmed:!1,error:t instanceof Error?t:new Error(String(t))}:{confirmed:!0}}return await J([e.launcherPid],5e3)?{confirmed:!0}:{confirmed:!1,error:new Error(`launcher pid ${e.launcherPid} remained alive`)}}return new Promise(t=>{let n=!1,r=!1;const o=u(i=>{r||(r=!0,t(i))},"settle"),f=u(()=>{try{fe(Ue(e.nodeId))}catch{}},"cleanupSocket"),a=u(i=>{Sr(e.tree,e.identities,i).then(o,s=>o({confirmed:!1,error:s instanceof Error?s:new Error(String(s))}))},"finish"),l=Me(e.nodeId);l.once("error",()=>{n||(f(),a(!1))}),l.once("connect",()=>{n=!0;try{l.write(Xe({type:"shutdown"}))}catch{}l.end(),f(),a(!0)})})}u(ve,"runPreparedBrokerTeardown");const q=new Map;function rt(e,t={},n){const r=q.get(e);if(r!==void 0)return n!==void 0&&r.after.push(n),r.completion;let o;const f=new Promise(i=>{o=i}),a={completion:f,resolve:o,after:n===void 0?[]:[n]};q.set(e,a);let l;try{l=ke(e,t)}catch(i){const s={confirmed:!1,error:i instanceof Error?i:new Error(String(i))};return q.delete(e),g({level:"error",event:"broker.teardown.unconfirmed",node_id:e,error:s.error}),a.resolve(s),f}return setImmediate(()=>{q.delete(e),ve(l).then(async i=>{if(i.confirmed)for(const s of a.after)try{await s()}catch(_){g({level:"error",event:"broker.teardown.after_failed",node_id:e,error:_})}else g({level:"error",event:"broker.teardown.unconfirmed",node_id:e,error:i.error});a.resolve(i)},i=>{const s={confirmed:!1,error:i instanceof Error?i:new Error(String(i))};g({level:"error",event:"broker.teardown.unconfirmed",node_id:e,error:s.error}),a.resolve(s)})}),f}u(rt,"requestBrokerTeardown");function _e(){return process.platform==="linux"&&j(z)}u(_e,"isolatedBrokerLauncherAvailable");const tt={async launch(e,t,n){if(Pe(v(e)?.grantee),v(e)?.isolated===!0&&!_e())throw new Error("isolated broker requires the Linux root launcher");Er(e,t,n.cwd);const r=nr();if(!r.reserveSlot(e))throw or(ir().automaticReviveCap,r.size());let o=null,f,a=!1;try{const l=qe(e);ue(l,{recursive:!0}),o=F(h(l,"broker.log"),"a");const i=ge(t),s={...Qe(t),CRTR_BROKER_ENGINE:i,CRTR_STORE_ADDRESS:dr()};if(i!==pr){const c=t.env.NODE_OPTIONS??process.env.NODE_OPTIONS;c!==void 0&&(s.NODE_OPTIONS=c)}cr()===void 0&&ar(e);const _=v(e)?.run_id;if(_){const c=v(_);if(!c||c.deleting_at)throw new Error("run is deleting")}const d=t.env.CRTR_EXECUTION_ID;if(!d)throw new Error("broker launch has no execution id");const R=mr(d),O=_e();if(v(e)?.isolated===!0&&!O)throw new Error("isolated broker requires the Linux root launcher");if(process.platform==="linux"&&process.env.CRTR_LAUNCHER_SOCKET&&!O)throw new Error("root launcher socket is unavailable; refusing an unsandboxed broker");if(O){const c=v(e);if(!c)throw new Error(`missing node ${e}`);const p=c.grantee??"app:terminal",x=await sr(e,{useMemo:!1});if(x.verdict==="denied")throw rr(x);if(x.verdict==="too_small")throw tr(x);const S=await De(p);Object.assign(s,Ne(p));const b={uid:process.getuid(),gid:S.gid},w=Z(e);for(const[m,E]of[[K(p).nodes,We(e,p)],[K(p).nodes,ze(e,p)],[w,Je(e)]])X(m,E,()=>{},!0),P(m,E,1528,b);P(w,h(w,"broker-launch.json"),416,b),V(w,Y(e))&&P(w,Y(e),416,b);const te=h(w,"system-prompt.md");V(w,te)&&P(w,te,432,b);for(const m of["output-schema.json","situational-context.md"]){const E=h(w,m);V(w,E)&&P(w,E,432,b)}const ne=c.isolated===!0,y=Ve(e,p,ne);ne&&(X(K(p).nodes,y,()=>{},!0),P(K(p).nodes,y,1528,b),s.HOME=y,s.PI_CODING_AGENT_DIR=h(y,".pi","agent"));const W=h(y,".pi","agent");X(y,W,()=>{},!0),hr(y,W);const oe=h(W,"settings.json"),Se=h(Q(H()),"../../builtin-pi-packages/pi-crtr-extensions");G(y,oe,`${JSON.stringify({packages:[Se]})}
3
+ `,{atomic:!0,mode:432}),P(y,oe,432);const ie=h(Z(e),`token-${d}`),L=F(ie,"wx+",384);fe(ie),xe(L,R);const se=[F(`/proc/self/fd/${L}`,"r"),F(`/proc/self/fd/${L}`,"r")];N(L);const ae={nodeId:e,app:p,sub:c.sub??"user:local",grantId:c.grant_id??null,scopes:c.scopes??null,isolated:c.isolated===!0,...c.isolated===!0?{cwd:c.cwd}:{},command:[process.execPath,H(),"--canvas-home",pe(),"--epoch",r.epoch(),e],env:Object.fromEntries(Object.entries({...s,...c.isolated===!0?{PWD:c.cwd}:{}}).filter(m=>m[1]!==void 0)),auditMissing:u(m=>g({level:"warn",event:"broker.mount.missing",node_id:e,fields:{source:m}}),"auditMissing")};try{let m;for(const D of se){const B=de(),k=Ce(ae);k.broker=!0;const $=await M({operation:"launch",spec:k},[D]);if(!("pid"in $))throw new Error("launcher did not return a pid");if(de()===B||Ae(ae,k)){m=$.pid;break}g({level:"info",event:"broker.launch.regranted",node_id:e,fields:{pid:$.pid}}),await M({operation:"stop",pid:$.pid,signal:"SIGKILL"}).catch(()=>{})}if(m===void 0)throw new Error("grant changed during every launch attempt");const E=m,be=Date.now(),ce=new Promise(D=>{const B=setInterval(()=>{I(E)||(clearInterval(B),M({operation:"exit.status",pid:E}).then(k=>"oomKilled"in k&&k.oomKilled,()=>!1).then(k=>{g({level:k?"warn":"info",event:"broker.exited",node_id:e,fields:{pid:E,uptime_ms:Date.now()-be,oom_killed:k}}),D({code:null,signal:null,...k?{oomKilled:!0}:{}})}))},250);B.unref()});N(o),o=null;const le={pid:E,exited:ce};return r.register(e,le),a=!0,ce.then(D=>r.onChildExit(e,D,d)),le}finally{for(const m of se)N(m)}}const C=await Ye(),T=Re(C,[H(),"--canvas-home",pe(),"--epoch",r.epoch(),e],{cwd:n.cwd,detached:!0,stdio:["ignore",o,o,"pipe"],env:s});f=T;const A=T.stdio[3];if(!A||!("end"in A))throw new Error("broker token pipe did not open");A.on("error",c=>g({event:"broker.token_pipe.failed",level:"error",node_id:e,error:c})),A.end(R),N(o),o=null;const ee=Date.now(),re=new Promise(c=>{let p=!1;const x=u(S=>{p||(p=!0,c(S))},"finish");T.once("exit",(S,b)=>{g({level:S===0?"info":"warn",event:"broker.exited",node_id:e,fields:{code:S,signal:b,uptime_ms:Date.now()-ee}}),x({code:S,signal:b})}),T.once("error",S=>{g({level:"error",event:"broker.launch.failed",node_id:e,error:S,fields:{uptime_ms:Date.now()-ee}}),x({code:null,signal:null})})});T.unref();const U={pid:T.pid??null,exited:re};return U.pid!==null&&(r.register(e,U),a=!0,re.then(c=>r.onChildExit(e,c,d))),U}catch(l){if(!a&&f?.pid!=null)try{f.kill("SIGTERM")}catch{}throw l}finally{a||r.releaseReservation(e),o!==null&&N(o)}},isAlive(e){return I((typeof e=="string"?v(e):e)?.pi_pid)},teardown(e,t={}){try{return ve(ke(e,t))}catch(n){return Promise.resolve({confirmed:!1,error:n instanceof Error?n:new Error(String(n))})}},signal(e,t){const n=v(e)?.pi_pid;if(n!=null)try{process.kill(n,t)}catch{}}};export{tt as headlessBrokerHost,_e as isolatedBrokerLauncherAvailable,Er as preflightBrokerLaunch,rt as requestBrokerTeardown};
@@ -0,0 +1,38 @@
1
+ import { type LauncherReply } from '../../launcher/spec.js';
2
+ /** The launcher's verdict, or a denial replayed from the memo. */
3
+ export type BrokerMemoryVerdict = {
4
+ verdict: 'admitted';
5
+ } | {
6
+ verdict: 'denied';
7
+ app: string;
8
+ level: 'app' | 'apps';
9
+ free_bytes: number;
10
+ needed_bytes: number;
11
+ } | {
12
+ verdict: 'too_small';
13
+ app: string;
14
+ max_bytes: number;
15
+ needed_bytes: number;
16
+ };
17
+ /** How long a denial answers further admissions of the same app without asking the launcher. */
18
+ export declare const DENIAL_MEMO_MS = 1000;
19
+ /** How long after a denial broker supervision treats the app as short of memory. */
20
+ export declare const MEMORY_PRESSURE_MS = 10000;
21
+ interface Deps {
22
+ available: () => boolean;
23
+ request: (app: string, nodeId: string) => Promise<LauncherReply>;
24
+ }
25
+ /** Tests replace the launcher; `undefined` restores it. Either way the denial memo is cleared. */
26
+ export declare function stubMemoryAdmissionForTests(stub?: Deps): void;
27
+ /**
28
+ * Whether the node's app has memory for one more broker. Admits with no I/O where no root launcher runs (macOS,
29
+ * Linux without one). A launcher error admits: the kernel cap stays the hard limit. `useMemo: false` is the host's
30
+ * backstop, which must reach the launcher because the caller's admission left a ticket there that a memo would hide.
31
+ */
32
+ export declare function admitBrokerMemory(nodeId: string, opts?: {
33
+ useMemo?: boolean;
34
+ now?: number;
35
+ }): Promise<BrokerMemoryVerdict>;
36
+ /** Whether the app (or the shared `apps/` level) was denied memory in the last `MEMORY_PRESSURE_MS`. */
37
+ export declare function memoryDeniedRecently(app: string, now: number): boolean;
38
+ export {};
@@ -0,0 +1 @@
1
+ var f=Object.defineProperty;var n=(e,r)=>f(e,"name",{value:r,configurable:!0});import{existsSync as p}from"node:fs";import{launcherRequest as u}from"../../launcher/client.js";import{LAUNCHER_SOCKET as _}from"../../launcher/spec.js";import{getNode as v}from"../canvas/index.js";import{emitEvent as y}from"../events/emit.js";import{isSafeNodeId as b}from"../canvas/paths.js";const M=1e3,x=1e4,a="*",l={available:n(()=>process.platform==="linux"&&p(_),"available"),request:n((e,r)=>u({operation:"memory.admit",app:e,nodeId:r}),"request")};let c=l;const s=new Map;function L(e){c=e??l,s.clear()}n(L,"stubMemoryAdmissionForTests");async function N(e,r={}){if(!c.available())return{verdict:"admitted"};const o=v(e)?.grantee??"app:terminal",d=r.now??Date.now();if(r.useMemo!==!1)for(const i of[o,a]){const m=s.get(i);if(m!==void 0&&d-m.at<M)return m.verdict}let t;try{t=await c.request(o,e)}catch(i){return y({level:"warn",event:"broker.memory_admission.failed",...b(e)?{node_id:e}:{},error:i}),{verdict:"admitted"}}if(!("verdict"in t))return{verdict:"admitted"};if(t.verdict==="denied"){const i={verdict:"denied",app:o,level:t.level,free_bytes:t.free_bytes,needed_bytes:t.needed_bytes};return s.set(t.level==="app"?o:a,{at:d,verdict:i}),i}return t.verdict==="too_small"?{verdict:"too_small",app:o,max_bytes:t.max_bytes,needed_bytes:t.needed_bytes}:{verdict:"admitted"}}n(N,"admitBrokerMemory");function k(e,r){return[e,a].some(o=>{const d=s.get(o);return d!==void 0&&r-d.at<x})}n(k,"memoryDeniedRecently");export{M as DENIAL_MEMO_MS,x as MEMORY_PRESSURE_MS,N as admitBrokerMemory,k as memoryDeniedRecently,L as stubMemoryAdmissionForTests};
@@ -1,3 +1,3 @@
1
- var Y=Object.defineProperty;var s=(e,r)=>Y(e,"name",{value:r,configurable:!0});import{assertGranteeRunsHere as q}from"./unsandboxed.js";import{existsSync as O}from"node:fs";import{conversationIdFromPath as p,conversationStore as h,ConversationNotFoundError as B}from"../conversation-store/index.js";import{isAbsolute as Q}from"node:path";import{findNodeBySessionFile as Z,getNode as R,updateNode as E,beginExecution as I,recordExecutionPid as ee,abortExecution as S,endLiveExecution as re,unbindSession as D,recordLaunch as ie,setFrozen as ne,clearFrozen as oe,fullName as T}from"../canvas/index.js";import{emitEvent as te}from"../events/emit.js";import{cancelCronsOnWake as se}from"../canvas/crons.js";import{isParked as le,transition as x}from"./lifecycle.js";import{publishNotice as ce}from"./close.js";import{InputError as $}from"../io.js";import{buildPiArgv as v}from"./launch.js";import{buildReviveKickoff as ae,drainBearings as ue,hasRetainedReports as fe,readGoal as g,readYieldMessage as he,RUNTIME_RESTART_CONTINUATION as de}from"./kickoff.js";import{readRoadmap as z}from"./roadmap.js";import{encodeKickoffOrigin as pe,KICKOFF_ORIGIN_ENV as ve}from"./stamp/protocol.js";import{headlessBrokerHost as H}from"./host.js";import{boundFleet as U,brokerCapReached as we,brokerThresholdsForDaemon as K}from"./fleet.js";import{beginBootFaultAttempt as me,clearFault as _e,hasAdmittedTurn as ke}from"../canvas/recovery.js";import{clearNodeLedger as G,forkNodeLedger as ge,noticeChangedWhileDone as ye}from"./ledger.js";import{forkSourceNode as be,rootOfSpine as Re}from"./nodes.js";import{isReviewCompanionBound as Ee}from"../review/companion.js";import{assertProfileActive as Se}from"../profiles/manifest.js";import{isPidAlive as xe}from"../canvas/pid.js";import{crtrHome as $e,jobDir as Ce}from"../canvas/paths.js";import{contextDir as Ne,ensureNodeDirs as Pe,nodeAgentDir as Ae}from"../canvas/node-agent-paths.js";import{discoverLifecycleHookRegistry as Fe}from"../command-hooks/discovery.js";import{invokeLifecycleHooks as Oe}from"../command-hooks/transport/exec-lifecycle.js";function Be(e,r){const i=e.pi_session_file,n=i?p(i):null;if(n===null)return{};try{const t=h().header(n);return r?{resumeSessionPath:t.path}:{resumeSessionPath:t.path,newCycle:!0}}catch(t){if(t instanceof B)return{};throw t}}s(Be,"resumeArgs");function L(e){const r=p(e);if(r===null)return!1;try{return h().header(r),!0}catch(i){if(i instanceof B)return!1;throw i}}s(L,"conversationExists");function De(e){const r=p(e);return r===null?!0:h().read(r).entries.some(i=>i.type==="message")}s(De,"sessionHasConversation");function Te(e){const r=p(e);if(r===null)return!0;const i=h().header(r).header.cwd;return typeof i!="string"||O(i)}s(Te,"sessionCwdExists");function ze(e,r,i){const n=r.pi_session_file;if(!n||!L(n))return{};const t=Z(n);return t!==null&&t.node_id!==e&&xe(t.pi_pid)?{}:Te(n)?Be(r,i):{}}s(ze,"resumableSessionArgs");function He(e){return z(e)===null&&he(e)===null&&!fe(e)}s(He,"isUnstartedBirth");function pr(e){const r=g(e);if(r!==null&&r.trim()!=="")return!0;const i=z(e);return i!==null&&i.trim()!==""}s(pr,"hasFreshGroundState");async function vr(e,r){const i=await M(e,r,!0);if(i.parked)throw new Error(`reviveNode: internal parked decision for activating revive ${e}`);return i.result}s(vr,"reviveNode");async function wr(e,r,i){return M(e,{...r,viewerAttach:!0},i)}s(wr,"decideAttachRevive");async function M(e,r,i){let n=R(e);if(n===null)throw new Error(`reviveNode: unknown node ${e}`);if(n=R(e)??n,n.run_id&&R(n.run_id)?.deleting_at)throw new Error("run is deleting");if(n.final_report!==null)throw new Error(`reviveNode: refusing to revive ${e} \u2014 its finalization latch is set (final_report=${n.final_report}). A final landed between the caller's reopen-gate check and this revive; re-run with --reopen if this retask is still wanted.`);Se(n.profile_id),q(n.grantee);const t=U();if(t.has(e))return x(e,n.intent==="refresh"?"refresh-launch":"revive"),{parked:!1,result:{window:null,session:n.tmux_session??null,resumed:!1,outcome:"already-live"}};if(t.isClaimed(e))return{parked:!1,result:{window:null,session:n.tmux_session??null,resumed:!1,outcome:"already-live"}};if(!i&&n.lifecycle==="resident"&&le(n))return{parked:!0};if(!t.reserveSlot(e)){if(r.capacity==="refuse")throw we(K().automaticReviveCap,t.size());return n.frozen_at==null&&(ne(e,new Date().toISOString()),re(e,{reason:"frozen"}),te({level:"info",event:"broker.frozen",node_id:e,fields:{live_count:t.size(),hard_cap:K().automaticReviveCap}})),{parked:!1,result:{window:null,session:n.tmux_session??null,resumed:!1,outcome:"frozen"}}}try{return{parked:!1,result:await Ue(e,n,r)}}finally{t.releaseReservation(e)}}s(M,"decideRevive");async function Ue(e,r,i){if(!O(r.cwd)){const w=r.fork_from!=null?`Restore ${r.cwd}, then revive the fork normally with \`crtr node lifecycle revive ${e}\`.`:r.managed_worktree?.state==="open"?"Restore or reconcile the recorded checkout, then use the managed-worktree close or abandon recovery.":`Recreate ${r.cwd}, or repair it with \`crtr node config --cwd <path> --node ${e}\`; durable node work resumes in a fresh conversation. Then retry \`crtr node lifecycle revive ${e}\`.`;throw new $({error:"cwd_missing",message:`${e} cannot be revived \u2014 its recorded cwd no longer exists: ${r.cwd}`,next:w})}r.host_kind!=="broker"&&(E(e,{host_kind:"broker"}),r.host_kind="broker"),r.review_binding!=null&&!Ee(e)&&(r.pi_session_id!=null||r.pi_session_file!=null)&&(D(e),r.pi_session_id=null,r.pi_session_file=null);const t=r.fork_from!=null&&r.pi_session_id==null,c=r.fork_source_file;if(t&&(typeof c!="string"||!Q(c)))throw new $({error:"fork_source_invalid",message:`${e} cannot be revived \u2014 its resolved fork source file is missing or invalid.`,next:`Restore the node's fork source metadata, then retry \`crtr node lifecycle revive ${e}\`.`});const d=t&&typeof c=="string";if(d&&!L(c))throw new $({error:"fork_from_missing",message:`${e} cannot be revived \u2014 its fork source no longer exists: ${c}`,next:`Restore ${c}, or update the node's fork source, then retry \`crtr node lifecycle revive ${e}\`.`});const u=I(e,{daemonEpoch:U().epoch()});try{r.cycles=(r.cycles??0)+1,E(e,{cycles:r.cycles});const j=r.intent==="refresh"||r.cycle_pending===!0?!1:i.resume,f=d?{}:ze(e,r,j),m=f.resumeSessionPath!==void 0,C=m&&f.newCycle===!0,_=m&&!C,N=!m&&(i.wakeReason===void 0||i.wakeReason==="runtime-restart-abort")&&He(e),y=_&&(i.wakeReason===void 0||i.wakeReason==="runtime-restart-abort")&&!ke(e)&&!De(f.resumeSessionPath)?g(e)?.trim():void 0,P=y!==void 0&&y!=="",W=d||N||P,A=r.cycle_pending===!0||C;E(e,{cycle_pending:A}),r.cycle_pending=A,!d&&!m&&(r.pi_session_id!=null||r.pi_session_file!=null)&&(D(e),r.pi_session_id=null,r.pi_session_file=null);let l,a=null;const b=s(()=>({type:"runtime",kind:"kickoff",...r.spawned_by!=null&&r.spawned_by!==""?{spawnedBy:r.spawned_by}:{}}),"birthOrigin");if(d){const o=g(e),F=p(c);if(F===null)throw new Error(`invalid fork source ${c}`);const X=h().header(h().fork(F,e)).path;l=v(r,{resumeSessionPath:X,...o!=null&&o!==""?{prompt:o}:{}}),o!=null&&o!==""&&(a=b()),ge(e,be(c)?.node_id??null)}else if(P)l=v(r,{...f,prompt:y}),a=b();else if(_)l=v(r,{...f,...i.wakeReason==="runtime-restart-abort"?{prompt:de}:{}}),i.wakeReason==="runtime-restart-abort"&&(a={type:"runtime",kind:"restart-continuation"});else if(N){const o=g(e)?.trim();l=v(r,o!==void 0&&o!==""?{prompt:o}:{}),o!==void 0&&o!==""&&(a=b()),G(e)}else{const o=ue(r,u);l=v(r,{...f,prompt:ae(r,o,i.wakeReason==="runtime-restart-abort"?void 0:i.wakeReason)}),r.run_id&&o.unreadMessageIds?.length&&(l.env.CRTR_KICKOFF_MESSAGE_IDS=JSON.stringify(o.unreadMessageIds)),a=i.wakeReason!==void 0&&i.wakeReason!=="runtime-restart-abort"?{type:"runtime",kind:"wake",wake:i.wakeReason}:{type:"runtime",kind:"revive",cycle:r.cycles??0},G(e)}l.env={...l.env,CRTR_SUBTREE:Re(e)},a!==null&&(l.env[ve]=pe(a)),r.status==="done"&&r.last_settled_at!=null&&ye(e,r.last_settled_at),x(e,r.intent==="refresh"?"refresh-launch":"revive"),_e(e,{preserveEpisode:!0});const J=me(e);i.recovery!==!0&&i.viewerAttach!==!0&&se(e),Pe(e,r.grantee);const V=Fe(r.cwd,r.profile_id).plans.get("node:start")??[];await Oe(V,{node:{id:e,name:r.name,kind:r.kind,mode:r.mode,lifecycle:r.lifecycle,cwd:r.cwd,nodeDir:Ae(e),contextDir:Ne(e),jobDir:Ce(e)},runtime:{isBirth:W,canvasHome:$e(),profile:r.profile_id??null}}),l.env={...l.env,CRTR_EXECUTION_ID:u};let k;try{if(k=await H.launch(e,l,{cwd:r.cwd,name:T(r),resuming:_}),k.pid===null)throw new Error(`broker host returned no pid for ${e}`)}catch(o){try{x(e,"crash",{reason:"launch_failed",outcome:{detail:{schema:"crtr.node-outcome-detail/v1",message:o instanceof Error?o.message:String(o)}}}),ce(e,"notice.crash",`Child crashed \u2014 ${T(r)} (${e}) failed to relaunch and is now dead: ${o.message}
1
+ var Y=Object.defineProperty;var l=(e,r)=>Y(e,"name",{value:r,configurable:!0});import{assertGranteeRunsHere as q}from"./unsandboxed.js";import{existsSync as B}from"node:fs";import{conversationIdFromPath as p,conversationStore as h,ConversationNotFoundError as O}from"../conversation-store/index.js";import{isAbsolute as Q}from"node:path";import{findNodeBySessionFile as Z,getNode as R,updateNode as E,beginExecution as I,recordExecutionPid as ee,abortExecution as S,endLiveExecution as re,unbindSession as T,recordLaunch as ie,setFrozen as ne,clearFrozen as te,fullName as D}from"../canvas/index.js";import{emitEvent as oe}from"../events/emit.js";import{cancelCronsOnWake as se}from"../canvas/crons.js";import{isParked as le,transition as x}from"./lifecycle.js";import{publishNotice as ce}from"./close.js";import{InputError as $}from"../io.js";import{buildPiArgv as v}from"./launch.js";import{buildReviveKickoff as ae,drainBearings as ue,hasRetainedReports as fe,readGoal as y,readYieldMessage as de,RUNTIME_RESTART_CONTINUATION as he}from"./kickoff.js";import{readRoadmap as z}from"./roadmap.js";import{encodeKickoffOrigin as pe,KICKOFF_ORIGIN_ENV as ve}from"./stamp/protocol.js";import{headlessBrokerHost as H}from"./host.js";import{appMemoryFull as me,appMemoryTooSmall as we,boundFleet as M,brokerCapReached as _e,brokerThresholdsForDaemon as U}from"./fleet.js";import{admitBrokerMemory as ke}from"./memory-admission.js";import{beginBootFaultAttempt as ye,clearFault as ge,hasAdmittedTurn as be}from"../canvas/recovery.js";import{clearNodeLedger as K,forkNodeLedger as Re,noticeChangedWhileDone as Ee}from"./ledger.js";import{forkSourceNode as Se,rootOfSpine as xe}from"./nodes.js";import{isReviewCompanionBound as $e}from"../review/companion.js";import{assertProfileActive as Ce}from"../profiles/manifest.js";import{isPidAlive as Ne}from"../canvas/pid.js";import{crtrHome as Fe,jobDir as Pe}from"../canvas/paths.js";import{contextDir as Ae,ensureNodeDirs as Be,nodeAgentDir as Oe}from"../canvas/node-agent-paths.js";import{discoverLifecycleHookRegistry as Te}from"../command-hooks/discovery.js";import{invokeLifecycleHooks as De}from"../command-hooks/transport/exec-lifecycle.js";function ze(e,r){const i=e.pi_session_file,n=i?p(i):null;if(n===null)return{};try{const t=h().header(n);return r?{resumeSessionPath:t.path}:{resumeSessionPath:t.path,newCycle:!0}}catch(t){if(t instanceof O)return{};throw t}}l(ze,"resumeArgs");function G(e){const r=p(e);if(r===null)return!1;try{return h().header(r),!0}catch(i){if(i instanceof O)return!1;throw i}}l(G,"conversationExists");function He(e){const r=p(e);return r===null?!0:h().read(r).entries.some(i=>i.type==="message")}l(He,"sessionHasConversation");function Me(e){const r=p(e);if(r===null)return!0;const i=h().header(r).header.cwd;return typeof i!="string"||B(i)}l(Me,"sessionCwdExists");function Ue(e,r,i){const n=r.pi_session_file;if(!n||!G(n))return{};const t=Z(n);return t!==null&&t.node_id!==e&&Ne(t.pi_pid)?{}:Me(n)?ze(r,i):{}}l(Ue,"resumableSessionArgs");function Ke(e){return z(e)===null&&de(e)===null&&!fe(e)}l(Ke,"isUnstartedBirth");function _r(e){const r=y(e);if(r!==null&&r.trim()!=="")return!0;const i=z(e);return i!==null&&i.trim()!==""}l(_r,"hasFreshGroundState");async function kr(e,r){const i=await L(e,r,!0);if(i.parked)throw new Error(`reviveNode: internal parked decision for activating revive ${e}`);return i.result}l(kr,"reviveNode");async function yr(e,r,i){return L(e,{...r,viewerAttach:!0},i)}l(yr,"decideAttachRevive");async function L(e,r,i){let n=R(e);if(n===null)throw new Error(`reviveNode: unknown node ${e}`);if(n=R(e)??n,n.run_id&&R(n.run_id)?.deleting_at)throw new Error("run is deleting");if(n.final_report!==null)throw new Error(`reviveNode: refusing to revive ${e} \u2014 its finalization latch is set (final_report=${n.final_report}). A final landed between the caller's reopen-gate check and this revive; re-run with --reopen if this retask is still wanted.`);Ce(n.profile_id),q(n.grantee);const t=M();if(t.has(e))return x(e,n.intent==="refresh"?"refresh-launch":"revive"),{parked:!1,result:{window:null,session:n.tmux_session??null,resumed:!1,outcome:"already-live"}};if(t.isClaimed(e))return{parked:!1,result:{window:null,session:n.tmux_session??null,resumed:!1,outcome:"already-live"}};if(!i&&n.lifecycle==="resident"&&le(n))return{parked:!0};const c=t.reserveSlot(e);let s=null;if(c){try{s=await ke(e)}catch(u){throw t.releaseReservation(e),u}s.verdict!=="admitted"&&t.releaseReservation(e)}if(s?.verdict==="too_small")throw we(s);if(!c||s?.verdict==="denied"){if(r.capacity==="refuse")throw s?.verdict==="denied"?me(s):_e(U().automaticReviveCap,t.size());return n.frozen_at==null&&(ne(e,new Date().toISOString()),re(e,{reason:"frozen"}),oe({level:"info",event:"broker.frozen",node_id:e,fields:s?.verdict==="denied"?{reason:"app_memory",free_bytes:s.free_bytes,needed_bytes:s.needed_bytes}:{reason:"broker_cap",live_count:t.size(),hard_cap:U().automaticReviveCap}})),{parked:!1,result:{window:null,session:n.tmux_session??null,resumed:!1,outcome:"frozen"}}}try{return{parked:!1,result:await Ge(e,n,r)}}finally{t.releaseReservation(e)}}l(L,"decideRevive");async function Ge(e,r,i){if(!B(r.cwd)){const m=r.fork_from!=null?`Restore ${r.cwd}, then revive the fork normally with \`crtr node lifecycle revive ${e}\`.`:r.managed_worktree?.state==="open"?"Restore or reconcile the recorded checkout, then use the managed-worktree close or abandon recovery.":`Recreate ${r.cwd}, or repair it with \`crtr node config --cwd <path> --node ${e}\`; durable node work resumes in a fresh conversation. Then retry \`crtr node lifecycle revive ${e}\`.`;throw new $({error:"cwd_missing",message:`${e} cannot be revived \u2014 its recorded cwd no longer exists: ${r.cwd}`,next:m})}r.host_kind!=="broker"&&(E(e,{host_kind:"broker"}),r.host_kind="broker"),r.review_binding!=null&&!$e(e)&&(r.pi_session_id!=null||r.pi_session_file!=null)&&(T(e),r.pi_session_id=null,r.pi_session_file=null);const t=r.fork_from!=null&&r.pi_session_id==null,c=r.fork_source_file;if(t&&(typeof c!="string"||!Q(c)))throw new $({error:"fork_source_invalid",message:`${e} cannot be revived \u2014 its resolved fork source file is missing or invalid.`,next:`Restore the node's fork source metadata, then retry \`crtr node lifecycle revive ${e}\`.`});const s=t&&typeof c=="string";if(s&&!G(c))throw new $({error:"fork_from_missing",message:`${e} cannot be revived \u2014 its fork source no longer exists: ${c}`,next:`Restore ${c}, or update the node's fork source, then retry \`crtr node lifecycle revive ${e}\`.`});const u=I(e,{daemonEpoch:M().epoch()});try{r.cycles=(r.cycles??0)+1,E(e,{cycles:r.cycles});const j=r.intent==="refresh"||r.cycle_pending===!0?!1:i.resume,d=s?{}:Ue(e,r,j),w=d.resumeSessionPath!==void 0,C=w&&d.newCycle===!0,_=w&&!C,N=!w&&(i.wakeReason===void 0||i.wakeReason==="runtime-restart-abort")&&Ke(e),g=_&&(i.wakeReason===void 0||i.wakeReason==="runtime-restart-abort")&&!be(e)&&!He(d.resumeSessionPath)?y(e)?.trim():void 0,F=g!==void 0&&g!=="",W=s||N||F,P=r.cycle_pending===!0||C;E(e,{cycle_pending:P}),r.cycle_pending=P,!s&&!w&&(r.pi_session_id!=null||r.pi_session_file!=null)&&(T(e),r.pi_session_id=null,r.pi_session_file=null);let a,f=null;const b=l(()=>({type:"runtime",kind:"kickoff",...r.spawned_by!=null&&r.spawned_by!==""?{spawnedBy:r.spawned_by}:{}}),"birthOrigin");if(s){const o=y(e),A=p(c);if(A===null)throw new Error(`invalid fork source ${c}`);const X=h().header(h().fork(A,e)).path;a=v(r,{resumeSessionPath:X,...o!=null&&o!==""?{prompt:o}:{}}),o!=null&&o!==""&&(f=b()),Re(e,Se(c)?.node_id??null)}else if(F)a=v(r,{...d,prompt:g}),f=b();else if(_)a=v(r,{...d,...i.wakeReason==="runtime-restart-abort"?{prompt:he}:{}}),i.wakeReason==="runtime-restart-abort"&&(f={type:"runtime",kind:"restart-continuation"});else if(N){const o=y(e)?.trim();a=v(r,o!==void 0&&o!==""?{prompt:o}:{}),o!==void 0&&o!==""&&(f=b()),K(e)}else{const o=ue(r,u);a=v(r,{...d,prompt:ae(r,o,i.wakeReason==="runtime-restart-abort"?void 0:i.wakeReason)}),r.run_id&&o.unreadMessageIds?.length&&(a.env.CRTR_KICKOFF_MESSAGE_IDS=JSON.stringify(o.unreadMessageIds)),f=i.wakeReason!==void 0&&i.wakeReason!=="runtime-restart-abort"?{type:"runtime",kind:"wake",wake:i.wakeReason}:{type:"runtime",kind:"revive",cycle:r.cycles??0},K(e)}a.env={...a.env,CRTR_SUBTREE:xe(e)},f!==null&&(a.env[ve]=pe(f)),r.status==="done"&&r.last_settled_at!=null&&Ee(e,r.last_settled_at),x(e,r.intent==="refresh"?"refresh-launch":"revive"),ge(e,{preserveEpisode:!0});const J=ye(e);i.recovery!==!0&&i.viewerAttach!==!0&&se(e),Be(e,r.grantee);const V=Te(r.cwd,r.profile_id).plans.get("node:start")??[];await De(V,{node:{id:e,name:r.name,kind:r.kind,mode:r.mode,lifecycle:r.lifecycle,cwd:r.cwd,nodeDir:Oe(e),contextDir:Ae(e),jobDir:Pe(e)},runtime:{isBirth:W,canvasHome:Fe(),profile:r.profile_id??null}}),a.env={...a.env,CRTR_EXECUTION_ID:u};let k;try{if(k=await H.launch(e,a,{cwd:r.cwd,name:D(r),resuming:_}),k.pid===null)throw new Error(`broker host returned no pid for ${e}`)}catch(o){try{x(e,"crash",{reason:"launch_failed",outcome:{detail:{schema:"crtr.node-outcome-detail/v1",message:o instanceof Error?o.message:String(o)}}}),ce(e,"notice.crash",`Child crashed \u2014 ${D(r)} (${e}) failed to relaunch and is now dead: ${o.message}
2
2
 
3
- It stays dead until you revive it \u2014 \`crtr node lifecycle revive ${e}\`.`,{reason:"child-crashed",child:e})}catch{}throw S(u,{reason:"launch_failed"}),new Error(`[broker] refusing to revive ${e}: ${o.message}`)}if(ie(e,new Date().toISOString()),ee(u,e,k.pid)==="shutdown")throw S(u,{reason:"superseded"}),H.teardown(e),new Error(`[broker] refusing to revive ${e}: execution was superseded during launch`);return oe(e),{window:null,session:r.tmux_session??null,resumed:_,launch:k,faultExplanation:J.explanation,outcome:"launched"}}catch(w){throw S(u,{reason:"launch_failed"}),w}}s(Ue,"launchRevive");export{wr as decideAttachRevive,pr as hasFreshGroundState,Be as resumeArgs,vr as reviveNode};
3
+ It stays dead until you revive it \u2014 \`crtr node lifecycle revive ${e}\`.`,{reason:"child-crashed",child:e})}catch{}throw S(u,{reason:"launch_failed"}),new Error(`[broker] refusing to revive ${e}: ${o.message}`)}if(ie(e,new Date().toISOString()),ee(u,e,k.pid)==="shutdown")throw S(u,{reason:"superseded"}),H.teardown(e),new Error(`[broker] refusing to revive ${e}: execution was superseded during launch`);return te(e),{window:null,session:r.tmux_session??null,resumed:_,launch:k,faultExplanation:J.explanation,outcome:"launched"}}catch(m){throw S(u,{reason:"launch_failed"}),m}}l(Ge,"launchRevive");export{yr as decideAttachRevive,_r as hasFreshGroundState,ze as resumeArgs,kr as reviveNode};