@oxygen-agent/cli 1.766.0 → 1.799.1
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/README.md +1 -1
- package/dist/index.js +55 -20
- package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.js +24 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/table-capacity.js +14 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/workflows/dist/graph/diff.d.ts +33 -0
- package/node_modules/@oxygen/workflows/dist/graph/diff.js +75 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +17 -1
- package/node_modules/@oxygen/workflows/dist/graph/index.d.ts +1 -0
- package/node_modules/@oxygen/workflows/dist/graph/index.js +1 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.js +30 -28
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +2 -2
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +1 -1
- package/node_modules/@oxygen/workflows/dist/graph/remap.js +0 -5
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +7 -8
- package/node_modules/@oxygen/workflows/dist/graph/types.js +0 -4
- package/node_modules/@oxygen/workflows/dist/index.js +0 -1
- package/node_modules/@oxygen/workflows/dist/portable.js +0 -9
- package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +5 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -6
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -6998,7 +6998,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6998
6998
|
.option("--label <label>", "Display label for the new column. Required unless --prompt-key supplies a default title.")
|
|
6999
6999
|
.option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
|
|
7000
7000
|
.option("--data-type <type>", "Column data type: text, numeric, boolean, jsonb, or timestamptz.")
|
|
7001
|
-
.option("--kind <kind>", "Column kind: manual, research, ai, formula, enrichment, tool, bind, or lookup. Defaults to manual. Use research for anything you would look up on the web.")
|
|
7001
|
+
.option("--kind <kind>", "Column kind: manual, research, ai, formula, enrichment, tool, bind, or lookup. Defaults to manual. Use research for anything you would look up on the web. `lookup` reads a value out of another table; to LINK two tables row-to-row use `oxygen tables relate` instead \u2014 relation columns are two-sided and cannot be added here.")
|
|
7002
7002
|
.option("--semantic-type <type>", "Optional semantic type such as company_domain.")
|
|
7003
7003
|
.option("--definition-json <json>", "Optional JSON object with column definition metadata.")
|
|
7004
7004
|
.option("--prompt <text-or-file>", "AI or research column prompt, or a path to a prompt file — a value that resolves to a readable file is read as one, matching --prompt everywhere else in this CLI. On its own it sets kind=ai and picks the data type (text, or jsonb with an output schema); pair it with --kind research to search the web per row instead. Reference other columns inline as {{column_key}} — no --input-mapping needed; unknown keys are rejected here instead of failing per row. Merges into --definition-json (the escape hatch for everything else); a `prompt` in both is an error.")
|
|
@@ -8216,7 +8216,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8216
8216
|
.command("limits")
|
|
8217
8217
|
.description("Plan-tier limits, spend-safety defaults, and storage capacity posture.")
|
|
8218
8218
|
.addCommand(new Command("show")
|
|
8219
|
-
.description("Show
|
|
8219
|
+
.description("Show limits and Tables capacity usage: 3M rows/Table, 25M/workspace, and 20/30 GiB PostgreSQL warning/limit; S3 excluded. Recovery: https://oxygen-agent.com/docs/safety/billing.")
|
|
8220
8220
|
.option("--json", "Print a JSON envelope.")
|
|
8221
8221
|
.action(async (options) => {
|
|
8222
8222
|
await handleAsyncAction("limits show", options, () => requestOxygen("/api/cli/limits"));
|
|
@@ -14645,6 +14645,7 @@ Run completion:
|
|
|
14645
14645
|
.option("--approved", "Authorize autonomous tool calls for this revision.")
|
|
14646
14646
|
.option("--max-credits <n>", "Optional explicit positive ceiling for each autonomous delivery; omit to use the plan-tier default.")
|
|
14647
14647
|
.option("--draft", "Save without going live: keeps the running version serving every trigger. Publish later by applying again without --draft.")
|
|
14648
|
+
.option("--review", "Show what publishing this would change against the version now serving triggers — added, removed and modified steps, what leaves Oxygen, the accounts used, and whether it needs standing authority. Writes nothing.")
|
|
14648
14649
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
14649
14650
|
.option("--json", "Print a JSON envelope.")
|
|
14650
14651
|
.action(async (options) => {
|
|
@@ -14659,6 +14660,7 @@ Run completion:
|
|
|
14659
14660
|
// byte-identical to what every previous CLI sent, so behaviour
|
|
14660
14661
|
// against an older server is unchanged rather than merely equivalent.
|
|
14661
14662
|
...(options.draft ? { publish: false } : {}),
|
|
14663
|
+
...(options.review ? { review: true } : {}),
|
|
14662
14664
|
...(options.approved ? { approved: true } : {}),
|
|
14663
14665
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
14664
14666
|
},
|
|
@@ -14720,13 +14722,21 @@ Run completion:
|
|
|
14720
14722
|
.description("List workflow automations.")
|
|
14721
14723
|
.addOption(new Option("--include-archived", "Deprecated no-op retained for compatibility.").hideHelp())
|
|
14722
14724
|
.option("--tag <tag>", "Only workflows carrying this workspace tag (see `oxygen tags list`).")
|
|
14725
|
+
.option("--node-testable", "Only canonical graph workflows, the ones `workflows call --node` can scope a test to. Legacy recipes and v1 step lists own their own execution order and are excluded.")
|
|
14726
|
+
.option("--search <text>", "Match on name, slug, trigger event, or an integration the workflow uses — so \"meeting\" or \"hubspot\" finds it without knowing what someone named it.")
|
|
14723
14727
|
.option("--json", "Print a JSON envelope.")
|
|
14728
|
+
.addHelpText("after", "\nEach row carries `format` (graph, recipe or steps) and `nodeTestable`, so you can tell which workflows support single-step testing without opening them.\n")
|
|
14724
14729
|
.action(async (options) => {
|
|
14725
14730
|
await handleAsyncAction("workflows list", options, async () => {
|
|
14726
14731
|
const params = new URLSearchParams();
|
|
14727
14732
|
const tag = readOption(options.tag);
|
|
14728
14733
|
if (tag)
|
|
14729
14734
|
params.set("tag", tag);
|
|
14735
|
+
if (options.nodeTestable)
|
|
14736
|
+
params.set("node_testable", "true");
|
|
14737
|
+
const search = readOption(options.search);
|
|
14738
|
+
if (search)
|
|
14739
|
+
params.set("search", search);
|
|
14730
14740
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
14731
14741
|
const data = await requestOxygen(`/api/cli/workflows${qs}`);
|
|
14732
14742
|
if (!options.json)
|
|
@@ -14783,39 +14793,52 @@ Run completion:
|
|
|
14783
14793
|
}), options));
|
|
14784
14794
|
}))
|
|
14785
14795
|
.addCommand(new Command("call")
|
|
14786
|
-
.description("
|
|
14796
|
+
.description("Run a workflow — or preview it first. `--preview` shows every step that leaves Oxygen, the accounts it would use, and what one run costs, without running anything. Enqueues asynchronously without simulating the trigger; follow the returned run_id with `oxygen workflows tail <run_id>`.")
|
|
14787
14797
|
.argument("[workflow]", "Workflow id, slug, or name.")
|
|
14788
14798
|
.option("--workflow <workflow>", "Workflow id, slug, or name.")
|
|
14789
14799
|
.option("--workflow-id <workflow_id>", "Workflow id or slug.")
|
|
14790
14800
|
.option("--workflow-name <workflow_name>", "Workflow name.")
|
|
14791
14801
|
.option("--input-json <json>", "Workflow input object. Defaults to {}.")
|
|
14792
|
-
.
|
|
14802
|
+
.option("--mode <mode>", "Execution mode. smoke-test/dry-run: 0 credits, no paid provider calls or external writes; internal reads still execute. live: may spend/write and requires approval + cap. Not needed with --preview, which runs nothing.")
|
|
14793
14803
|
.option("--idempotency-key <key>", "Optional idempotency key.")
|
|
14794
14804
|
.option("--max-credits <n>", "Required credit ceiling for live calls.")
|
|
14795
14805
|
.option("--approved", "Required for live calls after inspecting a dry run.")
|
|
14796
14806
|
.option("--revision <n>", "Run a specific saved version instead of the live one. Dry-run and smoke-test only: publish a version to run it live.")
|
|
14797
14807
|
.option("--node <node_id>", "Test one node: runs that node plus only the predecessors it needs, from the same saved graph. Dry-run and smoke-test only.")
|
|
14808
|
+
.option("--preview", "Show what a live run of this exact version would do — every step that leaves Oxygen, the accounts it would use, and the minimum number of billable steps and credits it would cost — without running anything. AI and enrichment steps bill per row and can cost more than that floor. Needs no --mode: it always describes a live run.")
|
|
14798
14809
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
14799
14810
|
.option("--json", "Print a JSON envelope.")
|
|
14800
14811
|
.addHelpText("after", "\nSafety: smoke_test and dry_run share one boundary: 0 credits, no paid provider calls, no external writes. Oxygen internal reads use current workspace data, and oxygen.http_json_request may make a real outbound GET. Every mode creates an inspectable Workflow run record.\n\n--node scopes a test to one step. Oxygen plans the slice on the server from the exact saved version and records it on the run, so the run shows precisely which nodes were allowed to execute; you never submit a node list. Selecting the trigger, a disabled node, or a node unreachable from the trigger is refused with the reason. Only canonical graph workflows support it.\n")
|
|
14801
14812
|
.action(async (workflowArg, options) => {
|
|
14802
14813
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
14803
14814
|
const revisionVersion = readPositiveNumber(options.revision);
|
|
14804
|
-
await handleAsyncAction("workflows call", options, async () =>
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14815
|
+
await handleAsyncAction("workflows call", options, async () => {
|
|
14816
|
+
// --mode stopped being a commander requiredOption so that --preview,
|
|
14817
|
+
// which executes nothing, no longer forces the caller to pick a mode
|
|
14818
|
+
// the server then discards. A real call still needs one. Thrown
|
|
14819
|
+
// INSIDE handleAsyncAction so it prints the CLI's error envelope and
|
|
14820
|
+
// exits 2 like any other usage error — thrown outside, it escaped as
|
|
14821
|
+
// an unhandled rejection and showed the user a Node stack trace.
|
|
14822
|
+
if (!options.preview && !readOption(options.mode)) {
|
|
14823
|
+
throw new OxygenError("invalid_request", "--mode is required. Use smoke-test, dry-run, or live. To see what a live run would do without running it, use --preview — it runs nothing and needs no mode.", { exitCode: 2 });
|
|
14824
|
+
}
|
|
14825
|
+
return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/call", {
|
|
14826
|
+
method: "POST",
|
|
14827
|
+
body: {
|
|
14828
|
+
workflow: readOption(workflowArg) ?? readOption(options.workflow),
|
|
14829
|
+
...(readOption(options.workflowId) ? { workflow_id: readOption(options.workflowId) } : {}),
|
|
14830
|
+
...(readOption(options.workflowName) ? { workflow_name: readOption(options.workflowName) } : {}),
|
|
14831
|
+
input: options.inputJson ? parseJsonObject(options.inputJson) : {},
|
|
14832
|
+
...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
|
|
14833
|
+
...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
|
|
14834
|
+
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
14835
|
+
...(options.approved ? { approved: true } : {}),
|
|
14836
|
+
...(revisionVersion !== undefined ? { revision_version: revisionVersion } : {}),
|
|
14837
|
+
...(readOption(options.node) ? { test_node_id: readOption(options.node) } : {}),
|
|
14838
|
+
...(options.preview ? { preview: true } : {}),
|
|
14839
|
+
},
|
|
14840
|
+
}), options);
|
|
14841
|
+
});
|
|
14819
14842
|
}))
|
|
14820
14843
|
.addCommand(new Command("webhooks")
|
|
14821
14844
|
.description("Workflow webhook trigger utilities.")
|
|
@@ -15014,9 +15037,12 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
15014
15037
|
.option("--workflow <workflow>", "Workflow id, slug, or name.")
|
|
15015
15038
|
.option("--workflow-id <workflow_id>", "Workflow id or slug.")
|
|
15016
15039
|
.option("--status <status>", "Filter by queued, running, waiting, awaiting_approval, completed, failed, canceling, or canceled.")
|
|
15040
|
+
.option("--mode <mode>", "Filter by execution mode: smoke_test, dry_run, or live.")
|
|
15041
|
+
.option("--cursor <cursor>", "Continue from a previous page. Pass the nextCursor the last response returned.")
|
|
15017
15042
|
.option("--limit <n>", "Maximum runs to return. Defaults to 50.")
|
|
15018
15043
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
15019
15044
|
.option("--json", "Print a JSON envelope.")
|
|
15045
|
+
.addHelpText("after", "\nPaging is anchored to the exact last run of the previous page, so runs arriving while you page are never skipped or repeated. A null nextCursor means the history ended.\n")
|
|
15020
15046
|
.action(async (options) => {
|
|
15021
15047
|
await handleAsyncAction("workflows runs", options, async () => {
|
|
15022
15048
|
const query = new URLSearchParams();
|
|
@@ -15025,6 +15051,10 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
15025
15051
|
query.set("workflow", workflow);
|
|
15026
15052
|
if (readOption(options.status))
|
|
15027
15053
|
query.set("status", readOption(options.status) ?? "");
|
|
15054
|
+
if (readOption(options.mode))
|
|
15055
|
+
query.set("mode", readOption(options.mode) ?? "");
|
|
15056
|
+
if (readOption(options.cursor))
|
|
15057
|
+
query.set("cursor", readOption(options.cursor) ?? "");
|
|
15028
15058
|
const limit = readPositiveInt(options.limit);
|
|
15029
15059
|
if (limit)
|
|
15030
15060
|
query.set("limit", String(limit));
|
|
@@ -15094,11 +15124,16 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
15094
15124
|
.addCommand(new Command("cancel")
|
|
15095
15125
|
.description("Cancel a queued or running workflow run.")
|
|
15096
15126
|
.argument("<run_id>", "Workflow run UUID.")
|
|
15127
|
+
.option("--preview", "Show what cancelling would and would not undo — steps in flight, and any external write already dispatched that cancelling cannot recall. Cancels nothing.")
|
|
15097
15128
|
.option("--json", "Print a JSON envelope.")
|
|
15129
|
+
.addHelpText("after", "\nCancelling stops further steps; it cannot recall a step already dispatched to a provider. Use --preview first when the run may be mid-write.\n")
|
|
15098
15130
|
.action(async (runId, options) => {
|
|
15099
15131
|
await handleAsyncAction("workflows cancel", options, () => requestOxygen("/api/cli/workflows/cancel", {
|
|
15100
15132
|
method: "POST",
|
|
15101
|
-
body: {
|
|
15133
|
+
body: {
|
|
15134
|
+
run_id: runId,
|
|
15135
|
+
...(options.preview ? { preview: true } : {}),
|
|
15136
|
+
},
|
|
15102
15137
|
}));
|
|
15103
15138
|
}))
|
|
15104
15139
|
.addCommand(new Command("approvals")
|
|
@@ -78,6 +78,24 @@ export declare function readWorkflowStepFailure(error: unknown): WorkflowStepFai
|
|
|
78
78
|
*/
|
|
79
79
|
export declare const AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE = "automation_actions_exceeded";
|
|
80
80
|
export declare function isAutomationUsageQuotaError(error: unknown): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The credit-era sibling of the code above, and the one most callers actually want.
|
|
83
|
+
*
|
|
84
|
+
* `automation_actions_exceeded` can only be thrown when the plan carries a non-null
|
|
85
|
+
* `monthlyAutomationActions`. Pricing Model 2.0 ([v1.330.0]) set that field to null on
|
|
86
|
+
* every base plan, so on current plans the admission gate in captureAutomationActions()
|
|
87
|
+
* is unreachable and assertAutomationActionsAvailable() refuses with `insufficient_credits`
|
|
88
|
+
* instead — actions bill AUTOMATION_ACTION_CREDITS each out of the one credit pool.
|
|
89
|
+
*
|
|
90
|
+
* Both codes mean the same thing to a caller: "this automation work was REFUSED admission,
|
|
91
|
+
* the customer must feel it, and the run must stop cleanly." Anything else is a metering
|
|
92
|
+
* write that failed and must be stepped over. Matching only the quota code is how a
|
|
93
|
+
* credit-exhausted cron ends up rethrowing out of the scheduler's enqueue loop instead of
|
|
94
|
+
* taking the graceful pause path — abandoning the lease and every remaining due trigger
|
|
95
|
+
* for that tenant's sweep.
|
|
96
|
+
*/
|
|
97
|
+
export declare const AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
|
|
98
|
+
export declare function isAutomationAdmissionRefusal(error: unknown): boolean;
|
|
81
99
|
/**
|
|
82
100
|
* Node network primitives bury the actionable failure in `error.cause`: undici's
|
|
83
101
|
* fetch rejects with a bare TypeError "fetch failed" and puts the real
|
|
@@ -215,6 +215,30 @@ export function isAutomationUsageQuotaError(error) {
|
|
|
215
215
|
&& typeof error === "object"
|
|
216
216
|
&& error.code === AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE;
|
|
217
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* The credit-era sibling of the code above, and the one most callers actually want.
|
|
220
|
+
*
|
|
221
|
+
* `automation_actions_exceeded` can only be thrown when the plan carries a non-null
|
|
222
|
+
* `monthlyAutomationActions`. Pricing Model 2.0 ([v1.330.0]) set that field to null on
|
|
223
|
+
* every base plan, so on current plans the admission gate in captureAutomationActions()
|
|
224
|
+
* is unreachable and assertAutomationActionsAvailable() refuses with `insufficient_credits`
|
|
225
|
+
* instead — actions bill AUTOMATION_ACTION_CREDITS each out of the one credit pool.
|
|
226
|
+
*
|
|
227
|
+
* Both codes mean the same thing to a caller: "this automation work was REFUSED admission,
|
|
228
|
+
* the customer must feel it, and the run must stop cleanly." Anything else is a metering
|
|
229
|
+
* write that failed and must be stepped over. Matching only the quota code is how a
|
|
230
|
+
* credit-exhausted cron ends up rethrowing out of the scheduler's enqueue loop instead of
|
|
231
|
+
* taking the graceful pause path — abandoning the lease and every remaining due trigger
|
|
232
|
+
* for that tenant's sweep.
|
|
233
|
+
*/
|
|
234
|
+
export const AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
|
|
235
|
+
export function isAutomationAdmissionRefusal(error) {
|
|
236
|
+
if (!error || typeof error !== "object")
|
|
237
|
+
return false;
|
|
238
|
+
const code = error.code;
|
|
239
|
+
return code === AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE
|
|
240
|
+
|| code === AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE;
|
|
241
|
+
}
|
|
218
242
|
const MAX_CAUSE_DEPTH = 4;
|
|
219
243
|
const MAX_CAUSE_MESSAGE_LENGTH = 200;
|
|
220
244
|
export function errorCauseChain(error, maxDepth = MAX_CAUSE_DEPTH) {
|
|
@@ -57,6 +57,7 @@ export * from "./sequences.js";
|
|
|
57
57
|
export * from "./suppression-entries.js";
|
|
58
58
|
export * from "./dnc-identities.js";
|
|
59
59
|
export * from "./table-limits.js";
|
|
60
|
+
export * from "./table-capacity.js";
|
|
60
61
|
export * from "./log.js";
|
|
61
62
|
export * from "./axiom-field-budget.js";
|
|
62
63
|
export { redactSecretsInString, sanitizeLogFields } from "./redaction.js";
|
|
@@ -57,6 +57,7 @@ export * from "./sequences.js";
|
|
|
57
57
|
export * from "./suppression-entries.js";
|
|
58
58
|
export * from "./dnc-identities.js";
|
|
59
59
|
export * from "./table-limits.js";
|
|
60
|
+
export * from "./table-capacity.js";
|
|
60
61
|
export * from "./log.js";
|
|
61
62
|
export * from "./axiom-field-budget.js";
|
|
62
63
|
// Narrow, deliberate export (ADR 0014): lets telemetry emitters regression-test
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-independent infrastructure envelope for durable Workspace Tables.
|
|
3
|
+
* Plans do not sell different storage entitlements; every workspace gets the
|
|
4
|
+
* same safety boundary and plans continue to differ through operation/rate and
|
|
5
|
+
* credit limits.
|
|
6
|
+
*/
|
|
7
|
+
export declare const WORKSPACE_TABLE_CAPACITY: Readonly<{
|
|
8
|
+
tableRowLimit: 3000000;
|
|
9
|
+
workspaceRowLimit: 25000000;
|
|
10
|
+
workspaceDatabaseWarningBytes: number;
|
|
11
|
+
workspaceDatabaseLimitBytes: number;
|
|
12
|
+
}>;
|
|
13
|
+
export declare const WORKSPACE_TABLE_DATABASE_STORAGE_SCOPE: "postgres_workspace_tables_heap_indexes_toast";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const GIB = 1024 ** 3;
|
|
2
|
+
/**
|
|
3
|
+
* Plan-independent infrastructure envelope for durable Workspace Tables.
|
|
4
|
+
* Plans do not sell different storage entitlements; every workspace gets the
|
|
5
|
+
* same safety boundary and plans continue to differ through operation/rate and
|
|
6
|
+
* credit limits.
|
|
7
|
+
*/
|
|
8
|
+
export const WORKSPACE_TABLE_CAPACITY = Object.freeze({
|
|
9
|
+
tableRowLimit: 3_000_000,
|
|
10
|
+
workspaceRowLimit: 25_000_000,
|
|
11
|
+
workspaceDatabaseWarningBytes: 20 * GIB,
|
|
12
|
+
workspaceDatabaseLimitBytes: 30 * GIB,
|
|
13
|
+
});
|
|
14
|
+
export const WORKSPACE_TABLE_DATABASE_STORAGE_SCOPE = "postgres_workspace_tables_heap_indexes_toast";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.799.1";
|
|
2
2
|
export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
|
|
3
3
|
export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
|
|
4
4
|
export declare const SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION = "1.747.0";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.799.1";
|
|
2
2
|
// The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
|
|
3
3
|
// operational route. Raising it hard-rejects every older CLI from the entire
|
|
4
4
|
// product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WorkflowGraphManifest } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* What changed between the revision currently serving triggers and the one about
|
|
4
|
+
* to replace it.
|
|
5
|
+
*
|
|
6
|
+
* Publishing is the moment a workflow stops being a draft and starts acting on
|
|
7
|
+
* the world, and until now the author approved it having been shown a credit cap
|
|
8
|
+
* and nothing else. "Approve this" is not a review if the thing being approved
|
|
9
|
+
* is invisible. This is the comparison that makes the review honest.
|
|
10
|
+
*
|
|
11
|
+
* Canvas position is deliberately NOT a change. `ui` carries x/y, so dragging a
|
|
12
|
+
* node two pixels would otherwise report the graph as modified and train the
|
|
13
|
+
* author to skim a diff that cries wolf — which is worse than no diff, because
|
|
14
|
+
* it looks like diligence.
|
|
15
|
+
*/
|
|
16
|
+
export type WorkflowGraphNodeChange = {
|
|
17
|
+
nodeId: string;
|
|
18
|
+
/** The author's own step name, which is what the canvas shows. */
|
|
19
|
+
label: string;
|
|
20
|
+
change: "added" | "removed" | "modified";
|
|
21
|
+
/** For a modified node, the top-level fields that actually differ. */
|
|
22
|
+
changedFields?: string[];
|
|
23
|
+
};
|
|
24
|
+
export type WorkflowGraphDiff = {
|
|
25
|
+
triggerChanged: boolean;
|
|
26
|
+
inputSchemaChanged: boolean;
|
|
27
|
+
nodes: WorkflowGraphNodeChange[];
|
|
28
|
+
edgesAdded: number;
|
|
29
|
+
edgesRemoved: number;
|
|
30
|
+
/** True when nothing semantic differs — a re-publish of the same graph. */
|
|
31
|
+
unchanged: boolean;
|
|
32
|
+
};
|
|
33
|
+
export declare function diffWorkflowGraphManifests(before: WorkflowGraphManifest, after: WorkflowGraphManifest): WorkflowGraphDiff;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
function label(node) {
|
|
2
|
+
return node.name?.trim() || node.id;
|
|
3
|
+
}
|
|
4
|
+
/** Everything about a node except where it sits on the canvas. */
|
|
5
|
+
function semanticNode(node) {
|
|
6
|
+
const { ui: _ui, ...rest } = node;
|
|
7
|
+
return rest;
|
|
8
|
+
}
|
|
9
|
+
function stable(value) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return "undefined";
|
|
12
|
+
if (value === null || typeof value !== "object")
|
|
13
|
+
return JSON.stringify(value) ?? "null";
|
|
14
|
+
if (Array.isArray(value))
|
|
15
|
+
return `[${value.map(stable).join(",")}]`;
|
|
16
|
+
const entries = Object.entries(value)
|
|
17
|
+
.filter(([, entry]) => entry !== undefined)
|
|
18
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
19
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stable(entry)}`).join(",")}}`;
|
|
20
|
+
}
|
|
21
|
+
function changedFields(before, after) {
|
|
22
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
23
|
+
return [...keys]
|
|
24
|
+
.filter((key) => stable(before[key]) !== stable(after[key]))
|
|
25
|
+
.sort();
|
|
26
|
+
}
|
|
27
|
+
function edgeKey(edge) {
|
|
28
|
+
return `${edge.source}->${edge.target}#${edge.source_handle ?? ""}`;
|
|
29
|
+
}
|
|
30
|
+
export function diffWorkflowGraphManifests(before, after) {
|
|
31
|
+
const beforeNodes = new Map(before.nodes.map((node) => [node.id, node]));
|
|
32
|
+
const afterNodes = new Map(after.nodes.map((node) => [node.id, node]));
|
|
33
|
+
const changes = [];
|
|
34
|
+
// Reported in the AFTER graph's order so the review reads like the canvas the
|
|
35
|
+
// author is looking at; removals are appended, since they have no position in it.
|
|
36
|
+
for (const node of after.nodes) {
|
|
37
|
+
const previous = beforeNodes.get(node.id);
|
|
38
|
+
if (!previous) {
|
|
39
|
+
changes.push({ nodeId: node.id, label: label(node), change: "added" });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const fields = changedFields(semanticNode(previous), semanticNode(node));
|
|
43
|
+
if (fields.length > 0) {
|
|
44
|
+
changes.push({
|
|
45
|
+
nodeId: node.id,
|
|
46
|
+
label: label(node),
|
|
47
|
+
change: "modified",
|
|
48
|
+
changedFields: fields,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const node of before.nodes) {
|
|
53
|
+
if (!afterNodes.has(node.id)) {
|
|
54
|
+
changes.push({ nodeId: node.id, label: label(node), change: "removed" });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const beforeEdges = new Set(before.edges.map(edgeKey));
|
|
58
|
+
const afterEdges = new Set(after.edges.map(edgeKey));
|
|
59
|
+
const edgesAdded = [...afterEdges].filter((key) => !beforeEdges.has(key)).length;
|
|
60
|
+
const edgesRemoved = [...beforeEdges].filter((key) => !afterEdges.has(key)).length;
|
|
61
|
+
const triggerChanged = stable(before.trigger) !== stable(after.trigger);
|
|
62
|
+
const inputSchemaChanged = stable(before.input_schema) !== stable(after.input_schema);
|
|
63
|
+
return {
|
|
64
|
+
triggerChanged,
|
|
65
|
+
inputSchemaChanged,
|
|
66
|
+
nodes: changes,
|
|
67
|
+
edgesAdded,
|
|
68
|
+
edgesRemoved,
|
|
69
|
+
unchanged: !triggerChanged
|
|
70
|
+
&& !inputSchemaChanged
|
|
71
|
+
&& changes.length === 0
|
|
72
|
+
&& edgesAdded === 0
|
|
73
|
+
&& edgesRemoved === 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -24,12 +24,28 @@ export class WorkflowValueCoercionError extends Error {
|
|
|
24
24
|
this.name = "WorkflowValueCoercionError";
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Build the JSON that would satisfy an unresolved `trigger.input.*` path.
|
|
29
|
+
*
|
|
30
|
+
* The product already knows the exact dotted path it could not resolve, so
|
|
31
|
+
* printing only the path makes the reader reconstruct by hand what the error
|
|
32
|
+
* could have handed them. Only `trigger.input.*` is answerable this way — a
|
|
33
|
+
* `steps.*` ref is produced by an earlier node, not by the caller, so no input
|
|
34
|
+
* would fix it and suggesting one would send the reader down a dead end.
|
|
35
|
+
*/
|
|
36
|
+
function unresolvedRefRemedy(path) {
|
|
37
|
+
const parts = path.split(".");
|
|
38
|
+
if (parts.length < 3 || parts[0] !== "trigger" || parts[1] !== "input")
|
|
39
|
+
return "";
|
|
40
|
+
const skeleton = parts.slice(2).reduceRight((inner, key) => ({ [key]: inner }), "…");
|
|
41
|
+
return ` Supply it with --input-json '${JSON.stringify(skeleton)}'.`;
|
|
42
|
+
}
|
|
27
43
|
/** A direct mapping named data the run does not carry; sending undefined is never intentional. */
|
|
28
44
|
export class WorkflowValueRefUnresolvedError extends Error {
|
|
29
45
|
path;
|
|
30
46
|
code = "workflow_value_ref_unresolved";
|
|
31
47
|
constructor(path) {
|
|
32
|
-
super(`Mapped field '${path}' did not resolve in this run
|
|
48
|
+
super(`Mapped field '${path}' did not resolve in this run.${unresolvedRefRemedy(path)}`);
|
|
33
49
|
this.path = path;
|
|
34
50
|
this.name = "WorkflowValueRefUnresolvedError";
|
|
35
51
|
}
|
|
@@ -24,7 +24,6 @@ const NODE_KINDS = new Set([
|
|
|
24
24
|
"wait",
|
|
25
25
|
"approval",
|
|
26
26
|
"code",
|
|
27
|
-
"workflow",
|
|
28
27
|
]);
|
|
29
28
|
const MERGE_STRATEGIES = new Set(["append", "first", "wait_all"]);
|
|
30
29
|
const STEP_EFFECTS = new Set(["none", "external_read", "external_write"]);
|
|
@@ -334,19 +333,6 @@ node, path, add, scope, options) {
|
|
|
334
333
|
case "code":
|
|
335
334
|
lintCodeNode(node, path, add, scope, options);
|
|
336
335
|
return;
|
|
337
|
-
case "workflow":
|
|
338
|
-
if (!isNonEmptyString(node.workflow_id)) {
|
|
339
|
-
add(`${path}.workflow_id`, "invalid_workflow_ref", "Workflow node workflow_id is required.");
|
|
340
|
-
}
|
|
341
|
-
validateValueRefRecord(node.input, `${path}.input`, add, scope);
|
|
342
|
-
// Authoring-time refusal, not just a runtime one. A sub-workflow needs its
|
|
343
|
-
// own durable run, lease, spend ceiling and cancellation semantics, none of
|
|
344
|
-
// which exist yet — so the worker rejects the node terminally. Without this
|
|
345
|
-
// rule a user could save the graph, arm a cron trigger on it, and discover
|
|
346
|
-
// the refusal at 03:00 on every delivery instead of at `workflows apply`.
|
|
347
|
-
// Remove BOTH refusals together when child runs ship.
|
|
348
|
-
add(`${path}.kind`, "unsupported_node_kind", "Sub-workflow nodes cannot run yet. Inline the child workflow's nodes for now.");
|
|
349
|
-
return;
|
|
350
336
|
default:
|
|
351
337
|
return;
|
|
352
338
|
}
|
|
@@ -886,6 +872,15 @@ function lintLoopNode(node, path, add, scope) {
|
|
|
886
872
|
if (node.concurrency !== undefined && !isPositiveInteger(node.concurrency)) {
|
|
887
873
|
add(`${path}.concurrency`, "invalid_loop_target", "Loop concurrency must be a positive integer.");
|
|
888
874
|
}
|
|
875
|
+
else if (typeof node.concurrency === "number" && node.concurrency > 1) {
|
|
876
|
+
// First-release runtime cut (founder-approved 2026-08-17). The worker runs
|
|
877
|
+
// loop bodies sequentially and always has; `concurrency` was accepted and
|
|
878
|
+
// then ignored, which is the worst of both — the author reads a promise of
|
|
879
|
+
// parallelism, the run delivers none, and nothing says so. Refusing is the
|
|
880
|
+
// honest contract until parallel bodies can interleave lease renewal, spend
|
|
881
|
+
// accounting and per-iteration rows without getting the money wrong.
|
|
882
|
+
add(`${path}.concurrency`, "invalid_loop_target", "Loop bodies run one item at a time. Remove concurrency, or set it to 1.");
|
|
883
|
+
}
|
|
889
884
|
}
|
|
890
885
|
function lintSetNode(node, path, add, scope) {
|
|
891
886
|
const fields = asArray(node.fields);
|
|
@@ -932,19 +927,31 @@ function lintApprovalNode(node, path, add, scope) {
|
|
|
932
927
|
function lintWaitNode(node, path, add, scope) {
|
|
933
928
|
const hasDuration = node.duration_seconds !== undefined;
|
|
934
929
|
const hasUntil = node.until !== undefined;
|
|
935
|
-
|
|
936
|
-
|
|
930
|
+
// First-release runtime cut (founder-approved 2026-08-17): duration waits only.
|
|
931
|
+
//
|
|
932
|
+
// `until` never did what its name promises. The worker re-evaluates it on a
|
|
933
|
+
// poll against the run's IMMUTABLE input, so it can only ever become true if it
|
|
934
|
+
// was already true — it cannot observe an event, a table write, or anything
|
|
935
|
+
// that happens after the run started. Offering it advertises a transition
|
|
936
|
+
// source that does not exist. Event-driven waits come back when there is a
|
|
937
|
+
// subscription behind them; until then a graph that needs one uses an approval
|
|
938
|
+
// node or a trigger.
|
|
939
|
+
//
|
|
940
|
+
// Existing revisions carrying `until` keep executing unchanged — this refuses
|
|
941
|
+
// the SAVE, not the historical run.
|
|
942
|
+
if (hasUntil) {
|
|
943
|
+
add(`${path}.until`, "invalid_wait", "Wait until is not available. Use a duration wait, or an approval node to pause for a decision.");
|
|
937
944
|
return;
|
|
938
945
|
}
|
|
939
|
-
if (hasDuration) {
|
|
940
|
-
|
|
941
|
-
|| !Number.isFinite(node.duration_seconds)
|
|
942
|
-
|| node.duration_seconds <= 0) {
|
|
943
|
-
add(`${path}.duration_seconds`, "invalid_wait", "Wait duration_seconds must be a positive number.");
|
|
944
|
-
}
|
|
946
|
+
if (!hasDuration) {
|
|
947
|
+
add(path, "invalid_wait", "Wait node requires duration_seconds.");
|
|
945
948
|
return;
|
|
946
949
|
}
|
|
947
|
-
|
|
950
|
+
if (typeof node.duration_seconds !== "number"
|
|
951
|
+
|| !Number.isFinite(node.duration_seconds)
|
|
952
|
+
|| node.duration_seconds <= 0) {
|
|
953
|
+
add(`${path}.duration_seconds`, "invalid_wait", "Wait duration_seconds must be a positive number.");
|
|
954
|
+
}
|
|
948
955
|
}
|
|
949
956
|
function isPositiveInteger(value) {
|
|
950
957
|
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
@@ -1092,11 +1099,6 @@ function graphNodeValueRefs(node, path) {
|
|
|
1092
1099
|
ref,
|
|
1093
1100
|
path: `${path}.configuration.${name}`,
|
|
1094
1101
|
}));
|
|
1095
|
-
case "workflow":
|
|
1096
|
-
return Object.entries(node.input).map(([name, ref]) => ({
|
|
1097
|
-
ref,
|
|
1098
|
-
path: `${path}.input.${name}`,
|
|
1099
|
-
}));
|
|
1100
1102
|
default:
|
|
1101
1103
|
return [];
|
|
1102
1104
|
}
|
|
@@ -204,7 +204,7 @@ export declare const workflowGraphManifestSchema: {
|
|
|
204
204
|
readonly type: "string";
|
|
205
205
|
};
|
|
206
206
|
readonly kind: {
|
|
207
|
-
readonly enum: readonly ["trigger", "tool", "filter", "switch", "loop", "merge", "set", "wait", "approval", "code"
|
|
207
|
+
readonly enum: readonly ["trigger", "tool", "filter", "switch", "loop", "merge", "set", "wait", "approval", "code"];
|
|
208
208
|
};
|
|
209
209
|
readonly ui: {
|
|
210
210
|
readonly type: "object";
|
|
@@ -994,7 +994,7 @@ export declare const portableWorkflowDefinitionSchema: {
|
|
|
994
994
|
readonly type: "string";
|
|
995
995
|
};
|
|
996
996
|
readonly kind: {
|
|
997
|
-
readonly enum: readonly ["trigger", "tool", "filter", "switch", "loop", "merge", "set", "wait", "approval", "code"
|
|
997
|
+
readonly enum: readonly ["trigger", "tool", "filter", "switch", "loop", "merge", "set", "wait", "approval", "code"];
|
|
998
998
|
};
|
|
999
999
|
readonly ui: {
|
|
1000
1000
|
readonly type: "object";
|
|
@@ -156,7 +156,7 @@ const nodeSchema = {
|
|
|
156
156
|
name: { type: "string" },
|
|
157
157
|
description: { type: "string" },
|
|
158
158
|
kind: {
|
|
159
|
-
enum: ["trigger", "tool", "filter", "switch", "loop", "merge", "set", "wait", "approval", "code"
|
|
159
|
+
enum: ["trigger", "tool", "filter", "switch", "loop", "merge", "set", "wait", "approval", "code"],
|
|
160
160
|
},
|
|
161
161
|
ui: {
|
|
162
162
|
type: "object",
|
|
@@ -185,11 +185,6 @@ export function remapNodeRefs(node, ids) {
|
|
|
185
185
|
return node.until === undefined
|
|
186
186
|
? node
|
|
187
187
|
: { ...node, until: remapValueRef(node.until, ids) };
|
|
188
|
-
case "workflow":
|
|
189
|
-
return {
|
|
190
|
-
...node,
|
|
191
|
-
input: Object.fromEntries(Object.entries(node.input).map(([key, ref]) => [key, remapValueRef(ref, ids)])),
|
|
192
|
-
};
|
|
193
188
|
// Missed when this module was first written, because the kind list was typed
|
|
194
189
|
// from memory instead of read off the WorkflowGraphNode union — so a
|
|
195
190
|
// duplicated approval node kept pointing at the original, silently, which is
|
|
@@ -22,6 +22,12 @@ export type WorkflowTestScope = {
|
|
|
22
22
|
export type WorkflowTestScopeReceipt = WorkflowTestScope & {
|
|
23
23
|
source_hash: string;
|
|
24
24
|
planned_node_ids: string[];
|
|
25
|
+
/**
|
|
26
|
+
* The nodes this scope deliberately excludes. Recorded so "the downstream step
|
|
27
|
+
* did not run" is something the product states, not something a reader infers
|
|
28
|
+
* from a missing row in the step list.
|
|
29
|
+
*/
|
|
30
|
+
excluded_node_ids?: string[];
|
|
25
31
|
};
|
|
26
32
|
export declare const RESERVED_NODE_IDS: ReadonlySet<string>;
|
|
27
33
|
/**
|
|
@@ -376,13 +382,7 @@ export type WorkflowGraphNode =
|
|
|
376
382
|
request?: Record<string, WorkflowValueRef>;
|
|
377
383
|
})
|
|
378
384
|
/** Legacy source, imported durable recipe, or typed oxygen-js-v1 code. */
|
|
379
|
-
| WorkflowGraphCodeNode
|
|
380
|
-
/** Calls another workflow as a child run. */
|
|
381
|
-
| (WorkflowGraphNodeBase & {
|
|
382
|
-
kind: "workflow";
|
|
383
|
-
workflow_id: string;
|
|
384
|
-
input: Record<string, WorkflowValueRef>;
|
|
385
|
-
});
|
|
385
|
+
| WorkflowGraphCodeNode;
|
|
386
386
|
export type WorkflowGraphNodeKind = WorkflowGraphNode["kind"];
|
|
387
387
|
/** Narrow a node union member by its `kind` discriminant. */
|
|
388
388
|
export type WorkflowGraphNodeOfKind<K extends WorkflowGraphNodeKind> = Extract<WorkflowGraphNode, {
|
|
@@ -475,5 +475,4 @@ export declare function isTriggerNode(node: WorkflowGraphNode): node is Workflow
|
|
|
475
475
|
export declare function isToolNode(node: WorkflowGraphNode): node is WorkflowGraphNodeOfKind<"tool">;
|
|
476
476
|
export declare function isSwitchNode(node: WorkflowGraphNode): node is WorkflowGraphNodeOfKind<"switch">;
|
|
477
477
|
export declare function isLoopNode(node: WorkflowGraphNode): node is WorkflowGraphNodeOfKind<"loop">;
|
|
478
|
-
export declare function isWorkflowSubgraphNode(node: WorkflowGraphNode): node is WorkflowGraphNodeOfKind<"workflow">;
|
|
479
478
|
export {};
|
|
@@ -65,7 +65,6 @@ export const MAX_WORKFLOW_LOOP_ITERATIONS = 1_000;
|
|
|
65
65
|
*/
|
|
66
66
|
export const BILLABLE_WORKFLOW_GRAPH_NODE_KINDS = new Set([
|
|
67
67
|
"tool",
|
|
68
|
-
"workflow",
|
|
69
68
|
]);
|
|
70
69
|
/** Whether a v2 node of this kind draws an automation action when it executes. */
|
|
71
70
|
export function workflowGraphNodeKindBills(kind) {
|
|
@@ -161,6 +160,3 @@ export function isSwitchNode(node) {
|
|
|
161
160
|
export function isLoopNode(node) {
|
|
162
161
|
return isNodeOfKind(node, "loop");
|
|
163
162
|
}
|
|
164
|
-
export function isWorkflowSubgraphNode(node) {
|
|
165
|
-
return isNodeOfKind(node, "workflow");
|
|
166
|
-
}
|
|
@@ -1938,7 +1938,6 @@ export function workflowManifestCanInvokeTool(manifest) {
|
|
|
1938
1938
|
&& manifest.tools_used.some((toolId) => isNonEmptyString(toolId));
|
|
1939
1939
|
const viaGraph = Array.isArray(manifest.nodes)
|
|
1940
1940
|
&& manifest.nodes.some((node) => isRecord(node) && (node.kind === "tool"
|
|
1941
|
-
|| node.kind === "workflow"
|
|
1942
1941
|
|| (isRecord(node.recipe)
|
|
1943
1942
|
&& Array.isArray(node.recipe.tools_used)
|
|
1944
1943
|
&& node.recipe.tools_used.some((toolId) => isNonEmptyString(toolId)))));
|
|
@@ -69,13 +69,6 @@ function portableNodeIssue(node, index) {
|
|
|
69
69
|
message: "Legacy execution context cannot be represented as a portable editable graph.",
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
if (node.kind === "workflow") {
|
|
73
|
-
return {
|
|
74
|
-
path: `$.graph.nodes.${index}`,
|
|
75
|
-
code: "workflow_node_not_portable",
|
|
76
|
-
message: "Child-workflow nodes are not executable yet and cannot be exported as a portable definition.",
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
72
|
return null;
|
|
80
73
|
}
|
|
81
74
|
function valueRefUsesWorkspaceAsset(ref) {
|
|
@@ -114,8 +107,6 @@ function nodeUsesWorkspaceAsset(node) {
|
|
|
114
107
|
return node.code !== undefined
|
|
115
108
|
? Object.values(node.code.inputs).some(valueRefUsesWorkspaceAsset)
|
|
116
109
|
: Object.values(node.configuration ?? {}).some(valueRefUsesWorkspaceAsset);
|
|
117
|
-
case "workflow":
|
|
118
|
-
return Object.values(node.input).some(valueRefUsesWorkspaceAsset);
|
|
119
110
|
case "trigger":
|
|
120
111
|
case "merge":
|
|
121
112
|
return false;
|
|
@@ -14,6 +14,9 @@ export type CronCadenceAssessment = {
|
|
|
14
14
|
floorActionsPer30Days: number;
|
|
15
15
|
includedActions: number;
|
|
16
16
|
floorShareOfIncluded: number;
|
|
17
|
+
basis: "plan_action_quota" | "plan_monthly_credits";
|
|
18
|
+
includedCredits: number | null;
|
|
19
|
+
floorCreditsPer30Days: number | null;
|
|
17
20
|
minimumViableIntervalMinutes: number | null;
|
|
18
21
|
verdict: "fits" | "impossible";
|
|
19
22
|
};
|
|
@@ -22,6 +25,8 @@ export declare function assessCronCadenceViability(input: {
|
|
|
22
25
|
cron: string | null | undefined;
|
|
23
26
|
includedActions: number | null;
|
|
24
27
|
overageEnabled: boolean;
|
|
28
|
+
includedCredits?: number | null;
|
|
29
|
+
creditsPerAction?: number | null;
|
|
25
30
|
}): CronCadenceAssessment | null;
|
|
26
31
|
export type CronAggressiveness = {
|
|
27
32
|
level: "aggressive" | "very_aggressive";
|
|
@@ -287,26 +287,53 @@ const MINUTES_PER_30_DAYS = 43_200;
|
|
|
287
287
|
// enabled (the org has opted into paying past the cap, so a cadence that
|
|
288
288
|
// overruns it bills instead of failing).
|
|
289
289
|
export function assessCronCadenceViability(input) {
|
|
290
|
-
if (!input.cron || input.
|
|
290
|
+
if (!input.cron || input.overageEnabled)
|
|
291
291
|
return null;
|
|
292
|
-
|
|
292
|
+
const creditsPerAction = typeof input.creditsPerAction === "number"
|
|
293
|
+
&& Number.isFinite(input.creditsPerAction)
|
|
294
|
+
&& input.creditsPerAction > 0
|
|
295
|
+
? input.creditsPerAction
|
|
296
|
+
: null;
|
|
297
|
+
const includedCredits = typeof input.includedCredits === "number"
|
|
298
|
+
&& Number.isFinite(input.includedCredits)
|
|
299
|
+
&& input.includedCredits > 0
|
|
300
|
+
? input.includedCredits
|
|
301
|
+
: null;
|
|
302
|
+
// Prefer a real action quota when the plan still has one; otherwise derive the
|
|
303
|
+
// equivalent action budget from monthly credits.
|
|
304
|
+
const basis = input.includedActions === null
|
|
305
|
+
? "plan_monthly_credits"
|
|
306
|
+
: "plan_action_quota";
|
|
307
|
+
const includedActions = basis === "plan_action_quota"
|
|
308
|
+
? input.includedActions
|
|
309
|
+
: (includedCredits !== null && creditsPerAction !== null
|
|
310
|
+
? Math.floor(includedCredits / creditsPerAction)
|
|
311
|
+
: null);
|
|
312
|
+
if (includedActions === null)
|
|
313
|
+
return null;
|
|
314
|
+
if (!Number.isFinite(includedActions) || includedActions <= 0)
|
|
293
315
|
return null;
|
|
294
316
|
const scheduledRunsPer30Days = estimateCronRunsPer30Days(input.cron);
|
|
295
317
|
if (scheduledRunsPer30Days === null)
|
|
296
318
|
return null;
|
|
297
319
|
const actionsPerRunFloor = estimateWorkflowRunAutomationActionsFloor(input.manifest);
|
|
298
320
|
const floorActionsPer30Days = scheduledRunsPer30Days * actionsPerRunFloor;
|
|
299
|
-
const runsAllowedPer30Days = Math.floor(
|
|
321
|
+
const runsAllowedPer30Days = Math.floor(includedActions / actionsPerRunFloor);
|
|
300
322
|
return {
|
|
301
323
|
scheduledRunsPer30Days,
|
|
302
324
|
actionsPerRunFloor,
|
|
303
325
|
floorActionsPer30Days,
|
|
304
|
-
includedActions
|
|
305
|
-
floorShareOfIncluded: floorActionsPer30Days /
|
|
326
|
+
includedActions,
|
|
327
|
+
floorShareOfIncluded: floorActionsPer30Days / includedActions,
|
|
328
|
+
basis,
|
|
329
|
+
includedCredits: basis === "plan_monthly_credits" ? includedCredits : null,
|
|
330
|
+
floorCreditsPer30Days: basis === "plan_monthly_credits" && creditsPerAction !== null
|
|
331
|
+
? Math.round(floorActionsPer30Days * creditsPerAction * 1000) / 1000
|
|
332
|
+
: null,
|
|
306
333
|
minimumViableIntervalMinutes: runsAllowedPer30Days > 0
|
|
307
334
|
? Math.ceil(MINUTES_PER_30_DAYS / runsAllowedPer30Days)
|
|
308
335
|
: null,
|
|
309
|
-
verdict: floorActionsPer30Days >
|
|
336
|
+
verdict: floorActionsPer30Days > includedActions ? "impossible" : "fits",
|
|
310
337
|
};
|
|
311
338
|
}
|
|
312
339
|
// Warn (never block) below this cadence; the billing gate above stays the
|