@oxygen-agent/cli 1.1010.721 → 1.1010.950
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/auto-update.d.ts +129 -0
- package/dist/auto-update.js +392 -0
- package/dist/command-manifest.js +14 -0
- package/dist/contract-flags.d.ts +42 -0
- package/dist/contract-flags.js +220 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/credentials.js +6 -3
- package/dist/functions-commands.js +1 -1
- package/dist/http-client.js +28 -4
- package/dist/index.js +622 -325
- package/dist/run-wait.d.ts +3 -1
- package/dist/run-wait.js +19 -5
- package/dist/sender-limits-flags.d.ts +14 -0
- package/dist/sender-limits-flags.js +157 -0
- package/dist/streamed-file-import.d.ts +58 -0
- package/dist/streamed-file-import.js +115 -0
- package/dist/ugc-commands.js +16 -2
- package/dist/update.d.ts +29 -0
- package/dist/update.js +62 -16
- package/dist/workflow-plan-limit-notices.d.ts +8 -0
- package/dist/workflow-plan-limit-notices.js +28 -0
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +6 -6
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +4 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +20 -8
- package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/cli-ugc/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +27 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +191 -35
- package/node_modules/@oxygen/shared/dist/billing.js +333 -42
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +55 -5
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +11 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.d.ts +186 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.js +260 -0
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.d.ts +67 -0
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +88 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +3 -1
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +3 -3
- package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
- package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
- package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
- package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -22
- package/node_modules/@oxygen/shared/dist/index.js +2 -42
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +41 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +117 -1
- package/node_modules/@oxygen/shared/dist/plan-band.js +175 -10
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +204 -6
- package/node_modules/@oxygen/shared/dist/plan-limits.js +197 -15
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +80 -36
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +80 -31
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +38 -20
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +47 -34
- package/node_modules/@oxygen/shared/dist/product-briefing-rules.js +4 -2
- package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +127 -0
- package/node_modules/@oxygen/shared/dist/repricing.js +407 -6
- package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/semver.js +41 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +5 -7
- package/node_modules/@oxygen/shared/dist/sending-limits.js +10 -16
- package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
- package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
- package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +15 -7
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +18 -5
- package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +35 -5
- package/node_modules/@oxygen/shared/dist/table-capacity.js +40 -8
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
- package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
- package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +5 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +17 -27
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
- package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
- package/node_modules/@oxygen/workflows/dist/index.js +152 -2
- package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as vm from "node:vm"; // skipcq: JS-C1003
|
|
3
|
-
import { isWorkflowGraphManifest, WORKFLOW_CODE_V1_DEFAULT_OUTPUT_BYTES, WORKFLOW_CODE_V1_DEFAULT_TIMEOUT_MS, WORKFLOW_CODE_V1_MAX_OUTPUT_BYTES,
|
|
3
|
+
import { isWorkflowGraphManifest, WORKFLOW_CODE_V1_DEFAULT_OUTPUT_BYTES, WORKFLOW_CODE_V1_DEFAULT_TIMEOUT_MS, WORKFLOW_CODE_V1_MAX_OUTPUT_BYTES, WORKFLOW_CODE_V1_MIN_OUTPUT_BYTES, WORKFLOW_CODE_V1_MIN_TIMEOUT_MS, } from "./graph/types.js";
|
|
4
4
|
import { lintWorkflowGraphManifest } from "./graph/lint.js";
|
|
5
5
|
export * from "./usage-estimate.js";
|
|
6
6
|
// Pure event-dispatch semantics, shared by the apps/web and apps/worker event
|
|
@@ -162,6 +162,148 @@ export function nextCronRunAfter(input) {
|
|
|
162
162
|
}
|
|
163
163
|
throw new Error(`Cron expression has no matching run time in the next ${MAX_CRON_LOOKAHEAD_DAYS} days.`);
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The first slot of `cron` strictly after `after` that is also at least
|
|
167
|
+
* `minIntervalMinutes` after `lastFireAt`. This is how a plan's minimum cron
|
|
168
|
+
* interval (repricing 2026-09, decision L4.2) is enforced at fire time: a
|
|
169
|
+
* schedule that fires closer together than the plan allows keeps running, only
|
|
170
|
+
* slower, and is never deleted (proposal P-47). With no last fire or a minimum
|
|
171
|
+
* of one minute or less it is exactly `nextCronRunAfter`.
|
|
172
|
+
*/
|
|
173
|
+
export function nextCronRunAfterMinInterval(input) {
|
|
174
|
+
const minIntervalMs = Math.max(0, input.minIntervalMinutes ?? 0) * 60_000;
|
|
175
|
+
let after = input.after;
|
|
176
|
+
if (input.lastFireAt && minIntervalMs > 60_000) {
|
|
177
|
+
// One millisecond before the earliest allowed instant: nextCronRunAfter
|
|
178
|
+
// returns the first whole-minute slot strictly after it, so a slot exactly
|
|
179
|
+
// `minIntervalMinutes` after the last fire is allowed.
|
|
180
|
+
const earliestAllowed = input.lastFireAt.getTime() + minIntervalMs - 1;
|
|
181
|
+
if (earliestAllowed > after.getTime())
|
|
182
|
+
after = new Date(earliestAllowed);
|
|
183
|
+
}
|
|
184
|
+
return nextCronRunAfter({ cron: input.cron, timezone: input.timezone ?? null, after });
|
|
185
|
+
}
|
|
186
|
+
// How many calendar days minCronGapMinutes scans for two matching days in a
|
|
187
|
+
// row: two leap cycles, so a Feb 29 or a day-of-month/weekday coincidence is
|
|
188
|
+
// always seen.
|
|
189
|
+
const CRON_GAP_DAY_SCAN = 366 * 8 + 2;
|
|
190
|
+
// Real-time simulation around a daylight-saving change covers the day it falls
|
|
191
|
+
// in plus this many whole days either side (so the fire before a long gap that
|
|
192
|
+
// straddles the change is inside the window), and stops after this many fires.
|
|
193
|
+
const CRON_GAP_DST_WINDOW_DAYS = 1;
|
|
194
|
+
const CRON_GAP_DST_MAX_FIRES = 5_000;
|
|
195
|
+
/**
|
|
196
|
+
* The smallest gap in minutes between two consecutive fires of `cron`, which is
|
|
197
|
+
* what a plan's minimum cron interval is checked against (repricing 2026-09,
|
|
198
|
+
* decision L4.2). It is the smallest gap, never the average: `0,5 9 * * *`
|
|
199
|
+
* fires once a day on average but twice five minutes apart, so its gap is 5.
|
|
200
|
+
*
|
|
201
|
+
* Gaps are measured in real time in `timezone`. A daylight-saving change can
|
|
202
|
+
* bring two fires closer than their wall-clock times suggest (01:55 and 03:05
|
|
203
|
+
* are ten minutes apart on the night the clock skips from 02:00 to 03:00, and
|
|
204
|
+
* a repeated hour can fire 01:45 and then 01:00 again), so the fires around
|
|
205
|
+
* each change in the coming year are simulated as well.
|
|
206
|
+
*
|
|
207
|
+
* Returns null when the schedule never fires twice within the scan (for
|
|
208
|
+
* example `0 9 29 2 *` fires once every few years and has no meaningful gap
|
|
209
|
+
* for a minimum to bind). Throws on an unparseable expression, like
|
|
210
|
+
* `nextCronRunAfter`.
|
|
211
|
+
*/
|
|
212
|
+
export function minCronGapMinutes(input) {
|
|
213
|
+
const schedule = parseCronSchedule(input.cron);
|
|
214
|
+
const timezone = input.timezone?.trim() || DEFAULT_WORKFLOW_CRON_TIMEZONE;
|
|
215
|
+
const fireMinutes = [...schedule.hours]
|
|
216
|
+
.flatMap((hour) => [...schedule.minutes].map((minute) => hour * 60 + minute))
|
|
217
|
+
.sort((a, b) => a - b);
|
|
218
|
+
let gap = Number.POSITIVE_INFINITY;
|
|
219
|
+
for (let index = 1; index < fireMinutes.length; index += 1) {
|
|
220
|
+
gap = Math.min(gap, (fireMinutes[index] ?? 0) - (fireMinutes[index - 1] ?? 0));
|
|
221
|
+
}
|
|
222
|
+
const dayGap = smallestMatchingDayGap(schedule);
|
|
223
|
+
const firstFire = fireMinutes[0];
|
|
224
|
+
const lastFire = fireMinutes[fireMinutes.length - 1];
|
|
225
|
+
if (dayGap !== null && firstFire !== undefined && lastFire !== undefined) {
|
|
226
|
+
gap = Math.min(gap, dayGap * 24 * 60 + firstFire - lastFire);
|
|
227
|
+
}
|
|
228
|
+
// A one-minute gap is the floor of the cron grammar; daylight saving cannot
|
|
229
|
+
// bring two fires closer than that.
|
|
230
|
+
if (gap > 1) {
|
|
231
|
+
const dstGap = smallestGapAroundDaylightSavingChanges(input.cron, timezone, input.now ?? new Date());
|
|
232
|
+
if (dstGap !== null)
|
|
233
|
+
gap = Math.min(gap, dstGap);
|
|
234
|
+
}
|
|
235
|
+
return Number.isFinite(gap) ? gap : null;
|
|
236
|
+
}
|
|
237
|
+
/** Fewest days between two calendar days the schedule's date fields both match. */
|
|
238
|
+
function smallestMatchingDayGap(schedule) {
|
|
239
|
+
// Calendar arithmetic only (no timezone): day-of-month, month and weekday
|
|
240
|
+
// matching is the same on every clock. A fixed leap-year start covers every
|
|
241
|
+
// weekday/day-of-month combination within the scan.
|
|
242
|
+
const start = Date.UTC(2024, 0, 1);
|
|
243
|
+
let previousMatch = null;
|
|
244
|
+
let smallest = null;
|
|
245
|
+
for (let day = 0; day < CRON_GAP_DAY_SCAN; day += 1) {
|
|
246
|
+
const date = new Date(start + day * 24 * 60 * 60 * 1000);
|
|
247
|
+
const matches = matchesCronDate(schedule, {
|
|
248
|
+
minute: 0,
|
|
249
|
+
hour: 0,
|
|
250
|
+
dayOfMonth: date.getUTCDate(),
|
|
251
|
+
month: date.getUTCMonth() + 1,
|
|
252
|
+
dayOfWeek: date.getUTCDay(),
|
|
253
|
+
});
|
|
254
|
+
if (!matches)
|
|
255
|
+
continue;
|
|
256
|
+
if (previousMatch !== null) {
|
|
257
|
+
const gap = day - previousMatch;
|
|
258
|
+
smallest = smallest === null ? gap : Math.min(smallest, gap);
|
|
259
|
+
if (smallest === 1)
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
previousMatch = day;
|
|
263
|
+
}
|
|
264
|
+
return smallest;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The smallest real-time gap between fires in the days around each
|
|
268
|
+
* daylight-saving change in the year after `now`; null when the zone has no
|
|
269
|
+
* change in that year or no two fires fall in any window.
|
|
270
|
+
*/
|
|
271
|
+
function smallestGapAroundDaylightSavingChanges(cron, timezone, now) {
|
|
272
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
273
|
+
const start = Math.floor(now.getTime() / dayMs) * dayMs;
|
|
274
|
+
let smallest = null;
|
|
275
|
+
let previousOffset = zonedOffsetMinutes(new Date(start), timezone);
|
|
276
|
+
for (let day = 1; day <= 367; day += 1) {
|
|
277
|
+
const offset = zonedOffsetMinutes(new Date(start + day * dayMs), timezone);
|
|
278
|
+
if (offset === previousOffset)
|
|
279
|
+
continue;
|
|
280
|
+
previousOffset = offset;
|
|
281
|
+
const windowStart = new Date(start + (day - 1 - CRON_GAP_DST_WINDOW_DAYS) * dayMs);
|
|
282
|
+
const windowEnd = start + (day + CRON_GAP_DST_WINDOW_DAYS) * dayMs;
|
|
283
|
+
let fire = nextCronRunAfter({ cron, timezone, after: windowStart });
|
|
284
|
+
for (let fires = 0; fires < CRON_GAP_DST_MAX_FIRES && fire.getTime() <= windowEnd; fires += 1) {
|
|
285
|
+
const next = nextCronRunAfter({ cron, timezone, after: fire });
|
|
286
|
+
if (next.getTime() > windowEnd)
|
|
287
|
+
break;
|
|
288
|
+
const gap = Math.round((next.getTime() - fire.getTime()) / 60_000);
|
|
289
|
+
smallest = smallest === null ? gap : Math.min(smallest, gap);
|
|
290
|
+
fire = next;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return smallest;
|
|
294
|
+
}
|
|
295
|
+
/** Minutes the zone's wall clock is ahead of UTC at `date`. */
|
|
296
|
+
function zonedOffsetMinutes(date, timezone) {
|
|
297
|
+
const formatter = getZonedFormatter(timezone);
|
|
298
|
+
const parts = new Map();
|
|
299
|
+
for (const part of formatter.formatToParts(date)) {
|
|
300
|
+
if (part.type !== "literal")
|
|
301
|
+
parts.set(part.type, part.value);
|
|
302
|
+
}
|
|
303
|
+
const wallClock = Date.UTC(Number(parts.get("year")), Number(parts.get("month")) - 1, Number(parts.get("day")), Number(parts.get("hour")), Number(parts.get("minute")));
|
|
304
|
+
const instant = Math.floor(date.getTime() / 60_000) * 60_000;
|
|
305
|
+
return Math.round((wallClock - instant) / 60_000);
|
|
306
|
+
}
|
|
165
307
|
export function transformStep(input) {
|
|
166
308
|
return {
|
|
167
309
|
__oxygen_workflow_step: true,
|
|
@@ -983,8 +1125,16 @@ export function lintWorkflowCodeSourceSafety(source, path = "$.source") {
|
|
|
983
1125
|
}
|
|
984
1126
|
return issues;
|
|
985
1127
|
}
|
|
1128
|
+
/**
|
|
1129
|
+
* The ceiling of the legacy synchronous `node:vm` path below. It stays at 10 s
|
|
1130
|
+
* (and its callers pass 1 s) when the typed Code node rose to 30 s (repricing
|
|
1131
|
+
* 2026-09, decision L4.3; proposal P-49): this path runs on the worker's own
|
|
1132
|
+
* event loop, so a long synchronous script would stall every other job and its
|
|
1133
|
+
* queue lock renewal. The typed Code node runs in a separate microVM instead.
|
|
1134
|
+
*/
|
|
1135
|
+
export const PURE_WORKFLOW_FUNCTION_MAX_TIMEOUT_MS = 10_000;
|
|
986
1136
|
export async function runPureWorkflowFunction(input) {
|
|
987
|
-
const timeoutMs = Math.max(WORKFLOW_CODE_V1_MIN_TIMEOUT_MS, Math.min(input.timeoutMs ?? WORKFLOW_CODE_V1_DEFAULT_TIMEOUT_MS,
|
|
1137
|
+
const timeoutMs = Math.max(WORKFLOW_CODE_V1_MIN_TIMEOUT_MS, Math.min(input.timeoutMs ?? WORKFLOW_CODE_V1_DEFAULT_TIMEOUT_MS, PURE_WORKFLOW_FUNCTION_MAX_TIMEOUT_MS));
|
|
988
1138
|
const maxOutputBytes = Math.max(WORKFLOW_CODE_V1_MIN_OUTPUT_BYTES, Math.min(input.maxOutputBytes ?? WORKFLOW_CODE_V1_DEFAULT_OUTPUT_BYTES, WORKFLOW_CODE_V1_MAX_OUTPUT_BYTES));
|
|
989
1139
|
const issues = [];
|
|
990
1140
|
validatePureFunctionSource(input.source, "$.source", (path, code, message) => {
|
|
@@ -3,7 +3,12 @@ export type WorkflowAutomationUsageEstimate = {
|
|
|
3
3
|
scheduledRunsPer30Days: number | null;
|
|
4
4
|
scheduledActionsPer30DaysFloor: number | null;
|
|
5
5
|
dynamicRuntimeActions: boolean;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Always false: row writes stopped billing per row (repricing 2026-09,
|
|
8
|
+
* decision 6.2). Kept for one release so older CLIs and pages that read it
|
|
9
|
+
* still parse; remove after that release.
|
|
10
|
+
*/
|
|
11
|
+
perRowFanout: false;
|
|
7
12
|
};
|
|
8
13
|
export declare function estimateWorkflowRunAutomationActionsFloor(manifest: unknown): number;
|
|
9
14
|
export declare function estimateWorkflowAutomationUsage(manifest: unknown, cron?: string | null): WorkflowAutomationUsageEstimate | null;
|
|
@@ -16,6 +21,8 @@ export type CronCadenceAssessment = {
|
|
|
16
21
|
floorShareOfIncluded: number;
|
|
17
22
|
basis: "plan_action_quota" | "plan_monthly_credits";
|
|
18
23
|
includedCredits: number | null;
|
|
24
|
+
spendableCredits: number | null;
|
|
25
|
+
allowanceCredits: number | null;
|
|
19
26
|
floorCreditsPer30Days: number | null;
|
|
20
27
|
minimumViableIntervalMinutes: number | null;
|
|
21
28
|
verdict: "fits" | "impossible";
|
|
@@ -27,6 +34,8 @@ export declare function assessCronCadenceViability(input: {
|
|
|
27
34
|
overageEnabled: boolean;
|
|
28
35
|
includedCredits?: number | null;
|
|
29
36
|
creditsPerAction?: number | null;
|
|
37
|
+
/** Credits the workspace can spend right now (its available balance). */
|
|
38
|
+
spendableCredits?: number | null;
|
|
30
39
|
}): CronCadenceAssessment | null;
|
|
31
40
|
export type CronAggressiveness = {
|
|
32
41
|
level: "aggressive" | "very_aggressive";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// Static automation
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// honest projection
|
|
1
|
+
// Static automation usage estimation shared by the web workflow page and the
|
|
2
|
+
// worker scheduler's admission gate. The unit is the workflow STEP: every step a
|
|
3
|
+
// live run executes bills one (repricing 2026-09, decision 6.1) and rows written
|
|
4
|
+
// inside a run are free (decision 6.2), so a row-writing step costs one step
|
|
5
|
+
// however many rows it writes. The per-run count derived from a manifest is a
|
|
6
|
+
// FLOOR, never a projection: recipe code emits one step per checkpoint (loops
|
|
7
|
+
// multiply) and a retried attempt bills again. Surfaces present these numbers as
|
|
8
|
+
// minimums; observed run history is the honest projection
|
|
9
|
+
// (projectObservedAutomationUsage below).
|
|
9
10
|
import { loopBodyNodeIds } from "./graph/topology.js";
|
|
10
11
|
import { isWorkflowGraphManifest, workflowGraphNodeKindBills, MAX_WORKFLOW_LOOP_ITERATIONS, } from "./graph/types.js";
|
|
11
|
-
const ROW_FANOUT_TOOL_IDS = new Set(["oxygen.rows_upsert"]);
|
|
12
12
|
function isRecord(value) {
|
|
13
13
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
14
|
}
|
|
@@ -64,21 +64,6 @@ function manifestHasDynamicRuntimeActions(manifest) {
|
|
|
64
64
|
return graph.nodes.some((node) => node.hasRecipe
|
|
65
65
|
|| (node.kind === "loop" && !node.disabled && declaredLoopIterations(node.maxIterations) === null));
|
|
66
66
|
}
|
|
67
|
-
function manifestWritesRowsPerRun(manifest) {
|
|
68
|
-
if (!isRecord(manifest))
|
|
69
|
-
return false;
|
|
70
|
-
const steps = Array.isArray(manifest.steps) ? manifest.steps : null;
|
|
71
|
-
if (steps) {
|
|
72
|
-
return steps.some((step) => isRecord(step) && step.kind === "tool" && typeof step.tool === "string" && ROW_FANOUT_TOOL_IDS.has(step.tool));
|
|
73
|
-
}
|
|
74
|
-
const graph = readWorkflowGraph(manifest);
|
|
75
|
-
if (graph) {
|
|
76
|
-
return graph.nodes.some((node) => (node.kind === "tool" && typeof node.tool === "string" && ROW_FANOUT_TOOL_IDS.has(node.tool))
|
|
77
|
-
|| node.recipeTools.some((tool) => ROW_FANOUT_TOOL_IDS.has(tool)));
|
|
78
|
-
}
|
|
79
|
-
const toolsUsed = Array.isArray(manifest.tools_used) ? manifest.tools_used : [];
|
|
80
|
-
return toolsUsed.some((tool) => typeof tool === "string" && ROW_FANOUT_TOOL_IDS.has(tool));
|
|
81
|
-
}
|
|
82
67
|
function readWorkflowGraph(manifest) {
|
|
83
68
|
if (!isWorkflowGraphManifest(manifest))
|
|
84
69
|
return null;
|
|
@@ -194,7 +179,7 @@ export function estimateWorkflowAutomationUsage(manifest, cron) {
|
|
|
194
179
|
scheduledRunsPer30Days,
|
|
195
180
|
scheduledActionsPer30DaysFloor: scheduledRunsPer30Days === null ? null : scheduledRunsPer30Days * actionsPerRunFloor,
|
|
196
181
|
dynamicRuntimeActions: manifestHasDynamicRuntimeActions(manifest),
|
|
197
|
-
perRowFanout:
|
|
182
|
+
perRowFanout: false,
|
|
198
183
|
};
|
|
199
184
|
}
|
|
200
185
|
// Fixed 30-day month, minute and hour fields treated independently; timezone
|
|
@@ -279,8 +264,15 @@ const MINUTES_PER_30_DAYS = 43_200;
|
|
|
279
264
|
// Cadence-vs-allowance arithmetic for a cron trigger, using the manifest FLOOR
|
|
280
265
|
// (see the header): the cheapest run this workflow can physically have. A
|
|
281
266
|
// verdict of "impossible" is therefore a lower bound that already overruns the
|
|
282
|
-
//
|
|
283
|
-
//
|
|
267
|
+
// allowance — the real run cost is only ever higher (recipe checkpoints, loops
|
|
268
|
+
// over run-time collections and retries add steps).
|
|
269
|
+
//
|
|
270
|
+
// On the credit basis the allowance is the plan's monthly credits OR the
|
|
271
|
+
// balance the workspace can spend, whichever is larger (proposal P-65): at 0.05
|
|
272
|
+
// a step an every-minute, 5-step cron costs 10,800 credits a month, more than
|
|
273
|
+
// the $99 plan includes, and a workspace that funded that from top-ups must be
|
|
274
|
+
// able to arm it. The fire-time gate charges against the balance, so a funded
|
|
275
|
+
// cron is never paused mid-month either.
|
|
284
276
|
//
|
|
285
277
|
// Returns null when the question is not decidable or not meaningful:
|
|
286
278
|
// no cron (on-demand), an unparseable cron, an unlimited allowance, or overage
|
|
@@ -299,6 +291,16 @@ export function assessCronCadenceViability(input) {
|
|
|
299
291
|
&& input.includedCredits > 0
|
|
300
292
|
? input.includedCredits
|
|
301
293
|
: null;
|
|
294
|
+
const spendableCredits = typeof input.spendableCredits === "number"
|
|
295
|
+
&& Number.isFinite(input.spendableCredits)
|
|
296
|
+
&& input.spendableCredits > 0
|
|
297
|
+
? input.spendableCredits
|
|
298
|
+
: null;
|
|
299
|
+
// A plan without monthly credits (free) is never judged here: its crons run on
|
|
300
|
+
// the balance and the fire-time gate is their only stop, as before.
|
|
301
|
+
const allowanceCredits = includedCredits === null
|
|
302
|
+
? null
|
|
303
|
+
: Math.max(includedCredits, spendableCredits ?? 0);
|
|
302
304
|
// Prefer a real action quota when the plan still has one; otherwise derive the
|
|
303
305
|
// equivalent action budget from monthly credits.
|
|
304
306
|
const basis = input.includedActions === null
|
|
@@ -306,8 +308,8 @@ export function assessCronCadenceViability(input) {
|
|
|
306
308
|
: "plan_action_quota";
|
|
307
309
|
const includedActions = basis === "plan_action_quota"
|
|
308
310
|
? input.includedActions
|
|
309
|
-
: (
|
|
310
|
-
? Math.floor(
|
|
311
|
+
: (allowanceCredits !== null && creditsPerAction !== null
|
|
312
|
+
? Math.floor(allowanceCredits / creditsPerAction)
|
|
311
313
|
: null);
|
|
312
314
|
if (includedActions === null)
|
|
313
315
|
return null;
|
|
@@ -327,6 +329,8 @@ export function assessCronCadenceViability(input) {
|
|
|
327
329
|
floorShareOfIncluded: floorActionsPer30Days / includedActions,
|
|
328
330
|
basis,
|
|
329
331
|
includedCredits: basis === "plan_monthly_credits" ? includedCredits : null,
|
|
332
|
+
spendableCredits: basis === "plan_monthly_credits" ? spendableCredits : null,
|
|
333
|
+
allowanceCredits: basis === "plan_monthly_credits" ? allowanceCredits : null,
|
|
330
334
|
floorCreditsPer30Days: basis === "plan_monthly_credits" && creditsPerAction !== null
|
|
331
335
|
? Math.round(floorActionsPer30Days * creditsPerAction * 1000) / 1000
|
|
332
336
|
: null,
|