@oxygen-agent/cli 1.813.1 → 1.830.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/http-client.js +1 -1
- package/dist/index.js +134 -50
- package/dist/runtime.js +2 -2
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +22 -3
- package/node_modules/@oxygen/shared/dist/sql-error.d.ts +8 -1
- package/node_modules/@oxygen/shared/dist/sql-error.js +11 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/http-client.js
CHANGED
|
@@ -205,7 +205,7 @@ function warnIfCliIsOlderThanApi(serverVersion, endpoint) {
|
|
|
205
205
|
return;
|
|
206
206
|
staleCliWarningVersions.add(warningKey);
|
|
207
207
|
process.stderr.write(`[${guidance.binaryName}] CLI version ${OXYGEN_VERSION} is older than Oxygen API version ${serverVersion}. `
|
|
208
|
-
+
|
|
208
|
+
+ `This compatible skew is non-blocking; the command can proceed. ${guidance.warningInstruction}\n`);
|
|
209
209
|
}
|
|
210
210
|
function isPatchOnlyVersionSkew(serverVersion, clientVersion) {
|
|
211
211
|
const server = parseSemver(serverVersion);
|
package/dist/index.js
CHANGED
|
@@ -247,6 +247,7 @@ const OXYGEN_WORDMARK = [
|
|
|
247
247
|
" \\___/ /_/\\_\\ |_| \\____|_____|_| \\_|",
|
|
248
248
|
].join("\n");
|
|
249
249
|
const LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD = 500;
|
|
250
|
+
const SAFE_IMPORT_WRITE_BATCH_SIZE = 500;
|
|
250
251
|
// The only numbers on `tables import --help` used to be --batch-size (a
|
|
251
252
|
// per-request chunk) and the background threshold, so users read 500 as a
|
|
252
253
|
// per-file cap. Echo the real PLAN_LIMITS ceilings instead of restating them.
|
|
@@ -4715,7 +4716,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4715
4716
|
.command("tables")
|
|
4716
4717
|
.description("Tenant workspace table commands.")
|
|
4717
4718
|
.addCommand(new Command("create")
|
|
4718
|
-
.description("Create a real Postgres-backed workspace table.")
|
|
4719
|
+
.description("Create a real Postgres-backed workspace table. Free — 0 Oxygen credits. To create a table and import a file in one step, use `tables import --create <name>`.")
|
|
4719
4720
|
.argument("<name>", "Display name for the table.")
|
|
4720
4721
|
.requiredOption("--columns-json <json>", 'JSON array of column definitions, e.g. [{"key":"name","label":"Name","dataType":"text"},{"key":"domain","label":"Domain","dataType":"text"}]. Inspect an existing shape with `oxygen tables describe <table>`.')
|
|
4721
4722
|
.option("--project <project>", "Project id or slug. Defaults to General.")
|
|
@@ -4750,6 +4751,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4750
4751
|
.description("Insert rows into a workspace table.")
|
|
4751
4752
|
.argument("<table>", "Table id or slug.")
|
|
4752
4753
|
.requiredOption("--rows-json <json>", 'JSON array of row objects keyed by column key, e.g. [{"name":"Acme","domain":"acme.test"},{"name":"Globex","domain":"globex.test"}].')
|
|
4754
|
+
.option("--request-id <id>", "Stable retry key. Reuse it after a timeout to return the committed write without duplicating rows.")
|
|
4753
4755
|
.option("--json", "Print a JSON envelope.")
|
|
4754
4756
|
.action(async (table, options) => {
|
|
4755
4757
|
await handleAsyncAction("tables insert", options, () => requestOxygen("/api/cli/tables/rows", {
|
|
@@ -4757,6 +4759,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4757
4759
|
body: {
|
|
4758
4760
|
table,
|
|
4759
4761
|
rows: parseJsonArray(options.rowsJson),
|
|
4762
|
+
...(readOption(options.requestId) ? { request_id: readOption(options.requestId) } : {}),
|
|
4760
4763
|
},
|
|
4761
4764
|
}));
|
|
4762
4765
|
}))
|
|
@@ -4832,7 +4835,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4832
4835
|
}));
|
|
4833
4836
|
}))
|
|
4834
4837
|
.addCommand(new Command("import")
|
|
4835
|
-
.description(`Import JSON, JSONL, CSV, or XLSX rows into a workspace table. Files over ${LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD} rows load durably in the background: the command returns a queued envelope and rows keep landing after it exits, so wait with the printed 'table-ingestions wait <id>' before reading row counts. ${IMPORT_FILE_LIMIT_HELP}`)
|
|
4838
|
+
.description(`Import JSON, JSONL, CSV, or XLSX rows into a workspace table. The table write is free — 0 Oxygen credits. Files over ${LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD} rows load durably in the background: the command returns a queued envelope and rows keep landing after it exits, so wait with the printed 'table-ingestions wait <id>' before reading row counts. ${IMPORT_FILE_LIMIT_HELP}`)
|
|
4836
4839
|
.argument("[table]", "Table id or slug. Omit when using --create.")
|
|
4837
4840
|
.requiredOption("--file <path>", "Input file path.")
|
|
4838
4841
|
.option("--format <format>", "json, jsonl, csv, or xlsx. Defaults from file extension.")
|
|
@@ -4840,7 +4843,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4840
4843
|
.option("--create <name>", "Create a new table with columns inferred from the file before importing.")
|
|
4841
4844
|
.option("--project <project>", "Project id or slug for --create. Defaults to General.")
|
|
4842
4845
|
.option("--upsert-key <key>", "Column key used to upsert instead of inserting.")
|
|
4843
|
-
.option("--batch-size <n>", "
|
|
4846
|
+
.option("--batch-size <n>", "Requested rows per import chunk, not a limit on the file. Defaults to 500; values up to 5000 are accepted, but Oxygen splits writes to at most 500 rows (and smaller for wide rows) so they fit request and worker lease budgets.")
|
|
4844
4847
|
.option("--background", "Enqueue durable import chunks for the background worker.")
|
|
4845
4848
|
.option("--sync", `Force foreground import even for files above ${LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD} rows. That threshold picks foreground vs background; it does not cap how many rows a file may hold.`)
|
|
4846
4849
|
.option("--max-concurrency <n>", "Maximum concurrent import chunks for background mode. Defaults to 5.")
|
|
@@ -4874,7 +4877,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4874
4877
|
.option("--project <project>", "Project id or slug for the new table. Defaults to General. Ignored with --into.")
|
|
4875
4878
|
.option("--into <table>", "Resume into an existing table (id or slug) instead of creating a new one. Requires --key. Use the table id printed by the failed run.")
|
|
4876
4879
|
.option("--key <column>", "Upsert key column. When set, rows are matched by this key instead of blindly inserted, so re-running the import is idempotent (no duplicates). Required with --into.")
|
|
4877
|
-
.option("--batch-size <n>", "
|
|
4880
|
+
.option("--batch-size <n>", "Requested rows per write group. Defaults to 500; values up to 5000 are accepted and split into safe writes of at most 500 rows.")
|
|
4878
4881
|
.option("--json", "Print a JSON envelope.")
|
|
4879
4882
|
.action(async (options) => {
|
|
4880
4883
|
await handleAsyncAction("tables import-bundle", options, () => importTableBundle(options));
|
|
@@ -4909,6 +4912,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4909
4912
|
.option("--fields <columns>", "Comma-separated column keys or ids to include.")
|
|
4910
4913
|
.option("--filter-json <json>", "Legacy filter object or array, e.g. '{\"column\":\"mobile_phone_e164\",\"op\":\"is_null\"}'. Mutually exclusive with --filter-tree-json/--sort-json.")
|
|
4911
4914
|
.option("--filter-tree-json <json>", "Airtable-style filter group, e.g. '{\"type\":\"group\",\"conjunction\":\"and\",\"children\":[{\"type\":\"leaf\",\"columnKey\":\"stage\",\"operator\":\"is\",\"value\":\"won\"}]}'. Mutually exclusive with --filter-json.")
|
|
4915
|
+
.option("--formula-values <mode>", "Formula filters are refused by default because displayed formulas are evaluated live. Refresh the formula's stored cells with `columns run <table> <column> --force` (0 credits), then pass 'materialized'; filtering uses that stored snapshot and returns a freshness note.")
|
|
4912
4916
|
.option("--sort-json <json>", "Ordered sort rules, e.g. '[{\"columnKey\":\"_created_at\",\"direction\":\"desc\"}]'. Earlier rules dominate. Mutually exclusive with --filter-json.")
|
|
4913
4917
|
.option("--no-system-fields", "Omit _row_id, _created_at, and _updated_at from returned rows (included by default).")
|
|
4914
4918
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -4917,11 +4921,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4917
4921
|
const limit = readPositiveInt(options.limit);
|
|
4918
4922
|
const filters = readFilterJsonOption(options.filterJson);
|
|
4919
4923
|
const filterTree = readJsonObjectOption(options.filterTreeJson);
|
|
4924
|
+
const formulaValues = readFormulaValuesOption(options.formulaValues);
|
|
4920
4925
|
const sorts = readSortJsonOption(options.sortJson);
|
|
4921
4926
|
const offset = readPositiveInt(options.offset);
|
|
4922
4927
|
if (filters && (filterTree || sorts)) {
|
|
4923
4928
|
throw new OxygenError("invalid_filter", "Pass either --filter-json (legacy) or --filter-tree-json/--sort-json, not both.", { exitCode: 1 });
|
|
4924
4929
|
}
|
|
4930
|
+
if (formulaValues && !filters && !filterTree) {
|
|
4931
|
+
throw new OxygenError("invalid_filter", "--formula-values requires --filter-json or --filter-tree-json.", { exitCode: 1 });
|
|
4932
|
+
}
|
|
4925
4933
|
if (readOption(options.cursor) && offset) {
|
|
4926
4934
|
throw new OxygenError("invalid_request", "Pass either --cursor or --offset, not both.", { exitCode: 1 });
|
|
4927
4935
|
}
|
|
@@ -4935,6 +4943,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4935
4943
|
...(readOption(options.fields) ? { fields: readCsvOption(options.fields) } : {}),
|
|
4936
4944
|
...(filters ? { filters } : {}),
|
|
4937
4945
|
...(filterTree ? { filterTree } : {}),
|
|
4946
|
+
...(formulaValues ? { formula_values: formulaValues } : {}),
|
|
4938
4947
|
...(sorts ? { sorts } : {}),
|
|
4939
4948
|
...(options.systemFields === false ? { include_system_fields: false } : {}),
|
|
4940
4949
|
},
|
|
@@ -7158,6 +7167,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7158
7167
|
.option("--limit <n>", "Run the next N rows whose target cell is still empty (--force runs the first N regardless). Repeat until rowCount is 0 to page through a table. Defaults to 10; inline deterministic runs have a hard cap of 25.")
|
|
7159
7168
|
.option("--all", "Run all rows. Requires --background.")
|
|
7160
7169
|
.option("--filter-json <json>", "Row selector filter object or array for background runs. Do not combine with --all, --limit, or --row-id.")
|
|
7170
|
+
.option("--formula-values <mode>", "With --filter-json on a formula column, first refresh that selector with `columns run <table> <column> --force` (0 credits), then pass 'materialized'. The run filters the stored snapshot and persists this freshness acknowledgement.")
|
|
7161
7171
|
.option("--force", "Run even when the target cell already has a value.")
|
|
7162
7172
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
7163
7173
|
.option("--background", "Create a durable background run for a free deterministic column. Paid AI/tool/enrichment/custom-HTTP server runs are always backgrounded.")
|
|
@@ -7174,11 +7184,18 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7174
7184
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
7175
7185
|
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
7176
7186
|
const filterSelection = readFilterSelectionOption(options.filterJson);
|
|
7187
|
+
const formulaValues = readFormulaValuesOption(options.formulaValues);
|
|
7188
|
+
if (formulaValues && !filterSelection) {
|
|
7189
|
+
throw new OxygenError("invalid_selection", "--formula-values requires --filter-json on columns run.", { exitCode: 1 });
|
|
7190
|
+
}
|
|
7191
|
+
const effectiveFilterSelection = filterSelection
|
|
7192
|
+
? { ...filterSelection, ...(formulaValues ? { formula_values: formulaValues } : {}) }
|
|
7193
|
+
: undefined;
|
|
7177
7194
|
const selectedModes = [
|
|
7178
7195
|
Boolean(options.all),
|
|
7179
7196
|
limit !== undefined,
|
|
7180
7197
|
Boolean(readOption(options.rowId)),
|
|
7181
|
-
Boolean(
|
|
7198
|
+
Boolean(effectiveFilterSelection),
|
|
7182
7199
|
].filter(Boolean).length;
|
|
7183
7200
|
if (selectedModes > 1) {
|
|
7184
7201
|
throw new OxygenError("invalid_selection", "Pass only one of --all, --limit, --row-id, or --filter-json.", {
|
|
@@ -7198,7 +7215,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7198
7215
|
exitCode: 1,
|
|
7199
7216
|
});
|
|
7200
7217
|
}
|
|
7201
|
-
if (
|
|
7218
|
+
if (effectiveFilterSelection) {
|
|
7202
7219
|
throw new OxygenError("invalid_column_run", "--filter-json requires --background for columns run.", {
|
|
7203
7220
|
exitCode: 1,
|
|
7204
7221
|
});
|
|
@@ -7214,7 +7231,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7214
7231
|
table,
|
|
7215
7232
|
column,
|
|
7216
7233
|
...(options.all ? { selection: { mode: "all" } } : {}),
|
|
7217
|
-
...(
|
|
7234
|
+
...(effectiveFilterSelection ? { selection: effectiveFilterSelection } : {}),
|
|
7218
7235
|
...(!options.all && readOption(options.rowId) ? { row_id: readOption(options.rowId) } : {}),
|
|
7219
7236
|
...(!options.all && !filterSelection && limit ? { limit } : {}),
|
|
7220
7237
|
...(options.force ? { force: true } : {}),
|
|
@@ -7568,6 +7585,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7568
7585
|
.option("--all", "Run against all rows, up to the server cap.")
|
|
7569
7586
|
.option("--row-ids <csv>", "Comma-separated row UUIDs to run.")
|
|
7570
7587
|
.option("--filter-json <json>", "Filter object or array for server-side row selection.")
|
|
7588
|
+
.option("--formula-values <mode>", "With --filter-json on a formula column, first refresh that selector with `columns run <table> <column> --force` (0 credits), then pass 'materialized'; the durable row scope filters the stored snapshot and records this freshness acknowledgement.")
|
|
7571
7589
|
.option("--force", "Run even when the target cell already has a value.")
|
|
7572
7590
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
7573
7591
|
.option("--approved", "Confirm the paid table action run after inspecting a dry run or preview.")
|
|
@@ -9719,8 +9737,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9719
9737
|
.option("--return <mode>", "Legacy response shape: raw, compact, or summary. Defaults to raw.")
|
|
9720
9738
|
.option("--return-mode <mode>", "Response shape: raw, compact, or summary. Prefer summary for large search responses.")
|
|
9721
9739
|
.option("--oxygen-cursor <cursor>", "Short Oxygen cursor returned as oxygen_next_cursor by a previous tool run.")
|
|
9722
|
-
.option("--max-credits <n>", "
|
|
9723
|
-
.option("--approved", "Required for live
|
|
9740
|
+
.option("--max-credits <n>", "Credit ceiling for live paid tools; not needed for no-bill tools.")
|
|
9741
|
+
.option("--approved", "Required for live paid tools and external writes after inspecting dry-run output.")
|
|
9724
9742
|
.option("--json", "Print a JSON envelope.")
|
|
9725
9743
|
.action(async (toolId, options) => {
|
|
9726
9744
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
@@ -13370,7 +13388,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13370
13388
|
program.addCommand(new Command("mailboxes")
|
|
13371
13389
|
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local credential-file transfer), pause/disable inboxes, connect EmailGuard monitoring, and inspect/control OXYGEN Warm-up. Fresh managed InboxKit Google/Microsoft/Azure orders activate exact-scope warm-up automatically after provisioning under their approved default-on add-on. Google reuses the managed credential just in time; Microsoft/Azure uses native export. Standalone warm-up plans and credit caps are for BYOK/imported mailboxes, managed opt-outs, or later separate enrollment. OXYGEN Warm-up never owns campaign dispatch. Safe import preflight: run `oxygen mailboxes compatibility --catalog-only --json`, then `oxygen mailboxes import --file <path> --validate-only --json` (add the credential source flags shown by import help when the file contains credentials).")
|
|
13372
13390
|
.addCommand(new Command("list")
|
|
13373
|
-
.description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), and a pool overview (including counts by source). To see only Google/Microsoft mailboxes that still need OAuth connection
|
|
13391
|
+
.description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), worker-owned connectionRepair and warmupRepair state, and a pool overview (including counts by source). mode=automatic means OXYGEN owns the next bounded retry — do not ask for browser consent or disable/re-enable an existing warm-up seat. To see only Google/Microsoft mailboxes that still need OAuth connection, including attempt budgets and manual remedies, use `oxygen mailboxes oauth-health --json`.")
|
|
13374
13392
|
.option("--status <status>", "Filter by status: active, paused, disabled, or provisioning (ordered, still being set up).")
|
|
13375
13393
|
.option("--tag <tags>", "Comma-separated workspace tags — matches mailboxes carrying ANY of these tags (see `oxygen tags list`).")
|
|
13376
13394
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -13430,7 +13448,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13430
13448
|
await handleAsyncAction("mailboxes health", options, () => requestOxygen("/api/cli/mailboxes/health"));
|
|
13431
13449
|
}))
|
|
13432
13450
|
.addCommand(new Command("compatibility")
|
|
13433
|
-
.description("Read-only compatibility report for every selected mailbox: generic origin class, real infrastructure tier (including InboxKit Azure), OXYGEN native-send connection quality, OXYGEN Warm-up path, EmailGuard monitoring path, and exact next actions. Native send distinguishes destination-bound OAuth, provider-managed API transport, admin delegation, and authorization_required. Use --catalog-only for the compact workspace-independent import contract. JSON data.compatibility contains checked rows; data.import_methods and data.import_fields describe accepted transfer inputs; data.provider_matrix is the current public product matrix; data.non_transferable_auth names credentials that must be reconnected; data.summary rolls up states; data.web_url opens the pool. Downstream states distinguish credential_required, consent_required, and vendor_blocked. Never decrypts a credential or calls a downstream provider.")
|
|
13451
|
+
.description("Read-only compatibility report for every selected mailbox: generic origin class, real infrastructure tier (including InboxKit Azure), OXYGEN native-send connection quality, OXYGEN Warm-up path, EmailGuard monitoring path, and exact next actions. Read automatic_repair for native send and warmup_automatic_repair for an existing warm-up seat: mode=automatic means OXYGEN owns the next bounded retry. Native send distinguishes destination-bound OAuth, provider-managed API transport, admin delegation, and authorization_required. Use --catalog-only for the compact workspace-independent import contract. JSON data.compatibility contains checked rows; data.import_methods and data.import_fields describe accepted transfer inputs; data.provider_matrix is the current public product matrix; data.non_transferable_auth names credentials that must be reconnected; data.summary rolls up states; data.web_url opens the pool. Downstream states distinguish credential_required, consent_required, and vendor_blocked. Never decrypts a credential or calls a downstream provider.")
|
|
13434
13452
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
13435
13453
|
.option("--catalog-only", "Return only the bounded import-method, field, auth-boundary, and public provider catalogs; do not read or return workspace mailbox rows.")
|
|
13436
13454
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -13722,7 +13740,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13722
13740
|
});
|
|
13723
13741
|
}))
|
|
13724
13742
|
.addCommand(new Command("oauth-health")
|
|
13725
|
-
.description("Show every Google/Microsoft inbox with neither a usable per-mailbox OAuth token nor verified delegation/application access. Results name connect_oauth_oxygen for imported/manual mailboxes and connect_oauth_inboxkit for that managed path. Microsoft can use one-admin tenant authorization for exact selected addresses or individual OAuth. Read-only — 0 Oxygen credits.")
|
|
13743
|
+
.description("Show every Google/Microsoft inbox with neither a usable per-mailbox OAuth token nor verified delegation/application access. automatic_repair marks Zapmail-linked rows whose safe retries OXYGEN owns in the background; browser-consent rows remain explicit. Results name connect_oauth_oxygen for imported/manual mailboxes and connect_oauth_inboxkit for that managed path. Microsoft can use one-admin tenant authorization for exact selected addresses or individual OAuth. Read-only — 0 Oxygen credits; it can pause no mailbox and sends no email.")
|
|
13726
13744
|
.option("--json", "Print a JSON envelope.")
|
|
13727
13745
|
.action(async (options) => {
|
|
13728
13746
|
await handleAsyncAction("mailboxes oauth-health", options, () => requestOxygen("/api/cli/mailboxes/oauth-health"));
|
|
@@ -16422,6 +16440,10 @@ function readTableRunSelection(options) {
|
|
|
16422
16440
|
const limit = readPositiveInt(options.limit);
|
|
16423
16441
|
const rowIds = readCsvOption(options.rowIds);
|
|
16424
16442
|
const filterSelection = readFilterSelectionOption(options.filterJson);
|
|
16443
|
+
const formulaValues = readFormulaValuesOption(options.formulaValues);
|
|
16444
|
+
if (formulaValues && !filterSelection) {
|
|
16445
|
+
throw new OxygenError("invalid_table_run", "--formula-values requires --filter-json on table-runs create.", { exitCode: 1 });
|
|
16446
|
+
}
|
|
16425
16447
|
const selectedModes = [hasAll, Boolean(limit), rowIds.length > 0, Boolean(filterSelection)].filter(Boolean).length;
|
|
16426
16448
|
if (selectedModes > 1) {
|
|
16427
16449
|
throw new OxygenError("invalid_table_run", "Pass only one of --all, --limit, --row-ids, or --filter-json.", {
|
|
@@ -16434,8 +16456,9 @@ function readTableRunSelection(options) {
|
|
|
16434
16456
|
return { mode: "limit", limit };
|
|
16435
16457
|
if (rowIds.length > 0)
|
|
16436
16458
|
return { mode: "row_ids", row_ids: rowIds };
|
|
16437
|
-
if (filterSelection)
|
|
16438
|
-
return filterSelection;
|
|
16459
|
+
if (filterSelection) {
|
|
16460
|
+
return { ...filterSelection, ...(formulaValues ? { formula_values: formulaValues } : {}) };
|
|
16461
|
+
}
|
|
16439
16462
|
throw new OxygenError("invalid_table_run", "Pass --all, --limit, --row-ids, or --filter-json.", {
|
|
16440
16463
|
exitCode: 1,
|
|
16441
16464
|
});
|
|
@@ -17742,6 +17765,15 @@ function readSortJsonOption(value) {
|
|
|
17742
17765
|
}
|
|
17743
17766
|
return parsed;
|
|
17744
17767
|
}
|
|
17768
|
+
function readFormulaValuesOption(value) {
|
|
17769
|
+
const raw = readOption(value);
|
|
17770
|
+
if (raw === null)
|
|
17771
|
+
return null;
|
|
17772
|
+
if (raw !== "materialized") {
|
|
17773
|
+
throw new OxygenError("invalid_filter", "--formula-values must be 'materialized' when provided.", { details: { formula_values: raw }, exitCode: 1 });
|
|
17774
|
+
}
|
|
17775
|
+
return raw;
|
|
17776
|
+
}
|
|
17745
17777
|
function parseStringArray(value) {
|
|
17746
17778
|
const parsed = parseJsonArray(value);
|
|
17747
17779
|
const labels = parsed
|
|
@@ -17778,6 +17810,7 @@ async function importRows(table, options) {
|
|
|
17778
17810
|
});
|
|
17779
17811
|
}
|
|
17780
17812
|
const batchSize = normalizeImportBatchSize(options.batchSize);
|
|
17813
|
+
const effectiveBatchSize = Math.min(batchSize, SAFE_IMPORT_WRITE_BATCH_SIZE);
|
|
17781
17814
|
const sourceHash = hashImportFile(options.file);
|
|
17782
17815
|
const shouldUseBackground = options.background
|
|
17783
17816
|
|| (!options.sync && parsedRows.length > LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD);
|
|
@@ -17791,6 +17824,7 @@ async function importRows(table, options) {
|
|
|
17791
17824
|
const staged = await tryEnqueueStagedFileImport(table, options, format, parsedRows, {
|
|
17792
17825
|
autoBackground: !options.background,
|
|
17793
17826
|
sourceHash,
|
|
17827
|
+
batchSize,
|
|
17794
17828
|
}, preparedBackgroundTarget);
|
|
17795
17829
|
if (staged)
|
|
17796
17830
|
return withImportWaitNextStep(staged);
|
|
@@ -17810,11 +17844,14 @@ async function importRows(table, options) {
|
|
|
17810
17844
|
let writeRequestCount = 0;
|
|
17811
17845
|
let requestTooLargeRetries = 0;
|
|
17812
17846
|
let minimumBatchSizeUsed = null;
|
|
17813
|
-
for (const batch of chunk(target.rows,
|
|
17847
|
+
for (const [batchIndex, batch] of chunk(target.rows, effectiveBatchSize).entries()) {
|
|
17814
17848
|
const result = await writeImportBatchWithAutoSplit({
|
|
17815
17849
|
tableRef: target.tableRef,
|
|
17816
17850
|
upsertKey: target.upsertKey,
|
|
17817
17851
|
rows: batch,
|
|
17852
|
+
...(!target.upsertKey
|
|
17853
|
+
? { requestId: `tables-import:${sourceHash}:${batchIndex}` }
|
|
17854
|
+
: {}),
|
|
17818
17855
|
});
|
|
17819
17856
|
rowCount += result.rowCount;
|
|
17820
17857
|
insertedCount += result.insertedCount;
|
|
@@ -17844,7 +17881,8 @@ async function importRows(table, options) {
|
|
|
17844
17881
|
...(warningsTruncated ? { warningsTruncated: true } : {}),
|
|
17845
17882
|
} : {}),
|
|
17846
17883
|
batchCount: writeRequestCount,
|
|
17847
|
-
batchSize,
|
|
17884
|
+
batchSize: effectiveBatchSize,
|
|
17885
|
+
...(effectiveBatchSize !== batchSize ? { requestedBatchSize: batchSize } : {}),
|
|
17848
17886
|
...(requestTooLargeRetries > 0 ? {
|
|
17849
17887
|
requestTooLargeRetries,
|
|
17850
17888
|
minimumBatchSizeUsed,
|
|
@@ -17985,6 +18023,7 @@ async function writeImportBatchWithAutoSplit(input) {
|
|
|
17985
18023
|
table: input.tableRef,
|
|
17986
18024
|
rows: input.rows,
|
|
17987
18025
|
...(input.upsertKey ? { key: input.upsertKey } : {}),
|
|
18026
|
+
...(!input.upsertKey && input.requestId ? { request_id: input.requestId } : {}),
|
|
17988
18027
|
},
|
|
17989
18028
|
});
|
|
17990
18029
|
return summarizeImportBatchWrite(result, input.rows.length);
|
|
@@ -17998,10 +18037,12 @@ async function writeImportBatchWithAutoSplit(input) {
|
|
|
17998
18037
|
const first = await writeImportBatchWithAutoSplit({
|
|
17999
18038
|
...input,
|
|
18000
18039
|
rows: input.rows.slice(0, midpoint),
|
|
18040
|
+
...(input.requestId ? { requestId: `${input.requestId}:0` } : {}),
|
|
18001
18041
|
});
|
|
18002
18042
|
const second = await writeImportBatchWithAutoSplit({
|
|
18003
18043
|
...input,
|
|
18004
18044
|
rows: input.rows.slice(midpoint),
|
|
18045
|
+
...(input.requestId ? { requestId: `${input.requestId}:1` } : {}),
|
|
18005
18046
|
});
|
|
18006
18047
|
return combineImportBatchWriteSummaries(first, second, 1);
|
|
18007
18048
|
}
|
|
@@ -18055,47 +18096,55 @@ table, options, format, parsedRows, context, preparedTarget = null) {
|
|
|
18055
18096
|
}
|
|
18056
18097
|
const fileBuffer = readFileSync(options.file);
|
|
18057
18098
|
const filename = basename(options.file);
|
|
18099
|
+
const contentType = importFileContentType(format);
|
|
18100
|
+
const traceId = randomUUID();
|
|
18058
18101
|
// Probe for a presigned upload URL. If object storage isn't configured the
|
|
18059
18102
|
// server returns object_storage_not_configured; signal the caller to fall
|
|
18060
18103
|
// back to the inline multipart import by returning null.
|
|
18061
|
-
|
|
18062
|
-
|
|
18063
|
-
|
|
18064
|
-
|
|
18065
|
-
|
|
18066
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18104
|
+
const requestPresign = async () => {
|
|
18105
|
+
try {
|
|
18106
|
+
return await requestOxygen("/api/cli/tables/import-url", {
|
|
18107
|
+
method: "POST",
|
|
18108
|
+
body: {
|
|
18109
|
+
file_name: filename,
|
|
18110
|
+
content_type: contentType,
|
|
18111
|
+
byte_length: fileBuffer.byteLength,
|
|
18112
|
+
trace_id: traceId,
|
|
18113
|
+
},
|
|
18114
|
+
});
|
|
18071
18115
|
}
|
|
18072
|
-
|
|
18073
|
-
|
|
18074
|
-
|
|
18075
|
-
|
|
18076
|
-
|
|
18077
|
-
|
|
18078
|
-
|
|
18116
|
+
catch (error) {
|
|
18117
|
+
if (error instanceof OxygenError && error.code === "object_storage_not_configured") {
|
|
18118
|
+
return null;
|
|
18119
|
+
}
|
|
18120
|
+
throw error;
|
|
18121
|
+
}
|
|
18122
|
+
};
|
|
18123
|
+
let presigned = await requestPresign();
|
|
18124
|
+
if (!presigned)
|
|
18079
18125
|
return null;
|
|
18080
18126
|
// Create the table for --create (or resolve the existing ref) before the
|
|
18081
18127
|
// upload so a presign success always pairs with a real target.
|
|
18082
18128
|
const target = preparedTarget ?? await prepareImportTarget(table, options, parsedRows);
|
|
18083
|
-
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
|
|
18092
|
-
|
|
18093
|
-
finally {
|
|
18094
|
-
clearTimeout(timer);
|
|
18129
|
+
let putResponse = await putImportFile(presigned, fileBuffer, contentType);
|
|
18130
|
+
// A direct-upload 403 most commonly means a stale/mismatched signature.
|
|
18131
|
+
// Refresh once under the same client trace so a transient presign never
|
|
18132
|
+
// strands a durable import, while persistent policy failures stay explicit.
|
|
18133
|
+
if (putResponse.status === 403) {
|
|
18134
|
+
const refreshed = await requestPresign();
|
|
18135
|
+
if (refreshed) {
|
|
18136
|
+
presigned = refreshed;
|
|
18137
|
+
putResponse = await putImportFile(presigned, fileBuffer, contentType);
|
|
18138
|
+
}
|
|
18095
18139
|
}
|
|
18096
18140
|
if (!putResponse.ok) {
|
|
18097
|
-
throw new OxygenError("import_upload_failed", `Uploading the import file to object storage failed (HTTP ${putResponse.status}).`, { details: { status: putResponse.status }, exitCode: 1 });
|
|
18141
|
+
throw new OxygenError("import_upload_failed", `Uploading the import file to object storage failed (HTTP ${putResponse.status}).`, { details: { status: putResponse.status, trace_id: traceId }, exitCode: 1 });
|
|
18098
18142
|
}
|
|
18143
|
+
const storageKey = readRecordString(presigned, "storage_key");
|
|
18144
|
+
const storageBucket = readRecordString(presigned, "storage_bucket");
|
|
18145
|
+
const storageProvider = readRecordString(presigned, "storage_provider") ?? "s3";
|
|
18146
|
+
if (!storageKey)
|
|
18147
|
+
return null;
|
|
18099
18148
|
const result = await requestOxygen("/api/cli/tables/import-staged", {
|
|
18100
18149
|
method: "POST",
|
|
18101
18150
|
timeoutMs: 120_000,
|
|
@@ -18109,6 +18158,10 @@ table, options, format, parsedRows, context, preparedTarget = null) {
|
|
|
18109
18158
|
byte_length: fileBuffer.byteLength,
|
|
18110
18159
|
sha256: context.sourceHash,
|
|
18111
18160
|
row_count: parsedRows.length,
|
|
18161
|
+
batch_size: context.batchSize,
|
|
18162
|
+
...(readPositiveInt(options.maxConcurrency)
|
|
18163
|
+
? { max_concurrency: readPositiveInt(options.maxConcurrency) }
|
|
18164
|
+
: {}),
|
|
18112
18165
|
...(target.upsertKey ? { upsert_key: target.upsertKey } : {}),
|
|
18113
18166
|
...(target.sourceKeyMap ? { source_key_map: target.sourceKeyMap } : {}),
|
|
18114
18167
|
},
|
|
@@ -18120,10 +18173,40 @@ table, options, format, parsedRows, context, preparedTarget = null) {
|
|
|
18120
18173
|
...(target.tableWebUrl ? { table_web_url: target.tableWebUrl } : {}),
|
|
18121
18174
|
background: true,
|
|
18122
18175
|
autoBackground: context.autoBackground,
|
|
18123
|
-
import_engine: "
|
|
18176
|
+
import_engine: "chunked_file_v2",
|
|
18124
18177
|
storage_provider: storageProvider,
|
|
18125
18178
|
};
|
|
18126
18179
|
}
|
|
18180
|
+
async function putImportFile(presigned, fileBuffer, contentType) {
|
|
18181
|
+
const uploadUrl = readRecordString(presigned, "upload_url");
|
|
18182
|
+
if (!uploadUrl)
|
|
18183
|
+
return new Response(null, { status: 500 });
|
|
18184
|
+
const controller = new AbortController();
|
|
18185
|
+
const timer = setTimeout(() => controller.abort(), 300_000);
|
|
18186
|
+
try {
|
|
18187
|
+
return await fetch(uploadUrl, {
|
|
18188
|
+
method: "PUT",
|
|
18189
|
+
headers: {
|
|
18190
|
+
"content-type": contentType,
|
|
18191
|
+
"content-length": String(fileBuffer.byteLength),
|
|
18192
|
+
},
|
|
18193
|
+
body: new Uint8Array(fileBuffer),
|
|
18194
|
+
signal: controller.signal,
|
|
18195
|
+
});
|
|
18196
|
+
}
|
|
18197
|
+
finally {
|
|
18198
|
+
clearTimeout(timer);
|
|
18199
|
+
}
|
|
18200
|
+
}
|
|
18201
|
+
function importFileContentType(format) {
|
|
18202
|
+
if (format === "csv")
|
|
18203
|
+
return "text/csv";
|
|
18204
|
+
if (format === "xlsx")
|
|
18205
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
18206
|
+
if (format === "jsonl")
|
|
18207
|
+
return "application/x-ndjson";
|
|
18208
|
+
return "application/json";
|
|
18209
|
+
}
|
|
18127
18210
|
async function enqueueImportFile(table, options, format, batchSize, context, preparedTarget = null) {
|
|
18128
18211
|
if (options.create && table) {
|
|
18129
18212
|
throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
|
|
@@ -18643,6 +18726,7 @@ options) {
|
|
|
18643
18726
|
throw new OxygenError("invalid_input", "--file is required.", { exitCode: 1 });
|
|
18644
18727
|
}
|
|
18645
18728
|
const batchSize = normalizeImportBatchSize(options.batchSize);
|
|
18729
|
+
const effectiveBatchSize = Math.min(batchSize, SAFE_IMPORT_WRITE_BATCH_SIZE);
|
|
18646
18730
|
const raw = readFileSync(resolve(path), "utf8");
|
|
18647
18731
|
const bundle = parseBundleFile(raw);
|
|
18648
18732
|
const columns = bundle.columns.map(bundleColumnToCreateInput);
|
|
@@ -18721,13 +18805,13 @@ options) {
|
|
|
18721
18805
|
file: path,
|
|
18722
18806
|
tableId: newTableId,
|
|
18723
18807
|
key: upsertKey ?? null,
|
|
18724
|
-
batchSize,
|
|
18808
|
+
batchSize: effectiveBatchSize,
|
|
18725
18809
|
});
|
|
18726
18810
|
let processed = 0;
|
|
18727
18811
|
let insertedCount = 0;
|
|
18728
18812
|
let updatedCount = 0;
|
|
18729
|
-
for (let offset = 0; offset < stagedRows.length; offset +=
|
|
18730
|
-
const batch = stagedRows.slice(offset, offset +
|
|
18813
|
+
for (let offset = 0; offset < stagedRows.length; offset += effectiveBatchSize) {
|
|
18814
|
+
const batch = stagedRows.slice(offset, offset + effectiveBatchSize);
|
|
18731
18815
|
if (batch.length === 0)
|
|
18732
18816
|
continue;
|
|
18733
18817
|
try {
|
package/dist/runtime.js
CHANGED
|
@@ -43,7 +43,7 @@ export function resolveCliUpdateGuidance(endpoint, env = process.env, argv = pro
|
|
|
43
43
|
return {
|
|
44
44
|
binaryName,
|
|
45
45
|
channel: "dev",
|
|
46
|
-
warningInstruction: "Rebuild `oxygen-dev` from the latest dev branch (`npm run install-dev-cli`)
|
|
46
|
+
warningInstruction: "Rebuild `oxygen-dev` from the latest dev branch (`npm run install-dev-cli`) to pick up the latest behavior.",
|
|
47
47
|
failureInstruction: "Rebuild `oxygen-dev` from the latest dev branch (`npm run install-dev-cli`) before using this command.",
|
|
48
48
|
details: {
|
|
49
49
|
cli_update_instruction: "Rebuild `oxygen-dev` from the latest dev branch (`npm run install-dev-cli`).",
|
|
@@ -67,7 +67,7 @@ export function resolveCliUpdateGuidance(endpoint, env = process.env, argv = pro
|
|
|
67
67
|
return {
|
|
68
68
|
binaryName,
|
|
69
69
|
channel: "npm",
|
|
70
|
-
warningInstruction: "Run `oxygen update`
|
|
70
|
+
warningInstruction: "Run `oxygen update` to pick up the latest behavior.",
|
|
71
71
|
failureInstruction: "Run `oxygen update` before using this command.",
|
|
72
72
|
details: {
|
|
73
73
|
cli_update_command: "oxygen update",
|
|
@@ -426,15 +426,15 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
426
426
|
id: "connected-linkedin",
|
|
427
427
|
layer: "External",
|
|
428
428
|
primitive: null,
|
|
429
|
-
owns: "The workspace LinkedIn account network/private data and connected-account writes used by Signals, Messages, Sequences, and Publishing.",
|
|
429
|
+
owns: "The workspace LinkedIn account network/private data, relationship context such as mutual connections, and connected-account writes used by Signals, Messages, Sequences, and Publishing.",
|
|
430
430
|
notFor: "Ordinary public or third-party LinkedIn research; use the cookieless native scraper.",
|
|
431
|
-
execution: "
|
|
431
|
+
execution: "For mutual/shared connection counts, resolve the connected account with `oxygen senders list --json`, inspect `oxygen tools get linkedin.users_get --json`, then run `oxygen tools run linkedin.users_get --input-json '{\"identifier\":\"<profile-url>\",\"account_id\":\"<connected-account-id>\",\"notify\":false}' --mode live --json`. This read is non-notifying and no-bill. An unavailable count is explicit and never means zero; retrying the same account/profile pair is not expected to reveal a provider-hidden count. Otherwise use the owning primitive gateway; the connected account provides identity and transport, not a competing product runtime.",
|
|
432
432
|
posture: "mixed",
|
|
433
433
|
gatewayTools: ["oxygen_senders_list", "oxygen_linkedin_connections_import", "oxygen_inbox_list"],
|
|
434
434
|
gatewayCommands: ["senders list", "connections import", "inbox list"],
|
|
435
435
|
skills: ["oxygen-linkedin-marketing", "oxygen-sequencer", "oxygen-unibox"],
|
|
436
436
|
endpointSections: ["linkedin"],
|
|
437
|
-
intentTerms: ["my linkedin", "our linkedin", "own linkedin", "connected linkedin", "connections", "followers", "profile viewers", "sales navigator", "recruiter", "linkedin inbox"],
|
|
437
|
+
intentTerms: ["my linkedin", "our linkedin", "own linkedin", "connected linkedin", "connections", "mutual connections", "shared connections", "connections in common", "followers", "profile viewers", "sales navigator", "recruiter", "linkedin inbox"],
|
|
438
438
|
},
|
|
439
439
|
{
|
|
440
440
|
id: "connected-whatsapp",
|
|
@@ -540,6 +540,13 @@ function explicitCapabilityIntent(query) {
|
|
|
540
540
|
// published post still routes to Posts.
|
|
541
541
|
if (isSecondPartyDecisionIntent(query))
|
|
542
542
|
return ROUTE_BY_ID.get("collaboration") ?? null;
|
|
543
|
+
// A mutual/shared-connection count is relationship context that only a
|
|
544
|
+
// connected account can expose. Route it before the generic public-profile
|
|
545
|
+
// rule, which otherwise sees "LinkedIn profile" and sends the user to the
|
|
546
|
+
// cookieless research catalog that cannot answer this question.
|
|
547
|
+
if (isMutualLinkedInConnectionsIntent(query)) {
|
|
548
|
+
return ROUTE_BY_ID.get("connected-linkedin") ?? null;
|
|
549
|
+
}
|
|
543
550
|
// A deferred acquisition motion starts with the public-data owner. This keeps
|
|
544
551
|
// "find/qualify now, contact later" from skipping straight to the final write
|
|
545
552
|
// step; the recommendation still hands the eventual initiation to Sequences.
|
|
@@ -617,6 +624,12 @@ function highestScoringRoute(query) {
|
|
|
617
624
|
return best?.card ?? null;
|
|
618
625
|
}
|
|
619
626
|
function recommendationsFor(card, query) {
|
|
627
|
+
if (card.id === "connected-linkedin" && isMutualLinkedInConnectionsIntent(query)) {
|
|
628
|
+
return {
|
|
629
|
+
tools: ["oxygen_tools_get", "oxygen_tools_run_live", "oxygen_senders_list"],
|
|
630
|
+
commands: ["tools get", "tools run", "senders list"],
|
|
631
|
+
};
|
|
632
|
+
}
|
|
620
633
|
if (card.id === "sending-infrastructure" && isMailboxDeleteIntent(query)) {
|
|
621
634
|
return {
|
|
622
635
|
tools: [
|
|
@@ -826,6 +839,12 @@ function recommendationsFor(card, query) {
|
|
|
826
839
|
}
|
|
827
840
|
return { tools: [...card.gatewayTools], commands: [...card.gatewayCommands] };
|
|
828
841
|
}
|
|
842
|
+
function isMutualLinkedInConnectionsIntent(query) {
|
|
843
|
+
const linkedInContext = /\blinkedin\b|\bprofiles?\b/.test(query);
|
|
844
|
+
const mutualContext = /\b(mutual|shared|in common)\b.{0,48}\b(connections?|relations?)\b/.test(query)
|
|
845
|
+
|| /\b(connections?|relations?)\b.{0,48}\b(mutual|shared|in common)\b/.test(query);
|
|
846
|
+
return linkedInContext && mutualContext;
|
|
847
|
+
}
|
|
829
848
|
function isMailboxOnboardingIntent(query) {
|
|
830
849
|
const operation = /\b(import|migrat\w*|onboard|register|upload|bring|connect existing|add existing)\b/.test(query);
|
|
831
850
|
const mailboxScope = /\b(mailbox(?:es)?|inbox(?:es)?|sender accounts?|email accounts?|sending pool)\b/.test(query);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Normalized failure category, derived from SQLSTATE first, then errno/message. */
|
|
2
|
-
export type SqlErrorCause = "connect_timeout" | "connection" | "statement_timeout" | "admin_shutdown" | "too_many_connections" | "insufficient_resources" | "auth" | "schema_drift" | "data_exception" | "integrity_constraint" | "serialization" | "deadlock" | "transaction_rollback" | "read_only" | "syntax_or_access" | "internal" | "unknown";
|
|
2
|
+
export type SqlErrorCause = "connect_timeout" | "connection" | "lock_timeout" | "statement_timeout" | "admin_shutdown" | "too_many_connections" | "insufficient_resources" | "auth" | "schema_drift" | "data_exception" | "integrity_constraint" | "serialization" | "deadlock" | "transaction_rollback" | "read_only" | "syntax_or_access" | "internal" | "unknown";
|
|
3
3
|
export type SqlErrorAttribution = {
|
|
4
4
|
/** Postgres SQLSTATE (5 chars) when present; null for connection-level errnos. */
|
|
5
5
|
pgCode: string | null;
|
|
@@ -53,6 +53,13 @@ export declare function redactSqlParameters(text: string): string;
|
|
|
53
53
|
* 40001/40P01 knowledge lives in one place. Never throws.
|
|
54
54
|
*/
|
|
55
55
|
export declare function isRetryableConcurrencyError(error?: unknown): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* True when a raw pg or drizzle-wrapped database failure is transient and a
|
|
58
|
+
* later durable-work attempt can safely try the transaction again. Unlike the
|
|
59
|
+
* narrower concurrency helper above, this includes lock/statement timeouts,
|
|
60
|
+
* connection failures, capacity pressure, and a temporarily read-only primary.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isRetryableSqlError(error?: unknown): boolean;
|
|
56
63
|
/**
|
|
57
64
|
* True when a write lost a concurrency race and is safe to re-read / retry: a
|
|
58
65
|
* serialization (40001) or deadlock (40P01) transaction abort, OR the XX000
|
|
@@ -55,6 +55,7 @@ const CONNECTION_MESSAGE = /connection terminated|connection reset|socket hang u
|
|
|
55
55
|
const TRANSIENT_CAUSES = new Set([
|
|
56
56
|
"connect_timeout",
|
|
57
57
|
"connection",
|
|
58
|
+
"lock_timeout",
|
|
58
59
|
"statement_timeout",
|
|
59
60
|
"admin_shutdown",
|
|
60
61
|
"too_many_connections",
|
|
@@ -170,6 +171,15 @@ export function isRetryableConcurrencyError(error) {
|
|
|
170
171
|
const cause = describeSqlError(error)?.cause;
|
|
171
172
|
return cause === "serialization" || cause === "deadlock";
|
|
172
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* True when a raw pg or drizzle-wrapped database failure is transient and a
|
|
176
|
+
* later durable-work attempt can safely try the transaction again. Unlike the
|
|
177
|
+
* narrower concurrency helper above, this includes lock/statement timeouts,
|
|
178
|
+
* connection failures, capacity pressure, and a temporarily read-only primary.
|
|
179
|
+
*/
|
|
180
|
+
export function isRetryableSqlError(error) {
|
|
181
|
+
return describeSqlError(error)?.transient === true;
|
|
182
|
+
}
|
|
173
183
|
/**
|
|
174
184
|
* True when a write lost a concurrency race and is safe to re-read / retry: a
|
|
175
185
|
* serialization (40001) or deadlock (40P01) transaction abort, OR the XX000
|
|
@@ -217,6 +227,7 @@ export function sqlErrorTelemetryAttributes(error) {
|
|
|
217
227
|
// Specific SQLSTATEs that must resolve before the coarser class-prefix lookup
|
|
218
228
|
// (e.g. 40001 serialization / 40P01 deadlock are more precise than class 40).
|
|
219
229
|
const EXACT_SQLSTATE_CAUSES = new Map([
|
|
230
|
+
["55P03", "lock_timeout"],
|
|
220
231
|
["57014", "statement_timeout"],
|
|
221
232
|
["53300", "too_many_connections"],
|
|
222
233
|
["40001", "serialization"],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.830.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.830.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:
|