@oxygen-agent/cli 1.1011.974 → 1.1011.1091
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 +18 -0
- package/dist/crm-support-flags.d.ts +1 -0
- package/dist/crm-support-flags.js +5 -1
- package/dist/customer-operation-commands.js +30 -1
- package/dist/help.js +10 -2
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +21 -10
- package/dist/index.js +272 -83
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +83 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +146 -11
- package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +19 -7
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +3 -3
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +21 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
- 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/whatsapp-sender-limits.d.ts +42 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +68 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
- package/node_modules/@oxygen/shared/package.json +5 -5
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
|
@@ -5,7 +5,7 @@ export const CRM_CUSTOM_COLUMN_SCHEMA = {
|
|
|
5
5
|
key: { type: "string" }, label: { type: "string" },
|
|
6
6
|
data_type: { type: "string", enum: ["text", "numeric", "boolean", "jsonb", "timestamptz"] },
|
|
7
7
|
dataType: { type: "string", enum: ["text", "numeric", "boolean", "jsonb", "timestamptz"] },
|
|
8
|
-
semantic_type: { type: "string" }, semanticType: { type: "string" },
|
|
8
|
+
semantic_type: { type: "string", minLength: 1, description: "Required with data_type; text for a generic field, or a semantic such as company.name, company.domain or email." }, semanticType: { type: "string", minLength: 1, description: "Alias of semantic_type." },
|
|
9
9
|
kind: { type: "string" }, attribute_kind: { type: "string" }, attributeKind: { type: "string" },
|
|
10
10
|
option_model_type: { type: "string" }, optionModelType: { type: "string" },
|
|
11
11
|
is_required: { type: "boolean" }, isRequired: { type: "boolean" },
|
|
@@ -21,6 +21,10 @@ export const CRM_CUSTOM_COLUMN_SCHEMA = {
|
|
|
21
21
|
attributeMetadata: { type: "object", additionalProperties: true },
|
|
22
22
|
},
|
|
23
23
|
required: ["key", "label"],
|
|
24
|
+
allOf: [
|
|
25
|
+
{ anyOf: [{ required: ["data_type"] }, { required: ["dataType"] }] },
|
|
26
|
+
{ anyOf: [{ required: ["semantic_type"] }, { required: ["semanticType"] }] },
|
|
27
|
+
],
|
|
24
28
|
additionalProperties: false,
|
|
25
29
|
};
|
|
26
30
|
export const CRM_CUSTOM_IDENTITY_SCHEMA = {
|
|
@@ -72,6 +72,20 @@ export declare const CRM_OBJECTS_QUERY_CONTRACT: import("./schema.js").ContractO
|
|
|
72
72
|
"x-clamp"?: true;
|
|
73
73
|
};
|
|
74
74
|
}, never>;
|
|
75
|
+
export declare const CRM_RECORD_QUERY_CONTRACT: import("./schema.js").ContractObject<{
|
|
76
|
+
readonly projection: {
|
|
77
|
+
type: "string";
|
|
78
|
+
enum: readonly ("compact" | "full")[];
|
|
79
|
+
description?: string;
|
|
80
|
+
};
|
|
81
|
+
readonly max_chars: {
|
|
82
|
+
type: "integer";
|
|
83
|
+
description?: string;
|
|
84
|
+
minimum?: number;
|
|
85
|
+
maximum?: number;
|
|
86
|
+
"x-clamp"?: true;
|
|
87
|
+
};
|
|
88
|
+
}, never>;
|
|
75
89
|
export declare const CRM_CONNECTIONS_QUERY_CONTRACT: import("./schema.js").ContractObject<{
|
|
76
90
|
readonly projection: {
|
|
77
91
|
type: "string";
|
|
@@ -30,6 +30,10 @@ export const CRM_OBJECTS_QUERY_CONTRACT = obj({
|
|
|
30
30
|
limit: int({ minimum: 1, maximum: 100, description: "Objects in one batch; default 20 for bounded reads." }),
|
|
31
31
|
max_chars: int({ minimum: 2000, maximum: 40_000, description: "Complete response window; oversized objects refuse without advancing." }),
|
|
32
32
|
});
|
|
33
|
+
export const CRM_RECORD_QUERY_CONTRACT = obj({
|
|
34
|
+
projection: enumOf(["compact", "full"], { description: "compact omits object configuration and oversized row fields, naming omissions. HTTP/CLI default full; MCP default compact." }),
|
|
35
|
+
max_chars: int({ minimum: 2000, maximum: 40_000, description: "Complete receipt window. Omitted cells remain available through the Table row inspector." }),
|
|
36
|
+
});
|
|
33
37
|
export const CRM_CONNECTIONS_QUERY_CONTRACT = obj({
|
|
34
38
|
projection: enumOf(["compact", "full"], { description: "Compact returns identifiers and short communication previews. HTTP default full." }),
|
|
35
39
|
max_chars: int({ minimum: 2000, maximum: 40_000, description: "Complete recent-window response; hasMore includes omitted links." }),
|
|
@@ -43,7 +43,7 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
43
43
|
operation("copilot.attachments.delete", "/api/cli/copilot/sessions/{session_id}/attachments/{attachment_id}", "DELETE", "copilot attachments delete", "oxygen_copilot_attachment_delete", "agents", "Detach an unsent Copilot attachment; attachments already quoted by a turn cannot be removed."),
|
|
44
44
|
operation("copilot.feedback", "/api/cli/copilot/sessions/{session_id}/turns/{turn_id}/feedback", "POST", "copilot feedback", "oxygen_copilot_feedback", "agents", "Rate one attended Copilot turn and optionally explain the rating.", { rating: enumOf(["up", "down"]), comment: text() }, ["rating"]),
|
|
45
45
|
operation("copilot.interrupt", "/api/cli/copilot/sessions/{session_id}/turns/{turn_id}/interrupt", "POST", "copilot interrupt", "oxygen_copilot_interrupt", "agents", "Request cancellation of one Copilot turn. Read the session again to confirm it stopped.", {}, [], mcpOnly),
|
|
46
|
-
operation("context.canonical.get", "/api/cli/context/canonical/get", "POST", "context canonical get", "oxygen_context_canonical_get", "knowledge", "Read the pinned
|
|
46
|
+
operation("context.canonical.get", "/api/cli/context/canonical/get", "POST", "context canonical get", "oxygen_context_canonical_get", "knowledge", "Read the current canonical (pinned) voice, brand or positioning that AI copy uses, or any type's pinned asset. present=false means none is pinned; grounding_when_absent says what AI copy uses instead.", { type: text("Type to read: voice, brand or positioning (the canonical copy AI uses), or another context type such as playbook or persona.") }, ["type"], { mutates: false }),
|
|
47
47
|
operation("crm.notes.update", "/api/cli/crm/notes/{note_id}", "PATCH", "crm notes update", "oxygen_crm_note_update", "crm", "Edit a CRM note's body, title or pinned state; an empty title clears it.", { body: text(), title: nullable(text()), pinned: bool() }, [], mcpOnly),
|
|
48
48
|
operation("crm.notes.delete", "/api/cli/crm/notes/{note_id}", "DELETE", "crm notes delete", "oxygen_crm_note_delete", "crm", "Delete one CRM note in this workspace.", {}, [], mcpOnly),
|
|
49
49
|
operation("crm.files.attach_inline", "/api/cli/crm/objects/{object}/records/{row_id}/files", "POST", "crm files attach-inline", "oxygen_crm_file_attach", "crm", CRM_FILE_ATTACH_DESCRIPTION, CRM_FILE_ATTACH_CONTRACT.properties, [...(CRM_FILE_ATTACH_CONTRACT.required ?? [])]),
|
|
@@ -62,7 +62,7 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
62
62
|
operation("directory.apply", "/api/cli/directory/apply", "POST", "directory apply", "oxygen_directory_apply", "crm", "Apply for an agency-directory listing for this workspace; reuses an existing open application.", { message: text(), website: text() }, [], mcpOnly),
|
|
63
63
|
operation("directory.logo", "/api/cli/directory/listing/logo", "POST", "directory logo upload", "oxygen_directory_logo_upload", "crm", "Upload a public agency-directory logo and read back the updated listing; PNG, JPEG, WebP or SVG, at most 2 MB.", { content_base64: text(), file_name: text(), content_type: enumOf(["image/png", "image/jpeg", "image/webp", "image/svg+xml"]) }, ["content_base64", "file_name", "content_type"], { multipart: { contentField: "content_base64", nameField: "file_name", typeField: "content_type", fileField: "file" } }),
|
|
64
64
|
operation("domains.delete", "/api/cli/domains/delete", "POST", "domains delete", "oxygen_domains_delete", "deliverability", "Preview domain removal and renewal cancellation; execute only with the exact plan hash and confirmation.", { domains: list(), approved: bool(), plan_hash: text(), confirmation: text() }, ["domains"], mcpOnly),
|
|
65
|
-
operation("knowledge.pages.archive", "/api/cli/knowledge/pages/archive", "POST", "knowledge page archive", "oxygen_knowledge_page_archive", "knowledge", "Archive a knowledge page
|
|
65
|
+
operation("knowledge.pages.archive", "/api/cli/knowledge/pages/archive", "POST", "knowledge page archive", "oxygen_knowledge_page_archive", "knowledge", "Archive a knowledge page; content/history remain, links become unresolved. Pass expected_revision for the version you reviewed.", { slug: text(), id: text(), expected_revision: int({ minimum: 1 }) }, [], mcpOnly),
|
|
66
66
|
operation("knowledge.sync.manifest", "/api/cli/knowledge/sync/manifest", "POST", "knowledge sync-manifest", "oxygen_knowledge_sync_manifest", "knowledge", "Read the wiki revision and content-hash inventory without downloading instruction bodies.", {}, [], { mutates: false }),
|
|
67
67
|
operation("knowledge.sync.delta", "/api/cli/knowledge/sync/delta", "POST", "knowledge sync", "oxygen_knowledge_sync_delta", "knowledge", "Read one resumable batch of wiki pages and tombstones, bounded by the server's byte budget.", { cursor: text(), limit: int({ minimum: 1, maximum: 200 }) }, [], { ...mcpOnly, mutates: false }),
|
|
68
68
|
operation("limits.provider.get", "/api/cli/limits/provider", "GET", "limits provider list", "oxygen_provider_limits_list", "billing", "Read provider request limits and BYOK warning defaults. MCP defaults to bounded summary pages; follow paging.next_offset with the same filter. HTTP/CLI omit pagination for the full legacy list. 0 credits.", {}, [], { contract: providerLimitListContract, generated: { cli: false, mcp: false } }),
|
|
@@ -102,7 +102,7 @@ export const CUSTOMER_OPERATIONS = [
|
|
|
102
102
|
operation("db.provision", "/api/cli/db/provision", "POST", "db provision", "oxygen_workspace_database_provision", "workspace", "Provision or retry this workspace database immediately; already-ready workspaces retain it. No preview or confirmation field; native workspace write access applies. May outlast the connector deadline: read oxygen_workspace_database_status before retrying. Hosting is plan-included; 0 credits.", {}, [], mcpOnly),
|
|
103
103
|
operation("sessions.list", "/api/cli/sessions", "GET", "sessions list", "oxygen_sessions_list", "workspace", "List your personal CLI sessions across workspaces; OAuth or personal user session required, organization API keys are refused."),
|
|
104
104
|
operation("sessions.revoke", "/api/cli/sessions/{session_id}", "DELETE", "sessions revoke", "oxygen_sessions_revoke", "workspace", "Revoke one of your personal CLI sessions; OAuth or personal user session required, organization API keys are refused."),
|
|
105
|
-
operation("billing.allowance", "/api/cli/billing/allowance", "GET", "billing allowance", "oxygen_billing_allowance", "billing", "
|
|
105
|
+
operation("billing.allowance", "/api/cli/billing/allowance", "GET", "billing allowance", "oxygen_billing_allowance", "billing", "Inspect this workspace's recurring credit allowance and period. 0 credits; reserves nothing. Due plan grants or credit expiry may apply.", {}, [], mcpOnly),
|
|
106
106
|
operation("billing.invoices", "/api/cli/billing/invoices", "GET", "billing invoices", "oxygen_billing_invoices", "billing", "Read this workspace's billing invoices and their hosted payment links.", {}, [], mcpOnly),
|
|
107
107
|
operation("billing.seats.set", "/api/cli/billing/seats", "POST", "billing seats set", "oxygen_billing_seats_set", "billing", "Set purchased legacy sending-seat quantity. Retired seats cannot increase; reducing existing seats lowers the next renewal charge.", { seat_key: enumOf(["linkedin", "whatsapp", "phone_number", "email_sender"]), quantity: int({ minimum: 0 }) }, ["seat_key", "quantity"], mcpOnly),
|
|
108
108
|
operation("collaboration.gates.list", "/api/cli/collab/gates", "GET", "approvals gate list", "oxygen_approval_gates_list", "collaboration", "Read this workspace's approval gates, optionally for one exact subject.", { subject: text() }, [], mcpOnly),
|
|
@@ -61,6 +61,12 @@ export type OperationSpec<C extends ContractObject = ContractObject> = {
|
|
|
61
61
|
};
|
|
62
62
|
mutates: boolean;
|
|
63
63
|
spends: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* MCP only: what a trimmed result should advise instead of the generic
|
|
66
|
+
* "smaller limit / a cursor", for a read whose single row can exceed the
|
|
67
|
+
* result ceiling (OXP-1.11.67).
|
|
68
|
+
*/
|
|
69
|
+
truncationHint?: string;
|
|
64
70
|
};
|
|
65
71
|
/** The route with its `{param}` segments filled in, each URL-encoded. */
|
|
66
72
|
export declare function operationPath(spec: Pick<OperationSpec, "route">, params: Readonly<Record<string, string>>): string;
|
|
@@ -12,6 +12,7 @@ export const PUBLISHING_AMPLIFICATION_SCOPE_KINDS = ["all", "tag", "post", "ugc_
|
|
|
12
12
|
* closed. Coarse tool annotations remain mixed; native identity/schema checks
|
|
13
13
|
* still run after this call-level classification. */
|
|
14
14
|
export const PUBLISHING_STORED_READ_SELECTORS = {
|
|
15
|
+
oxygen_publishing_comments: { argument: "action", values: ["list", "get"] },
|
|
15
16
|
oxygen_publishing_notes: { argument: "action", values: ["list"] },
|
|
16
17
|
oxygen_publishing_revisions: { argument: "action", values: ["list"] },
|
|
17
18
|
oxygen_publishing_analytics: { argument: "scope", values: ["summary", "timeseries", "post", "account"] },
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const TABLE_ROW_REMOVAL_FIELDS: {
|
|
2
|
+
row_ids: import("./schema.js").ArrayProperty<import("./schema.js").StringProperty>;
|
|
3
|
+
selection: import("./schema.js").OneOfProperty;
|
|
4
|
+
};
|
|
5
|
+
export declare const TABLE_ROW_REMOVAL_CONTRACT: import("./schema.js").ContractObject<{
|
|
6
|
+
row_ids: import("./schema.js").ArrayProperty<import("./schema.js").StringProperty>;
|
|
7
|
+
selection: import("./schema.js").OneOfProperty;
|
|
8
|
+
}, never>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { arr, enumOf, int, obj, str } from "./schema.js";
|
|
2
|
+
const rowIds = () => arr(str({ minLength: 1 }), { minItems: 1 });
|
|
3
|
+
// Removal uses the grid's scope, not the broader execution selection. In
|
|
4
|
+
// particular, execution's `filters` array is not a serialized browser filter.
|
|
5
|
+
const selection = {
|
|
6
|
+
description: '{"mode":"row_ids","row_ids":["id"]} or {"mode":"all_matching","filter":"<grid f>"}. Optional search, sort, range {offset,limit}, excluded_row_ids, crm_object. filter is base64url grid JSON; filters/limit are refused. Unqualified all_matching removes every row. Encoding guide: oxygen_skills_get({name:"oxygen-tables"}) or oxygen skills get oxygen-tables --json.',
|
|
7
|
+
oneOf: [
|
|
8
|
+
obj({ mode: enumOf(["row_ids"]), row_ids: rowIds() }, { required: ["mode", "row_ids"] }),
|
|
9
|
+
obj({
|
|
10
|
+
mode: enumOf(["all_matching"]),
|
|
11
|
+
filter: str({ description: "Serialized browser filter tree." }),
|
|
12
|
+
search: str(),
|
|
13
|
+
sort: str(),
|
|
14
|
+
range: obj({ offset: int({ minimum: 0 }), limit: int({ minimum: 1 }) }, { required: ["offset", "limit"] }),
|
|
15
|
+
excluded_row_ids: arr(str({ minLength: 1 })),
|
|
16
|
+
crm_object: str(),
|
|
17
|
+
}, { required: ["mode"] }),
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
export const TABLE_ROW_REMOVAL_FIELDS = { row_ids: rowIds(), selection };
|
|
21
|
+
export const TABLE_ROW_REMOVAL_CONTRACT = obj(TABLE_ROW_REMOVAL_FIELDS);
|
|
@@ -137,7 +137,7 @@ const ICP_BODY = `# ICP — ideal customer profile
|
|
|
137
137
|
`;
|
|
138
138
|
const POSITIONING_BODY = `# Positioning
|
|
139
139
|
|
|
140
|
-
> **Stub — fill me.** Non-canonical draft. Fill it, then
|
|
140
|
+
> **Stub — fill me.** Non-canonical draft. Fill it, then pin it (\`oxygen knowledge page pin positioning\`): the pin files a proposal a human approves, because canonical positioning grounds live copy. See [[conventions]].
|
|
141
141
|
|
|
142
142
|
## For whom
|
|
143
143
|
- Our [[icp]], and the moment they buy.
|
|
@@ -251,7 +251,7 @@ Banned hedges: just, hope this finds you well, wanted to reach out, circling bac
|
|
|
251
251
|
- **We would not send:** the same idea, wrong — and why it fails
|
|
252
252
|
|
|
253
253
|
## Canonical
|
|
254
|
-
Not pinned. Pinning it (\`oxygen knowledge page pin voice
|
|
254
|
+
Not pinned. Pinning it (\`oxygen knowledge page pin voice\`) files a proposal a human approves — canonical voice grounds live copy. Claims are [[brand]]; what we do is [[positioning]].
|
|
255
255
|
`;
|
|
256
256
|
const BRAND_BODY = `# Brand
|
|
257
257
|
|
|
@@ -31,3 +31,51 @@ export declare function linkedInQuotaBackoffUntil(error: unknown, now: Date): Da
|
|
|
31
31
|
* `sender_accounts.metadata` jsonb — expires; 0 when it is not parked.
|
|
32
32
|
*/
|
|
33
33
|
export declare function linkedInQuotaParkedUntil(metadata: unknown, key: string): number;
|
|
34
|
+
/**
|
|
35
|
+
* Where a sender's inbox sync is parked until, quota or failure backoff alike,
|
|
36
|
+
* in the freeform `sender_accounts.metadata` jsonb.
|
|
37
|
+
*/
|
|
38
|
+
export declare const INBOX_SYNC_BACKOFF_KEY = "inbox_sync_backoff_until";
|
|
39
|
+
/** The sender metadata key holding the latest non-quota inbox sync failure. */
|
|
40
|
+
export declare const INBOX_SYNC_FAILURE_KEY = "inbox_sync_failure";
|
|
41
|
+
export type InboxSyncFailureClass = "quota" | "authentication" | "transient" | "other";
|
|
42
|
+
export type InboxSyncFailureRecord = {
|
|
43
|
+
class: Exclude<InboxSyncFailureClass, "quota">;
|
|
44
|
+
code: string | null;
|
|
45
|
+
status: number | null;
|
|
46
|
+
/** Consecutive failed syncs, reset by a successful one. */
|
|
47
|
+
consecutive: number;
|
|
48
|
+
first_at: string;
|
|
49
|
+
last_at: string;
|
|
50
|
+
retry_at: string;
|
|
51
|
+
};
|
|
52
|
+
/** First park after a non-quota failure; it doubles per consecutive failure. */
|
|
53
|
+
export declare const INBOX_SYNC_FAILURE_BASE_BACKOFF_MS: number;
|
|
54
|
+
/** No non-quota failure parks a sender longer than this. */
|
|
55
|
+
export declare const INBOX_SYNC_FAILURE_MAX_BACKOFF_MS: number;
|
|
56
|
+
/** Quota, authentication (401/403), transient (capacity, 429, 5xx, retryable), else other. */
|
|
57
|
+
export declare function classifyInboxSyncFailure(error: unknown): InboxSyncFailureClass;
|
|
58
|
+
/** The latest non-quota inbox sync failure recorded on a sender, or null. */
|
|
59
|
+
export declare function readInboxSyncFailure(metadata: unknown): InboxSyncFailureRecord | null;
|
|
60
|
+
/**
|
|
61
|
+
* The park and record for a whole-sender inbox sync failure that is not a quota
|
|
62
|
+
* denial: 5 min, doubling per consecutive failure, capped at 2 h. Null for a
|
|
63
|
+
* quota denial, which keeps its own park (linkedInQuotaBackoffUntil).
|
|
64
|
+
*/
|
|
65
|
+
export declare function inboxSyncFailureBackoff(error: unknown, metadata: unknown, now: Date): {
|
|
66
|
+
until: Date;
|
|
67
|
+
record: InboxSyncFailureRecord;
|
|
68
|
+
} | null;
|
|
69
|
+
/**
|
|
70
|
+
* A sender's whole-inbox sync state for health reads: the last successful sync,
|
|
71
|
+
* the latest failure that was not a quota denial (cleared by the next success),
|
|
72
|
+
* and until when the background sync leaves the sender alone.
|
|
73
|
+
*/
|
|
74
|
+
export declare function inboxSyncHealth(sender: {
|
|
75
|
+
lastInboxSyncAt: Date | null;
|
|
76
|
+
metadata: unknown;
|
|
77
|
+
}, now?: Date): {
|
|
78
|
+
last_synced_at: string | null;
|
|
79
|
+
background_sync_paused_until: string | null;
|
|
80
|
+
last_failure: InboxSyncFailureRecord | null;
|
|
81
|
+
};
|
|
@@ -70,3 +70,97 @@ export function linkedInQuotaParkedUntil(metadata, key) {
|
|
|
70
70
|
const ms = Date.parse(raw);
|
|
71
71
|
return Number.isNaN(ms) ? 0 : ms;
|
|
72
72
|
}
|
|
73
|
+
// ===== Inbox sync failures that are not quota denials (OXP-1.11.72) =====
|
|
74
|
+
//
|
|
75
|
+
// A quota denial parks the sender until its budget resets (above). Every other
|
|
76
|
+
// whole-sender inbox sync failure used to leave the sender eligible on the next
|
|
77
|
+
// backstop pass: in the week to 2026-10-07 production retried one sender 45
|
|
78
|
+
// times through "Managed provider capacity is busy." and 24 senders every few
|
|
79
|
+
// minutes through a 27-minute LinkedIn 5xx incident, and nothing on the sender
|
|
80
|
+
// said so. These failures now park on a bounded, doubling backoff and leave an
|
|
81
|
+
// inspectable record that the next successful sync clears.
|
|
82
|
+
/**
|
|
83
|
+
* Where a sender's inbox sync is parked until, quota or failure backoff alike,
|
|
84
|
+
* in the freeform `sender_accounts.metadata` jsonb.
|
|
85
|
+
*/
|
|
86
|
+
export const INBOX_SYNC_BACKOFF_KEY = "inbox_sync_backoff_until";
|
|
87
|
+
/** The sender metadata key holding the latest non-quota inbox sync failure. */
|
|
88
|
+
export const INBOX_SYNC_FAILURE_KEY = "inbox_sync_failure";
|
|
89
|
+
// Refusals raised before the read leaves the process that clear on their own:
|
|
90
|
+
// the managed-provider limiter after waiting out its window, a runtime with no
|
|
91
|
+
// quota enforcer registered, and an unsizable worker fleet. The same set the
|
|
92
|
+
// LinkedIn ingestion drip defers on.
|
|
93
|
+
const TRANSIENT_CODES = new Set([
|
|
94
|
+
"rate_limit_exceeded",
|
|
95
|
+
"linkedin_quota_enforcement_unavailable",
|
|
96
|
+
"worker_fleet_discovery_unavailable",
|
|
97
|
+
]);
|
|
98
|
+
/** First park after a non-quota failure; it doubles per consecutive failure. */
|
|
99
|
+
export const INBOX_SYNC_FAILURE_BASE_BACKOFF_MS = 5 * 60 * 1000;
|
|
100
|
+
/** No non-quota failure parks a sender longer than this. */
|
|
101
|
+
export const INBOX_SYNC_FAILURE_MAX_BACKOFF_MS = 2 * 60 * 60 * 1000;
|
|
102
|
+
function errorStatus(error) {
|
|
103
|
+
const status = isRecord(error) ? error.status : undefined;
|
|
104
|
+
return typeof status === "number" && Number.isFinite(status) ? status : null;
|
|
105
|
+
}
|
|
106
|
+
/** Quota, authentication (401/403), transient (capacity, 429, 5xx, retryable), else other. */
|
|
107
|
+
export function classifyInboxSyncFailure(error) {
|
|
108
|
+
if (isLinkedInQuotaDenial(error))
|
|
109
|
+
return "quota";
|
|
110
|
+
const status = errorStatus(error);
|
|
111
|
+
if (status === 401 || status === 403)
|
|
112
|
+
return "authentication";
|
|
113
|
+
if (error instanceof OxygenError && TRANSIENT_CODES.has(error.code))
|
|
114
|
+
return "transient";
|
|
115
|
+
if (status === 429 || (status !== null && status >= 500))
|
|
116
|
+
return "transient";
|
|
117
|
+
if (isRecord(error) && error.retryable === true)
|
|
118
|
+
return "transient";
|
|
119
|
+
return "other";
|
|
120
|
+
}
|
|
121
|
+
/** The latest non-quota inbox sync failure recorded on a sender, or null. */
|
|
122
|
+
export function readInboxSyncFailure(metadata) {
|
|
123
|
+
const raw = isRecord(metadata) ? metadata[INBOX_SYNC_FAILURE_KEY] : undefined;
|
|
124
|
+
if (!isRecord(raw) || typeof raw.consecutive !== "number" || raw.consecutive < 1 || typeof raw.first_at !== "string")
|
|
125
|
+
return null;
|
|
126
|
+
return raw;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The park and record for a whole-sender inbox sync failure that is not a quota
|
|
130
|
+
* denial: 5 min, doubling per consecutive failure, capped at 2 h. Null for a
|
|
131
|
+
* quota denial, which keeps its own park (linkedInQuotaBackoffUntil).
|
|
132
|
+
*/
|
|
133
|
+
export function inboxSyncFailureBackoff(error, metadata, now) {
|
|
134
|
+
const failureClass = classifyInboxSyncFailure(error);
|
|
135
|
+
if (failureClass === "quota")
|
|
136
|
+
return null;
|
|
137
|
+
const previous = readInboxSyncFailure(metadata);
|
|
138
|
+
const consecutive = (previous?.consecutive ?? 0) + 1;
|
|
139
|
+
const delay = Math.min(INBOX_SYNC_FAILURE_BASE_BACKOFF_MS * 2 ** Math.min(consecutive - 1, 16), INBOX_SYNC_FAILURE_MAX_BACKOFF_MS);
|
|
140
|
+
const until = new Date(now.getTime() + delay);
|
|
141
|
+
return {
|
|
142
|
+
until,
|
|
143
|
+
record: {
|
|
144
|
+
class: failureClass,
|
|
145
|
+
code: error instanceof OxygenError ? error.code : null,
|
|
146
|
+
status: errorStatus(error),
|
|
147
|
+
consecutive,
|
|
148
|
+
first_at: previous?.first_at ?? now.toISOString(),
|
|
149
|
+
last_at: now.toISOString(),
|
|
150
|
+
retry_at: until.toISOString(),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A sender's whole-inbox sync state for health reads: the last successful sync,
|
|
156
|
+
* the latest failure that was not a quota denial (cleared by the next success),
|
|
157
|
+
* and until when the background sync leaves the sender alone.
|
|
158
|
+
*/
|
|
159
|
+
export function inboxSyncHealth(sender, now = new Date()) {
|
|
160
|
+
const parkedUntil = linkedInQuotaParkedUntil(sender.metadata, INBOX_SYNC_BACKOFF_KEY);
|
|
161
|
+
return {
|
|
162
|
+
last_synced_at: sender.lastInboxSyncAt ? sender.lastInboxSyncAt.toISOString() : null,
|
|
163
|
+
background_sync_paused_until: parkedUntil > now.getTime() ? new Date(parkedUntil).toISOString() : null,
|
|
164
|
+
last_failure: readInboxSyncFailure(sender.metadata),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -101,8 +101,9 @@ export const MANAGED_BALANCE_FLOORS = {
|
|
|
101
101
|
// to us, which sits at 0.00 in the normal case (it did on 2026-09-07) and goes
|
|
102
102
|
// negative after a refund. A floor here would page daily about nothing.
|
|
103
103
|
stripe: null,
|
|
104
|
-
//
|
|
105
|
-
//
|
|
104
|
+
// No balance concept at all. Neither is probed any more (Vercel left the stack
|
|
105
|
+
// on 2026-10-06); null keeps a historical row from ever reading as a low wallet
|
|
106
|
+
// under the generic default floor.
|
|
106
107
|
vercel: null,
|
|
107
108
|
neon: null,
|
|
108
109
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OXYGEN_BUILD_VERSION = "1.1011.
|
|
1
|
+
export declare const OXYGEN_BUILD_VERSION = "1.1011.1091";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
|
|
2
|
-
export const OXYGEN_BUILD_VERSION = "1.1011.
|
|
2
|
+
export const OXYGEN_BUILD_VERSION = "1.1011.1091";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WhatsApp sender-limit overlay: the keys a WhatsApp account accepts and
|
|
3
|
+
* their defaults and hard maximums. One owner for the quota enforcer
|
|
4
|
+
* (packages/integrations/src/whatsapp-quota.ts), the limits route, the MCP and
|
|
5
|
+
* CLI surfaces and the web form (OXP-11.3.19): the MCP tool once advertised a
|
|
6
|
+
* chats_reads_per_day key the route silently dropped and maxima (400 reads,
|
|
7
|
+
* 7200 s spacing, 3600 s jitter) the quota never allowed.
|
|
8
|
+
*
|
|
9
|
+
* Everything stays deliberately small: WhatsApp Web bans on volume.
|
|
10
|
+
*/
|
|
11
|
+
export declare const WHATSAPP_SENDER_LIMIT_KEYS: readonly ["messages_per_day", "likes_per_day", "messages_reads_per_day", "relations_reads_per_day", "total_actions_per_day", "total_reads_per_day", "min_action_spacing_seconds", "action_spacing_jitter_seconds"];
|
|
12
|
+
export type WhatsAppSenderLimitKey = (typeof WHATSAPP_SENDER_LIMIT_KEYS)[number];
|
|
13
|
+
export declare const WHATSAPP_SENDER_LIMIT_DEFAULTS: {
|
|
14
|
+
readonly messages_per_day: 20;
|
|
15
|
+
readonly likes_per_day: 20;
|
|
16
|
+
readonly messages_reads_per_day: 200;
|
|
17
|
+
readonly relations_reads_per_day: 40;
|
|
18
|
+
readonly total_actions_per_day: 40;
|
|
19
|
+
readonly total_reads_per_day: 240;
|
|
20
|
+
readonly min_action_spacing_seconds: 600;
|
|
21
|
+
readonly action_spacing_jitter_seconds: 300;
|
|
22
|
+
};
|
|
23
|
+
export declare const WHATSAPP_SENDER_LIMIT_MAXIMUMS: {
|
|
24
|
+
readonly messages_per_day: 40;
|
|
25
|
+
readonly likes_per_day: 40;
|
|
26
|
+
readonly messages_reads_per_day: 600;
|
|
27
|
+
readonly relations_reads_per_day: 100;
|
|
28
|
+
readonly total_actions_per_day: 80;
|
|
29
|
+
readonly total_reads_per_day: 700;
|
|
30
|
+
readonly min_action_spacing_seconds: 3600;
|
|
31
|
+
readonly action_spacing_jitter_seconds: 1800;
|
|
32
|
+
};
|
|
33
|
+
/** What each WhatsApp limit counts, for MCP and CLI help. */
|
|
34
|
+
export declare const WHATSAPP_SENDER_LIMIT_NOTES: Record<WhatsAppSenderLimitKey, string>;
|
|
35
|
+
/**
|
|
36
|
+
* The WhatsApp limits an account runs at: each stored override, floored and
|
|
37
|
+
* capped at its WhatsApp maximum, else the WhatsApp default. Read from the
|
|
38
|
+
* overrides alone: a sender's resolved `limits` start from the LinkedIn
|
|
39
|
+
* defaults (300 s spacing, 150 actions, 1000 reads a day), which a WhatsApp
|
|
40
|
+
* number must never inherit (OXP-11.3.21).
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveWhatsAppSenderLimits(overrides: Partial<Record<WhatsAppSenderLimitKey, unknown>> | null | undefined): Record<WhatsAppSenderLimitKey, number>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WhatsApp sender-limit overlay: the keys a WhatsApp account accepts and
|
|
3
|
+
* their defaults and hard maximums. One owner for the quota enforcer
|
|
4
|
+
* (packages/integrations/src/whatsapp-quota.ts), the limits route, the MCP and
|
|
5
|
+
* CLI surfaces and the web form (OXP-11.3.19): the MCP tool once advertised a
|
|
6
|
+
* chats_reads_per_day key the route silently dropped and maxima (400 reads,
|
|
7
|
+
* 7200 s spacing, 3600 s jitter) the quota never allowed.
|
|
8
|
+
*
|
|
9
|
+
* Everything stays deliberately small: WhatsApp Web bans on volume.
|
|
10
|
+
*/
|
|
11
|
+
export const WHATSAPP_SENDER_LIMIT_KEYS = [
|
|
12
|
+
"messages_per_day",
|
|
13
|
+
"likes_per_day",
|
|
14
|
+
"messages_reads_per_day",
|
|
15
|
+
"relations_reads_per_day",
|
|
16
|
+
"total_actions_per_day",
|
|
17
|
+
"total_reads_per_day",
|
|
18
|
+
"min_action_spacing_seconds",
|
|
19
|
+
"action_spacing_jitter_seconds",
|
|
20
|
+
];
|
|
21
|
+
export const WHATSAPP_SENDER_LIMIT_DEFAULTS = {
|
|
22
|
+
messages_per_day: 20,
|
|
23
|
+
likes_per_day: 20,
|
|
24
|
+
messages_reads_per_day: 200,
|
|
25
|
+
relations_reads_per_day: 40,
|
|
26
|
+
total_actions_per_day: 40,
|
|
27
|
+
total_reads_per_day: 240,
|
|
28
|
+
min_action_spacing_seconds: 600,
|
|
29
|
+
action_spacing_jitter_seconds: 300,
|
|
30
|
+
};
|
|
31
|
+
export const WHATSAPP_SENDER_LIMIT_MAXIMUMS = {
|
|
32
|
+
messages_per_day: 40,
|
|
33
|
+
likes_per_day: 40,
|
|
34
|
+
messages_reads_per_day: 600,
|
|
35
|
+
relations_reads_per_day: 100,
|
|
36
|
+
total_actions_per_day: 80,
|
|
37
|
+
total_reads_per_day: 700,
|
|
38
|
+
min_action_spacing_seconds: 3600,
|
|
39
|
+
action_spacing_jitter_seconds: 1800,
|
|
40
|
+
};
|
|
41
|
+
/** What each WhatsApp limit counts, for MCP and CLI help. */
|
|
42
|
+
export const WHATSAPP_SENDER_LIMIT_NOTES = {
|
|
43
|
+
messages_per_day: "Daily WhatsApp messages across direct warm traffic and opted-in Sequence actions; the warm-up ceiling (5, 10, 15, 20 by account age) wins when lower.",
|
|
44
|
+
likes_per_day: "Daily reactions.",
|
|
45
|
+
messages_reads_per_day: "Chat and message-history reads.",
|
|
46
|
+
relations_reads_per_day: "Chat-list reads.",
|
|
47
|
+
total_actions_per_day: "All sends: messages, reactions and edits.",
|
|
48
|
+
total_reads_per_day: "All reads.",
|
|
49
|
+
min_action_spacing_seconds: "Minimum seconds between WhatsApp actions.",
|
|
50
|
+
action_spacing_jitter_seconds: "Random seconds added on top of the spacing floor.",
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The WhatsApp limits an account runs at: each stored override, floored and
|
|
54
|
+
* capped at its WhatsApp maximum, else the WhatsApp default. Read from the
|
|
55
|
+
* overrides alone: a sender's resolved `limits` start from the LinkedIn
|
|
56
|
+
* defaults (300 s spacing, 150 actions, 1000 reads a day), which a WhatsApp
|
|
57
|
+
* number must never inherit (OXP-11.3.21).
|
|
58
|
+
*/
|
|
59
|
+
export function resolveWhatsAppSenderLimits(overrides) {
|
|
60
|
+
const limits = { ...WHATSAPP_SENDER_LIMIT_DEFAULTS };
|
|
61
|
+
for (const key of WHATSAPP_SENDER_LIMIT_KEYS) {
|
|
62
|
+
const value = overrides?.[key];
|
|
63
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
64
|
+
limits[key] = Math.min(Math.floor(value), WHATSAPP_SENDER_LIMIT_MAXIMUMS[key]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return limits;
|
|
68
|
+
}
|
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
export declare const WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS: readonly ["consecutive_failure_count", "consecutive_capacity_failure_count", "last_failure_code", "last_failure_at", "auto_paused_at", "auto_pause_reason"];
|
|
2
2
|
export declare function clearWorkflowTriggerAutoPauseMetadata(metadata: Record<string, unknown>): Record<string, unknown>;
|
|
3
|
+
export type WorkflowAutomationPauseCode = "automation_actions_exceeded" | "insufficient_credits";
|
|
4
|
+
export type TriggerAutomationPause = {
|
|
5
|
+
reason: string;
|
|
6
|
+
reasonCode?: WorkflowAutomationPauseCode | null;
|
|
7
|
+
blockedAt: string | null;
|
|
8
|
+
resumesAt: string | null;
|
|
9
|
+
};
|
|
10
|
+
/** Decode typed refusals and the two controlled messages written before codes were stored. */
|
|
11
|
+
export declare function readWorkflowAutomationPauseCode(code: unknown, reason: unknown): WorkflowAutomationPauseCode | null;
|
|
12
|
+
/** Customer copy never echoes arbitrary persisted error text or invents a credit refill date. */
|
|
13
|
+
export declare function workflowAutomationPauseMessage(pause: {
|
|
14
|
+
reason?: unknown;
|
|
15
|
+
reasonCode?: unknown;
|
|
16
|
+
resumesAt?: unknown;
|
|
17
|
+
}): string;
|
|
@@ -13,3 +13,36 @@ const WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEY_SET = new Set(WORKFLOW_TRIGGER_AU
|
|
|
13
13
|
export function clearWorkflowTriggerAutoPauseMetadata(metadata) {
|
|
14
14
|
return Object.fromEntries(Object.entries(metadata).filter(([key]) => !WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEY_SET.has(key)));
|
|
15
15
|
}
|
|
16
|
+
/** Decode typed refusals and the two controlled messages written before codes were stored. */
|
|
17
|
+
export function readWorkflowAutomationPauseCode(code, reason) {
|
|
18
|
+
// An older scheduler can replace the reason without replacing a newer
|
|
19
|
+
// worker's code. Only a controlled legacy reason may override that code.
|
|
20
|
+
const text = typeof reason === "string" ? reason.trim() : "";
|
|
21
|
+
if (text === "automation_actions_exceeded" || text === "This workspace has reached its monthly automation action limit.") {
|
|
22
|
+
return "automation_actions_exceeded";
|
|
23
|
+
}
|
|
24
|
+
if (text === "insufficient_credits" || text.startsWith("Not enough credits for this automation work (")) {
|
|
25
|
+
return "insufficient_credits";
|
|
26
|
+
}
|
|
27
|
+
if (code === "automation_actions_exceeded" || code === "insufficient_credits")
|
|
28
|
+
return code;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** Customer copy never echoes arbitrary persisted error text or invents a credit refill date. */
|
|
32
|
+
export function workflowAutomationPauseMessage(pause) {
|
|
33
|
+
const code = readWorkflowAutomationPauseCode(pause.reasonCode, pause.reason);
|
|
34
|
+
if (code === "insufficient_credits") {
|
|
35
|
+
return "Not enough credits for the next scheduled run. Add credits before the next scheduled attempt.";
|
|
36
|
+
}
|
|
37
|
+
if (code === "automation_actions_exceeded") {
|
|
38
|
+
const resume = typeof pause.resumesAt === "string" ? new Date(pause.resumesAt) : null;
|
|
39
|
+
if (resume && !Number.isNaN(resume.getTime())) {
|
|
40
|
+
const day = new Intl.DateTimeFormat("en-US", {
|
|
41
|
+
month: "short", day: "numeric", year: "numeric", timeZone: "UTC",
|
|
42
|
+
}).format(resume);
|
|
43
|
+
return `The monthly automation-action allowance is exhausted. Cron runs resume on ${day} when the window resets.`;
|
|
44
|
+
}
|
|
45
|
+
return "The monthly automation-action allowance is exhausted. Check billing before the next scheduled attempt.";
|
|
46
|
+
}
|
|
47
|
+
return "Automation admission was refused. Check billing before the next scheduled attempt.";
|
|
48
|
+
}
|
|
@@ -11,11 +11,6 @@
|
|
|
11
11
|
"import": "./dist/knowledge-vault-markdown.js",
|
|
12
12
|
"default": "./dist/knowledge-vault-markdown.js"
|
|
13
13
|
},
|
|
14
|
-
"./vercel-sandbox-fetch": {
|
|
15
|
-
"types": "./dist/vercel-sandbox-fetch.d.ts",
|
|
16
|
-
"import": "./dist/vercel-sandbox-fetch.js",
|
|
17
|
-
"default": "./dist/vercel-sandbox-fetch.js"
|
|
18
|
-
},
|
|
19
14
|
".": {
|
|
20
15
|
"types": "./dist/index.d.ts",
|
|
21
16
|
"import": "./dist/index.js",
|
|
@@ -291,6 +286,11 @@
|
|
|
291
286
|
"types": "./dist/provider-balance-signal.d.ts",
|
|
292
287
|
"import": "./dist/provider-balance-signal.js"
|
|
293
288
|
},
|
|
289
|
+
"./attempt-provenance": {
|
|
290
|
+
"types": "./dist/attempt-provenance.d.ts",
|
|
291
|
+
"import": "./dist/attempt-provenance.js",
|
|
292
|
+
"default": "./dist/attempt-provenance.js"
|
|
293
|
+
},
|
|
294
294
|
"./copilot-skills.generated": null
|
|
295
295
|
},
|
|
296
296
|
"dependencies": {}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
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
|
|
3
|
-
// live
|
|
4
|
-
// inside a run are free (decision 6.2),
|
|
2
|
+
// worker scheduler's admission gate. The unit is the workflow STEP: every
|
|
3
|
+
// billable node a live graph executes bills one (repricing 2026-09, decision
|
|
4
|
+
// 6.1). Control nodes and rows written inside a run are free (decision 6.2),
|
|
5
|
+
// so a row-writing step costs one step
|
|
5
6
|
// however many rows it writes. The per-run count derived from a manifest is a
|
|
6
7
|
// FLOOR, never a projection: recipe code emits one step per checkpoint (loops
|
|
7
8
|
// multiply) and a retried attempt bills again. Surfaces present these numbers as
|
|
@@ -167,7 +168,8 @@ function estimateWorkflowGraphActionsFloor(graph) {
|
|
|
167
168
|
}
|
|
168
169
|
// Nested caps multiply, and two large ones overflow Number's exact range;
|
|
169
170
|
// clamping keeps every downstream product (runs x floor) a finite number.
|
|
170
|
-
|
|
171
|
+
// Free control-only graphs have no step charge, including in previews.
|
|
172
|
+
return Math.min(floor, Number.MAX_SAFE_INTEGER);
|
|
171
173
|
}
|
|
172
174
|
export function estimateWorkflowAutomationUsage(manifest, cron) {
|
|
173
175
|
if (!isRecord(manifest))
|
|
@@ -320,7 +322,9 @@ export function assessCronCadenceViability(input) {
|
|
|
320
322
|
return null;
|
|
321
323
|
const actionsPerRunFloor = estimateWorkflowRunAutomationActionsFloor(input.manifest);
|
|
322
324
|
const floorActionsPer30Days = scheduledRunsPer30Days * actionsPerRunFloor;
|
|
323
|
-
const runsAllowedPer30Days =
|
|
325
|
+
const runsAllowedPer30Days = actionsPerRunFloor === 0
|
|
326
|
+
? MINUTES_PER_30_DAYS
|
|
327
|
+
: Math.floor(includedActions / actionsPerRunFloor);
|
|
324
328
|
return {
|
|
325
329
|
scheduledRunsPer30Days,
|
|
326
330
|
actionsPerRunFloor,
|
package/package.json
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/** The Sandbox SDK retries transport failures and 429/5xx responses even for
|
|
2
|
-
* POSTs. Visual execution cannot replay an ambiguously accepted mutation.
|
|
3
|
-
* The SDK recognizes AbortError as non-retryable; outcomeUnknown deliberately
|
|
4
|
-
* distinguishes this transport fence from ordinary caller cancellation. */
|
|
5
|
-
export declare class SandboxMutationOutcomeUnknownError extends Error {
|
|
6
|
-
readonly outcomeUnknown = true;
|
|
7
|
-
constructor(options?: ErrorOptions);
|
|
8
|
-
}
|
|
9
|
-
export declare function createSandboxNonReplayFetch(rawFetch?: typeof globalThis.fetch): typeof globalThis.fetch;
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** The Sandbox SDK retries transport failures and 429/5xx responses even for
|
|
2
|
-
* POSTs. Visual execution cannot replay an ambiguously accepted mutation.
|
|
3
|
-
* The SDK recognizes AbortError as non-retryable; outcomeUnknown deliberately
|
|
4
|
-
* distinguishes this transport fence from ordinary caller cancellation. */
|
|
5
|
-
export class SandboxMutationOutcomeUnknownError extends Error {
|
|
6
|
-
outcomeUnknown = true;
|
|
7
|
-
constructor(options) {
|
|
8
|
-
super("Sandbox mutation acceptance requires reconciliation.", options);
|
|
9
|
-
this.name = "AbortError";
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export function createSandboxNonReplayFetch(rawFetch = globalThis.fetch) {
|
|
13
|
-
return async (input, init) => {
|
|
14
|
-
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
15
|
-
if (["GET", "HEAD", "OPTIONS"].includes(method))
|
|
16
|
-
return rawFetch(input, init);
|
|
17
|
-
try {
|
|
18
|
-
// Redirects must not resubmit a mutation either. No request URL, body or
|
|
19
|
-
// authentication header is copied into the error or telemetry.
|
|
20
|
-
const response = await rawFetch(input, { ...init, redirect: "error" });
|
|
21
|
-
if (response.status === 429 || response.status >= 500) {
|
|
22
|
-
void response.body?.cancel().catch(() => { });
|
|
23
|
-
throw new SandboxMutationOutcomeUnknownError();
|
|
24
|
-
}
|
|
25
|
-
return response;
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
if (error instanceof SandboxMutationOutcomeUnknownError)
|
|
29
|
-
throw error;
|
|
30
|
-
throw new SandboxMutationOutcomeUnknownError({ cause: error });
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|