@oxygen-agent/cli 1.750.4 → 1.782.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/command-manifest.js +11 -1
- package/dist/help.js +8 -0
- package/dist/index.js +492 -96
- package/node_modules/@oxygen/shared/dist/billing.d.ts +88 -46
- package/node_modules/@oxygen/shared/dist/billing.js +134 -74
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +12 -4
- package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.js +23 -5
- package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +13 -2
- package/node_modules/@oxygen/shared/dist/future-signup-events.js +17 -2
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +106 -1
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +156 -45
- package/node_modules/@oxygen/shared/dist/index.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/index.js +5 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +69 -4
- package/node_modules/@oxygen/shared/dist/person-name.d.ts +40 -0
- package/node_modules/@oxygen/shared/dist/person-name.js +23 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +82 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +130 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/plan-limits.js +18 -2
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +50 -56
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +77 -90
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +62 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +91 -0
- package/node_modules/@oxygen/shared/dist/provider-funding-errors.d.ts +44 -0
- package/node_modules/@oxygen/shared/dist/provider-funding-errors.js +81 -0
- package/node_modules/@oxygen/shared/dist/publishing-limits.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/publishing-limits.js +24 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +27 -6
- package/node_modules/@oxygen/shared/dist/spend-safety.js +34 -6
- 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 +6 -3
- package/node_modules/@oxygen/shared/package.json +10 -0
- 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/topology.d.ts +27 -0
- package/node_modules/@oxygen/workflows/dist/graph/topology.js +95 -0
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +27 -8
- package/node_modules/@oxygen/workflows/dist/graph/types.js +2 -4
- package/node_modules/@oxygen/workflows/dist/index.js +0 -1
- package/node_modules/@oxygen/workflows/dist/portable.js +0 -9
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One vocabulary for "the provider refused because of money".
|
|
3
|
+
*
|
|
4
|
+
* A provider 402 has reached customers wearing four different costumes:
|
|
5
|
+
* - `provider_credits_exhausted` (tool runner, managed pool)
|
|
6
|
+
* - `connection_credits_exhausted` (tool runner, BYOK connection)
|
|
7
|
+
* - `provider_insufficient_credits` (enrichment column runner + waterfall)
|
|
8
|
+
* - `provider_credit_exhausted` (worker lane exclusion, singular)
|
|
9
|
+
* plus `provider_plan_limit` for the adjacent "your plan does not include this"
|
|
10
|
+
* refusal, which is also a money problem and also not retryable.
|
|
11
|
+
*
|
|
12
|
+
* Only the first two were known to the run-bottleneck classifier. The others
|
|
13
|
+
* fell through to a `/rate|limit|429/` test — which `provider_plan_limit`
|
|
14
|
+
* matches on the substring "limit" — or to the deferral branch, so a hard 402
|
|
15
|
+
* surfaced to the customer as `bottlenecks: [provider_rate_limit]`. That reads
|
|
16
|
+
* as "wait and retry": a partner agency spent hours lowering concurrency against
|
|
17
|
+
* an account that was simply out of credits, and the real 402 was only visible
|
|
18
|
+
* through a different command.
|
|
19
|
+
*
|
|
20
|
+
* The distinction that matters operationally: a rate limit clears by waiting, a
|
|
21
|
+
* funding failure never does. Anything that decides "should the customer wait?"
|
|
22
|
+
* must ask this predicate, not a regex over the code string.
|
|
23
|
+
*/
|
|
24
|
+
/** Codes meaning the provider refused for funding/entitlement reasons. Never retryable by waiting. */
|
|
25
|
+
export const PROVIDER_FUNDING_ERROR_CODES = [
|
|
26
|
+
"provider_credits_exhausted",
|
|
27
|
+
"provider_credits_exhausted_deferred",
|
|
28
|
+
"connection_credits_exhausted",
|
|
29
|
+
"provider_insufficient_credits",
|
|
30
|
+
"provider_credit_exhausted",
|
|
31
|
+
"provider_plan_limit",
|
|
32
|
+
"blocked_provider_funding",
|
|
33
|
+
];
|
|
34
|
+
const FUNDING_CODE_SET = new Set(PROVIDER_FUNDING_ERROR_CODES);
|
|
35
|
+
/**
|
|
36
|
+
* True when the code means "this failed over money", in any of its spellings.
|
|
37
|
+
*
|
|
38
|
+
* Matches on the exact code first, then falls back to a narrow shape test so a
|
|
39
|
+
* provider-specific variant (`peopledatalabs_credits_exhausted`) is still
|
|
40
|
+
* classified as funding rather than silently treated as retryable. Deliberately
|
|
41
|
+
* does NOT match on "limit" alone — `rate_limit_exceeded` is a real rate limit.
|
|
42
|
+
*/
|
|
43
|
+
export function isProviderFundingErrorCode(code) {
|
|
44
|
+
if (!code)
|
|
45
|
+
return false;
|
|
46
|
+
const normalized = code.trim().toLowerCase();
|
|
47
|
+
if (!normalized)
|
|
48
|
+
return false;
|
|
49
|
+
if (FUNDING_CODE_SET.has(normalized))
|
|
50
|
+
return true;
|
|
51
|
+
if (/_credits?_exhausted(_deferred)?$/.test(normalized))
|
|
52
|
+
return true;
|
|
53
|
+
if (/insufficient_credits?$/.test(normalized))
|
|
54
|
+
return true;
|
|
55
|
+
if (/^blocked_provider_funding/.test(normalized))
|
|
56
|
+
return true;
|
|
57
|
+
return /(?:payment_required|plan_limit|quota_exceeded)$/.test(normalized);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* True for a genuine rate limit — one that clears by waiting.
|
|
61
|
+
*
|
|
62
|
+
* A funding failure always wins: `provider_plan_limit` contains "limit" and
|
|
63
|
+
* would otherwise read as retryable.
|
|
64
|
+
*/
|
|
65
|
+
export function isProviderRateLimitErrorCode(code) {
|
|
66
|
+
if (!code)
|
|
67
|
+
return false;
|
|
68
|
+
if (isProviderFundingErrorCode(code))
|
|
69
|
+
return false;
|
|
70
|
+
return /rate|limit|429|capacity_deferred/i.test(code);
|
|
71
|
+
}
|
|
72
|
+
/** The customer-facing next step for a funding refusal, by credential ownership. */
|
|
73
|
+
export function providerFundingNextStep(credentialMode) {
|
|
74
|
+
if (credentialMode === "byok") {
|
|
75
|
+
return "Top up or upgrade the provider account behind your connected key, then retry. Waiting will not clear this.";
|
|
76
|
+
}
|
|
77
|
+
if (credentialMode === "managed") {
|
|
78
|
+
return "This is OXYGEN's managed provider account, not your credit balance — contact support. Waiting will not clear this; route the run to another provider in the meantime.";
|
|
79
|
+
}
|
|
80
|
+
return "Check the provider account's balance and plan entitlement, then retry. Waiting will not clear this.";
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broadcast character caps, in one place.
|
|
3
|
+
*
|
|
4
|
+
* These were defined three times — the web composer, the tenant-db lint, and an
|
|
5
|
+
* implicit `maxLength` on the MCP tool schema — and the MCP copy had drifted to
|
|
6
|
+
* 20,000. An X Premium account may post 25,000 characters, so a post between the
|
|
7
|
+
* two numbers was rejected by OXYGEN's own schema before X ever saw it, with an
|
|
8
|
+
* error that blamed the length rather than our stale constant. A customer hit
|
|
9
|
+
* exactly that after we told them the Premium cap was supported.
|
|
10
|
+
*
|
|
11
|
+
* `PUBLISHING_CONTENT_MAX_CHARS` is the outermost bound any surface should
|
|
12
|
+
* enforce: it must be >= the largest per-channel cap, because per-channel
|
|
13
|
+
* validation is where a too-long post gets an accurate, channel-specific error.
|
|
14
|
+
*/
|
|
15
|
+
/** X free tier. */
|
|
16
|
+
export declare const X_FREE_MAX_CHARS = 280;
|
|
17
|
+
/** X Premium. The largest single-post cap across every supported channel. */
|
|
18
|
+
export declare const X_PREMIUM_MAX_CHARS = 25000;
|
|
19
|
+
/**
|
|
20
|
+
* Outermost content bound for any publishing surface (CLI/MCP schema, API).
|
|
21
|
+
* Never set a surface's cap below this — let the per-channel lint produce the
|
|
22
|
+
* specific error instead of a generic schema rejection.
|
|
23
|
+
*/
|
|
24
|
+
export declare const PUBLISHING_CONTENT_MAX_CHARS = 25000;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broadcast character caps, in one place.
|
|
3
|
+
*
|
|
4
|
+
* These were defined three times — the web composer, the tenant-db lint, and an
|
|
5
|
+
* implicit `maxLength` on the MCP tool schema — and the MCP copy had drifted to
|
|
6
|
+
* 20,000. An X Premium account may post 25,000 characters, so a post between the
|
|
7
|
+
* two numbers was rejected by OXYGEN's own schema before X ever saw it, with an
|
|
8
|
+
* error that blamed the length rather than our stale constant. A customer hit
|
|
9
|
+
* exactly that after we told them the Premium cap was supported.
|
|
10
|
+
*
|
|
11
|
+
* `PUBLISHING_CONTENT_MAX_CHARS` is the outermost bound any surface should
|
|
12
|
+
* enforce: it must be >= the largest per-channel cap, because per-channel
|
|
13
|
+
* validation is where a too-long post gets an accurate, channel-specific error.
|
|
14
|
+
*/
|
|
15
|
+
/** X free tier. */
|
|
16
|
+
export const X_FREE_MAX_CHARS = 280;
|
|
17
|
+
/** X Premium. The largest single-post cap across every supported channel. */
|
|
18
|
+
export const X_PREMIUM_MAX_CHARS = 25_000;
|
|
19
|
+
/**
|
|
20
|
+
* Outermost content bound for any publishing surface (CLI/MCP schema, API).
|
|
21
|
+
* Never set a surface's cap below this — let the per-channel lint produce the
|
|
22
|
+
* specific error instead of a generic schema rejection.
|
|
23
|
+
*/
|
|
24
|
+
export const PUBLISHING_CONTENT_MAX_CHARS = X_PREMIUM_MAX_CHARS;
|
|
@@ -54,13 +54,34 @@ export declare function resolveByokProviderDailyCapEnforcementMode(configured?:
|
|
|
54
54
|
/**
|
|
55
55
|
* Implicit org-level DAILY budget guard, evaluated only when the org has no
|
|
56
56
|
* explicit org-scope daily budget policy and the plan has finite positive
|
|
57
|
-
* monthly credits.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* monthly credits. Expressed as a MULTIPLE of the plan's monthly grant spent in
|
|
58
|
+
* one UTC day: warn past WARN_MULTIPLE, hard-block at BLOCK_MULTIPLE. Setting
|
|
59
|
+
* any explicit org-daily policy fully suppresses the implicit pair.
|
|
60
|
+
*
|
|
61
|
+
* This is a runaway-loop backstop, NOT a spend ceiling. The available balance is
|
|
62
|
+
* the real bound on what a workspace can spend, and legitimate single-day work
|
|
63
|
+
* routinely exceeds a month's grant: standing up cold-email infrastructure
|
|
64
|
+
* (a managed domain plus three warmed inboxes is ~31k credits, so a full sending
|
|
65
|
+
* estate is hundreds of thousands in an afternoon) and large enrichment bursts
|
|
66
|
+
* funded by a top-up are both deliberate purchases, not loops. Raised 20x on
|
|
67
|
+
* 2026-08-17 for exactly that reason — at the previous 1x the guard refused a
|
|
68
|
+
* customer who had already paid for the credits, and the refusal was invisible
|
|
69
|
+
* on every surface. 20x still stops an unattended loop within hours.
|
|
70
|
+
*/
|
|
71
|
+
export declare const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
|
|
72
|
+
export declare const DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE = 20;
|
|
73
|
+
/** Resolved credit thresholds of the implicit org-daily guard for one plan. */
|
|
74
|
+
export type OrgDailySpendGuard = {
|
|
75
|
+
warnCredits: number;
|
|
76
|
+
blockCredits: number;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the implicit guard's thresholds from a plan's monthly grant. `null`
|
|
80
|
+
* means the guard is suppressed because the plan has no finite positive grant to
|
|
81
|
+
* scale from (free at 0, enterprise/custom at null) — the single place that
|
|
82
|
+
* decision is made, so enforcement and the read surfaces cannot drift apart.
|
|
61
83
|
*/
|
|
62
|
-
export declare
|
|
63
|
-
export declare const DEFAULT_ORG_DAILY_SPEND_BLOCK_PCT = 1;
|
|
84
|
+
export declare function resolveOrgDailySpendGuard(monthlyCredits: number | null | undefined): OrgDailySpendGuard | null;
|
|
64
85
|
export declare function resolveDefaultTriggerRunCreditCeiling(tier: PlanTier): number | null;
|
|
65
86
|
export declare function resolveDefaultAutoRunBatchCreditCeiling(tier: PlanTier): number | null;
|
|
66
87
|
export declare function resolveDefaultByokColumnRunMaxRows(tier: PlanTier): number | null;
|
|
@@ -71,13 +71,41 @@ export function resolveByokProviderDailyCapEnforcementMode(configured = process.
|
|
|
71
71
|
/**
|
|
72
72
|
* Implicit org-level DAILY budget guard, evaluated only when the org has no
|
|
73
73
|
* explicit org-scope daily budget policy and the plan has finite positive
|
|
74
|
-
* monthly credits.
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
74
|
+
* monthly credits. Expressed as a MULTIPLE of the plan's monthly grant spent in
|
|
75
|
+
* one UTC day: warn past WARN_MULTIPLE, hard-block at BLOCK_MULTIPLE. Setting
|
|
76
|
+
* any explicit org-daily policy fully suppresses the implicit pair.
|
|
77
|
+
*
|
|
78
|
+
* This is a runaway-loop backstop, NOT a spend ceiling. The available balance is
|
|
79
|
+
* the real bound on what a workspace can spend, and legitimate single-day work
|
|
80
|
+
* routinely exceeds a month's grant: standing up cold-email infrastructure
|
|
81
|
+
* (a managed domain plus three warmed inboxes is ~31k credits, so a full sending
|
|
82
|
+
* estate is hundreds of thousands in an afternoon) and large enrichment bursts
|
|
83
|
+
* funded by a top-up are both deliberate purchases, not loops. Raised 20x on
|
|
84
|
+
* 2026-08-17 for exactly that reason — at the previous 1x the guard refused a
|
|
85
|
+
* customer who had already paid for the credits, and the refusal was invisible
|
|
86
|
+
* on every surface. 20x still stops an unattended loop within hours.
|
|
78
87
|
*/
|
|
79
|
-
export const
|
|
80
|
-
export const
|
|
88
|
+
export const DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE = 5;
|
|
89
|
+
export const DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE = 20;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the implicit guard's thresholds from a plan's monthly grant. `null`
|
|
92
|
+
* means the guard is suppressed because the plan has no finite positive grant to
|
|
93
|
+
* scale from (free at 0, enterprise/custom at null) — the single place that
|
|
94
|
+
* decision is made, so enforcement and the read surfaces cannot drift apart.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveOrgDailySpendGuard(monthlyCredits) {
|
|
97
|
+
if (typeof monthlyCredits !== "number" || !Number.isFinite(monthlyCredits) || monthlyCredits <= 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
warnCredits: roundCredits(monthlyCredits * DEFAULT_ORG_DAILY_SPEND_WARN_MULTIPLE),
|
|
102
|
+
blockCredits: roundCredits(monthlyCredits * DEFAULT_ORG_DAILY_SPEND_BLOCK_MULTIPLE),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Credits carry 3 decimals everywhere (control-DB numeric(18,3)). */
|
|
106
|
+
function roundCredits(value) {
|
|
107
|
+
return Math.round(value * 1000) / 1000;
|
|
108
|
+
}
|
|
81
109
|
export function resolveDefaultTriggerRunCreditCeiling(tier) {
|
|
82
110
|
return DEFAULT_TRIGGER_RUN_CREDIT_CEILING[tier];
|
|
83
111
|
}
|
|
@@ -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.782.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.782.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:
|
|
@@ -26,8 +26,11 @@ export const OXYGEN_VERSION = "1.750.4";
|
|
|
26
26
|
// (oxygen sequences|inbox|senders, /api/cli/{sequences,inbox,senders}) and
|
|
27
27
|
// removed the old /api/cli/linkedin/* routes — older CLIs would 404.
|
|
28
28
|
export const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
|
|
29
|
-
// Per-surface floor for the whitelabel/managed-inbox purchase path, enforced
|
|
30
|
-
//
|
|
29
|
+
// Per-surface floor for the whitelabel/managed-inbox purchase path, enforced by
|
|
30
|
+
// BOTH paid inbox routes: /api/cli/managed-inboxes/subscribe and
|
|
31
|
+
// /api/cli/managed-inboxes/[domain]/mailboxes. (The comment used to say subscribe
|
|
32
|
+
// only; the expansion route has enforced it too since it shipped, and three
|
|
33
|
+
// separate reads of this file have since reasoned from the wrong half.)
|
|
31
34
|
//
|
|
32
35
|
// 1.326.2: Pricing Model 2.0 (2026-07-14) moved whitelabel inbox subscribe from the
|
|
33
36
|
// USD Stripe rail back to Oxygen credits: the preview now returns
|
|
@@ -61,6 +61,11 @@
|
|
|
61
61
|
"import": "./dist/object-storage.js",
|
|
62
62
|
"default": "./dist/object-storage.js"
|
|
63
63
|
},
|
|
64
|
+
"./person-name": {
|
|
65
|
+
"types": "./dist/person-name.d.ts",
|
|
66
|
+
"import": "./dist/person-name.js",
|
|
67
|
+
"default": "./dist/person-name.js"
|
|
68
|
+
},
|
|
64
69
|
"./select-options": {
|
|
65
70
|
"types": "./dist/select-options.d.ts",
|
|
66
71
|
"import": "./dist/select-options.js",
|
|
@@ -145,6 +150,11 @@
|
|
|
145
150
|
"types": "./dist/schedule-label.d.ts",
|
|
146
151
|
"import": "./dist/schedule-label.js",
|
|
147
152
|
"default": "./dist/schedule-label.js"
|
|
153
|
+
},
|
|
154
|
+
"./publishing-limits": {
|
|
155
|
+
"types": "./dist/publishing-limits.d.ts",
|
|
156
|
+
"import": "./dist/publishing-limits.js",
|
|
157
|
+
"default": "./dist/publishing-limits.js"
|
|
148
158
|
}
|
|
149
159
|
},
|
|
150
160
|
"dependencies": {}
|
|
@@ -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
|
|
@@ -52,6 +52,33 @@ export declare function topologicalOrder(graph: WorkflowGraphManifest): string[]
|
|
|
52
52
|
* execution order and the existing topological analysis.
|
|
53
53
|
*/
|
|
54
54
|
export declare function dominatingNodeIds(graph: WorkflowGraphManifest, targetNodeId: string): Set<string>;
|
|
55
|
+
export type WorkflowGraphNodeTestPlanIssueCode = "workflow_test_target_not_found" | "workflow_test_target_is_trigger" | "workflow_test_target_disabled" | "workflow_test_target_unreachable";
|
|
56
|
+
export type WorkflowGraphNodeTestPlan = {
|
|
57
|
+
targetNodeId: string;
|
|
58
|
+
/** Original manifest order, suitable for a durable receipt and UI highlight. */
|
|
59
|
+
nodeIds: string[];
|
|
60
|
+
/** Transient execution view; the immutable run manifest remains unchanged. */
|
|
61
|
+
manifest: WorkflowGraphManifest;
|
|
62
|
+
};
|
|
63
|
+
export type WorkflowGraphNodeTestPlanResult = {
|
|
64
|
+
ok: true;
|
|
65
|
+
plan: WorkflowGraphNodeTestPlan;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
code: WorkflowGraphNodeTestPlanIssueCode;
|
|
69
|
+
message: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Plan a safe selected-node test over the canonical graph.
|
|
73
|
+
*
|
|
74
|
+
* The slice contains the target and every possible predecessor, not every node
|
|
75
|
+
* that happens to appear earlier in topological order. Legal loop back-edges are
|
|
76
|
+
* ignored while finding predecessors so selecting a node halfway through a loop
|
|
77
|
+
* does not accidentally pull later body nodes into the test. Conversely, when a
|
|
78
|
+
* completed loop is upstream of the target (or is itself the target), its whole
|
|
79
|
+
* body is required because the loop's output includes those executions.
|
|
80
|
+
*/
|
|
81
|
+
export declare function planWorkflowGraphNodeTest(graph: WorkflowGraphManifest, targetNodeId: string): WorkflowGraphNodeTestPlanResult;
|
|
55
82
|
/**
|
|
56
83
|
* Illegal cycles: one entry per strongly connected component that still contains
|
|
57
84
|
* a cycle once legal loop back-edges are removed.
|