@oxygen-agent/cli 1.1010.650 → 1.1010.905

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.
Files changed (114) hide show
  1. package/README.md +1 -1
  2. package/dist/auto-update.d.ts +129 -0
  3. package/dist/auto-update.js +392 -0
  4. package/dist/command-manifest.js +15 -1
  5. package/dist/credentials.d.ts +2 -0
  6. package/dist/credentials.js +6 -3
  7. package/dist/functions-commands.js +1 -1
  8. package/dist/http-client.js +28 -4
  9. package/dist/inbox-needs-reply-notice.d.ts +12 -0
  10. package/dist/inbox-needs-reply-notice.js +51 -0
  11. package/dist/index.js +756 -177
  12. package/dist/run-wait.d.ts +3 -1
  13. package/dist/run-wait.js +19 -5
  14. package/dist/skills.js +48 -22
  15. package/dist/streamed-file-import.d.ts +58 -0
  16. package/dist/streamed-file-import.js +115 -0
  17. package/dist/update.d.ts +29 -0
  18. package/dist/update.js +62 -16
  19. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  20. package/dist/workflow-plan-limit-notices.js +28 -0
  21. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  22. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +50 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.js +94 -2
  24. package/node_modules/@oxygen/shared/dist/billing.d.ts +247 -37
  25. package/node_modules/@oxygen/shared/dist/billing.js +418 -45
  26. package/node_modules/@oxygen/shared/dist/capability-discovery.js +66 -6
  27. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
  28. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
  29. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  30. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  31. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  32. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  33. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  34. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  36. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  37. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +27 -0
  38. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +27 -0
  39. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  41. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +10 -1
  42. package/node_modules/@oxygen/shared/dist/feature-gates.js +12 -1
  43. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  44. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  45. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  46. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  47. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  48. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  49. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -23
  50. package/node_modules/@oxygen/shared/dist/index.js +4 -43
  51. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +114 -0
  52. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +150 -0
  53. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  54. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  55. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  56. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  57. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +19 -2
  58. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +234 -0
  59. package/node_modules/@oxygen/shared/dist/plan-band.js +312 -0
  60. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  61. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  62. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  63. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  64. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +335 -126
  65. package/node_modules/@oxygen/shared/dist/plan-limits.js +277 -86
  66. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +158 -49
  67. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +139 -41
  68. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +42 -23
  69. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +56 -37
  70. package/node_modules/@oxygen/shared/dist/process-resource.d.ts +4 -0
  71. package/node_modules/@oxygen/shared/dist/process-resource.js +25 -0
  72. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  73. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  74. package/node_modules/@oxygen/shared/dist/repricing.d.ts +257 -0
  75. package/node_modules/@oxygen/shared/dist/repricing.js +721 -0
  76. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  77. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  78. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +30 -0
  79. package/node_modules/@oxygen/shared/dist/sending-limits.js +43 -0
  80. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  81. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  82. package/node_modules/@oxygen/shared/dist/sequence-failures.js +4 -1
  83. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  84. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  85. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +33 -1
  86. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +71 -1
  87. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +68 -10
  88. package/node_modules/@oxygen/shared/dist/table-capacity.js +85 -4
  89. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  90. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  91. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  92. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  93. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +9 -0
  94. package/node_modules/@oxygen/shared/dist/telemetry.js +41 -2
  95. package/node_modules/@oxygen/shared/dist/trace-context.d.ts +29 -0
  96. package/node_modules/@oxygen/shared/dist/trace-context.js +88 -0
  97. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  98. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  99. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  100. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  101. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  102. package/node_modules/@oxygen/shared/dist/version.js +14 -27
  103. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  104. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  105. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  106. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  107. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  108. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  109. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  110. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  111. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  112. package/package.json +1 -1
  113. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +0 -64
  114. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +0 -90
@@ -1,5 +1,7 @@
1
1
  import { log } from "./log.js";
2
2
  import { resolveDeployEnv } from "./deploy-env.js";
3
+ import { processResourceIdentity } from "./process-resource.js";
4
+ import { OXYGEN_VERSION } from "./version.js";
3
5
  import { sanitizeLogFields } from "./redaction.js";
4
6
  import { trace } from "@opentelemetry/api";
5
7
  import { operationalLogSnapshot } from "./operational-telemetry.js";
@@ -44,6 +46,7 @@ async function rejectedRecords(response, batchLength) {
44
46
  reader.releaseLock();
45
47
  }
46
48
  }
49
+ const OTEL_TRACE_ID = /^[a-f\d]{32}$/i;
47
50
  // OTLP severity numbers (logs data model): the base of each 4-value band.
48
51
  const SEVERITY_NUMBERS = { debug: 5, info: 9, warn: 13, error: 17 };
49
52
  // Set by log() on every record; they become the OTLP record's own fields rather
@@ -197,6 +200,10 @@ export function createOtlpLogSink(options) {
197
200
  ?? process.env.NODE_ENV
198
201
  ?? null,
199
202
  "deployment.sha": process.env.OXYGEN_GIT_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA,
203
+ "service.version": OXYGEN_VERSION,
204
+ // host.name + service.instance.id: which box and which process emitted a
205
+ // line, the same identity the trace/metric resources now carry.
206
+ ...processResourceIdentity(),
200
207
  ...resourceOverrides,
201
208
  });
202
209
  const queue = [];
@@ -277,9 +284,19 @@ export function createOtlpLogSink(options) {
277
284
  droppedOverflow += 1;
278
285
  }
279
286
  const activeSpan = trace.getActiveSpan()?.spanContext();
287
+ // A request correlation id that is not an OTel trace id (the CLI's UUID
288
+ // `x-oxygen-trace-id`) cannot be the record's traceId, but it is the one key
289
+ // a customer can quote; keep it as an attribute instead of discarding it,
290
+ // and let the active span supply the real trace/span ids.
291
+ const requestTraceId = typeof record.trace_id === "string" && record.trace_id !== ""
292
+ && !OTEL_TRACE_ID.test(record.trace_id) ? record.trace_id : null;
293
+ const { trace_id: _requestTraceId, ...withoutRequestTraceId } = record;
294
+ const base = requestTraceId
295
+ ? { ...withoutRequestTraceId, "oxygen.request_trace_id": requestTraceId }
296
+ : record;
280
297
  queue.push(operationalLogSnapshot(sanitizeLogFields({
281
- ...record,
282
- ...(activeSpan && (!record.trace_id || record.trace_id === activeSpan.traceId)
298
+ ...base,
299
+ ...(activeSpan && (requestTraceId || !record.trace_id || record.trace_id === activeSpan.traceId)
283
300
  ? { trace_id: activeSpan.traceId, span_id: activeSpan.spanId }
284
301
  : {}),
285
302
  })));
@@ -0,0 +1,234 @@
1
+ import { type LimitsTier } from "./plan-limits.js";
2
+ import { type RepricingOptions } from "./repricing.js";
3
+ import { type WorkspaceTableCapacityStatus } from "./table-capacity.js";
4
+ /**
5
+ * The seven plan bands of the 2026-09 repricing (spec § 4, slice S06): free
6
+ * plus one band per Oxygen plan size. They sit BESIDE the five-rung
7
+ * `LimitsTier`, which cannot tell $49 from $99 or $199 from $499, so any limit
8
+ * that differs between those sizes (storage, the per-delivery spend default)
9
+ * is resolved from the band instead of the rung.
10
+ *
11
+ * A band never changes a limit by itself. Until a limit is re-keyed onto the
12
+ * band, it keeps reading its rung, and `PLAN_BAND_LIMITS_TIER` says which rung
13
+ * that is.
14
+ */
15
+ export type PlanBand = "free" | "49" | "99" | "199" | "499" | "999" | "1999";
16
+ export declare const PLAN_BAND_ORDER: readonly PlanBand[];
17
+ /** The limits rung each band enforces at today. */
18
+ export declare const PLAN_BAND_LIMITS_TIER: Readonly<Record<PlanBand, LimitsTier>>;
19
+ /**
20
+ * L4.1 (repricing 2026-09, slice S55; decision record "Worker share"): a
21
+ * tenant's share of the legacy worker's tenant queue while other tenants wait.
22
+ * A weight-16 tenant gets sixteen quanta for every one a weight-1 tenant gets;
23
+ * with nobody else waiting every tenant is served at once, whatever its weight.
24
+ * It is a scheduling share, never a sold limit: capacity is sized so contention
25
+ * is rare (`.agents/skills/oxygen-worker-performance`, "Worker share target").
26
+ * An org whose band is not yet resolved schedules at the free weight.
27
+ */
28
+ export declare const PLAN_BAND_WORKER_SHARE_WEIGHT: Readonly<Record<PlanBand, number>>;
29
+ /**
30
+ * PROPOSED (P-57, repricing spec § 4): a grandfathered or legacy plan takes the
31
+ * band of the limits rung it enforces at today, so no grandfathered customer
32
+ * loses a storage or rate limit. The pro rung spans $199–$499, whose columns
33
+ * differ only in the per-delivery spend default, which grandfathered plans
34
+ * resolve from their own monthly credits instead (P-62); it maps to the lower
35
+ * band. Enterprise enforces at the scale rung and therefore takes the top band.
36
+ */
37
+ export declare const LEGACY_PLAN_BAND_BY_LIMITS_TIER: Readonly<Record<LimitsTier, PlanBand>>;
38
+ /**
39
+ * Map a raw plan-tier string (subscription `tier`, metadata tier, legacy plan
40
+ * key, or an Oxygen plan key like `oxygen_499`) onto its band. Unknown or absent
41
+ * tiers land on the unentitled "free" band, the same fail-closed rule as
42
+ * `limitsTierForPlanTier`.
43
+ */
44
+ export declare function planBandForPlanTier(tier: string | null | undefined): PlanBand;
45
+ /**
46
+ * The band of an org whose limits rung is already resolved (no second lookup).
47
+ * An unentitled org keeps no plan tier, so it takes the free band.
48
+ */
49
+ export declare function planBandForLimitsResolution(resolution: {
50
+ planTier: string | null;
51
+ entitled: boolean;
52
+ }): PlanBand;
53
+ /** The band's monthly price, read from the Oxygen plan it names; null for free. */
54
+ export declare function planBandMonthlyPriceCents(band: PlanBand): number | null;
55
+ export declare function planBandLabel(band: PlanBand): string;
56
+ /**
57
+ * PROPOSED (P-54): the free plan keeps a 50-credit default. It has no monthly
58
+ * grant to take 10% of (10% of its one-time 1,000 would be 100).
59
+ */
60
+ export declare const FREE_PLAN_DEFAULT_DELIVERY_CREDIT_CEILING = 50;
61
+ /**
62
+ * A plan size's target default delivery ceiling: 10% of its monthly credits,
63
+ * rounded half up (decision L5.1; the volume bonus counts, the partner bonus
64
+ * does not, P-54). This is the target, not always the value in force: see
65
+ * `resolveDefaultTriggerRunCreditCeilingForPlan`.
66
+ */
67
+ export declare function planBandDeliveryCreditCeilingTarget(band: PlanBand): number;
68
+ /** What an org's plan resolves to: the raw plan tier and its limits rung. */
69
+ export type PlanSpendResolution = {
70
+ planTier: string | null;
71
+ limitsTier: LimitsTier;
72
+ };
73
+ /**
74
+ * The default credit ceiling of one LIVE workflow run fired by an unattended
75
+ * trigger (cron, webhook, event) that declares no `max_credits`, for a plan.
76
+ * `null` means no default applies (enterprise); an explicit cap always wins and
77
+ * is never clamped by this.
78
+ */
79
+ export declare function resolveDefaultTriggerRunCreditCeilingForPlan(resolution: PlanSpendResolution, options?: RepricingOptions): number | null;
80
+ /** The same default for one standing or webhook table auto-run batch. */
81
+ export declare function resolveDefaultAutoRunBatchCreditCeilingForPlan(resolution: PlanSpendResolution, options?: RepricingOptions): number | null;
82
+ /**
83
+ * Public webhook ingress (repricing 2026-09, decision L4.4, ratified
84
+ * 2026-09-26): deliveries a minute that one webhook target (a Table endpoint,
85
+ * a workflow or Agent trigger, the workspace's RB2B receiver) accepts. It used
86
+ * to be a flat 600 per target, with 60 per sender IP on most targets. Every
87
+ * value rises, so this is live on release rather than held behind the
88
+ * repricing switch.
89
+ */
90
+ export declare const PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE: Readonly<Record<PlanBand, number>>;
91
+ /**
92
+ * PROPOSED (P-50, repricing spec § 4.4): the decision raises the per-IP limit
93
+ * "to match", read as equal to the per-target limit (a share of 1). One sender
94
+ * (a form tool, a CRM) usually posts from one address, so any lower per-IP
95
+ * limit would cap a real integration below its plan's rate.
96
+ */
97
+ export declare const PUBLIC_WEBHOOK_INGRESS_PER_IP_SHARE_OF_TARGET = 1;
98
+ export declare const PUBLIC_WEBHOOK_INGRESS_WINDOW_SECONDS = 60;
99
+ export type PublicWebhookIngressLimits = {
100
+ perTargetPerMinute: number;
101
+ perSenderIpPerMinute: number;
102
+ windowSeconds: number;
103
+ };
104
+ /** The webhook ingress limits a band enforces. */
105
+ export declare function resolvePublicWebhookIngressLimits(band: PlanBand): PublicWebhookIngressLimits;
106
+ /** The webhook ingress limits of one band, as the `/api/cli/limits` contract reports them. */
107
+ export declare function describeWebhookIngress(band: PlanBand): PlanBandLimitsReport["webhooks"];
108
+ /**
109
+ * Request rates, per minute unless named otherwise. The totals bound every
110
+ * `/api/cli/*` call; the operation buckets bind first for the calls a script
111
+ * makes most (row reads and writes, live tool and AI calls, dry runs, bulk
112
+ * imports). A 429 names the bucket it hit in `error.details.bucket`: the
113
+ * totals are `cli.requests`, the others `tenant_read.requests`,
114
+ * `tenant_write.requests`, `tool_live.requests` / `ai_live.requests`,
115
+ * `tool_dry_run.requests` and `bulk_import.requests`.
116
+ */
117
+ export type ApiRateLimitsReport = {
118
+ org_requests_per_minute: number;
119
+ key_requests_per_minute: number;
120
+ tenant_read_requests_per_key_per_minute: number;
121
+ tenant_write_requests_per_key_per_minute: number;
122
+ live_action_requests_per_key_per_minute: number;
123
+ live_action_requests_per_org_per_hour: number;
124
+ tool_dry_run_requests_per_key_per_minute: number;
125
+ bulk_import_tables_per_org_per_hour: number;
126
+ /**
127
+ * Row budgets shared by every key in the workspace: each row a request reads
128
+ * or writes is one unit (`tenant_read.units`, `tenant_write.units`). The
129
+ * window is an hour on free and a day on paid plans.
130
+ */
131
+ rows_read_per_org: {
132
+ rows: number;
133
+ window_seconds: number;
134
+ };
135
+ rows_written_per_org: {
136
+ rows: number;
137
+ window_seconds: number;
138
+ };
139
+ };
140
+ /** The API rates a limits rung enforces, read from `PLAN_LIMITS`. */
141
+ export declare function describeApiRateLimits(tier: LimitsTier): ApiRateLimitsReport;
142
+ /**
143
+ * The limits in force today for one band, as the `/api/cli/limits` contract
144
+ * reports them. Every value is read from the constant that enforces it; nothing
145
+ * here is a second copy. Monthly credits are the Oxygen plan's grant (free has
146
+ * none, so its org-daily guard does not apply).
147
+ */
148
+ export type PlanBandLimitsReport = {
149
+ band: PlanBand;
150
+ label: string;
151
+ monthly_price_usd: number | null;
152
+ monthly_credits: number | null;
153
+ /**
154
+ * Credits granted above face value ($1 = 100 credits), repricing 2026-09
155
+ * decision 1.3: 0 up to $199, then +5% / +10% / +15% on $499 / $999 / $1,999.
156
+ * Already included in `monthly_credits`. Null on free, which has no plan grant.
157
+ */
158
+ volume_bonus_credits: number | null;
159
+ volume_bonus_percent: number | null;
160
+ /** What 100 plan credits cost on this size, the volume bonus included. */
161
+ usd_per_100_credits: number | null;
162
+ limits_tier: LimitsTier;
163
+ api: ApiRateLimitsReport;
164
+ storage: {
165
+ table_row_limit: number;
166
+ workspace_row_limit: number;
167
+ workspace_database_warning_bytes: number;
168
+ workspace_database_limit_bytes: number;
169
+ /** Equal to rows per Table (PROPOSED P-60): one file can fill an empty Table. */
170
+ import_max_rows_per_file: number;
171
+ import_max_file_bytes: number;
172
+ /**
173
+ * Where the values above stand against the ratified ladder (decision F.2):
174
+ * `in_force`, `scheduled` (they move to `target` at `effective_at`) or
175
+ * `awaiting_capacity_test` (`target` applies once the rows-per-Table
176
+ * capacity test at this size passes). A cut is announced only once the
177
+ * repricing date is set, so `target` is null while nothing is pending.
178
+ */
179
+ status: WorkspaceTableCapacityStatus["status"];
180
+ effective_at: string | null;
181
+ target: PlanBandStorageTarget | null;
182
+ };
183
+ spend: {
184
+ trigger_run_credit_ceiling: number | null;
185
+ auto_run_batch_credit_ceiling: number | null;
186
+ agent_run_max_total_credits: number;
187
+ agent_run_max_inference_credits: number;
188
+ /** Paid work the attended Copilot may start without an approval card. */
189
+ copilot_approval_free_per_call_credits: number;
190
+ copilot_approval_free_per_session_credits: number;
191
+ /** The platform's runaway bound on one attended Copilot turn (every plan). */
192
+ copilot_attended_turn_backstop_credits: number;
193
+ org_daily_guard_warn_credits: number | null;
194
+ org_daily_guard_block_credits: number | null;
195
+ byok_column_run_max_rows: number | null;
196
+ byok_provider_daily_calls: number | null;
197
+ };
198
+ /**
199
+ * Table work limits (L6): rows one write request may carry, and columns one
200
+ * action run or standing auto-run may carry.
201
+ */
202
+ tables: {
203
+ max_rows_per_write_request: number;
204
+ max_columns_per_run: number;
205
+ };
206
+ /** The raw signal-event feed: events per read and look-back days. */
207
+ signals: {
208
+ max_events: number;
209
+ max_window_days: number;
210
+ };
211
+ /** Public webhook ingress, per target and per sender IP (decision L4.4). */
212
+ webhooks: {
213
+ ingress_requests_per_target_per_minute: number;
214
+ ingress_requests_per_sender_ip_per_minute: number;
215
+ window_seconds: number;
216
+ };
217
+ workflows: {
218
+ /** Fastest cron schedule a workflow or Agent trigger may use, in force now. */
219
+ cron_min_interval_minutes: number;
220
+ };
221
+ };
222
+ /** The storage values a band moves to, in the `/api/cli/limits` field names. */
223
+ export type PlanBandStorageTarget = {
224
+ table_row_limit: number;
225
+ workspace_row_limit: number;
226
+ workspace_database_warning_bytes: number;
227
+ workspace_database_limit_bytes: number;
228
+ import_max_rows_per_file: number;
229
+ };
230
+ /** A band's storage status as `/api/cli/limits` reports it (`plan_bands[].storage` and `storage_capacity`). */
231
+ export declare function describePlanBandStorageStatus(band: PlanBand, options?: RepricingOptions): Pick<PlanBandLimitsReport["storage"], "status" | "effective_at" | "target">;
232
+ export declare function describePlanBandLimits(band: PlanBand, options?: RepricingOptions): PlanBandLimitsReport;
233
+ /** Every band's limits in force today, smallest first. */
234
+ export declare function describePlanBandLadder(options?: RepricingOptions): PlanBandLimitsReport[];
@@ -0,0 +1,312 @@
1
+ import { planVolumeBonusCredits, planVolumeBonusPercent, resolveBasePricingPlan } from "./billing.js";
2
+ import { planLimitsForTier, limitsTierForPlanTier, resolveCronMinIntervalMinutes, resolvePlanAgentCreditLimits, } from "./plan-limits.js";
3
+ import { COPILOT_ATTENDED_INFERENCE_CREDIT_CEILING, DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING, DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS, DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP, DEFAULT_TRIGGER_RUN_CREDIT_CEILING, copilotApprovalFreeAllowanceForBand, resolveOrgDailySpendGuard, resolveSpendSafetyPlanTier, } from "./spend-safety.js";
4
+ import { isRepricingInForce, tenthOfMonthlyCreditsRoundedHalfUp, } from "./repricing.js";
5
+ import { describeWorkspaceTableCapacityStatus, resolveWorkspaceTableCapacity, } from "./table-capacity.js";
6
+ export const PLAN_BAND_ORDER = [
7
+ "free",
8
+ "49",
9
+ "99",
10
+ "199",
11
+ "499",
12
+ "999",
13
+ "1999",
14
+ ];
15
+ /** The limits rung each band enforces at today. */
16
+ export const PLAN_BAND_LIMITS_TIER = Object.freeze({
17
+ free: "free",
18
+ "49": "starter",
19
+ "99": "starter",
20
+ "199": "pro",
21
+ "499": "pro",
22
+ "999": "team",
23
+ "1999": "scale",
24
+ });
25
+ /**
26
+ * L4.1 (repricing 2026-09, slice S55; decision record "Worker share"): a
27
+ * tenant's share of the legacy worker's tenant queue while other tenants wait.
28
+ * A weight-16 tenant gets sixteen quanta for every one a weight-1 tenant gets;
29
+ * with nobody else waiting every tenant is served at once, whatever its weight.
30
+ * It is a scheduling share, never a sold limit: capacity is sized so contention
31
+ * is rare (`.agents/skills/oxygen-worker-performance`, "Worker share target").
32
+ * An org whose band is not yet resolved schedules at the free weight.
33
+ */
34
+ export const PLAN_BAND_WORKER_SHARE_WEIGHT = Object.freeze({
35
+ free: 1,
36
+ "49": 2,
37
+ "99": 2,
38
+ "199": 4,
39
+ "499": 4,
40
+ "999": 8,
41
+ "1999": 16,
42
+ });
43
+ /**
44
+ * PROPOSED (P-57, repricing spec § 4): a grandfathered or legacy plan takes the
45
+ * band of the limits rung it enforces at today, so no grandfathered customer
46
+ * loses a storage or rate limit. The pro rung spans $199–$499, whose columns
47
+ * differ only in the per-delivery spend default, which grandfathered plans
48
+ * resolve from their own monthly credits instead (P-62); it maps to the lower
49
+ * band. Enterprise enforces at the scale rung and therefore takes the top band.
50
+ */
51
+ export const LEGACY_PLAN_BAND_BY_LIMITS_TIER = Object.freeze({
52
+ free: "free",
53
+ starter: "99",
54
+ pro: "199",
55
+ team: "999",
56
+ scale: "1999",
57
+ });
58
+ /**
59
+ * Map a raw plan-tier string (subscription `tier`, metadata tier, legacy plan
60
+ * key, or an Oxygen plan key like `oxygen_499`) onto its band. Unknown or absent
61
+ * tiers land on the unentitled "free" band, the same fail-closed rule as
62
+ * `limitsTierForPlanTier`.
63
+ */
64
+ export function planBandForPlanTier(tier) {
65
+ const plan = resolveBasePricingPlan(tier);
66
+ if (!plan || plan.tier === "free")
67
+ return "free";
68
+ if (plan.tier === "oxygen")
69
+ return oxygenPlanBand(plan.monthlyPriceCents);
70
+ return LEGACY_PLAN_BAND_BY_LIMITS_TIER[limitsTierForPlanTier(tier)];
71
+ }
72
+ /**
73
+ * The band of an org whose limits rung is already resolved (no second lookup).
74
+ * An unentitled org keeps no plan tier, so it takes the free band.
75
+ */
76
+ export function planBandForLimitsResolution(resolution) {
77
+ return resolution.entitled ? planBandForPlanTier(resolution.planTier) : "free";
78
+ }
79
+ /**
80
+ * An Oxygen plan sits on the largest size at or below its price. A plan with no
81
+ * resolvable price takes the entry size, the same rung `limitsTierForPlanTier`
82
+ * gives it.
83
+ */
84
+ function oxygenPlanBand(monthlyPriceCents) {
85
+ if (monthlyPriceCents === null)
86
+ return "49";
87
+ let band = "49";
88
+ for (const candidate of PLAN_BAND_ORDER) {
89
+ const price = planBandMonthlyPriceCents(candidate);
90
+ if (price !== null && price <= monthlyPriceCents)
91
+ band = candidate;
92
+ }
93
+ return band;
94
+ }
95
+ /** The band's monthly price, read from the Oxygen plan it names; null for free. */
96
+ export function planBandMonthlyPriceCents(band) {
97
+ if (band === "free")
98
+ return null;
99
+ return resolveBasePricingPlan(`oxygen_${band}`)?.monthlyPriceCents ?? null;
100
+ }
101
+ export function planBandLabel(band) {
102
+ return band === "free" ? "Free" : `Oxygen $${Number(band).toLocaleString("en-US")}`;
103
+ }
104
+ // ---- Default credit ceiling of one unattended delivery (decision L5.1) ------
105
+ /**
106
+ * PROPOSED (P-54): the free plan keeps a 50-credit default. It has no monthly
107
+ * grant to take 10% of (10% of its one-time 1,000 would be 100).
108
+ */
109
+ export const FREE_PLAN_DEFAULT_DELIVERY_CREDIT_CEILING = 50;
110
+ /**
111
+ * The grandfathered plans that take 10% of their OWN monthly credits (PROPOSED
112
+ * P-62: Starter 990, Pro 2,490, Team 7,490) rather than their band's. Other
113
+ * legacy plan keys take the band of the rung they enforce at (P-57).
114
+ */
115
+ const OWN_CREDITS_DELIVERY_CEILING_PLAN_KEYS = new Set(["starter", "pro", "team"]);
116
+ /**
117
+ * A plan size's target default delivery ceiling: 10% of its monthly credits,
118
+ * rounded half up (decision L5.1; the volume bonus counts, the partner bonus
119
+ * does not, P-54). This is the target, not always the value in force: see
120
+ * `resolveDefaultTriggerRunCreditCeilingForPlan`.
121
+ */
122
+ export function planBandDeliveryCreditCeilingTarget(band) {
123
+ if (band === "free")
124
+ return FREE_PLAN_DEFAULT_DELIVERY_CREDIT_CEILING;
125
+ const monthlyCredits = resolveBasePricingPlan(`oxygen_${band}`)?.monthlyCredits;
126
+ if (typeof monthlyCredits !== "number" || !(monthlyCredits > 0)) {
127
+ throw new Error(`plan-band: no monthly credits for the ${band} plan`);
128
+ }
129
+ return tenthOfMonthlyCreditsRoundedHalfUp(monthlyCredits);
130
+ }
131
+ function planBandForSpendResolution(resolution) {
132
+ return resolution.planTier
133
+ ? planBandForPlanTier(resolution.planTier)
134
+ : LEGACY_PLAN_BAND_BY_LIMITS_TIER[resolution.limitsTier];
135
+ }
136
+ function resolveDeliveryCreditCeiling(resolution, preRepricingTable, options) {
137
+ const before = preRepricingTable[resolveSpendSafetyPlanTier(resolution.planTier, resolution.limitsTier)];
138
+ // Enterprise runs on custom limits and has no default, before or after.
139
+ if (before === null || before === undefined)
140
+ return null;
141
+ const ownPlan = resolution.planTier && OWN_CREDITS_DELIVERY_CEILING_PLAN_KEYS.has(resolution.planTier)
142
+ ? resolveBasePricingPlan(resolution.planTier)
143
+ : null;
144
+ const target = ownPlan && typeof ownPlan.monthlyCredits === "number" && ownPlan.monthlyCredits > 0
145
+ ? tenthOfMonthlyCreditsRoundedHalfUp(ownPlan.monthlyCredits)
146
+ : planBandDeliveryCreditCeilingTarget(planBandForSpendResolution(resolution));
147
+ // A larger default ships on release; a smaller one is a tightening and waits
148
+ // for the effective-date switch (`spend.<band>.default_delivery_credit_ceiling`).
149
+ if (target >= before)
150
+ return target;
151
+ return isRepricingInForce(options) ? target : before;
152
+ }
153
+ /**
154
+ * The default credit ceiling of one LIVE workflow run fired by an unattended
155
+ * trigger (cron, webhook, event) that declares no `max_credits`, for a plan.
156
+ * `null` means no default applies (enterprise); an explicit cap always wins and
157
+ * is never clamped by this.
158
+ */
159
+ export function resolveDefaultTriggerRunCreditCeilingForPlan(resolution, options = {}) {
160
+ return resolveDeliveryCreditCeiling(resolution, DEFAULT_TRIGGER_RUN_CREDIT_CEILING, options);
161
+ }
162
+ /** The same default for one standing or webhook table auto-run batch. */
163
+ export function resolveDefaultAutoRunBatchCreditCeilingForPlan(resolution, options = {}) {
164
+ return resolveDeliveryCreditCeiling(resolution, DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING, options);
165
+ }
166
+ /** The spend resolution a plan size stands for (its Oxygen plan key). */
167
+ function bandSpendResolution(band) {
168
+ return { planTier: band === "free" ? null : `oxygen_${band}`, limitsTier: PLAN_BAND_LIMITS_TIER[band] };
169
+ }
170
+ /**
171
+ * Public webhook ingress (repricing 2026-09, decision L4.4, ratified
172
+ * 2026-09-26): deliveries a minute that one webhook target (a Table endpoint,
173
+ * a workflow or Agent trigger, the workspace's RB2B receiver) accepts. It used
174
+ * to be a flat 600 per target, with 60 per sender IP on most targets. Every
175
+ * value rises, so this is live on release rather than held behind the
176
+ * repricing switch.
177
+ */
178
+ export const PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE = Object.freeze({
179
+ free: 600,
180
+ "49": 1_500,
181
+ "99": 1_500,
182
+ "199": 3_000,
183
+ "499": 3_000,
184
+ "999": 6_000,
185
+ "1999": 12_000,
186
+ });
187
+ /**
188
+ * PROPOSED (P-50, repricing spec § 4.4): the decision raises the per-IP limit
189
+ * "to match", read as equal to the per-target limit (a share of 1). One sender
190
+ * (a form tool, a CRM) usually posts from one address, so any lower per-IP
191
+ * limit would cap a real integration below its plan's rate.
192
+ */
193
+ export const PUBLIC_WEBHOOK_INGRESS_PER_IP_SHARE_OF_TARGET = 1;
194
+ export const PUBLIC_WEBHOOK_INGRESS_WINDOW_SECONDS = 60;
195
+ /** The webhook ingress limits a band enforces. */
196
+ export function resolvePublicWebhookIngressLimits(band) {
197
+ const perTarget = PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE[band];
198
+ return {
199
+ perTargetPerMinute: perTarget,
200
+ perSenderIpPerMinute: Math.round(perTarget * PUBLIC_WEBHOOK_INGRESS_PER_IP_SHARE_OF_TARGET),
201
+ windowSeconds: PUBLIC_WEBHOOK_INGRESS_WINDOW_SECONDS,
202
+ };
203
+ }
204
+ /** The webhook ingress limits of one band, as the `/api/cli/limits` contract reports them. */
205
+ export function describeWebhookIngress(band) {
206
+ const ingress = resolvePublicWebhookIngressLimits(band);
207
+ return {
208
+ ingress_requests_per_target_per_minute: ingress.perTargetPerMinute,
209
+ ingress_requests_per_sender_ip_per_minute: ingress.perSenderIpPerMinute,
210
+ window_seconds: ingress.windowSeconds,
211
+ };
212
+ }
213
+ /** The API rates a limits rung enforces, read from `PLAN_LIMITS`. */
214
+ export function describeApiRateLimits(tier) {
215
+ const cli = planLimitsForTier(tier).cli;
216
+ return {
217
+ org_requests_per_minute: cli.global.orgRequestsPerMinute,
218
+ key_requests_per_minute: cli.global.keyRequestsPerMinute,
219
+ tenant_read_requests_per_key_per_minute: cli.tenantRead.requestsPerMinute,
220
+ tenant_write_requests_per_key_per_minute: cli.tenantWrite.requestsPerMinute,
221
+ // ai_live and tool_live are separate buckets with equal limits at every rung.
222
+ live_action_requests_per_key_per_minute: cli.toolLive.requestsPerMinute,
223
+ // Both org windows are hourly on every rung (pinned by plan-band.test.ts).
224
+ live_action_requests_per_org_per_hour: cli.toolLive.orgRequests.limit,
225
+ tool_dry_run_requests_per_key_per_minute: cli.toolDryRun.requestsPerMinute,
226
+ bulk_import_tables_per_org_per_hour: cli.bulkImport.orgRequests.limit,
227
+ rows_read_per_org: { rows: cli.tenantRead.orgUnits.limit, window_seconds: cli.tenantRead.orgUnits.windowSeconds },
228
+ rows_written_per_org: { rows: cli.tenantWrite.orgUnits.limit, window_seconds: cli.tenantWrite.orgUnits.windowSeconds },
229
+ };
230
+ }
231
+ function storageTarget(limits) {
232
+ return {
233
+ table_row_limit: limits.tableRowLimit,
234
+ workspace_row_limit: limits.workspaceRowLimit,
235
+ workspace_database_warning_bytes: limits.workspaceDatabaseWarningBytes,
236
+ workspace_database_limit_bytes: limits.workspaceDatabaseLimitBytes,
237
+ import_max_rows_per_file: limits.tableRowLimit,
238
+ };
239
+ }
240
+ /** A band's storage status as `/api/cli/limits` reports it (`plan_bands[].storage` and `storage_capacity`). */
241
+ export function describePlanBandStorageStatus(band, options = {}) {
242
+ const status = describeWorkspaceTableCapacityStatus(band, options);
243
+ return {
244
+ status: status.status,
245
+ effective_at: status.effectiveAt?.toISOString() ?? null,
246
+ target: status.target ? storageTarget(status.target) : null,
247
+ };
248
+ }
249
+ export function describePlanBandLimits(band, options = {}) {
250
+ const limitsTier = PLAN_BAND_LIMITS_TIER[band];
251
+ const storage = resolveWorkspaceTableCapacity(band, options);
252
+ const limits = planLimitsForTier(limitsTier);
253
+ const monthlyPriceCents = planBandMonthlyPriceCents(band);
254
+ const monthlyCredits = band === "free"
255
+ ? null
256
+ : resolveBasePricingPlan(`oxygen_${band}`)?.monthlyCredits ?? null;
257
+ const dailyGuard = resolveOrgDailySpendGuard(monthlyCredits, { ...options, freePlan: band === "free" });
258
+ const agentLimits = resolvePlanAgentCreditLimits(limitsTier, options);
259
+ const copilotAllowance = copilotApprovalFreeAllowanceForBand(band);
260
+ return {
261
+ band,
262
+ label: planBandLabel(band),
263
+ monthly_price_usd: monthlyPriceCents === null ? null : monthlyPriceCents / 100,
264
+ monthly_credits: monthlyCredits,
265
+ volume_bonus_credits: band === "free" ? null : planVolumeBonusCredits(`oxygen_${band}`),
266
+ volume_bonus_percent: band === "free" ? null : planVolumeBonusPercent(`oxygen_${band}`),
267
+ usd_per_100_credits: monthlyPriceCents === null || !monthlyCredits
268
+ ? null
269
+ // Four decimals: $1,999 buys 229,885 credits, $0.8696 per 100.
270
+ : Math.round((monthlyPriceCents / monthlyCredits) * 10_000) / 10_000,
271
+ limits_tier: limitsTier,
272
+ api: describeApiRateLimits(limitsTier),
273
+ storage: {
274
+ table_row_limit: storage.tableRowLimit,
275
+ workspace_row_limit: storage.workspaceRowLimit,
276
+ workspace_database_warning_bytes: storage.workspaceDatabaseWarningBytes,
277
+ workspace_database_limit_bytes: storage.workspaceDatabaseLimitBytes,
278
+ import_max_rows_per_file: storage.tableRowLimit,
279
+ import_max_file_bytes: limits.import.maxFileBytes,
280
+ ...describePlanBandStorageStatus(band, options),
281
+ },
282
+ spend: {
283
+ trigger_run_credit_ceiling: resolveDefaultTriggerRunCreditCeilingForPlan(bandSpendResolution(band), options),
284
+ auto_run_batch_credit_ceiling: resolveDefaultAutoRunBatchCreditCeilingForPlan(bandSpendResolution(band), options),
285
+ agent_run_max_total_credits: agentLimits.maxTotalCredits,
286
+ agent_run_max_inference_credits: agentLimits.maxInferenceCredits,
287
+ copilot_approval_free_per_call_credits: copilotAllowance.perCallCredits,
288
+ copilot_approval_free_per_session_credits: copilotAllowance.perSessionCredits,
289
+ copilot_attended_turn_backstop_credits: COPILOT_ATTENDED_INFERENCE_CREDIT_CEILING,
290
+ org_daily_guard_warn_credits: dailyGuard?.warnCredits ?? null,
291
+ org_daily_guard_block_credits: dailyGuard?.blockCredits ?? null,
292
+ byok_column_run_max_rows: DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS[limitsTier],
293
+ byok_provider_daily_calls: DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP[limitsTier],
294
+ },
295
+ tables: {
296
+ max_rows_per_write_request: limits.cli.maxWriteRowsPerRequest,
297
+ max_columns_per_run: limits.tableActions.maxActionsPerRun,
298
+ },
299
+ signals: {
300
+ max_events: limits.signals.maxEvents,
301
+ max_window_days: limits.signals.maxWindowDays,
302
+ },
303
+ webhooks: describeWebhookIngress(band),
304
+ workflows: {
305
+ cron_min_interval_minutes: resolveCronMinIntervalMinutes(limitsTier, options),
306
+ },
307
+ };
308
+ }
309
+ /** Every band's limits in force today, smallest first. */
310
+ export function describePlanBandLadder(options = {}) {
311
+ return PLAN_BAND_ORDER.map((band) => describePlanBandLimits(band, options));
312
+ }