@krapht/ork 1.0.0-alpha.10 → 1.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{__build_info__-DFF18K8y.mjs → __build_info__-CIchF6nr.mjs} +3 -72
- package/dist/bin.mjs +1066 -387
- package/dist/chips-BA7SrrH5.mjs +43 -0
- package/dist/{clean-DVaEH9e7.mjs → clean-Di-KT7Mf.mjs} +1 -1
- package/dist/{completion-Dv5kf598.mjs → completion-C5D0sEs5.mjs} +22 -15
- package/dist/{context-CEaKOVmG.mjs → context-BaNshWr9.mjs} +43 -31
- package/dist/{decide-3l9q8UtX.mjs → decide-B6PnKeqo.mjs} +1 -1
- package/dist/{find-DEjjOlTG.mjs → find-BicBbGMg.mjs} +1 -1
- package/dist/{format-Cb5_m-Qd.mjs → format-lWhN-jYR.mjs} +130 -35
- package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-CgkVouyG.mjs} +1 -1
- package/dist/{limits-BSwEjQST.mjs → limits-jZQp9ZJC.mjs} +1 -1
- package/dist/{list-Qxr6rwsY.mjs → list-CEaJ07jd.mjs} +1 -1
- package/dist/{mcp-CslnG5wf.mjs → mcp-DYgVA9f5.mjs} +6 -6
- package/dist/output-CM95njYt.mjs +421 -0
- package/dist/{pause-D7OVB5Ac.mjs → pause-D6PuSlNl.mjs} +4 -4
- package/dist/{relay-EceRIebO.mjs → relay-DNx7be9k.mjs} +2 -2
- package/dist/{remove-0IQZVWmI.mjs → remove-CpMb4NDl.mjs} +3 -3
- package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
- package/dist/{send-C0uwfUYi.mjs → send-CLs_fyg7.mjs} +3 -3
- package/dist/{src-cmmXKHn1.mjs → src-9iGkjnsy.mjs} +234 -150
- package/dist/{src-i5k2Y-kf.mjs → src-B6KvTm7B.mjs} +78 -11
- package/dist/{src-oaJYIddp.mjs → src-BnZgZGrf.mjs} +523 -81
- package/dist/{src-Df6KOIDC.mjs → src-D6_xjm-x.mjs} +1722 -825
- package/dist/ui/assets/{app-qs8Lofv6.js → app-CxURph7a.js} +16 -16
- package/dist/ui/assets/app-QoHVi-mb.css +1 -0
- package/dist/ui/assets/{index-DV1ZaOk-.js → index-By_8KKG6.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CBkHJdqk.mjs → ui-CIci4UBW.mjs} +4 -4
- package/dist/{usage-Cd8pEYyK.mjs → usage-GceBGMjU.mjs} +43 -32
- package/dist/{watch-DW5R2OPO.mjs → watch-BmoybMQg.mjs} +28 -16
- package/dist/{workflows-BT1gDRIV.mjs → workflows-DZvW3i1e.mjs} +3 -3
- package/dist/{worktrees-g1MQMGie.mjs → worktrees-Be51E2rB.mjs} +11 -10
- package/package.json +1 -1
- package/dist/output-Bqljtmp_.mjs +0 -40
- package/dist/ui/assets/app-C8uhHVYp.css +0 -1
|
@@ -3,7 +3,7 @@ import { LineCounter, YAMLMap, YAMLSeq, isAlias, isCollection, isMap, isNode, is
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { constants as constants$1, homedir, setPriority, tmpdir } from "node:os";
|
|
7
7
|
import { createConnection, createServer } from "node:net";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
@@ -86,6 +86,7 @@ const aws = defineIntegration({
|
|
|
86
86
|
region: z.string().optional()
|
|
87
87
|
},
|
|
88
88
|
args: ({ profile, region }) => [...profile === void 0 ? [] : ["--profile", profile], ...region === void 0 ? [] : ["--region", region]],
|
|
89
|
+
signedOut: ({ stderr }) => /Unable to locate credentials|ExpiredToken|InvalidClientTokenId|Token has expired|aws sso login|SSO session .* expired/iu.test(stderr),
|
|
89
90
|
doctor: async (exec, env) => {
|
|
90
91
|
if (!(await exec("aws", ["--version"])).ok) return {
|
|
91
92
|
name: "aws",
|
|
@@ -227,7 +228,7 @@ const installedCli = (id, program, login, rest) => ({
|
|
|
227
228
|
const unproven = rest.proven ? "" : ", unproven: not yet proven on a real session";
|
|
228
229
|
if (!version.ok) return {
|
|
229
230
|
name,
|
|
230
|
-
status:
|
|
231
|
+
status: "skip",
|
|
231
232
|
detail: `${program} CLI not found${unproven}`,
|
|
232
233
|
...rest.proven ? { hint: `Install ${program} to use the ${id} driver.` } : {}
|
|
233
234
|
};
|
|
@@ -247,7 +248,7 @@ const AGENT_DRIVERS = [
|
|
|
247
248
|
claudeCode,
|
|
248
249
|
installedCli("open-code", "opencode", "opencode auth login", {
|
|
249
250
|
label: "the `opencode` CLI",
|
|
250
|
-
proven:
|
|
251
|
+
proven: true,
|
|
251
252
|
onePerCheckout: false,
|
|
252
253
|
capabilities: {
|
|
253
254
|
questions: true,
|
|
@@ -255,13 +256,13 @@ const AGENT_DRIVERS = [
|
|
|
255
256
|
price: true,
|
|
256
257
|
midTurnMessages: false,
|
|
257
258
|
reportSchema: false,
|
|
258
|
-
turnLimit:
|
|
259
|
+
turnLimit: true,
|
|
259
260
|
mcpServers: true
|
|
260
261
|
}
|
|
261
262
|
}),
|
|
262
263
|
installedCli("command-code", "cmd", "cmd login", {
|
|
263
264
|
label: "the `cmd` CLI of Command Code",
|
|
264
|
-
proven:
|
|
265
|
+
proven: true,
|
|
265
266
|
onePerCheckout: true,
|
|
266
267
|
capabilities: {
|
|
267
268
|
questions: true,
|
|
@@ -299,6 +300,7 @@ const gh = defineIntegration({
|
|
|
299
300
|
program: "gh",
|
|
300
301
|
options: {},
|
|
301
302
|
args: () => [],
|
|
303
|
+
signedOut: ({ exitCode, stderr }) => exitCode === 4 || /\bgh auth login\b/u.test(stderr),
|
|
302
304
|
doctor: async (exec) => {
|
|
303
305
|
if (!(await exec("gh", ["--version"])).ok) return {
|
|
304
306
|
name: "gh",
|
|
@@ -1076,6 +1078,8 @@ const RulesSchema = z.strictObject({
|
|
|
1076
1078
|
permissions: ToolRulesSchema.optional().describe("Tool rules every Claude node gets on top of its own, e.g. deny: ['Bash(git push *)']."),
|
|
1077
1079
|
links: z.enum(["on", "off"]).optional().describe("Default: on, the user can link a running node to a node of another run. off: its nodes get no message tools unless they have someone to write to, and cannot be linked.")
|
|
1078
1080
|
});
|
|
1081
|
+
/** The OS priority Ork starts a run's sessions and tool commands at. */
|
|
1082
|
+
const PrioritySchema = z.enum(["low", "normal"]);
|
|
1079
1083
|
const ProjectSchema = z.strictObject({
|
|
1080
1084
|
schemaVersion: z.literal(1),
|
|
1081
1085
|
...RulesSchema.shape,
|
|
@@ -1083,7 +1087,9 @@ const ProjectSchema = z.strictObject({
|
|
|
1083
1087
|
quotas: QuotasSchema.optional().describe("Most this project may spend on this machine. The user's own caps apply too."),
|
|
1084
1088
|
calls: z.strictObject({ accept: z.array(z.string()).describe("The registered projects whose workflows may call this one's.") }).optional().describe("Default: any registered project may call this one's workflows."),
|
|
1085
1089
|
context: z.array(z.string()).optional().describe("Context files every node reads, relative to the project root; they come before the workflow's."),
|
|
1086
|
-
experimental: z.array(z.string()).optional().describe("Experimental features this project turns on.")
|
|
1090
|
+
experimental: z.array(z.string()).optional().describe("Experimental features this project turns on."),
|
|
1091
|
+
heavy: z.array(z.string().min(1)).optional().describe("The project's full-suite commands, as Bash permission patterns without Bash(…), e.g. pnpm check. A node with verify: may not run them in its session; vitest run does not cover vitest run <files>."),
|
|
1092
|
+
priority: PrioritySchema.optional().describe("Default: low, sessions and tool commands start at a lower OS priority. normal: as the ork process.")
|
|
1087
1093
|
});
|
|
1088
1094
|
/** `~/.ork/settings.yaml`: the user's own settings, and never a credential. */
|
|
1089
1095
|
const UserSettingsSchema = z.strictObject({
|
|
@@ -3213,7 +3219,7 @@ const filesOf = (dir, name) => ({
|
|
|
3213
3219
|
markdown: path.join(dir, `${name}.md`),
|
|
3214
3220
|
sidecar: path.join(dir, `${name}${AGENT_SIDECAR_SUFFIX}`)
|
|
3215
3221
|
});
|
|
3216
|
-
const write = (name, agent, options, mode) => {
|
|
3222
|
+
const write$1 = (name, agent, options, mode) => {
|
|
3217
3223
|
if (!AgentNameSchema.safeParse(name).success) return {
|
|
3218
3224
|
ok: false,
|
|
3219
3225
|
error: `\`${name}\` is not an agent name (lowercase letters, digits, '-' and '_')`
|
|
@@ -3253,7 +3259,7 @@ const write = (name, agent, options, mode) => {
|
|
|
3253
3259
|
};
|
|
3254
3260
|
};
|
|
3255
3261
|
/** Creates an agent in Ork's folder for the scope; never overwrites. */
|
|
3256
|
-
const createAgent = (name, agent, options) => write(name, agent, options, "create");
|
|
3262
|
+
const createAgent = (name, agent, options) => write$1(name, agent, options, "create");
|
|
3257
3263
|
/** Deletes an agent from Ork's folder for the scope. */
|
|
3258
3264
|
const deleteAgent = (name, options) => {
|
|
3259
3265
|
const dir = agentFolder(options);
|
|
@@ -4824,6 +4830,359 @@ const FEEDBACK_SEVERITIES = [
|
|
|
4824
4830
|
"medium",
|
|
4825
4831
|
"high"
|
|
4826
4832
|
];
|
|
4833
|
+
// ../core/src/runtime/ends.ts
|
|
4834
|
+
/**
|
|
4835
|
+
* @file How a node ended, as one of a closed set of kinds (specs/runtime/
|
|
4836
|
+
* session-ends.md): the registry that gives each kind its label, Ork's own
|
|
4837
|
+
* message, who should act and what to do, and `endFor`, which builds an end
|
|
4838
|
+
* with its raw detail scrubbed and capped. ends-legacy.ts reads one from runs
|
|
4839
|
+
* stored before ends existed.
|
|
4840
|
+
*/
|
|
4841
|
+
/** The most of a detail kept, in characters. */
|
|
4842
|
+
const END_DETAIL_LIMIT = 4096;
|
|
4843
|
+
const RETRY = "This may pass: `ork resume` tries again.";
|
|
4844
|
+
const usd$1 = (n) => `$${Number.isInteger(n) ? String(n) : n.toFixed(2)}`;
|
|
4845
|
+
const paren = (value) => value === void 0 ? "" : ` (${value})`;
|
|
4846
|
+
const colon = (reason) => reason === void 0 ? "" : `: ${reason}`;
|
|
4847
|
+
const info = (label, actor, message, hint, flags = {}) => ({
|
|
4848
|
+
label,
|
|
4849
|
+
message,
|
|
4850
|
+
actor,
|
|
4851
|
+
transient: flags.transient ?? false,
|
|
4852
|
+
stopsRun: flags.stopsRun ?? false,
|
|
4853
|
+
hint,
|
|
4854
|
+
docsAnchor: label.toLowerCase().replaceAll(" ", "-")
|
|
4855
|
+
});
|
|
4856
|
+
/** Each kind's label, message, actor, flags, hint and docs anchor. */
|
|
4857
|
+
const END_KINDS = {
|
|
4858
|
+
done: info("Done", null, () => "finished", ""),
|
|
4859
|
+
reported_failure: info("Reported failure", "agent", () => "reported that it failed", "Read its summary, fix what it names, and run it again."),
|
|
4860
|
+
no_report: info("No report", "agent", () => "ended without handing in a report", "Check that its brief says what to report; its session is in the Logs panel."),
|
|
4861
|
+
invalid_report: info("Invalid report", "agent", (f) => `handed in a report that breaks its declared output${colon(f.reason)}`, "Check that its brief asks for every field its `output.schema` declares."),
|
|
4862
|
+
turn_limit: info("Turn limit", "workflow", (f) => `reached its turn limit${paren(f.maxTurns === void 0 ? void 0 : String(f.maxTurns))}`, "Raise its `maxTurns`, or give it a smaller task."),
|
|
4863
|
+
budget_limit: info("Budget limit", "workflow", (f) => `reached its budget${paren(f.budgetUsd === void 0 ? void 0 : usd$1(f.budgetUsd))}`, "Raise `budgetUsd` on the node or `limits.budgetUsd` on the workflow, then `ork resume`."),
|
|
4864
|
+
timed_out: info("Timed out", "workflow", (f) => `reached its timeout${paren(f.timeout)}`, "Raise its `timeout`, or give it a smaller task."),
|
|
4865
|
+
context_too_long: info("Context too long", "workflow", () => "outgrew its model's context", "Give it a smaller task or less context, or a model with a larger context."),
|
|
4866
|
+
plan_limit: info("Plan limit", "account", (f) => `ran into a usage limit${paren(f.window?.replaceAll("_", " "))}`, `Wait until the limit resets. ${RETRY}`, {
|
|
4867
|
+
transient: true,
|
|
4868
|
+
stopsRun: true
|
|
4869
|
+
}),
|
|
4870
|
+
rate_limited: info("Rate limited", "provider", () => "was asked by its provider to slow down", RETRY, { transient: true }),
|
|
4871
|
+
provider_unavailable: info("Provider unavailable", "provider", () => "could not reach its provider, or the provider was overloaded", RETRY, { transient: true }),
|
|
4872
|
+
auth: info("Not signed in", "account", () => "could not sign in to its provider", "Sign in to the driver's tool again (`ork doctor` says how), then `ork resume`."),
|
|
4873
|
+
out_of_credits: info("Out of credits", "account", () => "found no credit left on its account", "Add credit to the account, or use another, then `ork resume`."),
|
|
4874
|
+
model_unavailable: info("Model unavailable", "account", () => "asked for something its model or provider can't serve", "Use another model or driver for the node."),
|
|
4875
|
+
interrupted: info("Interrupted", "user", () => "was stopped", "`ork resume` goes on from where it stopped."),
|
|
4876
|
+
tool_missing: info("Tool missing", "account", (f) => `could not start ${f.program ?? "its program"}: it isn't installed`, "Install it (`ork doctor` checks the drivers), then `ork resume`."),
|
|
4877
|
+
command_failed: info("Command failed", "workflow", (f) => `${f.program ?? "its command"} exited with ${f.exitCode === void 0 ? "an error" : String(f.exitCode)}`, "Its output is under Details."),
|
|
4878
|
+
invalid_output: info("Invalid output", "workflow", (f) => `gave JSON that breaks its declared output${colon(f.reason)}`, "Make the command print what its `output.schema` declares."),
|
|
4879
|
+
invalid_node: info("Invalid node", "workflow", (f) => `could not start${colon(f.reason)}`, "Fix the node in the workflow file (`ork validate` checks it)."),
|
|
4880
|
+
driver_crashed: info("Driver crashed", "ork", () => "lost its driver in a way Ork can't read", "Its last output is under Details; Ork files it as a bug."),
|
|
4881
|
+
unknown: info("Unknown", "ork", () => "ended for a reason Ork doesn't recognize", "Its detail is under Details; Ork files it as a bug, so a rule can be added.")
|
|
4882
|
+
};
|
|
4883
|
+
/**
|
|
4884
|
+
* An end of `kind`, its message from `facts`. The detail is scrubbed (with the
|
|
4885
|
+
* feedback redactor when `scrub` is given, secrets masked always) and capped.
|
|
4886
|
+
*/
|
|
4887
|
+
const endFor = (kind, facts = {}, detail, scrub) => {
|
|
4888
|
+
const end = {
|
|
4889
|
+
kind,
|
|
4890
|
+
message: END_KINDS[kind].message(facts)
|
|
4891
|
+
};
|
|
4892
|
+
if (detail === void 0 || detail.trim() === "") return end;
|
|
4893
|
+
const clean = scrub === void 0 ? maskSecrets(detail) : redact(detail, scrub);
|
|
4894
|
+
end.detail = clean.length > 4096 ? `${clean.slice(0, 4095)}…` : clean;
|
|
4895
|
+
return end;
|
|
4896
|
+
};
|
|
4897
|
+
// ../core/src/runtime/ends-legacy.ts
|
|
4898
|
+
/**
|
|
4899
|
+
* @file The legacy readers of how a node ended: a run stored before ends
|
|
4900
|
+
* existed gets a kind from the text it kept (specs/runtime/session-ends.md),
|
|
4901
|
+
* and the stored event is never rewritten.
|
|
4902
|
+
*/
|
|
4903
|
+
/**
|
|
4904
|
+
* Text Ork stored before ends existed, to a kind, in the plan's precedence:
|
|
4905
|
+
* interrupt, plan limit, budget, timeout, turn limit, provider error, missing
|
|
4906
|
+
* tool, failed command, crash. Frozen: old runs' text never changes, so this is
|
|
4907
|
+
* not the live provider classifier.
|
|
4908
|
+
*/
|
|
4909
|
+
const LEGACY_TEXT = [
|
|
4910
|
+
["interrupted", /\bstopped by the user\b|\bthe node was stopped\b|\bstopped while watching\b|\bwas interrupted\b|\bended interrupted\b|\bthe gate was withdrawn\b/u],
|
|
4911
|
+
["plan_limit", /\bClaude plan's .* limit was reached\b|\bquota of \$[\d.]+ is spent\b|\bended quota_exhausted\b/u],
|
|
4912
|
+
["budget_limit", /\(error_max_budget_usd\)|\bmaximum budget\b|\bnothing is left of the run's budget\b|\bbudget of \$[\d.]+ is spent\b|\bended budget_exhausted\b/u],
|
|
4913
|
+
["timed_out", /\btimed out after\b|\bno one answered the gate\b|\btimeout of \S+ passed\b|\bended timed_out\b/u],
|
|
4914
|
+
["turn_limit", /\(error_max_turns\)|\bmaximum number of turns\b/iu],
|
|
4915
|
+
["context_too_long", /\bprompt is too long\b|\bcontext[ _](?:length|window)\b/iu],
|
|
4916
|
+
["rate_limited", /\brate_limit_error\b|\brate.?limit(?:ed)?\b|\b(?:status|HTTP)\W{0,3}429\b/iu],
|
|
4917
|
+
["out_of_credits", /\bcredit balance\b|\binsufficient[ _](?:credit|funds|quota)\b/iu],
|
|
4918
|
+
["auth", /\bauthentication_error\b|\binvalid (?:x-)?api.?key\b|\bnot logged in\b|\/login\b|\b(?:status|HTTP)\W{0,3}401\b/iu],
|
|
4919
|
+
["provider_unavailable", /\boverloaded(?:_error)?\b|\b(?:status|HTTP)\W{0,3}5\d\d\b|\bapi_error\b|\bECONNRESET\b|\bETIMEDOUT\b|\bENOTFOUND\b|\bfetch failed\b/u],
|
|
4920
|
+
["model_unavailable", /\bassistant message prefill\b|\bInvalid `signature` in `thinking` block\b|\bnot_found_error\b|\binvalid_request_error\b/u],
|
|
4921
|
+
["tool_missing", /\bENOENT\b|\bcommand not found\b|\bno agent driver is ready\b/u],
|
|
4922
|
+
["invalid_output", /\$ORK_OUTPUT\b|\bthe JSON result is\b/u],
|
|
4923
|
+
["command_failed", /\bexited with \d+|^[\w.-]+ [\w-]+ failed: /u],
|
|
4924
|
+
["driver_crashed", /\bthe session ended without a result\b/u]
|
|
4925
|
+
];
|
|
4926
|
+
const textKind = (text) => LEGACY_TEXT.find(([, pattern]) => pattern.test(text))?.[0] ?? null;
|
|
4927
|
+
/** What `fieldProblems` writes (run/declared.ts). */
|
|
4928
|
+
const DECLARED_PROBLEM = /\bis missing, expected\b|\bis .+, expected\b|^the result isn't a JSON object$/u;
|
|
4929
|
+
const WITHOUT_REPORT = /^ended without a report \(([\w-]+)\): ([\s\S]*)$/u;
|
|
4930
|
+
/** A driver's subtype, read with the session's error text. */
|
|
4931
|
+
const subtypeKind = (subtype, text) => {
|
|
4932
|
+
if (subtype === "error_max_turns") return "turn_limit";
|
|
4933
|
+
if (subtype === "error_max_budget_usd") return "budget_limit";
|
|
4934
|
+
if (subtype === "error_max_structured_output_retries") return "invalid_report";
|
|
4935
|
+
const said = textKind(text);
|
|
4936
|
+
if (said !== null) return said;
|
|
4937
|
+
if (subtype === "error_driver") return "driver_crashed";
|
|
4938
|
+
return text === "no valid report" || subtype === "success" ? "no_report" : "unknown";
|
|
4939
|
+
};
|
|
4940
|
+
/**
|
|
4941
|
+
* How a finished node ended: its own `end`, or, for an event stored before ends
|
|
4942
|
+
* existed, a kind read from its `error` text (`unknown` when nothing matches),
|
|
4943
|
+
* with that text as the detail.
|
|
4944
|
+
*/
|
|
4945
|
+
const endOf = (finished) => {
|
|
4946
|
+
if (finished.end !== void 0) return finished.end;
|
|
4947
|
+
if (finished.ok) return endFor("done");
|
|
4948
|
+
const error = finished.error ?? "";
|
|
4949
|
+
return endFor(legacyKind(error, finished.output), {}, error);
|
|
4950
|
+
};
|
|
4951
|
+
const legacyKind = (error, output) => {
|
|
4952
|
+
if ("artifacts" in output) return DECLARED_PROBLEM.test(error) ? "invalid_report" : "reported_failure";
|
|
4953
|
+
const without = WITHOUT_REPORT.exec(error);
|
|
4954
|
+
if (without !== null) return subtypeKind(without[1] ?? "", without[2] ?? "");
|
|
4955
|
+
return textKind(error) ?? "unknown";
|
|
4956
|
+
};
|
|
4957
|
+
/**
|
|
4958
|
+
* How a session ended: its `end`, or, for an outcome stored before ends existed
|
|
4959
|
+
* (or a driver that does not set one yet), one read from its subtype, report
|
|
4960
|
+
* and errors.
|
|
4961
|
+
*/
|
|
4962
|
+
const outcomeEnd = (outcome) => {
|
|
4963
|
+
if (outcome.end !== void 0) return outcome.end;
|
|
4964
|
+
const { report } = outcome;
|
|
4965
|
+
if (report !== null && outcome.subtype === "success") return endFor(report.status === "fail" ? "reported_failure" : "done");
|
|
4966
|
+
const text = outcome.errors[0] ?? (report === null ? "no valid report" : "");
|
|
4967
|
+
return endFor(subtypeKind(outcome.subtype, text), {}, outcome.errors.join("\n"));
|
|
4968
|
+
};
|
|
4969
|
+
// ../core/src/feedback/end-signals.ts
|
|
4970
|
+
/**
|
|
4971
|
+
* @file The feedback a node's end files (specs/feedback.md,
|
|
4972
|
+
* specs/runtime/session-ends.md), by who should act on it: Ork's own bug, the
|
|
4973
|
+
* workflow's, or the agent's context. An account's, a provider's or the
|
|
4974
|
+
* user's end is a setup hint on the screen, not feedback.
|
|
4975
|
+
*/
|
|
4976
|
+
/**
|
|
4977
|
+
* Ends that are a node's work saying no, not a problem to act on: an agent that
|
|
4978
|
+
* reports `fail`, a command that exits non-zero. Neither is filed.
|
|
4979
|
+
*/
|
|
4980
|
+
const ANSWERS = /* @__PURE__ */ new Set(["reported_failure", "command_failed"]);
|
|
4981
|
+
/**
|
|
4982
|
+
* The feedback a node's end files, by who should act on it: Ork's own bug (with
|
|
4983
|
+
* the scrubbed detail, so a rule can be added), the workflow's, or the agent's
|
|
4984
|
+
* context. An account's, a provider's or the user's is not feedback.
|
|
4985
|
+
*/
|
|
4986
|
+
const endSignal = (node, end) => {
|
|
4987
|
+
const info = END_KINDS[end.kind];
|
|
4988
|
+
if (ANSWERS.has(end.kind)) return null;
|
|
4989
|
+
const base = {
|
|
4990
|
+
code: `end.${end.kind}`,
|
|
4991
|
+
title: `${node} ${end.message}`,
|
|
4992
|
+
body: info.hint,
|
|
4993
|
+
...end.detail === void 0 ? {} : { evidence: [end.detail] }
|
|
4994
|
+
};
|
|
4995
|
+
if (info.actor === "ork") return {
|
|
4996
|
+
...base,
|
|
4997
|
+
kind: "bug",
|
|
4998
|
+
target: "ork",
|
|
4999
|
+
severity: "medium"
|
|
5000
|
+
};
|
|
5001
|
+
if (info.actor === "workflow") return {
|
|
5002
|
+
...base,
|
|
5003
|
+
kind: "workflow",
|
|
5004
|
+
target: "workflow",
|
|
5005
|
+
severity: "low"
|
|
5006
|
+
};
|
|
5007
|
+
return info.actor === "agent" ? {
|
|
5008
|
+
...base,
|
|
5009
|
+
kind: "context-gap",
|
|
5010
|
+
target: "workflow",
|
|
5011
|
+
severity: "low"
|
|
5012
|
+
} : null;
|
|
5013
|
+
};
|
|
5014
|
+
// ../core/src/feedback/signals.ts
|
|
5015
|
+
const ORK_TOOL = "mcp__ork__";
|
|
5016
|
+
/**
|
|
5017
|
+
* An observer for one run's events: give it each event, and record what it
|
|
5018
|
+
* returns, in order (the event, its origin filled in when it is feedback, then
|
|
5019
|
+
* any feedback it signals).
|
|
5020
|
+
*/
|
|
5021
|
+
const createSignals = (options = {}) => {
|
|
5022
|
+
let workflow;
|
|
5023
|
+
const nodeTypes = /* @__PURE__ */ new Map();
|
|
5024
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
5025
|
+
const tools = /* @__PURE__ */ new Map();
|
|
5026
|
+
const asked = /* @__PURE__ */ new Map();
|
|
5027
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5028
|
+
const originOf = (node) => {
|
|
5029
|
+
const origin = {};
|
|
5030
|
+
if (workflow !== void 0) origin.workflow = workflow;
|
|
5031
|
+
if (options.workflowHash !== void 0) origin.workflowHash = options.workflowHash;
|
|
5032
|
+
const type = node === void 0 ? void 0 : nodeTypes.get(node);
|
|
5033
|
+
if (type !== void 0) origin.nodeType = type;
|
|
5034
|
+
const session = node === void 0 ? void 0 : sessions.get(node);
|
|
5035
|
+
if (session !== void 0) origin.model = session.model;
|
|
5036
|
+
if (session?.driver !== void 0) origin.driver = session.driver;
|
|
5037
|
+
if (options.version !== void 0) origin.version = options.version;
|
|
5038
|
+
return origin;
|
|
5039
|
+
};
|
|
5040
|
+
const record = (node, signal) => {
|
|
5041
|
+
const print = fingerprint(signal);
|
|
5042
|
+
if (seen.has(print)) return [];
|
|
5043
|
+
seen.add(print);
|
|
5044
|
+
return [{
|
|
5045
|
+
type: "feedback.recorded",
|
|
5046
|
+
...node === void 0 ? {} : { node },
|
|
5047
|
+
feedback: {
|
|
5048
|
+
source: "system",
|
|
5049
|
+
kind: signal.kind,
|
|
5050
|
+
target: signal.target,
|
|
5051
|
+
severity: signal.severity,
|
|
5052
|
+
title: signal.title,
|
|
5053
|
+
body: signal.body,
|
|
5054
|
+
evidence: signal.evidence ?? [],
|
|
5055
|
+
code: signal.code,
|
|
5056
|
+
fingerprint: print,
|
|
5057
|
+
origin: originOf(node)
|
|
5058
|
+
}
|
|
5059
|
+
}];
|
|
5060
|
+
};
|
|
5061
|
+
const answered = (question, answer) => {
|
|
5062
|
+
if (question.kind === "permission") {
|
|
5063
|
+
if (answer !== "deny") return null;
|
|
5064
|
+
const tool = question.question.replace(/^Allow /u, "").replace(/\?$/u, "");
|
|
5065
|
+
return [question.node, {
|
|
5066
|
+
code: "permission.denied",
|
|
5067
|
+
kind: "friction",
|
|
5068
|
+
target: "workflow",
|
|
5069
|
+
severity: "low",
|
|
5070
|
+
title: `${question.node} was denied ${tool}`,
|
|
5071
|
+
body: "Allow it in the node's `tools.allow` if it needs it, or tell it in its prompt not to use it.",
|
|
5072
|
+
evidence: [tool]
|
|
5073
|
+
}];
|
|
5074
|
+
}
|
|
5075
|
+
if (question.recommended === void 0 || answer === question.recommended) return null;
|
|
5076
|
+
return [question.node, {
|
|
5077
|
+
code: "recommendation.overridden",
|
|
5078
|
+
kind: "context-gap",
|
|
5079
|
+
target: "workflow",
|
|
5080
|
+
severity: "low",
|
|
5081
|
+
title: `The answer to ${question.node}'s question was not the one it recommended`,
|
|
5082
|
+
body: `Asked: ${question.question} Recommended: ${question.recommended}. Answered: ${answer}.`
|
|
5083
|
+
}];
|
|
5084
|
+
};
|
|
5085
|
+
/** The feedback an event signals, if any, and the node it is about. */
|
|
5086
|
+
const signalOf = (event) => {
|
|
5087
|
+
if (event.type === "loop.exhausted") return [event.from, {
|
|
5088
|
+
code: "loop.exhausted",
|
|
5089
|
+
kind: "workflow",
|
|
5090
|
+
target: "workflow",
|
|
5091
|
+
severity: "medium",
|
|
5092
|
+
title: `The loop ${event.from} -> ${event.to} ran out of its ${String(event.maxRounds)} rounds`,
|
|
5093
|
+
body: "Raise `maxRounds`, or make what the loop's nodes are asked to fix clearer so it settles sooner."
|
|
5094
|
+
}];
|
|
5095
|
+
if (event.type === "run.finished" && event.result.status === "budget_exhausted") return [void 0, {
|
|
5096
|
+
code: "budget.exhausted",
|
|
5097
|
+
kind: "workflow",
|
|
5098
|
+
target: "workflow",
|
|
5099
|
+
severity: "medium",
|
|
5100
|
+
title: "The run's budget ran out",
|
|
5101
|
+
body: `The run stopped after spending $${event.result.costUsd.toFixed(4)}. Raise \`limits.budgetUsd\`, or give nodes cheaper models or smaller budgets.`
|
|
5102
|
+
}];
|
|
5103
|
+
if (event.type === "decision.answered") {
|
|
5104
|
+
const question = asked.get(event.decision);
|
|
5105
|
+
return question === void 0 ? null : answered(question, event.answer);
|
|
5106
|
+
}
|
|
5107
|
+
if (event.type === "node.event" && event.event.type === "tool.result") {
|
|
5108
|
+
const name = tools.get(`${event.id}\0${event.event.id}`);
|
|
5109
|
+
if (!event.event.isError || name?.startsWith(ORK_TOOL) !== true) return null;
|
|
5110
|
+
const tool = name.slice(10);
|
|
5111
|
+
if (tool === "report") return [event.id, {
|
|
5112
|
+
code: "report.rejected",
|
|
5113
|
+
kind: "context-gap",
|
|
5114
|
+
target: "workflow",
|
|
5115
|
+
severity: "low",
|
|
5116
|
+
title: `${event.id}'s report was not accepted`,
|
|
5117
|
+
body: END_KINDS.invalid_report.hint,
|
|
5118
|
+
evidence: [name]
|
|
5119
|
+
}];
|
|
5120
|
+
return [event.id, {
|
|
5121
|
+
code: "tool.error",
|
|
5122
|
+
kind: "bug",
|
|
5123
|
+
target: "ork",
|
|
5124
|
+
severity: "medium",
|
|
5125
|
+
title: `Ork's ${tool} tool returned an error`,
|
|
5126
|
+
body: `A call to \`${tool}\` failed; the node's events show the call.`,
|
|
5127
|
+
evidence: [name]
|
|
5128
|
+
}];
|
|
5129
|
+
}
|
|
5130
|
+
if (event.type === "node.finished" && nodeTypes.get(event.id) === "human.gate" && event.output["answeredBy"] === "timeout") return [event.id, {
|
|
5131
|
+
code: "gate.timeout",
|
|
5132
|
+
kind: "workflow",
|
|
5133
|
+
target: "workflow",
|
|
5134
|
+
severity: "low",
|
|
5135
|
+
title: `The gate ${event.id} timed out and took its default`,
|
|
5136
|
+
body: "No one answered in time. Lengthen its `timeout`, or keep it if the default was right."
|
|
5137
|
+
}];
|
|
5138
|
+
if (event.type === "node.finished" && !event.ok) {
|
|
5139
|
+
const signal = endSignal(event.id, endOf(event));
|
|
5140
|
+
return signal === null ? null : [event.id, signal];
|
|
5141
|
+
}
|
|
5142
|
+
return null;
|
|
5143
|
+
};
|
|
5144
|
+
/** Keeps what later events and origins need. */
|
|
5145
|
+
const observe = (event) => {
|
|
5146
|
+
if (event.type === "run.started") workflow = event.workflow;
|
|
5147
|
+
if (event.type === "node.started") nodeTypes.set(event.id, event.nodeType);
|
|
5148
|
+
if (event.type === "decision.opened") {
|
|
5149
|
+
const question = {
|
|
5150
|
+
node: event.node,
|
|
5151
|
+
kind: event.kind,
|
|
5152
|
+
question: event.question
|
|
5153
|
+
};
|
|
5154
|
+
if (event.recommended !== void 0) question.recommended = event.recommended;
|
|
5155
|
+
asked.set(event.decision, question);
|
|
5156
|
+
}
|
|
5157
|
+
if (event.type !== "node.event") return;
|
|
5158
|
+
const inner = event.event;
|
|
5159
|
+
if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
|
|
5160
|
+
model: inner.model,
|
|
5161
|
+
...inner.driver === void 0 ? {} : { driver: inner.driver }
|
|
5162
|
+
});
|
|
5163
|
+
if (inner.type === "tool.called") tools.set(`${event.id}\0${inner.id}`, inner.name);
|
|
5164
|
+
};
|
|
5165
|
+
return (event) => {
|
|
5166
|
+
observe(event);
|
|
5167
|
+
if (event.type === "feedback.recorded") {
|
|
5168
|
+
if (seen.has(event.feedback.fingerprint)) return [];
|
|
5169
|
+
seen.add(event.feedback.fingerprint);
|
|
5170
|
+
const origin = {
|
|
5171
|
+
...originOf(event.node),
|
|
5172
|
+
...event.feedback.origin
|
|
5173
|
+
};
|
|
5174
|
+
return [{
|
|
5175
|
+
...event,
|
|
5176
|
+
feedback: {
|
|
5177
|
+
...event.feedback,
|
|
5178
|
+
origin
|
|
5179
|
+
}
|
|
5180
|
+
}];
|
|
5181
|
+
}
|
|
5182
|
+
const signal = signalOf(event);
|
|
5183
|
+
return signal === null ? [event] : [event, ...record(...signal)];
|
|
5184
|
+
};
|
|
5185
|
+
};
|
|
4827
5186
|
// ../core/src/project/mcp-file.ts
|
|
4828
5187
|
/**
|
|
4829
5188
|
* @file The project's `.mcp.json`, the file Claude Code reads too, so a server
|
|
@@ -4937,6 +5296,48 @@ const checkMcpServers = (workflow, options) => {
|
|
|
4937
5296
|
});
|
|
4938
5297
|
});
|
|
4939
5298
|
};
|
|
5299
|
+
// ../core/src/project/heavy.ts
|
|
5300
|
+
/**
|
|
5301
|
+
* @file A project's rules for the machine its runs share: the heavy commands
|
|
5302
|
+
* (its full-suite checks), denied to the session of a node with `verify:`
|
|
5303
|
+
* since Ork runs that check, and the OS priority, each with Ork's default.
|
|
5304
|
+
*/
|
|
5305
|
+
/** A `heavy` pattern as the Bash rule that denies it to a session. */
|
|
5306
|
+
const heavyRule = (pattern) => `Bash(${pattern})`;
|
|
5307
|
+
/**
|
|
5308
|
+
* The nodes, with the heavy commands denied to each Claude node that has
|
|
5309
|
+
* `verify:`; other nodes are left as they are. A pattern without `*` matches
|
|
5310
|
+
* only that command, so `vitest run` leaves `vitest run <files>` allowed.
|
|
5311
|
+
*/
|
|
5312
|
+
const withoutHeavy = (nodes, heavy) => {
|
|
5313
|
+
if (heavy.length === 0) return nodes;
|
|
5314
|
+
const rules = heavy.map((pattern) => heavyRule(pattern));
|
|
5315
|
+
return Object.fromEntries(Object.entries(nodes).map(([id, node]) => isClaudeNode(node) && "verify" in node && node.verify !== void 0 ? [id, {
|
|
5316
|
+
...node,
|
|
5317
|
+
tools: {
|
|
5318
|
+
...node.tools,
|
|
5319
|
+
deny: [.../* @__PURE__ */ new Set([...node.tools?.deny ?? [], ...rules])]
|
|
5320
|
+
}
|
|
5321
|
+
}] : [id, node]));
|
|
5322
|
+
};
|
|
5323
|
+
const words = (text) => text.trim().replaceAll(/\s+/gu, " ");
|
|
5324
|
+
/**
|
|
5325
|
+
* Whether `command` matches a `heavy` pattern, in the permission patterns'
|
|
5326
|
+
* syntax: `*` is any text, a trailing ` *` (or `:*`) also matches nothing, and
|
|
5327
|
+
* a pattern without `*` matches only that command.
|
|
5328
|
+
*/
|
|
5329
|
+
const matchesPattern = (pattern, command) => {
|
|
5330
|
+
const tail = words(pattern).replace(/:\*$/u, " *");
|
|
5331
|
+
const open = tail.endsWith(" *");
|
|
5332
|
+
const body = (open ? tail.slice(0, -2) : tail).split("*").map((part) => part.replaceAll(/[.+?^${}()|[\]\\]/gu, "\\$&")).join(".*");
|
|
5333
|
+
return new RegExp(`^${body}${open ? "(?: .*)?" : ""}$`, "su").test(words(command));
|
|
5334
|
+
};
|
|
5335
|
+
/** Whether `command` is one of the project's heavy commands. */
|
|
5336
|
+
const isHeavy = (heavy, command) => heavy.some((pattern) => matchesPattern(pattern, command));
|
|
5337
|
+
const machineRulesOf = (project) => ({
|
|
5338
|
+
heavy: project?.heavy ?? [],
|
|
5339
|
+
priority: project?.priority ?? "low"
|
|
5340
|
+
});
|
|
4940
5341
|
// ../core/src/project/templates.ts
|
|
4941
5342
|
/**
|
|
4942
5343
|
* @file Git name templates (`ork/{run}/{node}`, `{inputs.type}/{inputs.slug}`):
|
|
@@ -5028,7 +5429,8 @@ const dropUndefined = (value) => Object.fromEntries(Object.entries(value).filter
|
|
|
5028
5429
|
/**
|
|
5029
5430
|
* The workflow with its project's rules under it: git rules key by key and
|
|
5030
5431
|
* defaults key by key, practices one by one (the workflow's win), permissions
|
|
5031
|
-
* added together,
|
|
5432
|
+
* added together, the project's context files before the workflow's, and its
|
|
5433
|
+
* heavy commands denied to nodes with `verify:`.
|
|
5032
5434
|
*/
|
|
5033
5435
|
const applyProject = (workflow, options) => {
|
|
5034
5436
|
const { project } = options;
|
|
@@ -5062,6 +5464,7 @@ const applyProject = (workflow, options) => {
|
|
|
5062
5464
|
});
|
|
5063
5465
|
return dropUndefined({
|
|
5064
5466
|
...workflow,
|
|
5467
|
+
nodes: withoutHeavy(workflow.nodes, project.heavy ?? []),
|
|
5065
5468
|
defaults: Object.keys(defaults).length === 0 ? void 0 : defaults,
|
|
5066
5469
|
rules: Object.keys(rules).length === 0 ? void 0 : rules,
|
|
5067
5470
|
layers: Object.keys(layers).length === 0 ? void 0 : layers
|
|
@@ -6646,6 +7049,138 @@ const compileWorkflow = (authored, options = {}) => {
|
|
|
6646
7049
|
diagnostics
|
|
6647
7050
|
};
|
|
6648
7051
|
};
|
|
7052
|
+
// ../core/src/machine/priority.ts
|
|
7053
|
+
/**
|
|
7054
|
+
* @file Starting a child at the project's priority (`priority:`): `low`, the
|
|
7055
|
+
* default, starts it below Ork's own (under `nice`; on macOS also utility QoS
|
|
7056
|
+
* through `taskpolicy`), so the user's own apps come first; `normal` leaves
|
|
7057
|
+
* it. Only children: the `ork` process itself stays at normal priority.
|
|
7058
|
+
*/
|
|
7059
|
+
/** MacOS's tool for a program's QoS. */
|
|
7060
|
+
const TASKPOLICY = "/usr/sbin/taskpolicy";
|
|
7061
|
+
/** The POSIX tool that starts a program at a lower priority. */
|
|
7062
|
+
const NICE = "/usr/bin/nice";
|
|
7063
|
+
/** How far `nice` lowers a `low` child: the same as `PRIORITY_BELOW_NORMAL`. */
|
|
7064
|
+
const NICE_STEP = String(constants$1.priority.PRIORITY_BELOW_NORMAL);
|
|
7065
|
+
/**
|
|
7066
|
+
* Whether `bin` is a program `spawn` would find, so a wrapper can't hide its
|
|
7067
|
+
* absence.
|
|
7068
|
+
*/
|
|
7069
|
+
const found = (bin, env) => bin.includes("/") ? existsSync(bin) : (env["PATH"] ?? "").split(path.delimiter).some((dir) => dir !== "" && existsSync(path.join(dir, bin)));
|
|
7070
|
+
/**
|
|
7071
|
+
* What to spawn for `bin args` at `priority`. For `low`, when the program is
|
|
7072
|
+
* there: under `nice`, so it and what it starts run lower from the first
|
|
7073
|
+
* instruction, and on macOS under utility QoS as well; else the program itself,
|
|
7074
|
+
* which {@link lowerChild} lowers once it has started.
|
|
7075
|
+
*/
|
|
7076
|
+
const startAt = (priority, bin, args, env = process.env) => {
|
|
7077
|
+
if (priority !== "low" || !found(bin, env)) return [bin, [...args]];
|
|
7078
|
+
const niced = existsSync(NICE) ? [
|
|
7079
|
+
NICE,
|
|
7080
|
+
"-n",
|
|
7081
|
+
NICE_STEP,
|
|
7082
|
+
bin
|
|
7083
|
+
] : [bin];
|
|
7084
|
+
const [first = bin, ...rest] = process.platform === "darwin" && existsSync(TASKPOLICY) ? [
|
|
7085
|
+
TASKPOLICY,
|
|
7086
|
+
"-c",
|
|
7087
|
+
"utility",
|
|
7088
|
+
...niced
|
|
7089
|
+
] : niced;
|
|
7090
|
+
return [first, [...rest, ...args]];
|
|
7091
|
+
};
|
|
7092
|
+
/**
|
|
7093
|
+
* Lowers a started child's priority for `low`, best effort; what it starts
|
|
7094
|
+
* later inherits it.
|
|
7095
|
+
*/
|
|
7096
|
+
const lowerChild = (priority, pid) => {
|
|
7097
|
+
if (priority !== "low" || pid === void 0) return;
|
|
7098
|
+
try {
|
|
7099
|
+
setPriority(pid, constants$1.priority.PRIORITY_BELOW_NORMAL);
|
|
7100
|
+
} catch {}
|
|
7101
|
+
};
|
|
7102
|
+
// ../core/src/run/machine.ts
|
|
7103
|
+
/**
|
|
7104
|
+
* @file A node's share of the machine (the `machine` of its context): the
|
|
7105
|
+
* project's priority for its commands, and a machine-wide slot around each
|
|
7106
|
+
* heavy one. A wait is never silent: `slot.waiting` names who holds the
|
|
7107
|
+
* slots, `slot.taken` ends it, and the node's clock (its owners' and the
|
|
7108
|
+
* run's too) stands still meanwhile, so a timeout counts from the slot.
|
|
7109
|
+
*/
|
|
7110
|
+
/** A wait for a slot, as `ork logs`, `ork tree` and the UI say it. */
|
|
7111
|
+
const slotWaitLine = (holders) => holders.length === 0 ? "waiting for a check slot" : `waiting for a check slot, held by ${holders.join(", ")}`;
|
|
7112
|
+
const none = () => {};
|
|
7113
|
+
/** The port for one activation; none for a run that shares nothing. */
|
|
7114
|
+
const machinePort = (options, runId, id, clock) => {
|
|
7115
|
+
const { machine } = options;
|
|
7116
|
+
if (machine === void 0) return void 0;
|
|
7117
|
+
const { rules, ledger } = machine;
|
|
7118
|
+
return {
|
|
7119
|
+
priority: rules.priority,
|
|
7120
|
+
slot: async (command, signal) => {
|
|
7121
|
+
if (ledger === void 0 || !isHeavy(rules.heavy, command)) return none;
|
|
7122
|
+
let waited = false;
|
|
7123
|
+
const release = await clock.waitFor(id, () => ledger.take({
|
|
7124
|
+
run: runId,
|
|
7125
|
+
node: id,
|
|
7126
|
+
pid: process.pid
|
|
7127
|
+
}, {
|
|
7128
|
+
signal,
|
|
7129
|
+
onWait: (holders) => {
|
|
7130
|
+
waited = true;
|
|
7131
|
+
options.onEvent({
|
|
7132
|
+
type: "slot.waiting",
|
|
7133
|
+
id,
|
|
7134
|
+
holders: holders.map((held) => `${held.run}/${held.node}`)
|
|
7135
|
+
});
|
|
7136
|
+
}
|
|
7137
|
+
}));
|
|
7138
|
+
if (release === null) return none;
|
|
7139
|
+
if (waited) options.onEvent({
|
|
7140
|
+
type: "slot.taken",
|
|
7141
|
+
id
|
|
7142
|
+
});
|
|
7143
|
+
return release;
|
|
7144
|
+
}
|
|
7145
|
+
};
|
|
7146
|
+
};
|
|
7147
|
+
/**
|
|
7148
|
+
* A step's clock that also stands still while its command waits for a slot, and
|
|
7149
|
+
* the machine port that tells it so; for a step's own timeout.
|
|
7150
|
+
*/
|
|
7151
|
+
const slotClock = (base, machine) => {
|
|
7152
|
+
let waiting = 0;
|
|
7153
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
7154
|
+
const tell = () => {
|
|
7155
|
+
for (const listener of listeners) listener();
|
|
7156
|
+
};
|
|
7157
|
+
return {
|
|
7158
|
+
clock: {
|
|
7159
|
+
held: () => waiting > 0 || base?.held() === true,
|
|
7160
|
+
watch: (listener) => {
|
|
7161
|
+
listeners.add(listener);
|
|
7162
|
+
const unwatch = base?.watch(listener);
|
|
7163
|
+
return () => {
|
|
7164
|
+
listeners.delete(listener);
|
|
7165
|
+
unwatch?.();
|
|
7166
|
+
};
|
|
7167
|
+
}
|
|
7168
|
+
},
|
|
7169
|
+
machine: machine === void 0 ? void 0 : {
|
|
7170
|
+
...machine,
|
|
7171
|
+
slot: async (command, signal) => {
|
|
7172
|
+
waiting += 1;
|
|
7173
|
+
tell();
|
|
7174
|
+
try {
|
|
7175
|
+
return await machine.slot(command, signal);
|
|
7176
|
+
} finally {
|
|
7177
|
+
waiting -= 1;
|
|
7178
|
+
tell();
|
|
7179
|
+
}
|
|
7180
|
+
}
|
|
7181
|
+
}
|
|
7182
|
+
};
|
|
7183
|
+
};
|
|
6649
7184
|
// ../core/src/project/calls.ts
|
|
6650
7185
|
/**
|
|
6651
7186
|
* @file `ork validate`'s check of `workflow.call` nodes against what they call
|
|
@@ -7055,6 +7590,66 @@ const createLoops = (options) => {
|
|
|
7055
7590
|
overList
|
|
7056
7591
|
};
|
|
7057
7592
|
};
|
|
7593
|
+
// ../core/src/run/ended.ts
|
|
7594
|
+
/**
|
|
7595
|
+
* @file A node's result as it ended (specs/runtime/session-ends.md): one place
|
|
7596
|
+
* that sets `end`, makes a failure's `error` its message and marks a
|
|
7597
|
+
* transient kind, and that the engine's modules build ends through.
|
|
7598
|
+
*/
|
|
7599
|
+
/**
|
|
7600
|
+
* The result as it ended: a failure's `error` is the end's message (what the
|
|
7601
|
+
* run summary lists), and a transient kind marks it `transient`.
|
|
7602
|
+
*/
|
|
7603
|
+
const endedWith = (result, end) => ({
|
|
7604
|
+
...result,
|
|
7605
|
+
end,
|
|
7606
|
+
...result.ok ? {} : { error: end.message },
|
|
7607
|
+
...END_KINDS[end.kind].transient ? { transient: true } : {}
|
|
7608
|
+
});
|
|
7609
|
+
/** The reason a run's signal is aborted with when `timeout` passed. */
|
|
7610
|
+
const runTimedOut = (timeout) => timeout === void 0 ? { kind: "timed_out" } : {
|
|
7611
|
+
kind: "timed_out",
|
|
7612
|
+
timeout
|
|
7613
|
+
};
|
|
7614
|
+
const isTimedOut = (reason) => typeof reason === "object" && reason !== null && "kind" in reason && reason.kind === "timed_out";
|
|
7615
|
+
/**
|
|
7616
|
+
* The end of a node its run's signal stopped: `timed_out` when the run's
|
|
7617
|
+
* timeout aborted it, `interrupted` otherwise.
|
|
7618
|
+
*/
|
|
7619
|
+
const stoppedEnd = (signal, detail) => {
|
|
7620
|
+
const reason = signal.reason;
|
|
7621
|
+
if (!isTimedOut(reason)) return endFor("interrupted", {}, detail);
|
|
7622
|
+
const facts = reason.timeout === void 0 ? {} : { timeout: reason.timeout };
|
|
7623
|
+
return endFor("timed_out", facts, detail);
|
|
7624
|
+
};
|
|
7625
|
+
/** A failure that ended as `end`, its message the output's summary. */
|
|
7626
|
+
const failedWith = (end, output) => endedWith({
|
|
7627
|
+
ok: false,
|
|
7628
|
+
output: output ?? {
|
|
7629
|
+
status: "fail",
|
|
7630
|
+
summary: end.message
|
|
7631
|
+
}
|
|
7632
|
+
}, end);
|
|
7633
|
+
/**
|
|
7634
|
+
* The result with its end's detail scrubbed with the feedback redactor (the
|
|
7635
|
+
* run's `roots` and the home folder), as a session's is
|
|
7636
|
+
* (specs/runtime/session-ends.md), whichever node type set it.
|
|
7637
|
+
*/
|
|
7638
|
+
const scrubbedEnd = (result, roots) => {
|
|
7639
|
+
const { end } = result;
|
|
7640
|
+
if (end?.detail === void 0) return result;
|
|
7641
|
+
const detail = redact(end.detail, {
|
|
7642
|
+
roots,
|
|
7643
|
+
home: homedir()
|
|
7644
|
+
});
|
|
7645
|
+
return {
|
|
7646
|
+
...result,
|
|
7647
|
+
end: {
|
|
7648
|
+
...end,
|
|
7649
|
+
detail
|
|
7650
|
+
}
|
|
7651
|
+
};
|
|
7652
|
+
};
|
|
7058
7653
|
// ../core/src/run/declared.ts
|
|
7059
7654
|
/**
|
|
7060
7655
|
* @file Holding a node to what it declares of its output (RFC-010): a missing
|
|
@@ -7127,514 +7722,112 @@ const declaredProblem = (declared, value) => {
|
|
|
7127
7722
|
* still masks to the record is the file's, with its real values.
|
|
7128
7723
|
*/
|
|
7129
7724
|
const unmasked = (forEach, recorded) => "items" in forEach && JSON.stringify(forEach.items, maskReplacer) === JSON.stringify(recorded) ? [...forEach.items] : recorded;
|
|
7130
|
-
// ../core/src/run/
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
/**
|
|
7139
|
-
const
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
parent: node.parent
|
|
7146
|
-
});
|
|
7147
|
-
/** Whom a session may write to, by how it knows them. */
|
|
7148
|
-
const contactsOf = (id, nodes, channels) => {
|
|
7149
|
-
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
7725
|
+
// ../core/src/run/slots.ts
|
|
7726
|
+
/**
|
|
7727
|
+
* @file Session slots: at most `limits.concurrency` Claude sessions run at
|
|
7728
|
+
* once. Queued activations take a free slot when the scheduler pumps;
|
|
7729
|
+
* delegated children and leaders returning from `delegate` wait for one and
|
|
7730
|
+
* are served first, so a leader waiting on its children never holds a slot
|
|
7731
|
+
* they need.
|
|
7732
|
+
*/
|
|
7733
|
+
/** Only a Claude session takes a slot; tools and gates are not limited. */
|
|
7734
|
+
const needsSlot = (node) => isClaudeNode(node);
|
|
7735
|
+
/** A call spends through its run, so it needs budget as a session does. */
|
|
7736
|
+
const spendsBudget = (node) => needsSlot(node) || node.type === "workflow.call";
|
|
7737
|
+
const createSlots = (limit, onFree) => {
|
|
7738
|
+
let used = 0;
|
|
7739
|
+
const waiting = [];
|
|
7150
7740
|
return {
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7741
|
+
limit,
|
|
7742
|
+
tryTake: () => {
|
|
7743
|
+
if (used >= limit || waiting.length > 0) return false;
|
|
7744
|
+
used += 1;
|
|
7745
|
+
return true;
|
|
7746
|
+
},
|
|
7747
|
+
take: () => {
|
|
7748
|
+
if (used < limit && waiting.length === 0) {
|
|
7749
|
+
used += 1;
|
|
7750
|
+
return Promise.resolve();
|
|
7751
|
+
}
|
|
7752
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
7753
|
+
waiting.push(resolve);
|
|
7754
|
+
return promise;
|
|
7755
|
+
},
|
|
7756
|
+
release: () => {
|
|
7757
|
+
const next = waiting.shift();
|
|
7758
|
+
if (next === void 0) {
|
|
7759
|
+
used -= 1;
|
|
7760
|
+
onFree();
|
|
7761
|
+
} else next();
|
|
7762
|
+
}
|
|
7154
7763
|
};
|
|
7155
7764
|
};
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7765
|
+
// ../core/src/run/fan-out.ts
|
|
7766
|
+
/** The node's `forEach`, whatever its type; `undefined` when it has none. */
|
|
7767
|
+
const forEachOf = (node) => node !== void 0 && "forEach" in node ? node.forEach : void 0;
|
|
7768
|
+
/** The list a node runs over, or why there isn't one. */
|
|
7769
|
+
const listOf = (forEach, scope) => {
|
|
7770
|
+
if ("items" in forEach) return {
|
|
7771
|
+
ok: true,
|
|
7772
|
+
list: forEach.items
|
|
7773
|
+
};
|
|
7774
|
+
const read = templateValue(forEach.from, scope);
|
|
7775
|
+
if (!read.ok) return read;
|
|
7776
|
+
const { value } = read;
|
|
7777
|
+
if (Array.isArray(value)) return {
|
|
7778
|
+
ok: true,
|
|
7779
|
+
list: value
|
|
7780
|
+
};
|
|
7781
|
+
const name = String(templatePath(forEach.from)?.at(-1) ?? forEach.from);
|
|
7782
|
+
return {
|
|
7783
|
+
ok: false,
|
|
7784
|
+
error: declaredProblem({ [name]: { list: {} } }, { [name]: value }) ?? `${name} is not a list`
|
|
7785
|
+
};
|
|
7161
7786
|
};
|
|
7162
|
-
/**
|
|
7163
|
-
const
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
if (total <= 0) return 0;
|
|
7168
|
-
const before = reported.get(key) ?? 0;
|
|
7169
|
-
return total < before ? total : total - before;
|
|
7787
|
+
/** The output a `from` list comes from, whose freshness resume asks about. */
|
|
7788
|
+
const sourceOf = (forEach, scope) => {
|
|
7789
|
+
if (!("from" in forEach)) return void 0;
|
|
7790
|
+
const [root, id] = templatePath(forEach.from) ?? [];
|
|
7791
|
+
return root === "nodes" ? scope.nodes[String(id)]?.output : void 0;
|
|
7170
7792
|
};
|
|
7793
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7171
7794
|
/**
|
|
7172
|
-
* What
|
|
7173
|
-
* `
|
|
7174
|
-
*
|
|
7795
|
+
* What `results` keeps of an item: its declared fields (a program's are under
|
|
7796
|
+
* `json`), its summary, Ork's own branch and worktree, and how a program
|
|
7797
|
+
* failed. The full output stays in the item's own `node.finished`.
|
|
7175
7798
|
*/
|
|
7176
|
-
const
|
|
7177
|
-
const
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
key,
|
|
7209
|
-
cost: usage.costUsd
|
|
7210
|
-
});
|
|
7211
|
-
else ownSpend(reported, key, usage.costUsd);
|
|
7212
|
-
}
|
|
7213
|
-
return {
|
|
7214
|
-
total,
|
|
7215
|
-
reported,
|
|
7216
|
-
forks
|
|
7217
|
-
};
|
|
7218
|
-
};
|
|
7219
|
-
// ../core/src/run/reserves.ts
|
|
7220
|
-
const createReserves = () => {
|
|
7221
|
-
const held = /* @__PURE__ */ new Map();
|
|
7222
|
-
const waiters = /* @__PURE__ */ new Set();
|
|
7223
|
-
const wake = () => {
|
|
7224
|
-
const woken = [...waiters];
|
|
7225
|
-
waiters.clear();
|
|
7226
|
-
for (const resolve of woken) resolve();
|
|
7227
|
-
};
|
|
7228
|
-
return {
|
|
7229
|
-
/** All that is set aside now. */
|
|
7230
|
-
total: () => [...held.values()].reduce((sum, reserve) => sum + reserve.usd, 0),
|
|
7231
|
-
/**
|
|
7232
|
-
* `id` sets aside `usd` (nothing, or 0, lets it go). `waitable` is false
|
|
7233
|
-
* for a leader, whose children must never wait on it.
|
|
7234
|
-
*/
|
|
7235
|
-
reserve: (id, usd, waitable) => {
|
|
7236
|
-
const before = held.get(id)?.usd ?? 0;
|
|
7237
|
-
if (usd === void 0 || usd <= 0) held.delete(id);
|
|
7238
|
-
else held.set(id, {
|
|
7239
|
-
usd,
|
|
7240
|
-
waitable
|
|
7241
|
-
});
|
|
7242
|
-
if ((usd ?? 0) < before) wake();
|
|
7243
|
-
},
|
|
7244
|
-
/** The node ended: whatever it still held is let go. */
|
|
7245
|
-
drop: (id) => {
|
|
7246
|
-
if (held.delete(id)) wake();
|
|
7247
|
-
},
|
|
7248
|
-
/**
|
|
7249
|
-
* Waits until a reserve shrinks. `false` at once when no other node holds
|
|
7250
|
-
* one worth waiting on, or when `signal` aborts.
|
|
7251
|
-
*/
|
|
7252
|
-
freed: (id, signal) => {
|
|
7253
|
-
if (![...held].some(([other, reserve]) => other !== id && reserve.waitable) || signal.aborted) return Promise.resolve(false);
|
|
7254
|
-
return new Promise((resolve) => {
|
|
7255
|
-
const done = (freed) => () => {
|
|
7256
|
-
waiters.delete(onFree);
|
|
7257
|
-
signal.removeEventListener("abort", onAbort);
|
|
7258
|
-
resolve(freed);
|
|
7259
|
-
};
|
|
7260
|
-
const onFree = done(true);
|
|
7261
|
-
const onAbort = done(false);
|
|
7262
|
-
waiters.add(onFree);
|
|
7263
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
7264
|
-
});
|
|
7265
|
-
}
|
|
7266
|
-
};
|
|
7267
|
-
};
|
|
7268
|
-
// ../core/src/run/spending.ts
|
|
7269
|
-
/**
|
|
7270
|
-
* A result with only what this activation spent (ADR-046): a session's report
|
|
7271
|
-
* holds its total so far, while its tokens are this process's own. `reported`
|
|
7272
|
-
* holds each session's total.
|
|
7273
|
-
*/
|
|
7274
|
-
const withOwnSpend = (id, result, reported) => {
|
|
7275
|
-
const { sessionId, usage } = result;
|
|
7276
|
-
if (sessionId === void 0 || usage === void 0) return result;
|
|
7277
|
-
const own = {
|
|
7278
|
-
...usage,
|
|
7279
|
-
costUsd: ownSpend(reported, `${id}/${sessionId}`, usage.costUsd)
|
|
7280
|
-
};
|
|
7281
|
-
return {
|
|
7282
|
-
...result,
|
|
7283
|
-
usage: own,
|
|
7284
|
-
output: "usage" in result.output ? {
|
|
7285
|
-
...result.output,
|
|
7286
|
-
usage: own
|
|
7287
|
-
} : result.output
|
|
7288
|
-
};
|
|
7289
|
-
};
|
|
7290
|
-
/** A run's spending so far; a resumed run starts from what it had spent. */
|
|
7291
|
-
const createSpend = (budgetUsd, earlier, limits) => {
|
|
7292
|
-
const reported = new Map(earlier?.reported);
|
|
7293
|
-
let spent = earlier?.total ?? 0;
|
|
7294
|
-
/** What running sessions last reported, not yet in `spent`, by `node/session`. */
|
|
7295
|
-
const inFlight = /* @__PURE__ */ new Map();
|
|
7296
|
-
const committed = () => spent + [...inFlight.values()].reduce((sum, cost) => sum + cost, 0);
|
|
7297
|
-
/** Caps running sessions were given (ADR-077): out of what is offered. */
|
|
7298
|
-
const reserves = createReserves();
|
|
7299
|
-
const budgetLeft = () => budgetUsd === void 0 ? void 0 : budgetUsd - committed() - reserves.total();
|
|
7300
|
-
return {
|
|
7301
|
-
total: () => spent,
|
|
7302
|
-
/** A node sets aside its session's cap; nothing or 0 lets it go. */
|
|
7303
|
-
reserve: reserves.reserve,
|
|
7304
|
-
/** Waits until another node's reserve frees; `false` when none can. */
|
|
7305
|
-
freed: reserves.freed,
|
|
7306
|
-
/** What a session has spent in this activation, from its total so far. */
|
|
7307
|
-
ownOf: (id, sessionId, total) => unreported(reported, `${id}/${sessionId}`, total),
|
|
7308
|
-
/** A running session said what it has spent so far. */
|
|
7309
|
-
report: (id, sessionId, total) => {
|
|
7310
|
-
const key = `${id}/${sessionId}`;
|
|
7311
|
-
inFlight.set(key, unreported(reported, key, total));
|
|
7312
|
-
},
|
|
7313
|
-
/** The node ended: `count` has what its sessions last reported. */
|
|
7314
|
-
settled: (id) => {
|
|
7315
|
-
for (const key of inFlight.keys()) if (key.startsWith(`${id}/`)) inFlight.delete(key);
|
|
7316
|
-
reserves.drop(id);
|
|
7317
|
-
},
|
|
7318
|
-
/** What a session may spend: the least of the budget and any quota. */
|
|
7319
|
-
left: () => {
|
|
7320
|
-
const known = [budgetLeft(), limits?.().leftUsd].filter((left) => left !== void 0);
|
|
7321
|
-
return known.length === 0 ? void 0 : Math.min(...known);
|
|
7322
|
-
},
|
|
7323
|
-
budgetLeft,
|
|
7324
|
-
/** What the limits across runs allow now. */
|
|
7325
|
-
quota: () => limits?.() ?? {},
|
|
7326
|
-
/**
|
|
7327
|
-
* What stops anything new from starting: a quota first, as a budget can be
|
|
7328
|
-
* raised.
|
|
7329
|
-
*/
|
|
7330
|
-
spentOut: () => {
|
|
7331
|
-
const blocked = limits?.().blocked;
|
|
7332
|
-
if (blocked !== void 0) return {
|
|
7333
|
-
status: "quota_exhausted",
|
|
7334
|
-
reason: blocked
|
|
7335
|
-
};
|
|
7336
|
-
return budgetUsd !== void 0 && committed() >= budgetUsd ? {
|
|
7337
|
-
status: "budget_exhausted",
|
|
7338
|
-
reason: `the run's budget of $${String(budgetUsd)} is spent`
|
|
7339
|
-
} : null;
|
|
7340
|
-
},
|
|
7341
|
-
/**
|
|
7342
|
-
* Counts a result, and gives it back with only its own spend. A replayed
|
|
7343
|
-
* result is as stored, and already in what the run started from.
|
|
7344
|
-
*/
|
|
7345
|
-
count: (id, result, replayed) => {
|
|
7346
|
-
if (replayed) return result;
|
|
7347
|
-
const own = withOwnSpend(id, result, reported);
|
|
7348
|
-
spent += own.usage?.costUsd ?? 0;
|
|
7349
|
-
return own;
|
|
7350
|
-
}
|
|
7351
|
-
};
|
|
7352
|
-
};
|
|
7353
|
-
/** A run's spending, from what the scheduler is given. */
|
|
7354
|
-
const spendOf = (budgetUsd, options) => createSpend(budgetUsd, options.replay?.earlier, options.limits);
|
|
7355
|
-
// ../core/src/run/gather.ts
|
|
7356
|
-
const asList = (value) => Array.isArray(value) ? value : [];
|
|
7357
|
-
/** `before` then `now`, leaving out an entry already there. */
|
|
7358
|
-
const joined = (before, now) => {
|
|
7359
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7360
|
-
const out = [];
|
|
7361
|
-
for (const entry of [...asList(before), ...asList(now)]) {
|
|
7362
|
-
const key = JSON.stringify(entry);
|
|
7363
|
-
if (seen.has(key)) continue;
|
|
7364
|
-
seen.add(key);
|
|
7365
|
-
out.push(entry);
|
|
7366
|
-
}
|
|
7367
|
-
return out;
|
|
7368
|
-
};
|
|
7369
|
-
/**
|
|
7370
|
-
* `result` with each gathered field holding every round's entries so far.
|
|
7371
|
-
* `previous` is the node's output from its last round, already gathered.
|
|
7372
|
-
*/
|
|
7373
|
-
const joinRounds = (fields, previous, result) => {
|
|
7374
|
-
if (fields.length === 0 || previous === void 0) return result;
|
|
7375
|
-
const output = { ...result.output };
|
|
7376
|
-
for (const field of fields) output[field] = joined(previous[field], result.output[field]);
|
|
7377
|
-
return {
|
|
7378
|
-
...result,
|
|
7379
|
-
output
|
|
7380
|
-
};
|
|
7381
|
-
};
|
|
7382
|
-
/** `result` with `node`'s `output.gather` fields joined to its earlier rounds. */
|
|
7383
|
-
const gatherRounds = (node, previous, result) => joinRounds(gatheredFieldsOf(node), previous, result);
|
|
7384
|
-
/**
|
|
7385
|
-
* What `activation`'s rounds reported so far, to gather onto. A spawned
|
|
7386
|
-
* instance gathers nothing earlier: its id (`impl-2`) is reused by a different
|
|
7387
|
-
* worker in the leader's next round. A delegated child that hasn't run since a
|
|
7388
|
-
* resume starts from what it reported before it, since its leader's recorded
|
|
7389
|
-
* rounds are handed back whole and never run it again.
|
|
7390
|
-
*/
|
|
7391
|
-
const previousOf = (activation, current, replay) => {
|
|
7392
|
-
const { brief, template, id } = activation;
|
|
7393
|
-
if (brief !== void 0 && template !== void 0) return void 0;
|
|
7394
|
-
if (current !== void 0 || brief === void 0) return current;
|
|
7395
|
-
return replay?.lastOutput?.(id);
|
|
7396
|
-
};
|
|
7397
|
-
/** Gathers each activation's result onto what its node reported before. */
|
|
7398
|
-
const createGatherer = (outputs, replay, nodeOf) => (activation, result) => gatherRounds(nodeOf(activation), previousOf(activation, outputs[activation.id], replay), result);
|
|
7399
|
-
// ../core/src/run/results.ts
|
|
7400
|
-
/** Control nodes pass their input through; a join's input is its output. */
|
|
7401
|
-
const passThrough = (input) => ({
|
|
7402
|
-
output: input ?? {},
|
|
7403
|
-
ok: true
|
|
7404
|
-
});
|
|
7405
|
-
const recordRun = (entry, result) => {
|
|
7406
|
-
entry.runs += 1;
|
|
7407
|
-
entry.ok = result.ok;
|
|
7408
|
-
entry.output = result.output;
|
|
7409
|
-
entry.costUsd += result.usage?.costUsd ?? 0;
|
|
7410
|
-
if (result.error === void 0) delete entry.error;
|
|
7411
|
-
else entry.error = result.error;
|
|
7412
|
-
};
|
|
7413
|
-
const startedEvent = (activation, nodeType, round, agent) => {
|
|
7414
|
-
const event = {
|
|
7415
|
-
type: "node.started",
|
|
7416
|
-
id: activation.id,
|
|
7417
|
-
nodeType,
|
|
7418
|
-
round
|
|
7419
|
-
};
|
|
7420
|
-
if (activation.brief !== void 0) event.owner = activation.brief.from;
|
|
7421
|
-
if (agent !== void 0) event.agent = agent;
|
|
7422
|
-
const { item } = activation;
|
|
7423
|
-
if (item !== void 0) event.item = {
|
|
7424
|
-
of: item.of,
|
|
7425
|
-
index: item.index
|
|
7426
|
-
};
|
|
7427
|
-
return event;
|
|
7428
|
-
};
|
|
7429
|
-
const finishedEvent = (id, round, result) => {
|
|
7430
|
-
const event = {
|
|
7431
|
-
type: "node.finished",
|
|
7432
|
-
id,
|
|
7433
|
-
round,
|
|
7434
|
-
ok: result.ok,
|
|
7435
|
-
output: result.output
|
|
7436
|
-
};
|
|
7437
|
-
if (result.usage !== void 0) event.usage = result.usage;
|
|
7438
|
-
if (result.error !== void 0) event.error = result.error;
|
|
7439
|
-
return event;
|
|
7440
|
-
};
|
|
7441
|
-
const failure = (error) => ({
|
|
7442
|
-
output: {
|
|
7443
|
-
status: "fail",
|
|
7444
|
-
summary: error
|
|
7445
|
-
},
|
|
7446
|
-
ok: false,
|
|
7447
|
-
error
|
|
7448
|
-
});
|
|
7449
|
-
/** The node's entry in the run summary, created on first use. */
|
|
7450
|
-
const summaryOf = (summaries, id) => {
|
|
7451
|
-
const existing = summaries.get(id);
|
|
7452
|
-
if (existing !== void 0) return existing;
|
|
7453
|
-
const created = {
|
|
7454
|
-
id,
|
|
7455
|
-
runs: 0,
|
|
7456
|
-
ok: null,
|
|
7457
|
-
output: null,
|
|
7458
|
-
costUsd: 0
|
|
7459
|
-
};
|
|
7460
|
-
summaries.set(id, created);
|
|
7461
|
-
return created;
|
|
7462
|
-
};
|
|
7463
|
-
/** A run's summary; a resumed run's leaders keep their forks' spend (ADR-063). */
|
|
7464
|
-
const createSummaries = (earlier) => {
|
|
7465
|
-
const summaries = /* @__PURE__ */ new Map();
|
|
7466
|
-
const summary = (id) => summaryOf(summaries, id);
|
|
7467
|
-
for (const [id, costUsd] of earlier?.forks ?? []) summary(id).costUsd += costUsd;
|
|
7468
|
-
return {
|
|
7469
|
-
summaries,
|
|
7470
|
-
summary
|
|
7471
|
-
};
|
|
7472
|
-
};
|
|
7473
|
-
/** The tighter of the workflow's budget and what a caller left it, if any. */
|
|
7474
|
-
const runBudget = (graph, settings) => {
|
|
7475
|
-
const limits = [graph.workflow.limits?.budgetUsd, settings?.budgetCap].filter((limit) => limit !== void 0);
|
|
7476
|
-
return limits.length === 0 ? {} : { budgetUsd: Math.min(...limits) };
|
|
7477
|
-
};
|
|
7478
|
-
/** The `run.started` event: the workflow, its entries, nodes and inputs. */
|
|
7479
|
-
const runStartedEvent = (graph, runId, options) => ({
|
|
7480
|
-
type: "run.started",
|
|
7481
|
-
runId,
|
|
7482
|
-
workflow: graph.workflow.name,
|
|
7483
|
-
entries: graph.entries,
|
|
7484
|
-
nodes: declaredNodes(graph),
|
|
7485
|
-
edges: graph.edges.map(({ from, to, when, maxRounds }) => ({
|
|
7486
|
-
from,
|
|
7487
|
-
to,
|
|
7488
|
-
...when === void 0 ? {} : { when },
|
|
7489
|
-
...maxRounds === void 0 ? {} : { maxRounds }
|
|
7490
|
-
})),
|
|
7491
|
-
channels: graph.channels,
|
|
7492
|
-
inputs: options.inputs,
|
|
7493
|
-
...options.settings === void 0 ? {} : { settings: options.settings },
|
|
7494
|
-
...runBudget(graph, options.settings),
|
|
7495
|
-
...options.orkVersion === void 0 ? {} : { ork: options.orkVersion }
|
|
7496
|
-
});
|
|
7497
|
-
/** The unread mail of nodes that never ran; a node that ran saw its notice. */
|
|
7498
|
-
const unreadOf = (nodes, unread) => {
|
|
7499
|
-
const ran = new Set(nodes.filter((node) => node.runs > 0).map((node) => node.id));
|
|
7500
|
-
const left = [...unread ?? []].filter(([node]) => !ran.has(node)).map(([node, messages]) => ({
|
|
7501
|
-
node,
|
|
7502
|
-
messages
|
|
7503
|
-
}));
|
|
7504
|
-
return left.length === 0 ? {} : { unread: left };
|
|
7505
|
-
};
|
|
7506
|
-
/**
|
|
7507
|
-
* The run's result: failed when any reason was given, unless it stopped. A
|
|
7508
|
-
* skipped node or a handled failure leaves it `succeeded`, and is counted.
|
|
7509
|
-
*/
|
|
7510
|
-
const runResult = (run) => ({
|
|
7511
|
-
runId: run.runId,
|
|
7512
|
-
status: run.stopped ?? (run.reasons.length > 0 ? "failed" : "succeeded"),
|
|
7513
|
-
costUsd: run.costUsd,
|
|
7514
|
-
nodes: run.nodes,
|
|
7515
|
-
reasons: run.reasons,
|
|
7516
|
-
worktrees: run.worktrees,
|
|
7517
|
-
...run.outputs === void 0 ? {} : { outputs: run.outputs },
|
|
7518
|
-
skipped: run.nodes.filter((node) => node.runs === 0).length,
|
|
7519
|
-
failuresHandled: run.failuresHandled,
|
|
7520
|
-
...unreadOf(run.nodes, run.unread)
|
|
7521
|
-
});
|
|
7522
|
-
/** A workflow's `outputs`, rendered as the run ends. */
|
|
7523
|
-
const renderOutputs = (outputs, scope) => outputs === void 0 ? void 0 : Object.fromEntries(Object.entries(outputs).map(([name, text]) => {
|
|
7524
|
-
const template = parseTemplate(text);
|
|
7525
|
-
return [name, template.ok ? renderTemplate(template.parts, scope) : text];
|
|
7526
|
-
}));
|
|
7527
|
-
/** Closes the run's worktrees; a failure is one more reason, not a crash. */
|
|
7528
|
-
const closeWorkspace = (workspace, reasons) => (workspace?.close() ?? Promise.resolve([])).catch((error) => {
|
|
7529
|
-
reasons.push(`cleaning up worktrees: ${String(error)}`);
|
|
7530
|
-
return [];
|
|
7531
|
-
});
|
|
7532
|
-
// ../core/src/run/slots.ts
|
|
7533
|
-
/**
|
|
7534
|
-
* @file Session slots: at most `limits.concurrency` Claude sessions run at
|
|
7535
|
-
* once. Queued activations take a free slot when the scheduler pumps;
|
|
7536
|
-
* delegated children and leaders returning from `delegate` wait for one and
|
|
7537
|
-
* are served first, so a leader waiting on its children never holds a slot
|
|
7538
|
-
* they need.
|
|
7539
|
-
*/
|
|
7540
|
-
/** Only a Claude session takes a slot; tools and gates are not limited. */
|
|
7541
|
-
const needsSlot = (node) => isClaudeNode(node);
|
|
7542
|
-
/** A call spends through its run, so it needs budget as a session does. */
|
|
7543
|
-
const spendsBudget = (node) => needsSlot(node) || node.type === "workflow.call";
|
|
7544
|
-
const createSlots = (limit, onFree) => {
|
|
7545
|
-
let used = 0;
|
|
7546
|
-
const waiting = [];
|
|
7547
|
-
return {
|
|
7548
|
-
limit,
|
|
7549
|
-
tryTake: () => {
|
|
7550
|
-
if (used >= limit || waiting.length > 0) return false;
|
|
7551
|
-
used += 1;
|
|
7552
|
-
return true;
|
|
7553
|
-
},
|
|
7554
|
-
take: () => {
|
|
7555
|
-
if (used < limit && waiting.length === 0) {
|
|
7556
|
-
used += 1;
|
|
7557
|
-
return Promise.resolve();
|
|
7558
|
-
}
|
|
7559
|
-
const { promise, resolve } = Promise.withResolvers();
|
|
7560
|
-
waiting.push(resolve);
|
|
7561
|
-
return promise;
|
|
7562
|
-
},
|
|
7563
|
-
release: () => {
|
|
7564
|
-
const next = waiting.shift();
|
|
7565
|
-
if (next === void 0) {
|
|
7566
|
-
used -= 1;
|
|
7567
|
-
onFree();
|
|
7568
|
-
} else next();
|
|
7569
|
-
}
|
|
7570
|
-
};
|
|
7571
|
-
};
|
|
7572
|
-
// ../core/src/run/fan-out.ts
|
|
7573
|
-
/** The node's `forEach`, whatever its type; `undefined` when it has none. */
|
|
7574
|
-
const forEachOf = (node) => node !== void 0 && "forEach" in node ? node.forEach : void 0;
|
|
7575
|
-
/** The list a node runs over, or why there isn't one. */
|
|
7576
|
-
const listOf = (forEach, scope) => {
|
|
7577
|
-
if ("items" in forEach) return {
|
|
7578
|
-
ok: true,
|
|
7579
|
-
list: forEach.items
|
|
7580
|
-
};
|
|
7581
|
-
const read = templateValue(forEach.from, scope);
|
|
7582
|
-
if (!read.ok) return read;
|
|
7583
|
-
const { value } = read;
|
|
7584
|
-
if (Array.isArray(value)) return {
|
|
7585
|
-
ok: true,
|
|
7586
|
-
list: value
|
|
7587
|
-
};
|
|
7588
|
-
const name = String(templatePath(forEach.from)?.at(-1) ?? forEach.from);
|
|
7589
|
-
return {
|
|
7590
|
-
ok: false,
|
|
7591
|
-
error: declaredProblem({ [name]: { list: {} } }, { [name]: value }) ?? `${name} is not a list`
|
|
7592
|
-
};
|
|
7593
|
-
};
|
|
7594
|
-
/** The output a `from` list comes from, whose freshness resume asks about. */
|
|
7595
|
-
const sourceOf = (forEach, scope) => {
|
|
7596
|
-
if (!("from" in forEach)) return void 0;
|
|
7597
|
-
const [root, id] = templatePath(forEach.from) ?? [];
|
|
7598
|
-
return root === "nodes" ? scope.nodes[String(id)]?.output : void 0;
|
|
7599
|
-
};
|
|
7600
|
-
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7601
|
-
/**
|
|
7602
|
-
* What `results` keeps of an item: its declared fields (a program's are under
|
|
7603
|
-
* `json`), its summary, Ork's own branch and worktree, and how a program
|
|
7604
|
-
* failed. The full output stays in the item's own `node.finished`.
|
|
7605
|
-
*/
|
|
7606
|
-
const entryOf = (declared, { ok, output, error }) => {
|
|
7607
|
-
const json = isRecord$1(output["json"]) ? output["json"] : {};
|
|
7608
|
-
const fields = Object.keys(declared ?? {}).map((field) => [field, Object.hasOwn(json, field) ? json[field] : output[field]]);
|
|
7609
|
-
const own = [
|
|
7610
|
-
"summary",
|
|
7611
|
-
"branch",
|
|
7612
|
-
"worktree",
|
|
7613
|
-
"merge"
|
|
7614
|
-
].map((field) => [field, output[field]]);
|
|
7615
|
-
const failed = !ok && typeof output["exitCode"] === "number" ? [["exitCode", output["exitCode"]], ["error", error]] : [];
|
|
7616
|
-
return Object.fromEntries([
|
|
7617
|
-
...fields,
|
|
7618
|
-
...own,
|
|
7619
|
-
...failed
|
|
7620
|
-
].filter(([, value]) => value !== void 0));
|
|
7621
|
-
};
|
|
7622
|
-
/** Whether an item passed but its work could not reach its owner. */
|
|
7623
|
-
const mergeFailed = ({ output }) => isRecord$1(output["merge"]) && output["merge"]["ok"] === false;
|
|
7624
|
-
const statusOf$2 = (result) => {
|
|
7625
|
-
if (mergeFailed(result)) return "fail";
|
|
7626
|
-
const { ok, output } = result;
|
|
7627
|
-
const status = output["status"];
|
|
7628
|
-
if (typeof status === "string") return status;
|
|
7629
|
-
return ok ? "pass" : "fail";
|
|
7630
|
-
};
|
|
7631
|
-
/** Why a fan-out did not succeed, naming the first item that failed. */
|
|
7632
|
-
const whyNot = (count, failures, skipped) => {
|
|
7633
|
-
const parts = [`${String(failures.length)} of ${String(count)} items failed`];
|
|
7634
|
-
const [first] = failures;
|
|
7635
|
-
if (first !== void 0) parts[0] = `${parts[0] ?? ""}; first, ${first}`;
|
|
7636
|
-
if (skipped > 0) parts.push(`${String(skipped)} not started`);
|
|
7637
|
-
return parts.join("; ");
|
|
7799
|
+
const entryOf = (declared, { ok, output, error }) => {
|
|
7800
|
+
const json = isRecord$1(output["json"]) ? output["json"] : {};
|
|
7801
|
+
const fields = Object.keys(declared ?? {}).map((field) => [field, Object.hasOwn(json, field) ? json[field] : output[field]]);
|
|
7802
|
+
const own = [
|
|
7803
|
+
"summary",
|
|
7804
|
+
"branch",
|
|
7805
|
+
"worktree",
|
|
7806
|
+
"merge"
|
|
7807
|
+
].map((field) => [field, output[field]]);
|
|
7808
|
+
const failed = !ok && typeof output["exitCode"] === "number" ? [["exitCode", output["exitCode"]], ["error", error]] : [];
|
|
7809
|
+
return Object.fromEntries([
|
|
7810
|
+
...fields,
|
|
7811
|
+
...own,
|
|
7812
|
+
...failed
|
|
7813
|
+
].filter(([, value]) => value !== void 0));
|
|
7814
|
+
};
|
|
7815
|
+
/** Whether an item passed but its work could not reach its owner. */
|
|
7816
|
+
const mergeFailed = ({ output }) => isRecord$1(output["merge"]) && output["merge"]["ok"] === false;
|
|
7817
|
+
const statusOf$2 = (result) => {
|
|
7818
|
+
if (mergeFailed(result)) return "fail";
|
|
7819
|
+
const { ok, output } = result;
|
|
7820
|
+
const status = output["status"];
|
|
7821
|
+
if (typeof status === "string") return status;
|
|
7822
|
+
return ok ? "pass" : "fail";
|
|
7823
|
+
};
|
|
7824
|
+
/** Why a fan-out did not succeed, naming the first item that failed. */
|
|
7825
|
+
const whyNot = (count, failures, skipped) => {
|
|
7826
|
+
const parts = [`${String(failures.length)} of ${String(count)} items failed`];
|
|
7827
|
+
const [first] = failures;
|
|
7828
|
+
if (first !== void 0) parts[0] = `${parts[0] ?? ""}; first, ${first}`;
|
|
7829
|
+
if (skipped > 0) parts.push(`${String(skipped)} not started`);
|
|
7830
|
+
return parts.join("; ");
|
|
7638
7831
|
};
|
|
7639
7832
|
/**
|
|
7640
7833
|
* Waits until the item may start: not while it is held, and without keeping a
|
|
@@ -7659,7 +7852,7 @@ const createFanOut = (deps) => {
|
|
|
7659
7852
|
ok: true,
|
|
7660
7853
|
list: unmasked(forEach, recorded)
|
|
7661
7854
|
};
|
|
7662
|
-
if (!listed.ok) return
|
|
7855
|
+
if (!listed.ok) return failedWith(endFor("invalid_node", { reason: listed.error }));
|
|
7663
7856
|
let { list } = listed;
|
|
7664
7857
|
const recordList = () => {
|
|
7665
7858
|
options.onEvent({
|
|
@@ -7685,7 +7878,10 @@ const createFanOut = (deps) => {
|
|
|
7685
7878
|
if (chosen === null) {
|
|
7686
7879
|
const reason = `the list of ${id} has ${String(list.length)} items, over maxItems ${String(forEach.maxItems)}`;
|
|
7687
7880
|
return stops.ended(id, {
|
|
7688
|
-
...
|
|
7881
|
+
...failedWith(endFor("invalid_node", { reason }), {
|
|
7882
|
+
status: "fail",
|
|
7883
|
+
summary: reason
|
|
7884
|
+
}),
|
|
7689
7885
|
limited: reason,
|
|
7690
7886
|
stopsAs: "failed"
|
|
7691
7887
|
});
|
|
@@ -7701,6 +7897,8 @@ const createFanOut = (deps) => {
|
|
|
7701
7897
|
const results = list.map(() => ({}));
|
|
7702
7898
|
const statuses = list.map(() => "skipped");
|
|
7703
7899
|
const failures = [];
|
|
7900
|
+
/** How the first item that failed ended: the fanned node's own end. */
|
|
7901
|
+
let firstEnd;
|
|
7704
7902
|
const mayStart = () => !signal.aborted && !deps.isStopped() && (forEach.onFailure !== "stop" || failures.length === 0);
|
|
7705
7903
|
const runItem = async (index) => {
|
|
7706
7904
|
const itemId = `${id}.${String(index)}`;
|
|
@@ -7721,8 +7919,10 @@ const createFanOut = (deps) => {
|
|
|
7721
7919
|
});
|
|
7722
7920
|
results[index] = entryOf(declared, result);
|
|
7723
7921
|
statuses[index] = statusOf$2(result);
|
|
7724
|
-
if (!result.ok)
|
|
7725
|
-
|
|
7922
|
+
if (!result.ok) {
|
|
7923
|
+
firstEnd ??= endOf(result);
|
|
7924
|
+
failures.push(`${itemId}: ${result.error ?? "failed"}`);
|
|
7925
|
+
} else if (mergeFailed(result)) failures.push(`${itemId}: its work could not be merged`);
|
|
7726
7926
|
} finally {
|
|
7727
7927
|
if (session) slots.release();
|
|
7728
7928
|
}
|
|
@@ -7748,6 +7948,7 @@ const createFanOut = (deps) => {
|
|
|
7748
7948
|
count: list.length
|
|
7749
7949
|
},
|
|
7750
7950
|
...ok ? {} : { error: whyNot(list.length, failures, skipped) },
|
|
7951
|
+
...ok || firstEnd === void 0 ? {} : { end: firstEnd },
|
|
7751
7952
|
...!ok && deps.isStopped() ? { limited: deps.reasons[0] ?? "the run stopped" } : {}
|
|
7752
7953
|
};
|
|
7753
7954
|
return {
|
|
@@ -7764,7 +7965,14 @@ const createRunChild = (hooks) => async (child, brief) => {
|
|
|
7764
7965
|
await hooks.slots.take();
|
|
7765
7966
|
try {
|
|
7766
7967
|
await hooks.holds.forNode(child.id, child.template).wait("start", hooks.signal);
|
|
7767
|
-
if (hooks.isStopped() || hooks.budgetSpent())
|
|
7968
|
+
if (hooks.isStopped() || hooks.budgetSpent()) {
|
|
7969
|
+
const why = `not started: ${hooks.reasons[0] ?? "the run stopped"}`;
|
|
7970
|
+
const end = hooks.budgetSpent() ? endFor("budget_limit", {}, why) : stoppedEnd(hooks.signal, why);
|
|
7971
|
+
return failedWith(end, {
|
|
7972
|
+
status: "fail",
|
|
7973
|
+
summary: why
|
|
7974
|
+
});
|
|
7975
|
+
}
|
|
7768
7976
|
return await hooks.perform({
|
|
7769
7977
|
...child,
|
|
7770
7978
|
input: void 0,
|
|
@@ -7876,154 +8084,561 @@ const createOwnership = (hooks) => {
|
|
|
7876
8084
|
brief: task.brief
|
|
7877
8085
|
};
|
|
7878
8086
|
});
|
|
7879
|
-
return runJobs(jobs);
|
|
8087
|
+
return runJobs(jobs);
|
|
8088
|
+
};
|
|
8089
|
+
return {
|
|
8090
|
+
children,
|
|
8091
|
+
fanned: (owner?.children ?? []).filter((id) => isFanned(id)),
|
|
8092
|
+
templates,
|
|
8093
|
+
workersLeft: () => Math.max(0, maxWorkers - spawned.length),
|
|
8094
|
+
delegate,
|
|
8095
|
+
spawn
|
|
8096
|
+
};
|
|
8097
|
+
};
|
|
8098
|
+
// ../core/src/run/scope.ts
|
|
8099
|
+
/**
|
|
8100
|
+
* @file A run's id and folder, and the scope templates and `when:` conditions
|
|
8101
|
+
* read during it.
|
|
8102
|
+
*/
|
|
8103
|
+
/** The run's id (given, or a new one) and its `.ork/runs/<run>` folder. */
|
|
8104
|
+
const runPlace = (options) => {
|
|
8105
|
+
const runId = options.runId ?? randomUUID().slice(0, 8);
|
|
8106
|
+
return {
|
|
8107
|
+
runId,
|
|
8108
|
+
runDir: options.runDir ?? path.join(options.cwd, ".ork", "runs", runId)
|
|
8109
|
+
};
|
|
8110
|
+
};
|
|
8111
|
+
/**
|
|
8112
|
+
* The folders every node of a run reads from: the workflow's, the project's,
|
|
8113
|
+
* Ork's.
|
|
8114
|
+
*/
|
|
8115
|
+
const foldersOf = (options) => ({
|
|
8116
|
+
workflowDir: options.workflowDir ?? options.cwd,
|
|
8117
|
+
projectRoot: options.projectRoot ?? options.cwd,
|
|
8118
|
+
...options.orkDir === void 0 ? {} : { orkDir: options.orkDir }
|
|
8119
|
+
});
|
|
8120
|
+
/** What templates and `when:` read during a run. */
|
|
8121
|
+
const runScope = (inputs, outputs, runId, item) => {
|
|
8122
|
+
const scope = {
|
|
8123
|
+
inputs,
|
|
8124
|
+
nodes: Object.fromEntries(Object.entries(outputs).map(([id, output]) => [id, { output }]))
|
|
8125
|
+
};
|
|
8126
|
+
if (runId !== void 0) scope.run = { id: runId };
|
|
8127
|
+
if (item !== void 0) {
|
|
8128
|
+
scope.item = item.value;
|
|
8129
|
+
scope.index = item.index;
|
|
8130
|
+
}
|
|
8131
|
+
return scope;
|
|
8132
|
+
};
|
|
8133
|
+
// ../core/src/run/context.ts
|
|
8134
|
+
/**
|
|
8135
|
+
* @file One activation's execute context: what the scheduler hands a node's
|
|
8136
|
+
* executor. Its inputs and the outputs so far, where it works, the budget
|
|
8137
|
+
* left, the human, its hold and its own stop signal (ADR-041), its ports on
|
|
8138
|
+
* the bus, the session it resumes, its share of the machine, and, for a
|
|
8139
|
+
* leader, its ownership of its children.
|
|
8140
|
+
*/
|
|
8141
|
+
const nodeContext = (deps, activation, cwd, round, clock) => {
|
|
8142
|
+
const { options, runId } = deps;
|
|
8143
|
+
const { graph } = options;
|
|
8144
|
+
const { id, template } = activation;
|
|
8145
|
+
const context = {
|
|
8146
|
+
runId,
|
|
8147
|
+
id,
|
|
8148
|
+
workflow: graph.workflow,
|
|
8149
|
+
inputs: options.inputs,
|
|
8150
|
+
outputs: deps.outputs,
|
|
8151
|
+
input: activation.input,
|
|
8152
|
+
round,
|
|
8153
|
+
cwd,
|
|
8154
|
+
runDir: deps.runDir,
|
|
8155
|
+
...foldersOf(options),
|
|
8156
|
+
budgetLeftUsd: deps.spend.left(),
|
|
8157
|
+
budgetLeftNow: deps.spend.budgetLeft,
|
|
8158
|
+
reserveBudget: (usd) => {
|
|
8159
|
+
deps.spend.reserve(id, usd, deps.nodeOf(activation)?.type !== "claude.leader");
|
|
8160
|
+
},
|
|
8161
|
+
budgetFreed: (signal) => deps.spend.freed(id, signal),
|
|
8162
|
+
quota: deps.spend.quota,
|
|
8163
|
+
ownSpend: (sessionId, total) => deps.spend.ownOf(id, sessionId, total),
|
|
8164
|
+
human: deps.human,
|
|
8165
|
+
hold: deps.holds.forNode(id, template),
|
|
8166
|
+
...deps.ports.forNode(id, template, cwd),
|
|
8167
|
+
...options.calls === void 0 ? {} : { calls: options.calls },
|
|
8168
|
+
emit: (event) => {
|
|
8169
|
+
deps.ports.owners.seen(id, event, cwd);
|
|
8170
|
+
if (event.type === "session.finished" && event.fork !== true) {
|
|
8171
|
+
const { sessionId, usage } = event.outcome;
|
|
8172
|
+
deps.spend.report(id, sessionId, usage.costUsd);
|
|
8173
|
+
}
|
|
8174
|
+
options.onEvent({
|
|
8175
|
+
type: "node.event",
|
|
8176
|
+
id,
|
|
8177
|
+
event
|
|
8178
|
+
});
|
|
8179
|
+
},
|
|
8180
|
+
signal: deps.stops.signalFor(id, template)
|
|
8181
|
+
};
|
|
8182
|
+
const owner = ownerPort(deps, id, template);
|
|
8183
|
+
if (owner !== void 0) context.owner = owner;
|
|
8184
|
+
if (template !== void 0) context.template = template;
|
|
8185
|
+
const machine = clock === void 0 ? void 0 : machinePort(options, runId, id, clock);
|
|
8186
|
+
if (machine !== void 0) context.machine = machine;
|
|
8187
|
+
if (activation.brief !== void 0) context.brief = activation.brief;
|
|
8188
|
+
if (activation.item !== void 0) context.item = activation.item;
|
|
8189
|
+
const resume = options.replay?.cutOff?.(activation) ?? resumeOf(deps.nodeOf(activation), deps.sessionIds.get(id) ?? options.replay?.previousSession?.(activation));
|
|
8190
|
+
if (resume !== void 0) context.resume = resume;
|
|
8191
|
+
if (deps.nodeOf(activation)?.type === "claude.leader") context.ownership = createOwnership({
|
|
8192
|
+
graph,
|
|
8193
|
+
leader: id,
|
|
8194
|
+
runChild: deps.runChild,
|
|
8195
|
+
isRunning: (other) => deps.running.has(other),
|
|
8196
|
+
checkpoint: () => options.workspace?.checkpoint(id) ?? Promise.resolve(null),
|
|
8197
|
+
releaseClaims: () => {
|
|
8198
|
+
const held = context.claims?.held() ?? [];
|
|
8199
|
+
for (const resource of held) context.claims?.release(resource);
|
|
8200
|
+
return held;
|
|
8201
|
+
},
|
|
8202
|
+
lend: deps.slots.release,
|
|
8203
|
+
reclaim: deps.slots.take
|
|
8204
|
+
});
|
|
8205
|
+
return context;
|
|
8206
|
+
};
|
|
8207
|
+
/** Where `id`'s questions go first, if its leader takes them (ADR-063). */
|
|
8208
|
+
const ownerPort = (deps, id, template) => deps.ports.owners.portFor(id, template, {
|
|
8209
|
+
budgetLeftUsd: deps.spend.left,
|
|
8210
|
+
charge: (owner, sessionId, usage) => {
|
|
8211
|
+
const own = deps.spend.count(owner, {
|
|
8212
|
+
ok: true,
|
|
8213
|
+
output: {},
|
|
8214
|
+
sessionId,
|
|
8215
|
+
usage
|
|
8216
|
+
}, false);
|
|
8217
|
+
deps.summary(owner).costUsd += own.usage?.costUsd ?? 0;
|
|
8218
|
+
},
|
|
8219
|
+
emit: (owner, event) => {
|
|
8220
|
+
deps.options.onEvent({
|
|
8221
|
+
type: "node.event",
|
|
8222
|
+
id: owner,
|
|
8223
|
+
event
|
|
8224
|
+
});
|
|
8225
|
+
}
|
|
8226
|
+
});
|
|
8227
|
+
/**
|
|
8228
|
+
* The session a node that runs again continues, when it resumes (ADR-033): by
|
|
8229
|
+
* `session: resume`, the default for a leader; a worker starts fresh unless it
|
|
8230
|
+
* says `resume`.
|
|
8231
|
+
*/
|
|
8232
|
+
const resumeOf = (node, sessionId) => {
|
|
8233
|
+
if (node === void 0 || !isClaudeNode(node)) return void 0;
|
|
8234
|
+
if (node.type === "claude.subagent") return void 0;
|
|
8235
|
+
return (node.session ?? (node.type === "claude.leader" ? "resume" : "fresh")) === "resume" ? sessionId : void 0;
|
|
8236
|
+
};
|
|
8237
|
+
// ../core/src/run/contacts.ts
|
|
8238
|
+
const SESSION_TYPES$5 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
|
|
8239
|
+
const isSession$1 = (node) => SESSION_TYPES$5.has(node?.type ?? "");
|
|
8240
|
+
const isTool = (node) => node?.type.startsWith("tool.") ?? false;
|
|
8241
|
+
const channelPeersOf = (id, channels) => channels.flatMap(([a, b]) => {
|
|
8242
|
+
if (a === id) return [b];
|
|
8243
|
+
return b === id ? [a] : [];
|
|
8244
|
+
});
|
|
8245
|
+
/** Every declared node, with its owner, as a run records them. */
|
|
8246
|
+
const declaredNodes = (graph) => [...graph.nodes.values()].map(({ id, node }) => node.parent === void 0 ? {
|
|
8247
|
+
id,
|
|
8248
|
+
type: node.type
|
|
8249
|
+
} : {
|
|
8250
|
+
id,
|
|
8251
|
+
type: node.type,
|
|
8252
|
+
parent: node.parent
|
|
8253
|
+
});
|
|
8254
|
+
/** Whom a session may write to, by how it knows them. */
|
|
8255
|
+
const contactsOf = (id, nodes, channels) => {
|
|
8256
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
8257
|
+
return {
|
|
8258
|
+
owner: byId.get(id)?.parent,
|
|
8259
|
+
children: nodes.filter((node) => node.parent === id && isSession$1(node)).map((node) => node.id),
|
|
8260
|
+
channels: channelPeersOf(id, channels).filter((peer) => !isTool(byId.get(peer)))
|
|
8261
|
+
};
|
|
8262
|
+
};
|
|
8263
|
+
/** Whom a program in the tool node `id` may write to. */
|
|
8264
|
+
const scriptContactsOf = (id, nodes, channels) => {
|
|
8265
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
8266
|
+
const owner = byId.get(id)?.parent;
|
|
8267
|
+
return [.../* @__PURE__ */ new Set([...owner !== void 0 && isSession$1(byId.get(owner)) ? [owner] : [], ...channelPeersOf(id, channels).filter((peer) => isSession$1(byId.get(peer)))])];
|
|
8268
|
+
};
|
|
8269
|
+
/** Whether a program in a tool node may write to the session `id`. */
|
|
8270
|
+
const hearsFromScripts = (id, nodes, channels) => nodes.some((node) => isTool(node) && scriptContactsOf(node.id, nodes, channels).includes(id));
|
|
8271
|
+
// ../core/src/usage/spend.ts
|
|
8272
|
+
/** What a report would add to the session's earlier ones, counting nothing. */
|
|
8273
|
+
const unreported = (reported, key, total) => {
|
|
8274
|
+
if (total <= 0) return 0;
|
|
8275
|
+
const before = reported.get(key) ?? 0;
|
|
8276
|
+
return total < before ? total : total - before;
|
|
8277
|
+
};
|
|
8278
|
+
/**
|
|
8279
|
+
* What a report adds to the session's earlier ones, keeping the total in
|
|
8280
|
+
* `reported`. A zero says nothing, so it is ignored; a lower total is a session
|
|
8281
|
+
* that started counting again, so all of it is new.
|
|
8282
|
+
*/
|
|
8283
|
+
const ownSpend = (reported, key, total) => {
|
|
8284
|
+
const own = unreported(reported, key, total);
|
|
8285
|
+
if (total > 0) reported.set(key, total);
|
|
8286
|
+
return own;
|
|
8287
|
+
};
|
|
8288
|
+
/**
|
|
8289
|
+
* What a run's events say it spent, for `ork resume`. The total is what its
|
|
8290
|
+
* nodes recorded. A session's report counts once its node finished, or when
|
|
8291
|
+
* `ork chat` made it outside any node: a session cut off before its node
|
|
8292
|
+
* finished has recorded nothing, so its next total counts the part that was cut
|
|
8293
|
+
* off. A leader's fork is never resumed, so its report counts when it comes,
|
|
8294
|
+
* whether or not its leader finished.
|
|
8295
|
+
*/
|
|
8296
|
+
const earlierSpend = (events) => {
|
|
8297
|
+
const reported = /* @__PURE__ */ new Map();
|
|
8298
|
+
const open = /* @__PURE__ */ new Map();
|
|
8299
|
+
const forks = /* @__PURE__ */ new Map();
|
|
8300
|
+
let total = 0;
|
|
8301
|
+
for (const event of events) if (event.type === "node.started") open.set(event.id, null);
|
|
8302
|
+
else if (event.type === "node.finished") {
|
|
8303
|
+
total += event.usage?.costUsd ?? 0;
|
|
8304
|
+
const last = open.get(event.id);
|
|
8305
|
+
if (last !== null && last !== void 0) ownSpend(reported, last.key, last.cost);
|
|
8306
|
+
open.delete(event.id);
|
|
8307
|
+
} else if (event.type === "node.event" && event.event.type === "session.finished") {
|
|
8308
|
+
const { sessionId, usage } = event.event.outcome;
|
|
8309
|
+
const key = `${event.id}/${sessionId}`;
|
|
8310
|
+
if (event.event.fork === true) {
|
|
8311
|
+
const own = ownSpend(reported, key, usage.costUsd);
|
|
8312
|
+
total += own;
|
|
8313
|
+
forks.set(event.id, (forks.get(event.id) ?? 0) + own);
|
|
8314
|
+
} else if (open.has(event.id)) open.set(event.id, {
|
|
8315
|
+
key,
|
|
8316
|
+
cost: usage.costUsd
|
|
8317
|
+
});
|
|
8318
|
+
else ownSpend(reported, key, usage.costUsd);
|
|
8319
|
+
}
|
|
8320
|
+
return {
|
|
8321
|
+
total,
|
|
8322
|
+
reported,
|
|
8323
|
+
forks
|
|
8324
|
+
};
|
|
8325
|
+
};
|
|
8326
|
+
// ../core/src/run/reserves.ts
|
|
8327
|
+
const createReserves = () => {
|
|
8328
|
+
const held = /* @__PURE__ */ new Map();
|
|
8329
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
8330
|
+
const wake = () => {
|
|
8331
|
+
const woken = [...waiters];
|
|
8332
|
+
waiters.clear();
|
|
8333
|
+
for (const resolve of woken) resolve();
|
|
7880
8334
|
};
|
|
7881
8335
|
return {
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
8336
|
+
/** All that is set aside now. */
|
|
8337
|
+
total: () => [...held.values()].reduce((sum, reserve) => sum + reserve.usd, 0),
|
|
8338
|
+
/**
|
|
8339
|
+
* `id` sets aside `usd` (nothing, or 0, lets it go). `waitable` is false
|
|
8340
|
+
* for a leader, whose children must never wait on it.
|
|
8341
|
+
*/
|
|
8342
|
+
reserve: (id, usd, waitable) => {
|
|
8343
|
+
const before = held.get(id)?.usd ?? 0;
|
|
8344
|
+
if (usd === void 0 || usd <= 0) held.delete(id);
|
|
8345
|
+
else held.set(id, {
|
|
8346
|
+
usd,
|
|
8347
|
+
waitable
|
|
8348
|
+
});
|
|
8349
|
+
if ((usd ?? 0) < before) wake();
|
|
8350
|
+
},
|
|
8351
|
+
/** The node ended: whatever it still held is let go. */
|
|
8352
|
+
drop: (id) => {
|
|
8353
|
+
if (held.delete(id)) wake();
|
|
8354
|
+
},
|
|
8355
|
+
/**
|
|
8356
|
+
* Waits until a reserve shrinks. `false` at once when no other node holds
|
|
8357
|
+
* one worth waiting on, or when `signal` aborts.
|
|
8358
|
+
*/
|
|
8359
|
+
freed: (id, signal) => {
|
|
8360
|
+
if (![...held].some(([other, reserve]) => other !== id && reserve.waitable) || signal.aborted) return Promise.resolve(false);
|
|
8361
|
+
return new Promise((resolve) => {
|
|
8362
|
+
const done = (freed) => () => {
|
|
8363
|
+
waiters.delete(onFree);
|
|
8364
|
+
signal.removeEventListener("abort", onAbort);
|
|
8365
|
+
resolve(freed);
|
|
8366
|
+
};
|
|
8367
|
+
const onFree = done(true);
|
|
8368
|
+
const onAbort = done(false);
|
|
8369
|
+
waiters.add(onFree);
|
|
8370
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
8371
|
+
});
|
|
8372
|
+
}
|
|
7888
8373
|
};
|
|
7889
8374
|
};
|
|
7890
|
-
// ../core/src/run/
|
|
8375
|
+
// ../core/src/run/spending.ts
|
|
7891
8376
|
/**
|
|
7892
|
-
*
|
|
7893
|
-
*
|
|
8377
|
+
* A result with only what this activation spent (ADR-046): a session's report
|
|
8378
|
+
* holds its total so far, while its tokens are this process's own. `reported`
|
|
8379
|
+
* holds each session's total.
|
|
7894
8380
|
*/
|
|
7895
|
-
|
|
7896
|
-
const
|
|
7897
|
-
|
|
8381
|
+
const withOwnSpend = (id, result, reported) => {
|
|
8382
|
+
const { sessionId, usage } = result;
|
|
8383
|
+
if (sessionId === void 0 || usage === void 0) return result;
|
|
8384
|
+
const own = {
|
|
8385
|
+
...usage,
|
|
8386
|
+
costUsd: ownSpend(reported, `${id}/${sessionId}`, usage.costUsd)
|
|
8387
|
+
};
|
|
7898
8388
|
return {
|
|
7899
|
-
|
|
7900
|
-
|
|
8389
|
+
...result,
|
|
8390
|
+
usage: own,
|
|
8391
|
+
output: "usage" in result.output ? {
|
|
8392
|
+
...result.output,
|
|
8393
|
+
usage: own
|
|
8394
|
+
} : result.output
|
|
8395
|
+
};
|
|
8396
|
+
};
|
|
8397
|
+
/** A run's spending so far; a resumed run starts from what it had spent. */
|
|
8398
|
+
const createSpend = (budgetUsd, earlier, limits) => {
|
|
8399
|
+
const reported = new Map(earlier?.reported);
|
|
8400
|
+
let spent = earlier?.total ?? 0;
|
|
8401
|
+
/** What running sessions last reported, not yet in `spent`, by `node/session`. */
|
|
8402
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
8403
|
+
const committed = () => spent + [...inFlight.values()].reduce((sum, cost) => sum + cost, 0);
|
|
8404
|
+
/** Caps running sessions were given (ADR-077): out of what is offered. */
|
|
8405
|
+
const reserves = createReserves();
|
|
8406
|
+
const budgetLeft = () => budgetUsd === void 0 ? void 0 : budgetUsd - committed() - reserves.total();
|
|
8407
|
+
return {
|
|
8408
|
+
total: () => spent,
|
|
8409
|
+
/** A node sets aside its session's cap; nothing or 0 lets it go. */
|
|
8410
|
+
reserve: reserves.reserve,
|
|
8411
|
+
/** Waits until another node's reserve frees; `false` when none can. */
|
|
8412
|
+
freed: reserves.freed,
|
|
8413
|
+
/** What a session has spent in this activation, from its total so far. */
|
|
8414
|
+
ownOf: (id, sessionId, total) => unreported(reported, `${id}/${sessionId}`, total),
|
|
8415
|
+
/** A running session said what it has spent so far. */
|
|
8416
|
+
report: (id, sessionId, total) => {
|
|
8417
|
+
const key = `${id}/${sessionId}`;
|
|
8418
|
+
inFlight.set(key, unreported(reported, key, total));
|
|
8419
|
+
},
|
|
8420
|
+
/** The node ended: `count` has what its sessions last reported. */
|
|
8421
|
+
settled: (id) => {
|
|
8422
|
+
for (const key of inFlight.keys()) if (key.startsWith(`${id}/`)) inFlight.delete(key);
|
|
8423
|
+
reserves.drop(id);
|
|
8424
|
+
},
|
|
8425
|
+
/** What a session may spend: the least of the budget and any quota. */
|
|
8426
|
+
left: () => {
|
|
8427
|
+
const known = [budgetLeft(), limits?.().leftUsd].filter((left) => left !== void 0);
|
|
8428
|
+
return known.length === 0 ? void 0 : Math.min(...known);
|
|
8429
|
+
},
|
|
8430
|
+
budgetLeft,
|
|
8431
|
+
/** What the limits across runs allow now. */
|
|
8432
|
+
quota: () => limits?.() ?? {},
|
|
8433
|
+
/**
|
|
8434
|
+
* What stops anything new from starting: a quota first, as a budget can be
|
|
8435
|
+
* raised.
|
|
8436
|
+
*/
|
|
8437
|
+
spentOut: () => {
|
|
8438
|
+
const blocked = limits?.().blocked;
|
|
8439
|
+
if (blocked !== void 0) return {
|
|
8440
|
+
status: "quota_exhausted",
|
|
8441
|
+
reason: blocked
|
|
8442
|
+
};
|
|
8443
|
+
return budgetUsd !== void 0 && committed() >= budgetUsd ? {
|
|
8444
|
+
status: "budget_exhausted",
|
|
8445
|
+
reason: `the run's budget of $${String(budgetUsd)} is spent`
|
|
8446
|
+
} : null;
|
|
8447
|
+
},
|
|
8448
|
+
/**
|
|
8449
|
+
* Counts a result, and gives it back with only its own spend. A replayed
|
|
8450
|
+
* result is as stored, and already in what the run started from.
|
|
8451
|
+
*/
|
|
8452
|
+
count: (id, result, replayed) => {
|
|
8453
|
+
if (replayed) return result;
|
|
8454
|
+
const own = withOwnSpend(id, result, reported);
|
|
8455
|
+
spent += own.usage?.costUsd ?? 0;
|
|
8456
|
+
return own;
|
|
8457
|
+
}
|
|
7901
8458
|
};
|
|
7902
8459
|
};
|
|
8460
|
+
/** A run's spending, from what the scheduler is given. */
|
|
8461
|
+
const spendOf = (budgetUsd, options) => createSpend(budgetUsd, options.replay?.earlier, options.limits);
|
|
8462
|
+
// ../core/src/run/gather.ts
|
|
8463
|
+
const asList = (value) => Array.isArray(value) ? value : [];
|
|
8464
|
+
/** `before` then `now`, leaving out an entry already there. */
|
|
8465
|
+
const joined = (before, now) => {
|
|
8466
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8467
|
+
const out = [];
|
|
8468
|
+
for (const entry of [...asList(before), ...asList(now)]) {
|
|
8469
|
+
const key = JSON.stringify(entry);
|
|
8470
|
+
if (seen.has(key)) continue;
|
|
8471
|
+
seen.add(key);
|
|
8472
|
+
out.push(entry);
|
|
8473
|
+
}
|
|
8474
|
+
return out;
|
|
8475
|
+
};
|
|
7903
8476
|
/**
|
|
7904
|
-
*
|
|
7905
|
-
*
|
|
8477
|
+
* `result` with each gathered field holding every round's entries so far.
|
|
8478
|
+
* `previous` is the node's output from its last round, already gathered.
|
|
7906
8479
|
*/
|
|
7907
|
-
const
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
const scope = {
|
|
7915
|
-
inputs,
|
|
7916
|
-
nodes: Object.fromEntries(Object.entries(outputs).map(([id, output]) => [id, { output }]))
|
|
8480
|
+
const joinRounds = (fields, previous, result) => {
|
|
8481
|
+
if (fields.length === 0 || previous === void 0) return result;
|
|
8482
|
+
const output = { ...result.output };
|
|
8483
|
+
for (const field of fields) output[field] = joined(previous[field], result.output[field]);
|
|
8484
|
+
return {
|
|
8485
|
+
...result,
|
|
8486
|
+
output
|
|
7917
8487
|
};
|
|
7918
|
-
if (runId !== void 0) scope.run = { id: runId };
|
|
7919
|
-
if (item !== void 0) {
|
|
7920
|
-
scope.item = item.value;
|
|
7921
|
-
scope.index = item.index;
|
|
7922
|
-
}
|
|
7923
|
-
return scope;
|
|
7924
8488
|
};
|
|
7925
|
-
|
|
8489
|
+
/** `result` with `node`'s `output.gather` fields joined to its earlier rounds. */
|
|
8490
|
+
const gatherRounds = (node, previous, result) => joinRounds(gatheredFieldsOf(node), previous, result);
|
|
7926
8491
|
/**
|
|
7927
|
-
*
|
|
7928
|
-
*
|
|
7929
|
-
*
|
|
7930
|
-
*
|
|
7931
|
-
*
|
|
8492
|
+
* What `activation`'s rounds reported so far, to gather onto. A spawned
|
|
8493
|
+
* instance gathers nothing earlier: its id (`impl-2`) is reused by a different
|
|
8494
|
+
* worker in the leader's next round. A delegated child that hasn't run since a
|
|
8495
|
+
* resume starts from what it reported before it, since its leader's recorded
|
|
8496
|
+
* rounds are handed back whole and never run it again.
|
|
7932
8497
|
*/
|
|
7933
|
-
const
|
|
7934
|
-
const {
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
8498
|
+
const previousOf = (activation, current, replay) => {
|
|
8499
|
+
const { brief, template, id } = activation;
|
|
8500
|
+
if (brief !== void 0 && template !== void 0) return void 0;
|
|
8501
|
+
if (current !== void 0 || brief === void 0) return current;
|
|
8502
|
+
return replay?.lastOutput?.(id);
|
|
8503
|
+
};
|
|
8504
|
+
/** Gathers each activation's result onto what its node reported before. */
|
|
8505
|
+
const createGatherer = (outputs, replay, nodeOf) => (activation, result) => gatherRounds(nodeOf(activation), previousOf(activation, outputs[activation.id], replay), result);
|
|
8506
|
+
// ../core/src/run/results.ts
|
|
8507
|
+
/** Control nodes pass their input through; a join's input is its output. */
|
|
8508
|
+
const passThrough = (input) => ({
|
|
8509
|
+
output: input ?? {},
|
|
8510
|
+
ok: true
|
|
8511
|
+
});
|
|
8512
|
+
const recordRun = (entry, result) => {
|
|
8513
|
+
entry.runs += 1;
|
|
8514
|
+
entry.ok = result.ok;
|
|
8515
|
+
entry.output = result.output;
|
|
8516
|
+
entry.costUsd += result.usage?.costUsd ?? 0;
|
|
8517
|
+
if (result.error === void 0) delete entry.error;
|
|
8518
|
+
else entry.error = result.error;
|
|
8519
|
+
if (result.ok) delete entry.end;
|
|
8520
|
+
else entry.end = endOf(result);
|
|
8521
|
+
};
|
|
8522
|
+
const startedEvent = (activation, nodeType, round, agent) => {
|
|
8523
|
+
const event = {
|
|
8524
|
+
type: "node.started",
|
|
8525
|
+
id: activation.id,
|
|
8526
|
+
nodeType,
|
|
8527
|
+
round
|
|
8528
|
+
};
|
|
8529
|
+
if (activation.brief !== void 0) event.owner = activation.brief.from;
|
|
8530
|
+
if (agent !== void 0) event.agent = agent;
|
|
8531
|
+
const { item } = activation;
|
|
8532
|
+
if (item !== void 0) event.item = {
|
|
8533
|
+
of: item.of,
|
|
8534
|
+
index: item.index
|
|
8535
|
+
};
|
|
8536
|
+
return event;
|
|
8537
|
+
};
|
|
8538
|
+
const finishedEvent = (id, round, result) => {
|
|
8539
|
+
const event = {
|
|
8540
|
+
type: "node.finished",
|
|
7939
8541
|
id,
|
|
7940
|
-
workflow: graph.workflow,
|
|
7941
|
-
inputs: options.inputs,
|
|
7942
|
-
outputs: deps.outputs,
|
|
7943
|
-
input: activation.input,
|
|
7944
8542
|
round,
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
...foldersOf(options),
|
|
7948
|
-
budgetLeftUsd: deps.spend.left(),
|
|
7949
|
-
budgetLeftNow: deps.spend.budgetLeft,
|
|
7950
|
-
reserveBudget: (usd) => {
|
|
7951
|
-
deps.spend.reserve(id, usd, deps.nodeOf(activation)?.type !== "claude.leader");
|
|
7952
|
-
},
|
|
7953
|
-
budgetFreed: (signal) => deps.spend.freed(id, signal),
|
|
7954
|
-
quota: deps.spend.quota,
|
|
7955
|
-
ownSpend: (sessionId, total) => deps.spend.ownOf(id, sessionId, total),
|
|
7956
|
-
human: deps.human,
|
|
7957
|
-
hold: deps.holds.forNode(id, template),
|
|
7958
|
-
...deps.ports.forNode(id, template, cwd),
|
|
7959
|
-
...options.calls === void 0 ? {} : { calls: options.calls },
|
|
7960
|
-
emit: (event) => {
|
|
7961
|
-
deps.ports.owners.seen(id, event, cwd);
|
|
7962
|
-
if (event.type === "session.finished" && event.fork !== true) {
|
|
7963
|
-
const { sessionId, usage } = event.outcome;
|
|
7964
|
-
deps.spend.report(id, sessionId, usage.costUsd);
|
|
7965
|
-
}
|
|
7966
|
-
options.onEvent({
|
|
7967
|
-
type: "node.event",
|
|
7968
|
-
id,
|
|
7969
|
-
event
|
|
7970
|
-
});
|
|
7971
|
-
},
|
|
7972
|
-
signal: deps.stops.signalFor(id, template)
|
|
8543
|
+
ok: result.ok,
|
|
8544
|
+
output: result.output
|
|
7973
8545
|
};
|
|
7974
|
-
|
|
7975
|
-
if (
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
if (activation.item !== void 0) context.item = activation.item;
|
|
7979
|
-
const resume = options.replay?.cutOff?.(activation) ?? resumeOf(deps.nodeOf(activation), deps.sessionIds.get(id) ?? options.replay?.previousSession?.(activation));
|
|
7980
|
-
if (resume !== void 0) context.resume = resume;
|
|
7981
|
-
if (deps.nodeOf(activation)?.type === "claude.leader") context.ownership = createOwnership({
|
|
7982
|
-
graph,
|
|
7983
|
-
leader: id,
|
|
7984
|
-
runChild: deps.runChild,
|
|
7985
|
-
isRunning: (other) => deps.running.has(other),
|
|
7986
|
-
checkpoint: () => options.workspace?.checkpoint(id) ?? Promise.resolve(null),
|
|
7987
|
-
releaseClaims: () => {
|
|
7988
|
-
const held = context.claims?.held() ?? [];
|
|
7989
|
-
for (const resource of held) context.claims?.release(resource);
|
|
7990
|
-
return held;
|
|
7991
|
-
},
|
|
7992
|
-
lend: deps.slots.release,
|
|
7993
|
-
reclaim: deps.slots.take
|
|
7994
|
-
});
|
|
7995
|
-
return context;
|
|
8546
|
+
if (result.usage !== void 0) event.usage = result.usage;
|
|
8547
|
+
if (result.error !== void 0) event.error = result.error;
|
|
8548
|
+
event.end = endOf(result);
|
|
8549
|
+
return event;
|
|
7996
8550
|
};
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
const own = deps.spend.count(owner, {
|
|
8002
|
-
ok: true,
|
|
8003
|
-
output: {},
|
|
8004
|
-
sessionId,
|
|
8005
|
-
usage
|
|
8006
|
-
}, false);
|
|
8007
|
-
deps.summary(owner).costUsd += own.usage?.costUsd ?? 0;
|
|
8551
|
+
const failure = (error) => ({
|
|
8552
|
+
output: {
|
|
8553
|
+
status: "fail",
|
|
8554
|
+
summary: error
|
|
8008
8555
|
},
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8556
|
+
ok: false,
|
|
8557
|
+
error
|
|
8558
|
+
});
|
|
8559
|
+
/** The node's entry in the run summary, created on first use. */
|
|
8560
|
+
const summaryOf = (summaries, id) => {
|
|
8561
|
+
const existing = summaries.get(id);
|
|
8562
|
+
if (existing !== void 0) return existing;
|
|
8563
|
+
const created = {
|
|
8564
|
+
id,
|
|
8565
|
+
runs: 0,
|
|
8566
|
+
ok: null,
|
|
8567
|
+
output: null,
|
|
8568
|
+
costUsd: 0
|
|
8569
|
+
};
|
|
8570
|
+
summaries.set(id, created);
|
|
8571
|
+
return created;
|
|
8572
|
+
};
|
|
8573
|
+
/** A run's summary; a resumed run's leaders keep their forks' spend (ADR-063). */
|
|
8574
|
+
const createSummaries = (earlier) => {
|
|
8575
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
8576
|
+
const summary = (id) => summaryOf(summaries, id);
|
|
8577
|
+
for (const [id, costUsd] of earlier?.forks ?? []) summary(id).costUsd += costUsd;
|
|
8578
|
+
return {
|
|
8579
|
+
summaries,
|
|
8580
|
+
summary
|
|
8581
|
+
};
|
|
8582
|
+
};
|
|
8583
|
+
/** The tighter of the workflow's budget and what a caller left it, if any. */
|
|
8584
|
+
const runBudget = (graph, settings) => {
|
|
8585
|
+
const limits = [graph.workflow.limits?.budgetUsd, settings?.budgetCap].filter((limit) => limit !== void 0);
|
|
8586
|
+
return limits.length === 0 ? {} : { budgetUsd: Math.min(...limits) };
|
|
8587
|
+
};
|
|
8588
|
+
/** The `run.started` event: the workflow, its entries, nodes and inputs. */
|
|
8589
|
+
const runStartedEvent = (graph, runId, options) => ({
|
|
8590
|
+
type: "run.started",
|
|
8591
|
+
runId,
|
|
8592
|
+
workflow: graph.workflow.name,
|
|
8593
|
+
entries: graph.entries,
|
|
8594
|
+
nodes: declaredNodes(graph),
|
|
8595
|
+
edges: graph.edges.map(({ from, to, when, maxRounds }) => ({
|
|
8596
|
+
from,
|
|
8597
|
+
to,
|
|
8598
|
+
...when === void 0 ? {} : { when },
|
|
8599
|
+
...maxRounds === void 0 ? {} : { maxRounds }
|
|
8600
|
+
})),
|
|
8601
|
+
channels: graph.channels,
|
|
8602
|
+
inputs: options.inputs,
|
|
8603
|
+
...options.settings === void 0 ? {} : { settings: options.settings },
|
|
8604
|
+
...runBudget(graph, options.settings),
|
|
8605
|
+
...options.orkVersion === void 0 ? {} : { ork: options.orkVersion }
|
|
8016
8606
|
});
|
|
8607
|
+
/** The unread mail of nodes that never ran; a node that ran saw its notice. */
|
|
8608
|
+
const unreadOf = (nodes, unread) => {
|
|
8609
|
+
const ran = new Set(nodes.filter((node) => node.runs > 0).map((node) => node.id));
|
|
8610
|
+
const left = [...unread ?? []].filter(([node]) => !ran.has(node)).map(([node, messages]) => ({
|
|
8611
|
+
node,
|
|
8612
|
+
messages
|
|
8613
|
+
}));
|
|
8614
|
+
return left.length === 0 ? {} : { unread: left };
|
|
8615
|
+
};
|
|
8017
8616
|
/**
|
|
8018
|
-
* The
|
|
8019
|
-
*
|
|
8020
|
-
* says `resume`.
|
|
8617
|
+
* The run's result: failed when any reason was given, unless it stopped. A
|
|
8618
|
+
* skipped node or a handled failure leaves it `succeeded`, and is counted.
|
|
8021
8619
|
*/
|
|
8022
|
-
const
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8620
|
+
const runResult = (run) => ({
|
|
8621
|
+
runId: run.runId,
|
|
8622
|
+
status: run.stopped ?? (run.reasons.length > 0 ? "failed" : "succeeded"),
|
|
8623
|
+
costUsd: run.costUsd,
|
|
8624
|
+
nodes: run.nodes,
|
|
8625
|
+
reasons: run.reasons,
|
|
8626
|
+
worktrees: run.worktrees,
|
|
8627
|
+
...run.outputs === void 0 ? {} : { outputs: run.outputs },
|
|
8628
|
+
skipped: run.nodes.filter((node) => node.runs === 0).length,
|
|
8629
|
+
failuresHandled: run.failuresHandled,
|
|
8630
|
+
...unreadOf(run.nodes, run.unread)
|
|
8631
|
+
});
|
|
8632
|
+
/** A workflow's `outputs`, rendered as the run ends. */
|
|
8633
|
+
const renderOutputs = (outputs, scope) => outputs === void 0 ? void 0 : Object.fromEntries(Object.entries(outputs).map(([name, text]) => {
|
|
8634
|
+
const template = parseTemplate(text);
|
|
8635
|
+
return [name, template.ok ? renderTemplate(template.parts, scope) : text];
|
|
8636
|
+
}));
|
|
8637
|
+
/** Closes the run's worktrees; a failure is one more reason, not a crash. */
|
|
8638
|
+
const closeWorkspace = (workspace, reasons) => (workspace?.close() ?? Promise.resolve([])).catch((error) => {
|
|
8639
|
+
reasons.push(`cleaning up worktrees: ${String(error)}`);
|
|
8640
|
+
return [];
|
|
8641
|
+
});
|
|
8027
8642
|
// ../core/src/store/nest-items.ts
|
|
8028
8643
|
/** Whether a tree node is an item of a fanned node (`build.2`). */
|
|
8029
8644
|
const isItem = (node) => node.template !== void 0 && /\.\d+$/u.test(node.id);
|
|
@@ -8307,6 +8922,13 @@ const runTree = (events) => {
|
|
|
8307
8922
|
}
|
|
8308
8923
|
return roots;
|
|
8309
8924
|
};
|
|
8925
|
+
/** The nodes of a run waiting for a machine-wide slot now. */
|
|
8926
|
+
const slotWaitsOf = (events) => {
|
|
8927
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
8928
|
+
for (const event of events) if (event.type === "slot.waiting") waiting.add(event.id);
|
|
8929
|
+
else if (event.type === "slot.taken" || event.type === "node.finished") waiting.delete(event.id);
|
|
8930
|
+
return [...waiting];
|
|
8931
|
+
};
|
|
8310
8932
|
/** Folds a run's `ork pause` and `ork continue` commands (ADR-041). */
|
|
8311
8933
|
const pausedOf = (events) => {
|
|
8312
8934
|
let run = false;
|
|
@@ -8499,7 +9121,10 @@ const createStops = (run) => {
|
|
|
8499
9121
|
own.delete(id);
|
|
8500
9122
|
return stopped.delete(id) ? {
|
|
8501
9123
|
...result,
|
|
8502
|
-
...
|
|
9124
|
+
...failedWith(endFor("interrupted"), {
|
|
9125
|
+
status: "fail",
|
|
9126
|
+
summary: "stopped by the user"
|
|
9127
|
+
})
|
|
8503
9128
|
} : result;
|
|
8504
9129
|
}
|
|
8505
9130
|
};
|
|
@@ -8569,6 +9194,8 @@ const createClock = (options) => {
|
|
|
8569
9194
|
else resting.delete(id);
|
|
8570
9195
|
changed();
|
|
8571
9196
|
},
|
|
9197
|
+
/** Stands the node's clock (and its owners' and the run's) still for `wait`. */
|
|
9198
|
+
waitFor: asking,
|
|
8572
9199
|
/** The same channel, noting who waits on the user for how long. */
|
|
8573
9200
|
human: (channel) => ({
|
|
8574
9201
|
permit: (request) => asking(request.nodeId, () => channel.permit(request)),
|
|
@@ -8588,18 +9215,17 @@ const createClock = (options) => {
|
|
|
8588
9215
|
};
|
|
8589
9216
|
};
|
|
8590
9217
|
const timedOut = (timeout, partial) => {
|
|
8591
|
-
const
|
|
8592
|
-
return {
|
|
9218
|
+
const end = endFor("timed_out", { timeout });
|
|
9219
|
+
return endedWith({
|
|
8593
9220
|
...partial,
|
|
8594
9221
|
ok: false,
|
|
8595
|
-
error,
|
|
8596
9222
|
output: {
|
|
8597
9223
|
...partial?.output,
|
|
8598
9224
|
status: "fail",
|
|
8599
|
-
summary:
|
|
9225
|
+
summary: end.message,
|
|
8600
9226
|
timedOut: true
|
|
8601
9227
|
}
|
|
8602
|
-
};
|
|
9228
|
+
}, end);
|
|
8603
9229
|
};
|
|
8604
9230
|
/** Runs `start` under `timeout`, if it has one. */
|
|
8605
9231
|
const withTimeout = async (bound) => {
|
|
@@ -8650,9 +9276,28 @@ const watchRunTimeout = (watched) => {
|
|
|
8650
9276
|
cancelled.abort();
|
|
8651
9277
|
};
|
|
8652
9278
|
};
|
|
9279
|
+
// ../core/src/run/watch.ts
|
|
9280
|
+
/**
|
|
9281
|
+
* @file A `watch:` on a tool or MCP node (RFC-011): the node's look, repeated
|
|
9282
|
+
* until its condition holds, with no model turn. The last look is its output.
|
|
9283
|
+
* A failed look, such as a network error, is tried again up to `misses` in a
|
|
9284
|
+
* row; an answer the node didn't declare fails it at once, as waiting can't
|
|
9285
|
+
* mend that. The time between looks isn't work, so a pause keeps it and the
|
|
9286
|
+
* run's clock stands still. It records what changed, not every look.
|
|
9287
|
+
*/
|
|
9288
|
+
/**
|
|
9289
|
+
* The ends of a failed look that the next look may mend besides a transient
|
|
9290
|
+
* kind: what a tool's non-zero exit or an MCP call's error ends as.
|
|
9291
|
+
*/
|
|
9292
|
+
const LOOKS_AGAIN = /* @__PURE__ */ new Set([
|
|
9293
|
+
"command_failed",
|
|
9294
|
+
"auth",
|
|
9295
|
+
"tool_missing",
|
|
9296
|
+
"interrupted"
|
|
9297
|
+
]);
|
|
8653
9298
|
const SAID_LIMIT = 1024;
|
|
8654
9299
|
/** What a look says, for telling whether it changed and showing it. */
|
|
8655
|
-
const saidBy = (result, declared) => {
|
|
9300
|
+
const saidBy$1 = (result, declared) => {
|
|
8656
9301
|
const { json, exitCode, stdout, text } = result.output;
|
|
8657
9302
|
const value = declared !== void 0 && typeof json === "object" && json !== null ? Object.fromEntries(Object.keys(declared).map((name) => [name, Reflect.get(json, name)])) : json ?? {
|
|
8658
9303
|
exitCode,
|
|
@@ -8685,7 +9330,7 @@ const leftBehind = (signal, graceMs) => {
|
|
|
8685
9330
|
const runWatch = async (options) => {
|
|
8686
9331
|
const { id, watch, declared, look, hold, signal, graceMs, onEvent } = options;
|
|
8687
9332
|
const until = parseExpr(watch.until);
|
|
8688
|
-
if (!until.ok) return
|
|
9333
|
+
if (!until.ok) return failedWith(endFor("invalid_node", { reason: `watch.until: ${until.message}` }));
|
|
8689
9334
|
const every = options.everyMs ?? timeoutMs(watch.every) ?? 0;
|
|
8690
9335
|
const misses = watch.misses ?? 3;
|
|
8691
9336
|
const limit = new AbortController();
|
|
@@ -8706,17 +9351,16 @@ const runWatch = async (options) => {
|
|
|
8706
9351
|
};
|
|
8707
9352
|
const expiredResult = () => {
|
|
8708
9353
|
const error = `watched for ${watch.timeout}, and ${markValue(watch.until)} never held`;
|
|
8709
|
-
return {
|
|
9354
|
+
return endedWith({
|
|
8710
9355
|
...last,
|
|
8711
9356
|
ok: false,
|
|
8712
|
-
error,
|
|
8713
9357
|
output: {
|
|
8714
9358
|
...last?.output,
|
|
8715
9359
|
status: "fail",
|
|
8716
9360
|
summary: error,
|
|
8717
9361
|
timedOut: true
|
|
8718
9362
|
}
|
|
8719
|
-
};
|
|
9363
|
+
}, endFor("timed_out", { timeout: watch.timeout }, error));
|
|
8720
9364
|
};
|
|
8721
9365
|
/** Stands still while paused, and records the hold at once. */
|
|
8722
9366
|
const rest = async () => {
|
|
@@ -8761,11 +9405,14 @@ const runWatch = async (options) => {
|
|
|
8761
9405
|
for (;;) {
|
|
8762
9406
|
await hold?.wait("start", active);
|
|
8763
9407
|
const result = active.aborted ? null : await lookOnce();
|
|
8764
|
-
if (signal.aborted) return ended("stopped",
|
|
9408
|
+
if (signal.aborted) return ended("stopped", failedWith(endFor("interrupted"), {
|
|
9409
|
+
status: "fail",
|
|
9410
|
+
summary: "stopped while watching"
|
|
9411
|
+
}));
|
|
8765
9412
|
if (limit.signal.aborted || result === null) return ended("timed_out", expiredResult());
|
|
8766
9413
|
looks += 1;
|
|
8767
9414
|
last = result;
|
|
8768
|
-
const said = saidBy(result, declared);
|
|
9415
|
+
const said = saidBy$1(result, declared);
|
|
8769
9416
|
if (said !== lastSaid) {
|
|
8770
9417
|
lastSaid = said;
|
|
8771
9418
|
onEvent({
|
|
@@ -8781,7 +9428,7 @@ const runWatch = async (options) => {
|
|
|
8781
9428
|
missed = 0;
|
|
8782
9429
|
if (holds(result.output)) return ended("met", result);
|
|
8783
9430
|
} else {
|
|
8784
|
-
if (!(result.transient === true || result.output["timedOut"] === true)) return ended("failed", result);
|
|
9431
|
+
if (!(result.transient === true || LOOKS_AGAIN.has(result.end?.kind) || result.output["timedOut"] === true)) return ended("failed", result);
|
|
8785
9432
|
missed += 1;
|
|
8786
9433
|
if (missed >= misses) return ended("failed", {
|
|
8787
9434
|
...result,
|
|
@@ -8798,19 +9445,36 @@ const runWatch = async (options) => {
|
|
|
8798
9445
|
const repeatsCall = (event) => event.type === "tool.called" || event.type === "tool.result";
|
|
8799
9446
|
// ../core/src/run/execute.ts
|
|
8800
9447
|
/** Runs an activation; control nodes pass their input through. */
|
|
8801
|
-
const
|
|
9448
|
+
const saidBy = (error) => error instanceof Error ? error.message : String(error);
|
|
9449
|
+
/**
|
|
9450
|
+
* A thrown error's end: a failed git or missing program by its text, else
|
|
9451
|
+
* `fallback`.
|
|
9452
|
+
*/
|
|
9453
|
+
const thrown = (error, fallback) => {
|
|
9454
|
+
const text = saidBy(error);
|
|
9455
|
+
const read = endOf({
|
|
9456
|
+
ok: false,
|
|
9457
|
+
output: {},
|
|
9458
|
+
error: text
|
|
9459
|
+
});
|
|
9460
|
+
return failedWith(read.kind === "unknown" ? endFor(fallback, { reason: text }, text) : read);
|
|
9461
|
+
};
|
|
9462
|
+
/** Runs an activation, its end's detail scrubbed of the run's paths. */
|
|
9463
|
+
const createExecute = (deps) => async (activation, round) => scrubbedEnd(await executeOne(deps, activation, round), [deps.options.cwd, deps.options.projectRoot ?? ""]);
|
|
9464
|
+
const executeOne = async (deps, activation, round) => {
|
|
8802
9465
|
const { options, contextDeps, clock, graceMs, nodeOf } = deps;
|
|
8803
9466
|
const node = nodeOf(activation);
|
|
8804
|
-
if (node === void 0) return
|
|
9467
|
+
if (node === void 0) return failedWith(endFor("unknown", {}, `unknown node ${activation.id}`));
|
|
8805
9468
|
if (node.type.startsWith("control.")) return passThrough(activation.input);
|
|
8806
9469
|
const executor = options.executors[node.type];
|
|
8807
|
-
if (executor === void 0) return
|
|
9470
|
+
if (executor === void 0) return failedWith(endFor("unknown", {}, `no executor for ${node.type}`));
|
|
8808
9471
|
try {
|
|
8809
9472
|
const forEach = activation.item === void 0 ? forEachOf(node) : void 0;
|
|
8810
9473
|
if (forEach !== void 0) return await deps.fanOut(activation, forEach, round);
|
|
8811
9474
|
const { workspace } = options;
|
|
8812
|
-
const place = await workspace?.prepare(activation.id, activation.template) ?? { cwd: options.cwd };
|
|
8813
|
-
|
|
9475
|
+
const place = await (workspace?.prepare(activation.id, activation.template) ?? Promise.resolve({ cwd: options.cwd })).catch((error) => thrown(error, "invalid_node"));
|
|
9476
|
+
if ("ok" in place) return place;
|
|
9477
|
+
const context = nodeContext(contextDeps, activation, place.cwd, round, clock);
|
|
8814
9478
|
const once = (signal, emit) => withTimeout({
|
|
8815
9479
|
timeout: timeoutOf(node, options.graph.workflow),
|
|
8816
9480
|
clock: clock.forNode(activation.id),
|
|
@@ -8842,9 +9506,10 @@ const createExecute = (deps) => async (activation, round) => {
|
|
|
8842
9506
|
},
|
|
8843
9507
|
everyMs: options.watchEveryMs
|
|
8844
9508
|
});
|
|
8845
|
-
|
|
9509
|
+
const ended = result.ok || !context.signal.aborted ? result : endedWith(result, stoppedEnd(context.signal));
|
|
9510
|
+
return await workspace?.finish(activation.id, ended) ?? ended;
|
|
8846
9511
|
} catch (error) {
|
|
8847
|
-
return
|
|
9512
|
+
return thrown(error, "unknown");
|
|
8848
9513
|
}
|
|
8849
9514
|
};
|
|
8850
9515
|
// ../core/src/run/bus-calls.ts
|
|
@@ -9358,7 +10023,7 @@ const failed = (id, why) => {
|
|
|
9358
10023
|
return reason === "" ? `${id} failed` : `${id} failed: ${reason}`;
|
|
9359
10024
|
};
|
|
9360
10025
|
/** A program that ran says how it exited; anything else has its own reason. */
|
|
9361
|
-
const toolLine = (id, result) => {
|
|
10026
|
+
const toolLine$1 = (id, result) => {
|
|
9362
10027
|
const { output, ok, error } = result;
|
|
9363
10028
|
const code = output["exitCode"];
|
|
9364
10029
|
if (!(typeof code === "number" && code >= 0 && output["timedOut"] !== true && (ok || code !== 0))) return ok ? `${id} passed` : failed(id, error ?? "");
|
|
@@ -9368,7 +10033,7 @@ const toolLine = (id, result) => {
|
|
|
9368
10033
|
/** Ork's own line about a result, when a rule gives no body. */
|
|
9369
10034
|
const defaultRuleBody = (id, node, result) => {
|
|
9370
10035
|
const { output, ok } = result;
|
|
9371
|
-
if (node !== void 0 && isToolNode(node)) return toolLine(id, result);
|
|
10036
|
+
if (node !== void 0 && isToolNode(node)) return toolLine$1(id, result);
|
|
9372
10037
|
const summary = output["summary"];
|
|
9373
10038
|
const said = typeof summary === "string" ? summary : "";
|
|
9374
10039
|
if (!ok) return failed(id, result.error ?? said);
|
|
@@ -9636,7 +10301,7 @@ const startRun = (options) => {
|
|
|
9636
10301
|
expire: (reason) => {
|
|
9637
10302
|
stop("timed_out", reason);
|
|
9638
10303
|
options.calls?.stopAll();
|
|
9639
|
-
abort.abort();
|
|
10304
|
+
abort.abort(runTimedOut(limits.timeout));
|
|
9640
10305
|
settle();
|
|
9641
10306
|
},
|
|
9642
10307
|
leave: () => {
|
|
@@ -10929,7 +11594,7 @@ const mailboxInstructions = (mailbox, owner) => {
|
|
|
10929
11594
|
const verifyInstructions = (node) => {
|
|
10930
11595
|
if (!("verify" in node) || node.verify === void 0) return [];
|
|
10931
11596
|
const { command } = node.verify;
|
|
10932
|
-
return [`- A report that is not \`fail\` or \`changes_requested\` is accepted only if ${command === void 0 || command.includes("{{") ? "a check the workflow defines" : `\`${command}\``} passes when you report. If it fails, you are told why and asked to fix it, so run it yourself first if you can
|
|
11597
|
+
return [`- A report that is not \`fail\` or \`changes_requested\` is accepted only if ${command === void 0 || command.includes("{{") ? "a check the workflow defines" : `\`${command}\``} passes when you report. If it fails, you are told why and asked to fix it, ${(command !== void 0 && node.tools?.deny?.includes(heavyRule(command))) === true ? "so run targeted checks (such as the tests you changed) first: Ork runs the full check, and this session may not." : "so run it yourself first if you can."}`];
|
|
10933
11598
|
};
|
|
10934
11599
|
/** Ork's own instructions for a node, kept short: they cost tokens every turn. */
|
|
10935
11600
|
const harnessInstructions = (workflow, id, node, options = {}) => {
|
|
@@ -11279,6 +11944,112 @@ const explainWorkflowText = (text, file, nodeId, where) => {
|
|
|
11279
11944
|
sources: sourcesOf(result.explanation, place, where.home ?? homedir())
|
|
11280
11945
|
} : result;
|
|
11281
11946
|
};
|
|
11947
|
+
// ../core/src/usage/quota.ts
|
|
11948
|
+
const WINDOWS = [
|
|
11949
|
+
"day",
|
|
11950
|
+
"week",
|
|
11951
|
+
"month"
|
|
11952
|
+
];
|
|
11953
|
+
const FIELDS = {
|
|
11954
|
+
day: "dayUsd",
|
|
11955
|
+
week: "weekUsd",
|
|
11956
|
+
month: "monthUsd"
|
|
11957
|
+
};
|
|
11958
|
+
/** Where a window began and when the next begins, in local time. */
|
|
11959
|
+
const windowOf = (window, now) => {
|
|
11960
|
+
const [year, month, day] = [
|
|
11961
|
+
now.getFullYear(),
|
|
11962
|
+
now.getMonth(),
|
|
11963
|
+
now.getDate()
|
|
11964
|
+
];
|
|
11965
|
+
if (window === "month") return {
|
|
11966
|
+
since: new Date(year, month, 1),
|
|
11967
|
+
until: new Date(year, month + 1)
|
|
11968
|
+
};
|
|
11969
|
+
if (window === "day") return {
|
|
11970
|
+
since: new Date(year, month, day),
|
|
11971
|
+
until: new Date(year, month, day + 1)
|
|
11972
|
+
};
|
|
11973
|
+
const monday = day - (now.getDay() + 6) % 7;
|
|
11974
|
+
return {
|
|
11975
|
+
since: new Date(year, month, monday),
|
|
11976
|
+
until: new Date(year, month, monday + 7)
|
|
11977
|
+
};
|
|
11978
|
+
};
|
|
11979
|
+
/** The caps that apply: the user's on everything, and the project's on itself. */
|
|
11980
|
+
const capsOf = (user, project, root) => WINDOWS.flatMap((window) => {
|
|
11981
|
+
const field = FIELDS[window];
|
|
11982
|
+
const everywhere = user?.[field];
|
|
11983
|
+
const here = project?.[field];
|
|
11984
|
+
const caps = [];
|
|
11985
|
+
if (everywhere !== void 0) caps.push({
|
|
11986
|
+
window,
|
|
11987
|
+
capUsd: everywhere
|
|
11988
|
+
});
|
|
11989
|
+
if (here !== void 0 && root !== void 0) caps.push({
|
|
11990
|
+
window,
|
|
11991
|
+
capUsd: here,
|
|
11992
|
+
project: root
|
|
11993
|
+
});
|
|
11994
|
+
return caps;
|
|
11995
|
+
});
|
|
11996
|
+
/** Each cap with what has been spent against it. */
|
|
11997
|
+
const standingOf = (store, caps, now) => caps.map((cap) => {
|
|
11998
|
+
const { since, until } = windowOf(cap.window, now);
|
|
11999
|
+
const spentUsd = store.spentSince(since.toISOString(), cap.project);
|
|
12000
|
+
return {
|
|
12001
|
+
...cap,
|
|
12002
|
+
spentUsd,
|
|
12003
|
+
leftUsd: cap.capUsd - spentUsd,
|
|
12004
|
+
resetsAt: until.toISOString()
|
|
12005
|
+
};
|
|
12006
|
+
});
|
|
12007
|
+
/** Dollars: cents, or more digits for an amount under a dollar. */
|
|
12008
|
+
const usd = (amount) => `$${amount.toFixed(amount > 0 && amount < 1 ? 4 : 2)}`;
|
|
12009
|
+
const at = (iso) => new Date(iso).toLocaleString(void 0, {
|
|
12010
|
+
dateStyle: "medium",
|
|
12011
|
+
timeStyle: "short"
|
|
12012
|
+
});
|
|
12013
|
+
const capReason = (spent) => `${spent.project === void 0 ? "your" : "this project's"} ${spent.window} quota of ${usd(spent.capUsd)} is spent (${usd(spent.spentUsd)}); it lifts on ${at(spent.resetsAt)}, or raise it in ${spent.project === void 0 ? "~/.ork/settings.yaml" : ".ork/project.yaml"}`;
|
|
12014
|
+
/** What a run or a call refused at its start says first, for its caller to read. */
|
|
12015
|
+
const NOT_STARTED = "Not started: ";
|
|
12016
|
+
/** Why a plan limit stops a run. */
|
|
12017
|
+
const planReason = (limit) => `your Claude plan's ${limit.window.replaceAll("_", " ")} limit was reached${limit.resetsAt === void 0 ? "" : `; it resets on ${at(limit.resetsAt)}, or use ork usage --clear-plan-limits if it has lifted`}`;
|
|
12018
|
+
/**
|
|
12019
|
+
* The windows that hold the whole account. One for a single model, such as
|
|
12020
|
+
* `seven_day_opus`, stops only the session it refused: other models may be
|
|
12021
|
+
* free.
|
|
12022
|
+
*/
|
|
12023
|
+
const ACCOUNT_WINDOWS = /* @__PURE__ */ new Set([
|
|
12024
|
+
"five_hour",
|
|
12025
|
+
"seven_day",
|
|
12026
|
+
"overage",
|
|
12027
|
+
"plan"
|
|
12028
|
+
]);
|
|
12029
|
+
/** The plan limits in force, the one that lifts last first. */
|
|
12030
|
+
const refusing = (limits, now) => limits.filter((limit) => limit.status === "rejected" && ACCOUNT_WINDOWS.has(limit.window) && limit.resetsAt !== void 0 && Date.parse(limit.resetsAt) > now.getTime()).toSorted((a, b) => String(b.resetsAt).localeCompare(String(a.resetsAt)));
|
|
12031
|
+
/** How long a report with no reset time stays worth showing. */
|
|
12032
|
+
const SHOWN_MS = 864e5;
|
|
12033
|
+
/** What the caps and the plan allow a run now. */
|
|
12034
|
+
const limitsNow = (store, caps, now) => {
|
|
12035
|
+
const standing = standingOf(store, caps, now);
|
|
12036
|
+
const [plan] = refusing(store.planLimits(), now);
|
|
12037
|
+
const spent = standing.find((cap) => cap.leftUsd <= 0);
|
|
12038
|
+
const blocked = plan === void 0 ? spent === void 0 ? void 0 : capReason(spent) : planReason(plan);
|
|
12039
|
+
return {
|
|
12040
|
+
...standing.length === 0 ? {} : { leftUsd: Math.min(...standing.map((cap) => cap.leftUsd)) },
|
|
12041
|
+
...blocked === void 0 ? {} : { blocked }
|
|
12042
|
+
};
|
|
12043
|
+
};
|
|
12044
|
+
/** What `ork usage` and the panel show. */
|
|
12045
|
+
const limitsReport = (store, { caps, problems }, now) => ({
|
|
12046
|
+
problems,
|
|
12047
|
+
caps: standingOf(store, caps, now),
|
|
12048
|
+
plan: store.planLimits().filter((limit) => {
|
|
12049
|
+
if (limit.status === "allowed") return false;
|
|
12050
|
+
return (limit.resetsAt === void 0 ? Date.parse(limit.at) + SHOWN_MS : Date.parse(limit.resetsAt)) > now.getTime();
|
|
12051
|
+
})
|
|
12052
|
+
});
|
|
11282
12053
|
// ../core/src/remote/follow.ts
|
|
11283
12054
|
/**
|
|
11284
12055
|
* @file Following a run's events from another process (`ork logs -f`, the local
|
|
@@ -11362,31 +12133,58 @@ const noWait = (id) => ({
|
|
|
11362
12133
|
run: id
|
|
11363
12134
|
}
|
|
11364
12135
|
});
|
|
11365
|
-
/**
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
12136
|
+
/**
|
|
12137
|
+
* A call that could not start its run: refused by a limit across runs
|
|
12138
|
+
* (RFC-007), or something the calling node asks for that isn't there.
|
|
12139
|
+
*/
|
|
12140
|
+
const startFailed = (error) => failedWith(error.includes("Not started: ") ? endFor("plan_limit", {}, error) : endFor("invalid_node", { reason: error }), {
|
|
12141
|
+
status: "fail",
|
|
12142
|
+
summary: error
|
|
12143
|
+
});
|
|
12144
|
+
/** How a run that ended `status` ended, when its nodes don't say. */
|
|
12145
|
+
const STATUS_KINDS = {
|
|
12146
|
+
succeeded: "done",
|
|
12147
|
+
failed: "unknown",
|
|
12148
|
+
budget_exhausted: "budget_limit",
|
|
12149
|
+
quota_exhausted: "plan_limit",
|
|
12150
|
+
timed_out: "timed_out",
|
|
12151
|
+
interrupted: "interrupted"
|
|
12152
|
+
};
|
|
12153
|
+
/**
|
|
12154
|
+
* How a called run ended, carried up to the node that called it: a limit the
|
|
12155
|
+
* run stopped at, or else how its first failed node ended.
|
|
12156
|
+
*/
|
|
12157
|
+
const runEnd = (result, detail) => {
|
|
12158
|
+
const kind = STATUS_KINDS[result.status];
|
|
12159
|
+
const node = result.nodes.find((entry) => entry.ok === false);
|
|
12160
|
+
return kind === "unknown" && node !== void 0 ? endOf({
|
|
12161
|
+
ok: false,
|
|
12162
|
+
output: node.output ?? {},
|
|
12163
|
+
...node.error === void 0 ? {} : { error: node.error },
|
|
12164
|
+
...node.end === void 0 ? {} : { end: node.end }
|
|
12165
|
+
}) : endFor(kind, {}, detail);
|
|
11377
12166
|
};
|
|
12167
|
+
/** A node's report as a call's result; a failing one says why. */
|
|
12168
|
+
const reportResult = (id, output, ok, usage, end = endFor(ok ? "done" : "reported_failure")) => endedWith({
|
|
12169
|
+
ok,
|
|
12170
|
+
output: {
|
|
12171
|
+
...output,
|
|
12172
|
+
run: id
|
|
12173
|
+
},
|
|
12174
|
+
usage
|
|
12175
|
+
}, end);
|
|
11378
12176
|
/**
|
|
11379
12177
|
* What the node last reported, for a caller coming back to the run: a resumed
|
|
11380
12178
|
* run hands its kept results back with no new events, so none would come.
|
|
11381
12179
|
*/
|
|
11382
12180
|
const storedReport = (watch, id, node) => {
|
|
11383
12181
|
const found = watch.store.events(id).flatMap(({ event }) => event.type === "node.finished" && event.id === node && event.ok ? [event.output] : []).at(-1);
|
|
11384
|
-
return found === void 0 ? void 0 : reportResult(id,
|
|
12182
|
+
return found === void 0 ? void 0 : reportResult(id, found, true, usageOf(watch.newCost(id)));
|
|
11385
12183
|
};
|
|
11386
12184
|
const resultOf = (id, result, cost) => {
|
|
11387
12185
|
const ok = result.status === "succeeded";
|
|
11388
12186
|
const summary = ok ? `Run ${id} succeeded.` : `Run ${id} ended ${result.status}: ${result.reasons.join("; ")}`;
|
|
11389
|
-
return {
|
|
12187
|
+
return endedWith({
|
|
11390
12188
|
ok,
|
|
11391
12189
|
output: {
|
|
11392
12190
|
status: ok ? "pass" : "fail",
|
|
@@ -11395,9 +12193,8 @@ const resultOf = (id, result, cost) => {
|
|
|
11395
12193
|
runStatus: result.status,
|
|
11396
12194
|
outputs: result.outputs ?? {}
|
|
11397
12195
|
},
|
|
11398
|
-
usage: usageOf(cost)
|
|
11399
|
-
|
|
11400
|
-
};
|
|
12196
|
+
usage: usageOf(cost)
|
|
12197
|
+
}, ok ? endFor("done") : runEnd(result, summary));
|
|
11401
12198
|
};
|
|
11402
12199
|
/** The first thing `pick` finds in the run's events after `after`, if any. */
|
|
11403
12200
|
const followRun = async ({ store, home }, id, after, signal, pick) => {
|
|
@@ -11420,9 +12217,12 @@ const waitForRun = async (watch, id, after, signal) => {
|
|
|
11420
12217
|
const cost = watch.newCost(id);
|
|
11421
12218
|
if (result !== void 0) return resultOf(id, result, cost);
|
|
11422
12219
|
const [run] = watch.store.lookup(id);
|
|
11423
|
-
const
|
|
12220
|
+
const said = `Run ${id} ended without a result (${run === void 0 ? "gone" : liveStatus(run)}).`;
|
|
11424
12221
|
return {
|
|
11425
|
-
...
|
|
12222
|
+
...failedWith(endFor("unknown", {}, said), {
|
|
12223
|
+
status: "fail",
|
|
12224
|
+
summary: said
|
|
12225
|
+
}),
|
|
11426
12226
|
usage: usageOf(cost)
|
|
11427
12227
|
};
|
|
11428
12228
|
};
|
|
@@ -11431,20 +12231,34 @@ const waitForNode = async (watch, id, node, after, signal) => {
|
|
|
11431
12231
|
const seen = await followRun(watch, id, after, signal, (event) => {
|
|
11432
12232
|
if (event.type === "node.finished" && event.id === node) return {
|
|
11433
12233
|
ok: event.ok,
|
|
11434
|
-
output: event.output
|
|
12234
|
+
output: event.output,
|
|
12235
|
+
end: endOf(event)
|
|
11435
12236
|
};
|
|
11436
12237
|
return event.type === "run.finished" ? { ended: event.result } : void 0;
|
|
11437
12238
|
});
|
|
11438
12239
|
const usage = usageOf(watch.newCost(id));
|
|
11439
|
-
if (seen === void 0)
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
12240
|
+
if (seen === void 0) {
|
|
12241
|
+
const said = `Run ${id} ended before ${node} reported.`;
|
|
12242
|
+
return {
|
|
12243
|
+
...failedWith(endFor("unknown", {}, said), {
|
|
12244
|
+
status: "fail",
|
|
12245
|
+
summary: said
|
|
12246
|
+
}),
|
|
12247
|
+
usage
|
|
12248
|
+
};
|
|
12249
|
+
}
|
|
12250
|
+
if ("ended" in seen) {
|
|
12251
|
+
const said = `${node} of run ${id} did not report: the run ended ${seen.ended.status}.`;
|
|
12252
|
+
const end = seen.ended.status === "succeeded" ? endFor("no_report", {}, said) : runEnd(seen.ended, said);
|
|
12253
|
+
return {
|
|
12254
|
+
...failedWith(end, {
|
|
12255
|
+
status: "fail",
|
|
12256
|
+
summary: said
|
|
12257
|
+
}),
|
|
12258
|
+
usage
|
|
12259
|
+
};
|
|
12260
|
+
}
|
|
12261
|
+
return reportResult(id, seen.output, seen.ok, usage, seen.end);
|
|
11448
12262
|
};
|
|
11449
12263
|
// ../core/src/remote/detach.ts
|
|
11450
12264
|
/**
|
|
@@ -11976,18 +12790,18 @@ const usageRowsOf = (run, events) => {
|
|
|
11976
12790
|
}
|
|
11977
12791
|
return rows;
|
|
11978
12792
|
};
|
|
11979
|
-
const SOURCE$
|
|
12793
|
+
const SOURCE$2 = `SELECT seq, at, data FROM events
|
|
11980
12794
|
WHERE run_id = ? AND (
|
|
11981
12795
|
type IN ('run.started', 'node.started', 'node.finished')
|
|
11982
12796
|
OR (type = 'node.event'
|
|
11983
12797
|
AND json_extract(data, '$.event.type') IN ('session.started', 'session.finished'))
|
|
11984
12798
|
) ORDER BY seq`;
|
|
11985
|
-
const INSERT = `INSERT OR IGNORE INTO usage (
|
|
12799
|
+
const INSERT$1 = `INSERT OR IGNORE INTO usage (
|
|
11986
12800
|
run_id, seq, at, project, workflow, node, template, model, driver, kind,
|
|
11987
12801
|
round, failed, cost_usd, input_tokens, output_tokens, cache_read_tokens,
|
|
11988
12802
|
cache_write_tokens, turns, duration_ms
|
|
11989
12803
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
11990
|
-
const rowsFromEvents = (db, run) => usageRowsOf(run, db.prepare(SOURCE$
|
|
12804
|
+
const rowsFromEvents = (db, run) => usageRowsOf(run, db.prepare(SOURCE$2).all(run.id).map((r) => ({
|
|
11991
12805
|
seq: Number(r["seq"]),
|
|
11992
12806
|
at: text(r, "at"),
|
|
11993
12807
|
event: eventOf(text(r, "data"))
|
|
@@ -11997,7 +12811,7 @@ const writeRow = (insert, row) => {
|
|
|
11997
12811
|
};
|
|
11998
12812
|
/** Fills the ledger from the runs recorded before it existed. */
|
|
11999
12813
|
const backfillUsage = (db) => {
|
|
12000
|
-
const insert = db.prepare(INSERT);
|
|
12814
|
+
const insert = db.prepare(INSERT$1);
|
|
12001
12815
|
for (const run of db.prepare("SELECT id, project, workflow FROM runs").all()) {
|
|
12002
12816
|
const rows = rowsFromEvents(db, {
|
|
12003
12817
|
id: text(run, "id"),
|
|
@@ -12059,7 +12873,7 @@ const alive = (row) => {
|
|
|
12059
12873
|
};
|
|
12060
12874
|
/** The ledger's queries; `recordUsage` is the store's own, called on append. */
|
|
12061
12875
|
const usageQueries = (db) => {
|
|
12062
|
-
const insert = db.prepare(INSERT);
|
|
12876
|
+
const insert = db.prepare(INSERT$1);
|
|
12063
12877
|
const spentAll = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) AS spent FROM usage WHERE at >= ?");
|
|
12064
12878
|
const spentHere = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) AS spent FROM usage WHERE at >= ? AND project = ?");
|
|
12065
12879
|
const runById = db.prepare("SELECT id, project, workflow FROM runs WHERE id = ?");
|
|
@@ -12384,21 +13198,26 @@ const resumeNode = async (talk, run, request, node, signal) => {
|
|
|
12384
13198
|
const refused = await talk.resumeSession(run, node, body, signal);
|
|
12385
13199
|
if (refused !== null) return failure(refused);
|
|
12386
13200
|
const finished = store.events(run.id, from).flatMap(({ event }) => event.type === "node.event" && event.event.type === "session.finished" && event.id === node ? [event.event.outcome] : []).at(-1);
|
|
12387
|
-
if (finished?.report === null || finished === void 0)
|
|
13201
|
+
if (finished?.report === null || finished === void 0) {
|
|
13202
|
+
const said = `${node} ended without a report when resumed.`;
|
|
13203
|
+
return failedWith(endFor("no_report", {}, said), {
|
|
13204
|
+
status: "fail",
|
|
13205
|
+
summary: said
|
|
13206
|
+
});
|
|
13207
|
+
}
|
|
12388
13208
|
const ok = finished.report.status !== "fail";
|
|
12389
13209
|
const own = usageRowsOf(run, store.events(run.id)).filter((row) => row.seq > from && row.node === node).reduce((sum, row) => sum + row.costUsd, 0);
|
|
12390
|
-
return {
|
|
13210
|
+
return endedWith({
|
|
12391
13211
|
ok,
|
|
12392
13212
|
output: {
|
|
12393
13213
|
...finished.report,
|
|
12394
13214
|
run: run.id
|
|
12395
13215
|
},
|
|
12396
|
-
...ok ? {} : { error: finished.report.summary },
|
|
12397
13216
|
usage: {
|
|
12398
13217
|
...finished.usage,
|
|
12399
13218
|
costUsd: own + talk.newCost(run.id)
|
|
12400
13219
|
}
|
|
12401
|
-
};
|
|
13220
|
+
}, endFor(ok ? "done" : "reported_failure"));
|
|
12402
13221
|
};
|
|
12403
13222
|
/** A later activation: as a message, or by resuming a session that has ended. */
|
|
12404
13223
|
const continueConversation = async (talk, id, request, signal) => {
|
|
@@ -12417,7 +13236,13 @@ const continueConversation = async (talk, id, request, signal) => {
|
|
|
12417
13236
|
if (isLive()) await followRun(talk, id, lastSeq(store, id), signal, finishedRun);
|
|
12418
13237
|
const [run] = store.lookup(id);
|
|
12419
13238
|
if (run === void 0) return failure(`Run ${id} is gone.`);
|
|
12420
|
-
if (signal.aborted)
|
|
13239
|
+
if (signal.aborted) {
|
|
13240
|
+
const said = `Stopped while run ${id} was finishing.`;
|
|
13241
|
+
return failedWith(endFor("interrupted", {}, said), {
|
|
13242
|
+
status: "fail",
|
|
13243
|
+
summary: said
|
|
13244
|
+
});
|
|
13245
|
+
}
|
|
12421
13246
|
return resumeNode(talk, run, request, node, signal);
|
|
12422
13247
|
};
|
|
12423
13248
|
// ../core/src/remote/calls.ts
|
|
@@ -12513,7 +13338,7 @@ const createCalls = (options) => {
|
|
|
12513
13338
|
};
|
|
12514
13339
|
const converse = async (request, signal) => {
|
|
12515
13340
|
const unadopted = await adoptConversation(request, signal);
|
|
12516
|
-
if (unadopted !== null) return
|
|
13341
|
+
if (unadopted !== null) return startFailed(unadopted);
|
|
12517
13342
|
const known = conversations.get(request.callId);
|
|
12518
13343
|
if (known !== void 0) {
|
|
12519
13344
|
const done = stopOnAbort(known, signal);
|
|
@@ -12525,7 +13350,7 @@ const createCalls = (options) => {
|
|
|
12525
13350
|
}
|
|
12526
13351
|
const begun = await begin(request);
|
|
12527
13352
|
adopt.delete(request.callId);
|
|
12528
|
-
if (!begun.ok) return
|
|
13353
|
+
if (!begun.ok) return startFailed(begun.error);
|
|
12529
13354
|
const { id, after, fresh } = begun;
|
|
12530
13355
|
conversations.set(request.callId, id);
|
|
12531
13356
|
const done = stopOnAbort(id, signal);
|
|
@@ -12534,7 +13359,7 @@ const createCalls = (options) => {
|
|
|
12534
13359
|
const refused = await linkChannels(talk, id, request, signal);
|
|
12535
13360
|
if (refused !== null) {
|
|
12536
13361
|
stop(id);
|
|
12537
|
-
return
|
|
13362
|
+
return startFailed(refused);
|
|
12538
13363
|
}
|
|
12539
13364
|
}
|
|
12540
13365
|
if (fresh) tellInput(talk, id, request);
|
|
@@ -12550,7 +13375,7 @@ const createCalls = (options) => {
|
|
|
12550
13375
|
};
|
|
12551
13376
|
const callOnce = async (request, signal) => {
|
|
12552
13377
|
const begun = await begin(request);
|
|
12553
|
-
if (!begun.ok) return
|
|
13378
|
+
if (!begun.ok) return startFailed(begun.error);
|
|
12554
13379
|
const { id, after } = begun;
|
|
12555
13380
|
const done = stopOnAbort(id, signal);
|
|
12556
13381
|
try {
|
|
@@ -13510,6 +14335,82 @@ const projectQueries = (db) => {
|
|
|
13510
14335
|
}
|
|
13511
14336
|
};
|
|
13512
14337
|
};
|
|
14338
|
+
// ../core/src/store/tool-cpu.ts
|
|
14339
|
+
const SOURCE$1 = `SELECT seq, at, data FROM events
|
|
14340
|
+
WHERE run_id = ? AND (
|
|
14341
|
+
type = 'run.started'
|
|
14342
|
+
OR (type = 'node.event' AND json_extract(data, '$.event.type') = 'tool.result'
|
|
14343
|
+
AND json_extract(data, '$.event.cpu') IS NOT NULL)
|
|
14344
|
+
) ORDER BY seq`;
|
|
14345
|
+
const INSERT = `INSERT OR IGNORE INTO tool_cpu (
|
|
14346
|
+
run_id, seq, at, project, workflow, node, template, user_ms, system_ms
|
|
14347
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
14348
|
+
/** A run's tool commands' rows, from its events. */
|
|
14349
|
+
const rowsOf = (db, run) => {
|
|
14350
|
+
const declared = /* @__PURE__ */ new Set();
|
|
14351
|
+
const rows = [];
|
|
14352
|
+
for (const r of db.prepare(SOURCE$1).all(run.id)) {
|
|
14353
|
+
const event = eventOf(text(r, "data"));
|
|
14354
|
+
if (event.type === "run.started") {
|
|
14355
|
+
for (const node of event.nodes) declared.add(node.id);
|
|
14356
|
+
continue;
|
|
14357
|
+
}
|
|
14358
|
+
if (event.type !== "node.event" || event.event.type !== "tool.result") continue;
|
|
14359
|
+
const { cpu } = event.event;
|
|
14360
|
+
if (cpu === void 0) continue;
|
|
14361
|
+
const template = declared.has(event.id) ? void 0 : templateOf(event.id, declared);
|
|
14362
|
+
rows.push({
|
|
14363
|
+
runId: run.id,
|
|
14364
|
+
seq: Number(r["seq"]),
|
|
14365
|
+
at: text(r, "at"),
|
|
14366
|
+
project: run.project,
|
|
14367
|
+
workflow: run.workflow,
|
|
14368
|
+
node: event.id,
|
|
14369
|
+
...template === void 0 ? {} : { template },
|
|
14370
|
+
userMs: cpu.userMs,
|
|
14371
|
+
systemMs: cpu.systemMs
|
|
14372
|
+
});
|
|
14373
|
+
}
|
|
14374
|
+
return rows;
|
|
14375
|
+
};
|
|
14376
|
+
const write = (db, rows) => {
|
|
14377
|
+
const insert = db.prepare(INSERT);
|
|
14378
|
+
for (const row of rows) insert.run(row.runId, row.seq, row.at, row.project, row.workflow, row.node, row.template ?? null, row.userMs, row.systemMs);
|
|
14379
|
+
};
|
|
14380
|
+
const runsOf = (db, sql, ...values) => db.prepare(sql).all(...values).map((run) => ({
|
|
14381
|
+
id: text(run, "id"),
|
|
14382
|
+
project: text(run, "project"),
|
|
14383
|
+
workflow: text(run, "workflow")
|
|
14384
|
+
}));
|
|
14385
|
+
/** Fills the table from the runs recorded before it existed. */
|
|
14386
|
+
const backfillToolCpu = (db) => {
|
|
14387
|
+
for (const run of runsOf(db, "SELECT id, project, workflow FROM runs")) write(db, rowsOf(db, run));
|
|
14388
|
+
};
|
|
14389
|
+
const toolCpuQueries = (db) => ({
|
|
14390
|
+
/** Writes a run's rows; the store's own, on each timed `tool.result`. */
|
|
14391
|
+
recordToolCpu: (runId) => {
|
|
14392
|
+
const [run] = runsOf(db, "SELECT id, project, workflow FROM runs WHERE id = ?", runId);
|
|
14393
|
+
if (run !== void 0) write(db, rowsOf(db, run));
|
|
14394
|
+
},
|
|
14395
|
+
toolCpu: (filter = {}) => {
|
|
14396
|
+
const { sql, values } = whereOf(filter);
|
|
14397
|
+
return db.prepare(`SELECT * FROM tool_cpu ${sql} ORDER BY at, run_id, seq`).all(...values).map((r) => {
|
|
14398
|
+
const row = {
|
|
14399
|
+
runId: text(r, "run_id"),
|
|
14400
|
+
seq: Number(r["seq"]),
|
|
14401
|
+
at: text(r, "at"),
|
|
14402
|
+
project: text(r, "project"),
|
|
14403
|
+
workflow: text(r, "workflow"),
|
|
14404
|
+
node: text(r, "node"),
|
|
14405
|
+
userMs: Number(r["user_ms"]),
|
|
14406
|
+
systemMs: Number(r["system_ms"])
|
|
14407
|
+
};
|
|
14408
|
+
const template = text(r, "template");
|
|
14409
|
+
if (template !== "") row.template = template;
|
|
14410
|
+
return row;
|
|
14411
|
+
});
|
|
14412
|
+
}
|
|
14413
|
+
});
|
|
13513
14414
|
// ../core/src/store/triggers.ts
|
|
13514
14415
|
const recordOf$1 = (row) => {
|
|
13515
14416
|
const acceptedHash = text(row, "accepted_hash");
|
|
@@ -13587,8 +14488,8 @@ const triggerQueries = (db) => {
|
|
|
13587
14488
|
};
|
|
13588
14489
|
// ../core/src/store/queries.ts
|
|
13589
14490
|
/**
|
|
13590
|
-
* Every query set on `db`; `insertFeedback` and `
|
|
13591
|
-
* own.
|
|
14491
|
+
* Every query set on `db`; `insertFeedback`, `recordUsage` and `recordToolCpu`
|
|
14492
|
+
* are the store's own.
|
|
13592
14493
|
*/
|
|
13593
14494
|
const storeQueries = (db) => ({
|
|
13594
14495
|
...decisionQueries(db),
|
|
@@ -13596,6 +14497,7 @@ const storeQueries = (db) => ({
|
|
|
13596
14497
|
...feedbackQueries(db),
|
|
13597
14498
|
...projectQueries(db),
|
|
13598
14499
|
...usageQueries(db),
|
|
14500
|
+
...toolCpuQueries(db),
|
|
13599
14501
|
...planLimitQueries(db),
|
|
13600
14502
|
...triggerQueries(db),
|
|
13601
14503
|
...draftQueries(db)
|
|
@@ -13799,7 +14701,23 @@ const MIGRATIONS = [
|
|
|
13799
14701
|
ALTER TABLE runs ADD COLUMN failures_handled INTEGER;`,
|
|
13800
14702
|
`ALTER TABLE decisions ADD COLUMN note TEXT;`,
|
|
13801
14703
|
`ALTER TABLE decisions ADD COLUMN owner TEXT;`,
|
|
13802
|
-
`ALTER TABLE decisions ADD COLUMN document TEXT
|
|
14704
|
+
`ALTER TABLE decisions ADD COLUMN document TEXT;`,
|
|
14705
|
+
(db) => {
|
|
14706
|
+
db.exec(`CREATE TABLE tool_cpu (
|
|
14707
|
+
run_id TEXT NOT NULL,
|
|
14708
|
+
seq INTEGER NOT NULL,
|
|
14709
|
+
at TEXT NOT NULL,
|
|
14710
|
+
project TEXT NOT NULL,
|
|
14711
|
+
workflow TEXT NOT NULL,
|
|
14712
|
+
node TEXT NOT NULL,
|
|
14713
|
+
template TEXT,
|
|
14714
|
+
user_ms INTEGER NOT NULL,
|
|
14715
|
+
system_ms INTEGER NOT NULL,
|
|
14716
|
+
PRIMARY KEY (run_id, seq)
|
|
14717
|
+
) WITHOUT ROWID;
|
|
14718
|
+
CREATE INDEX tool_cpu_by_time ON tool_cpu (at);`);
|
|
14719
|
+
backfillToolCpu(db);
|
|
14720
|
+
}
|
|
13803
14721
|
];
|
|
13804
14722
|
const versionOf = (db) => Number(db.prepare("PRAGMA user_version").get()?.["user_version"] ?? 0);
|
|
13805
14723
|
/**
|
|
@@ -13925,8 +14843,9 @@ const openStore = (file = storePath(), options = {}) => {
|
|
|
13925
14843
|
const allRuns = db.prepare("SELECT * FROM runs ORDER BY started_at DESC, rowid DESC LIMIT ?");
|
|
13926
14844
|
const projectRuns = db.prepare("SELECT * FROM runs WHERE project = ? ORDER BY started_at DESC, rowid DESC LIMIT ?");
|
|
13927
14845
|
const eventsAfter = db.prepare("SELECT seq, at, data FROM events WHERE run_id = ? AND seq > ? ORDER BY seq");
|
|
14846
|
+
const controlEvents = db.prepare("SELECT seq, at, data FROM events WHERE run_id = ? AND type IN ('run.command', 'run.started', 'run.finished') ORDER BY seq");
|
|
13928
14847
|
const runNodes = db.prepare("SELECT node FROM events WHERE run_id = ? AND node IS NOT NULL GROUP BY node ORDER BY MIN(seq)");
|
|
13929
|
-
const { insertFeedback, recordUsage, recordPlanLimit, ...queries } = storeQueries(db);
|
|
14848
|
+
const { insertFeedback, recordUsage, recordToolCpu, recordPlanLimit, ...queries } = storeQueries(db);
|
|
13930
14849
|
return {
|
|
13931
14850
|
...queries,
|
|
13932
14851
|
begin: (run) => {
|
|
@@ -13953,6 +14872,7 @@ const openStore = (file = storePath(), options = {}) => {
|
|
|
13953
14872
|
insertEvent.run(runId, seq, at, event.type, nodeOf(event), JSON.stringify(event, maskReplacer));
|
|
13954
14873
|
if (event.type === "node.finished" && event.usage !== void 0) addCost.run(event.usage.costUsd, runId);
|
|
13955
14874
|
if (event.type === "node.event" && event.event.type === "session.finished") recordUsage(runId);
|
|
14875
|
+
if (event.type === "node.event" && event.event.type === "tool.result" && event.event.cpu !== void 0) recordToolCpu(runId);
|
|
13956
14876
|
if (event.type === "node.event" && event.event.type === "plan.limit") recordPlanLimit(event.event, at);
|
|
13957
14877
|
if (event.type === "feedback.recorded") insertFeedback(runId, seq, at, event.node, event.feedback);
|
|
13958
14878
|
if (event.type === "run.finished") {
|
|
@@ -13981,12 +14901,95 @@ const openStore = (file = storePath(), options = {}) => {
|
|
|
13981
14901
|
at: text(r, "at"),
|
|
13982
14902
|
event: eventOf(text(r, "data"))
|
|
13983
14903
|
})),
|
|
14904
|
+
controlEvents: (runId) => controlEvents.all(runId).map((r) => ({
|
|
14905
|
+
runId,
|
|
14906
|
+
seq: Number(r["seq"]),
|
|
14907
|
+
at: text(r, "at"),
|
|
14908
|
+
event: eventOf(text(r, "data"))
|
|
14909
|
+
})),
|
|
13984
14910
|
nodes: (runId) => runNodes.all(runId).map((r) => text(r, "node")),
|
|
13985
14911
|
close: () => {
|
|
13986
14912
|
db.close();
|
|
13987
14913
|
}
|
|
13988
14914
|
};
|
|
13989
14915
|
};
|
|
14916
|
+
// ../core/src/store/activity.ts
|
|
14917
|
+
/**
|
|
14918
|
+
* @file What each node of a run is doing, and when it started and finished,
|
|
14919
|
+
* folded from the stored events so nothing new has to be recorded. The tree
|
|
14920
|
+
* (view.ts) folds the events alone; this needs when they were written, and
|
|
14921
|
+
* keeps a screen's one-line answer to "what is it doing" in one place.
|
|
14922
|
+
*/
|
|
14923
|
+
const LONGEST_LINE = 120;
|
|
14924
|
+
/**
|
|
14925
|
+
* The fields of a tool's input that say what it's working on, best first. Only
|
|
14926
|
+
* these are shown, so what else a tool was given stays in the logs.
|
|
14927
|
+
*/
|
|
14928
|
+
const TARGETS = [
|
|
14929
|
+
"file_path",
|
|
14930
|
+
"notebook_path",
|
|
14931
|
+
"path",
|
|
14932
|
+
"url",
|
|
14933
|
+
"pattern",
|
|
14934
|
+
"query",
|
|
14935
|
+
"command"
|
|
14936
|
+
];
|
|
14937
|
+
const oneLine = (text) => {
|
|
14938
|
+
const line = text.replaceAll(/\s+/gu, " ").trim();
|
|
14939
|
+
return line.length > LONGEST_LINE ? `${line.slice(0, 119)}…` : line;
|
|
14940
|
+
};
|
|
14941
|
+
const toolLine = ({ name, input }) => {
|
|
14942
|
+
const target = TARGETS.map((field) => input[field]).find((value) => typeof value === "string" && value.trim() !== "");
|
|
14943
|
+
if (typeof target !== "string") return name;
|
|
14944
|
+
return oneLine(target.startsWith(`${name} `) ? target : `${name} ${target}`);
|
|
14945
|
+
};
|
|
14946
|
+
/** What a finished node said, or why it failed. */
|
|
14947
|
+
const finishedNow = (ok, error, summary) => {
|
|
14948
|
+
if (!ok) return error === void 0 ? void 0 : oneLine(error);
|
|
14949
|
+
return typeof summary === "string" && summary.trim() !== "" ? oneLine(summary) : void 0;
|
|
14950
|
+
};
|
|
14951
|
+
/** Each node's activity, by id; a node that never started has none. */
|
|
14952
|
+
const runActivity = (stored) => {
|
|
14953
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
14954
|
+
const of = (id) => {
|
|
14955
|
+
const found = nodes.get(id);
|
|
14956
|
+
if (found !== void 0) return found;
|
|
14957
|
+
const fresh = {};
|
|
14958
|
+
nodes.set(id, fresh);
|
|
14959
|
+
return fresh;
|
|
14960
|
+
};
|
|
14961
|
+
for (const { at, event } of stored) if (event.type === "node.started") {
|
|
14962
|
+
const node = of(event.id);
|
|
14963
|
+
node.startedAt ??= at;
|
|
14964
|
+
delete node.finishedAt;
|
|
14965
|
+
delete node.now;
|
|
14966
|
+
} else if (event.type === "node.event") {
|
|
14967
|
+
if (event.event.type === "tool.called") of(event.id).now = toolLine(event.event);
|
|
14968
|
+
} else if (event.type === "slot.waiting") of(event.id).now = oneLine(slotWaitLine(event.holders));
|
|
14969
|
+
else if (event.type === "slot.taken") delete of(event.id).now;
|
|
14970
|
+
else if (event.type === "decision.opened") of(event.node).now = oneLine(`Asked: ${event.question}`);
|
|
14971
|
+
else if (event.type === "decision.answered") of(event.node).now = oneLine(`Answered: ${event.answer}`);
|
|
14972
|
+
else if (event.type === "decision.withdrawn") delete of(event.node).now;
|
|
14973
|
+
else if (event.type === "node.finished") {
|
|
14974
|
+
const node = of(event.id);
|
|
14975
|
+
node.finishedAt = at;
|
|
14976
|
+
const now = finishedNow(event.ok, event.error, event.output["summary"]);
|
|
14977
|
+
if (now === void 0) delete node.now;
|
|
14978
|
+
else node.now = now;
|
|
14979
|
+
}
|
|
14980
|
+
return nodes;
|
|
14981
|
+
};
|
|
14982
|
+
/** The ownership tree, with what each node is doing and when it ran. */
|
|
14983
|
+
const activeTree = (stored) => {
|
|
14984
|
+
const activity = runActivity(stored);
|
|
14985
|
+
const dress = (node) => {
|
|
14986
|
+
Object.assign(node, activity.get(node.id));
|
|
14987
|
+
for (const child of node.children) dress(child);
|
|
14988
|
+
};
|
|
14989
|
+
const roots = runTree(stored.map(({ event }) => event));
|
|
14990
|
+
for (const root of roots) dress(root);
|
|
14991
|
+
return roots;
|
|
14992
|
+
};
|
|
13990
14993
|
// ../core/src/triggers/check.ts
|
|
13991
14994
|
/**
|
|
13992
14995
|
* @file Checking `triggers.yaml` against the project's workflows (RFC-011):
|
|
@@ -14653,112 +15656,6 @@ const planUpgrade = (options) => {
|
|
|
14653
15656
|
const RUNS_LOOKED_AT = 500;
|
|
14654
15657
|
/** The ids of runs still running anywhere on this machine, newest first. */
|
|
14655
15658
|
const runningRunIds = (store) => store.runs({ limit: RUNS_LOOKED_AT }).filter((run) => liveStatus(run) === "running").map((run) => run.id);
|
|
14656
|
-
// ../core/src/usage/quota.ts
|
|
14657
|
-
const WINDOWS = [
|
|
14658
|
-
"day",
|
|
14659
|
-
"week",
|
|
14660
|
-
"month"
|
|
14661
|
-
];
|
|
14662
|
-
const FIELDS = {
|
|
14663
|
-
day: "dayUsd",
|
|
14664
|
-
week: "weekUsd",
|
|
14665
|
-
month: "monthUsd"
|
|
14666
|
-
};
|
|
14667
|
-
/** Where a window began and when the next begins, in local time. */
|
|
14668
|
-
const windowOf = (window, now) => {
|
|
14669
|
-
const [year, month, day] = [
|
|
14670
|
-
now.getFullYear(),
|
|
14671
|
-
now.getMonth(),
|
|
14672
|
-
now.getDate()
|
|
14673
|
-
];
|
|
14674
|
-
if (window === "month") return {
|
|
14675
|
-
since: new Date(year, month, 1),
|
|
14676
|
-
until: new Date(year, month + 1)
|
|
14677
|
-
};
|
|
14678
|
-
if (window === "day") return {
|
|
14679
|
-
since: new Date(year, month, day),
|
|
14680
|
-
until: new Date(year, month, day + 1)
|
|
14681
|
-
};
|
|
14682
|
-
const monday = day - (now.getDay() + 6) % 7;
|
|
14683
|
-
return {
|
|
14684
|
-
since: new Date(year, month, monday),
|
|
14685
|
-
until: new Date(year, month, monday + 7)
|
|
14686
|
-
};
|
|
14687
|
-
};
|
|
14688
|
-
/** The caps that apply: the user's on everything, and the project's on itself. */
|
|
14689
|
-
const capsOf = (user, project, root) => WINDOWS.flatMap((window) => {
|
|
14690
|
-
const field = FIELDS[window];
|
|
14691
|
-
const everywhere = user?.[field];
|
|
14692
|
-
const here = project?.[field];
|
|
14693
|
-
const caps = [];
|
|
14694
|
-
if (everywhere !== void 0) caps.push({
|
|
14695
|
-
window,
|
|
14696
|
-
capUsd: everywhere
|
|
14697
|
-
});
|
|
14698
|
-
if (here !== void 0 && root !== void 0) caps.push({
|
|
14699
|
-
window,
|
|
14700
|
-
capUsd: here,
|
|
14701
|
-
project: root
|
|
14702
|
-
});
|
|
14703
|
-
return caps;
|
|
14704
|
-
});
|
|
14705
|
-
/** Each cap with what has been spent against it. */
|
|
14706
|
-
const standingOf = (store, caps, now) => caps.map((cap) => {
|
|
14707
|
-
const { since, until } = windowOf(cap.window, now);
|
|
14708
|
-
const spentUsd = store.spentSince(since.toISOString(), cap.project);
|
|
14709
|
-
return {
|
|
14710
|
-
...cap,
|
|
14711
|
-
spentUsd,
|
|
14712
|
-
leftUsd: cap.capUsd - spentUsd,
|
|
14713
|
-
resetsAt: until.toISOString()
|
|
14714
|
-
};
|
|
14715
|
-
});
|
|
14716
|
-
/** Dollars: cents, or more digits for an amount under a dollar. */
|
|
14717
|
-
const usd = (amount) => `$${amount.toFixed(amount > 0 && amount < 1 ? 4 : 2)}`;
|
|
14718
|
-
const at = (iso) => new Date(iso).toLocaleString(void 0, {
|
|
14719
|
-
dateStyle: "medium",
|
|
14720
|
-
timeStyle: "short"
|
|
14721
|
-
});
|
|
14722
|
-
const capReason = (spent) => `${spent.project === void 0 ? "your" : "this project's"} ${spent.window} quota of ${usd(spent.capUsd)} is spent (${usd(spent.spentUsd)}); it lifts on ${at(spent.resetsAt)}, or raise it in ${spent.project === void 0 ? "~/.ork/settings.yaml" : ".ork/project.yaml"}`;
|
|
14723
|
-
/** What a run or a call refused at its start says first, for its caller to read. */
|
|
14724
|
-
const NOT_STARTED = "Not started: ";
|
|
14725
|
-
/** Why a plan limit stops a run. */
|
|
14726
|
-
const planReason = (limit) => `your Claude plan's ${limit.window.replaceAll("_", " ")} limit was reached${limit.resetsAt === void 0 ? "" : `; it resets on ${at(limit.resetsAt)}, or use ork usage --clear-plan-limits if it has lifted`}`;
|
|
14727
|
-
/**
|
|
14728
|
-
* The windows that hold the whole account. One for a single model, such as
|
|
14729
|
-
* `seven_day_opus`, stops only the session it refused: other models may be
|
|
14730
|
-
* free.
|
|
14731
|
-
*/
|
|
14732
|
-
const ACCOUNT_WINDOWS = /* @__PURE__ */ new Set([
|
|
14733
|
-
"five_hour",
|
|
14734
|
-
"seven_day",
|
|
14735
|
-
"overage",
|
|
14736
|
-
"plan"
|
|
14737
|
-
]);
|
|
14738
|
-
/** The plan limits in force, the one that lifts last first. */
|
|
14739
|
-
const refusing = (limits, now) => limits.filter((limit) => limit.status === "rejected" && ACCOUNT_WINDOWS.has(limit.window) && limit.resetsAt !== void 0 && Date.parse(limit.resetsAt) > now.getTime()).toSorted((a, b) => String(b.resetsAt).localeCompare(String(a.resetsAt)));
|
|
14740
|
-
/** How long a report with no reset time stays worth showing. */
|
|
14741
|
-
const SHOWN_MS = 864e5;
|
|
14742
|
-
/** What the caps and the plan allow a run now. */
|
|
14743
|
-
const limitsNow = (store, caps, now) => {
|
|
14744
|
-
const standing = standingOf(store, caps, now);
|
|
14745
|
-
const [plan] = refusing(store.planLimits(), now);
|
|
14746
|
-
const spent = standing.find((cap) => cap.leftUsd <= 0);
|
|
14747
|
-
const blocked = plan === void 0 ? spent === void 0 ? void 0 : capReason(spent) : planReason(plan);
|
|
14748
|
-
return {
|
|
14749
|
-
...standing.length === 0 ? {} : { leftUsd: Math.min(...standing.map((cap) => cap.leftUsd)) },
|
|
14750
|
-
...blocked === void 0 ? {} : { blocked }
|
|
14751
|
-
};
|
|
14752
|
-
};
|
|
14753
|
-
/** What `ork usage` and the panel show. */
|
|
14754
|
-
const limitsReport = (store, { caps, problems }, now) => ({
|
|
14755
|
-
problems,
|
|
14756
|
-
caps: standingOf(store, caps, now),
|
|
14757
|
-
plan: store.planLimits().filter((limit) => {
|
|
14758
|
-
if (limit.status === "allowed") return false;
|
|
14759
|
-
return (limit.resetsAt === void 0 ? Date.parse(limit.at) + SHOWN_MS : Date.parse(limit.resetsAt)) > now.getTime();
|
|
14760
|
-
})
|
|
14761
|
-
});
|
|
14762
15659
|
// ../core/src/usage/caps.ts
|
|
14763
15660
|
/**
|
|
14764
15661
|
* @file Which limits across runs apply where (RFC-007): the user's settings and
|
|
@@ -15113,6 +16010,11 @@ const commitWork = async (worktree, never, message) => {
|
|
|
15113
16010
|
};
|
|
15114
16011
|
}
|
|
15115
16012
|
};
|
|
16013
|
+
/** A node's result once its work could not be committed: a failed `git commit`. */
|
|
16014
|
+
const uncommitted = (result, error) => endedWith({
|
|
16015
|
+
...result,
|
|
16016
|
+
ok: false
|
|
16017
|
+
}, endFor("command_failed", { program: "git commit" }, error));
|
|
15116
16018
|
// ../core/src/worktree/close.ts
|
|
15117
16019
|
const reasonOf = (options, id) => {
|
|
15118
16020
|
const why = options.unmerged.get(id);
|
|
@@ -15294,12 +16196,7 @@ const createWorkspace = (options) => {
|
|
|
15294
16196
|
if (work.committed) event(id, "committed", worktree);
|
|
15295
16197
|
if (work.error === void 0) return next;
|
|
15296
16198
|
unmerged.set(holder, `uncommitted: ${work.error}`);
|
|
15297
|
-
|
|
15298
|
-
return {
|
|
15299
|
-
...next,
|
|
15300
|
-
ok: false,
|
|
15301
|
-
error
|
|
15302
|
-
};
|
|
16199
|
+
return uncommitted(next, work.error);
|
|
15303
16200
|
};
|
|
15304
16201
|
const finish = async (id, result) => {
|
|
15305
16202
|
const from = continued.get(id);
|
|
@@ -15355,4 +16252,4 @@ const createWorkspace = (options) => {
|
|
|
15355
16252
|
close
|
|
15356
16253
|
};
|
|
15357
16254
|
};
|
|
15358
|
-
export {
|
|
16255
|
+
export { DETACHED_ENV as $, skillsIn as $n, endOf as $t, lines as A, linkTools as An, AGENT_DRIVERS as Ar, nestItems as At, linkNodes as B, placeFiles as Bn, lowerChild as Bt, openStore as C, wakeRun as Cn, TOOL_PREFIX as Cr, runStatus as Ct, removeWorktree as D, planAgentMigration as Dn, readyDriver as Dr, processAlive as Dt, inspectProject as E, maskSecrets as En, driverChecks as Er, templateOf as Et, driverQueues as F, catalogAgent as Fn, knownExperiments as Fr, failedWith as Ft, requestContinue as G, workflowDirOf as Gn, PROJECT_FILE as Gt, tellNode as H, projectsHome as Hn, envReadsOf as Ht, createCalls as I, loadAgents as In, stoppedEnd as It, tellLiveRun as J, LESSON_CAP_TOKENS as Jn, machineRulesOf as Jt, requestPause as K, inside as Kn, gitRulesOf as Kt, addressOf as L, hasInRepoFiles as Ln, calledWorkflows as Lt, parseInputPairs as M, toolsInUse as Mn, firstLine as Mr, runScope as Mt, resolveInputs as N, createAgent as Nn, EXPERIMENTS as Nr, declaredProblem as Nt, git as O, applySteps as On, AGENT_TOOL_LAYOUTS as Or, stillTheSameProcess as Ot, driverRefusals as P, deleteAgent as Pn, experimentsOn as Pr, endedWith as Pt, listKey as Q, writePlainFile as Qn, createSignals as Qt, endLink as R, inRepoPlace as Rn, slotClock as Rt, isNodeEvent as S, socketRoom as Sn, INTEGRATIONS as Sr, pausedOf as St, BRANCH_PREFIX as T, maskReplacer as Tn, PREFERRED_DRIVER as Tr, slotWaitsOf as Tt, unlinkNodes as U, runDirOf as Un, nodeSays as Ut, peerOf as V, placeFor as Vn, startAt as Vt, flattenTree as W, shownPath as Wn, declaredOutputOf as Wt, detachResume as X, guardedBase as Xn, mcpNotText as Xt, adoptedCalls as Y, estimateTokens as Yn, MCP_FILE as Yt, resumable as Z, plainPathInside as Zn, serversIn as Zt, readProjectTriggers as _, parseTemplate as _n, isToolNode as _r, startRun as _t, releaseNotes as a, FEEDBACK_SEVERITIES as an, programsRunBy as ar, planReason as at, watchingPid as b, serveRun as bn, retiredDriverHint as br, heldTimeout as bt, detectInstaller as c, planPost as cn, serializeWorkflow as cr, explainSession as ct, parseVersion as d, publishPost as dn, WorkflowSchema as dr, sessionDriver as dt, outcomeEnd as en, WorkflowEditSchema as er, detachRun as et, lateTriggers as f, createFeedback as fn, CheckReportSchema as fr, parseReport as ft, declaredTriggers as g, evaluate as gn, isClaudeNode as gr, walkSummary as gt, triggerStanding as h, targetOf as hn, NodeSchema as hr, walkChecks as ht, planUpgrade as i, FEEDBACK_KINDS as in, beforeOf as ir, limitsReport as it, clearGitLocalEnv as j, toolLinks as jn, DRIVER_IDS as jr, scriptContactsOf as jt, gitOk as k, planMoveInto as kn, ORK_LAYOUT as kr, isItem as kt, compareVersions as l, runGh as ln, markValue as lr, planNodeSession as lt, createWatcher as m, normalizeTitle as mn, settingsOf as mr, checkWorkflowText as mt, resolveBase as n, END_KINDS as nn, newWorkflowText as nr, NOT_STARTED as nt, runningRunIds as o, FEEDBACK_TARGETS as on, renderJsonSchema as or, usd as ot, triggersReport as p, fingerprint as pn, expandEdge as pr, reportSchemaFor as pt, requestStop as q, projectRootOf as qn, loadProject as qt, capsHere as r, endFor as rn, readDocument as rr, limitsNow as rt, runningScript as s, DISCUSSIONS as sn, parseWorkflow as sr, explainWorkflowText as st, createWorkspace as t, END_DETAIL_LIMIT as tn, applyEdits as tr, followEvents as tt, isNewer as u, sendPost as un, EdgeInputSchema as ur, namingNoDriver as ut, workflowTextHash as v, renderTemplate as vn, DriverSchema as vr, MESSAGE_LIMIT as vt, storePath as w, MASK as wn, integrationFor as wr, runTree as wt, activeTree as x, socketPath as xn, timeoutMs as xr, liveStatus as xt, takeWatchLock as y, readCondition as yn, NodeIdSchema as yr, userMessageText as yt, endLinksOf as z, newProjectId as zn, slotWaitLine as zt };
|