@oxygen-agent/cli 1.717.9 → 1.720.7
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/index.js +16 -9
- package/node_modules/@oxygen/shared/dist/axiom-field-budget.d.ts +99 -0
- package/node_modules/@oxygen/shared/dist/axiom-field-budget.js +112 -17
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/index.js +8 -1
- 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/index.js
CHANGED
|
@@ -939,7 +939,7 @@ async function readMailboxImportFile(path) {
|
|
|
939
939
|
buffer = readFileSync(path);
|
|
940
940
|
}
|
|
941
941
|
catch {
|
|
942
|
-
throw new OxygenError("mailbox_import_file_unreadable", `Couldn't read --file '${basename(path)}'. Check that the path exists and is readable. CSV/JSON/JSONL/XLSX identity files are accepted; credential exports require --from credentials --vendor <source>. See https://oxygen-agent.com/docs/providers/
|
|
942
|
+
throw new OxygenError("mailbox_import_file_unreadable", `Couldn't read --file '${basename(path)}'. Check that the path exists and is readable. CSV/JSON/JSONL/XLSX identity files are accepted; credential exports require --from credentials --vendor <source>. See https://oxygen-agent.com/docs/providers/mailboxes.`, { exitCode: 1 });
|
|
943
943
|
}
|
|
944
944
|
if (buffer.byteLength > SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES) {
|
|
945
945
|
throw new OxygenError("invalid_request", `Mailbox import files must be ${SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES / 1024 / 1024} MB or smaller.`, { exitCode: 1 });
|
|
@@ -2635,9 +2635,9 @@ export function createProgram() {
|
|
|
2635
2635
|
}));
|
|
2636
2636
|
program
|
|
2637
2637
|
.command("support")
|
|
2638
|
-
.description("Open and track Plain support conversations for the active OXYGEN organization.")
|
|
2638
|
+
.description("Open and track Plain support conversations for the active OXYGEN organization. Filing is a zero-credit write to the canonical Plain queue.")
|
|
2639
2639
|
.addCommand(new Command("file")
|
|
2640
|
-
.description("Create a Plain support Thread. Use when you're stuck on an OXYGEN operation.")
|
|
2640
|
+
.description("Create a real, zero-credit Plain support Thread immediately. There is no preview: review the exact subject, body, category, and severity before running it. Use when you're stuck on an OXYGEN operation.")
|
|
2641
2641
|
.requiredOption("--subject <subject>", "One-line summary of the problem.")
|
|
2642
2642
|
.option("--body <body>", "What you were doing, what happened, and what you tried.")
|
|
2643
2643
|
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
@@ -2809,7 +2809,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2809
2809
|
})), { hidden: true });
|
|
2810
2810
|
program
|
|
2811
2811
|
.command("feedback")
|
|
2812
|
-
.description("Send feedback or a bug report to the OXYGEN team.
|
|
2812
|
+
.description("Send feedback or a bug report to the OXYGEN team. This is the same immediate, zero-credit canonical Plain Thread write as `support file`; transcripts are never attached unless explicitly requested.")
|
|
2813
2813
|
.option("-m, --message <message>", "Your feedback or bug report.")
|
|
2814
2814
|
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
2815
2815
|
.option("--category <category>", "Optional category label. Defaults to 'feedback'.")
|
|
@@ -13102,7 +13102,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13102
13102
|
' JSON: {"mailboxes":[{"email_address":"ada@send-acme.com","provider":"google","app_password":"<Google mailbox app password>"}]}',
|
|
13103
13103
|
" Only Google app passwords enter the encrypted seven-day transfer vault. Microsoft rows remain identity-only. Generic SMTP passwords and OAuth/MFA/delegation secrets are rejected.",
|
|
13104
13104
|
" Validation: add --validate-only to parse the complete real file and return safe aggregate counts plus the provider-specific exact-account OAuth review plan without authentication, a network request, or a workspace write. The plan groups only supplied addresses into reviews of at most 10; it never discovers a domain, and the later online preview may skip existing grants. Any parse, shape, provider, platform, tenant, secret-policy, or duplicate-conflict error rejects the entire file before the first mailbox write and names mailboxes[index]; validation-only never writes. A later import infrastructure failure may interrupt the upsert; re-run the same file because import is idempotent by address.",
|
|
13105
|
-
" Docs: https://oxygen-agent.com/docs/providers/
|
|
13105
|
+
" Docs: https://oxygen-agent.com/docs/providers/mailboxes",
|
|
13106
13106
|
" Skill: oxygen-email-infra (`oxygen skills install --skill oxygen-email-infra`).",
|
|
13107
13107
|
"",
|
|
13108
13108
|
].join("\n"))
|
|
@@ -13301,7 +13301,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13301
13301
|
}))
|
|
13302
13302
|
.addCommand(new Command("connect-oauth")
|
|
13303
13303
|
.description("Connect Google/Microsoft mailboxes to OXYGEN native send. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen requires an exact address list and never discovers a domain directory. Microsoft supports --authorization-mode tenant (one administrator approval, up to 500 exact selected addresses, recommended for dedicated sending tenants) or individual (one account sign-in per mailbox, max 10 per run, recommended for ordinary company/personal mailboxes). Microsoft's application grant is tenant-wide at the provider; OXYGEN—not Microsoft—enforces the selected-address boundary at verification and send time unless the tenant separately configures Exchange Application RBAC. Connecting authorization does not change whether a mailbox is active or disabled. Google uses individual OAuth. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. Managed authorization paths remain available through their vendor options. All paths cost 0 Oxygen credits.")
|
|
13304
|
-
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/
|
|
13304
|
+
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
13305
13305
|
.option("--provider <provider>", "Mailbox provider to provision: google or microsoft.")
|
|
13306
13306
|
.option("--vendor <vendor>", "Authorization path: oxygen for imported/manual mailboxes, zapmail (default), or inboxkit.")
|
|
13307
13307
|
.option("--mailboxes <list>", "Comma-separated exact mailbox addresses. Required for vendor=oxygen (tenant mode max 500; individual mode max 10).")
|
|
@@ -13441,10 +13441,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13441
13441
|
}))
|
|
13442
13442
|
.addCommand(new Command("warmup")
|
|
13443
13443
|
.description("OXYGEN Warm-up for the sending pool — the weeks of low-volume conversational mail that make a new inbox look trusted before you cold-send from it. It is managed and credit-billed at 3,000 credits per warming inbox per month ($3). Fresh managed InboxKit Google/Microsoft/Azure orders with the default-on warm-up add-on activate automatically after provisioning under the approved order quote; do not run a second `warmup enable`. Standalone preview → approval is for BYOK/imported mailboxes, a managed opt-out, or later separate enrollment. InboxKit auto-export stays off because Oxygen submits only the exact authorized UIDs, and any non-cancelled InboxKit warm-up blocks the handoff so one mailbox cannot warm twice. OXYGEN Warm-up never owns campaign dispatch: native OXYGEN Sequences still own campaigns and sends. Eligible non-InboxKit Microsoft mailboxes use the separate `mailboxes warmup microsoft` OAuth fallback. Inboxes already warming on the retired TrulyInbox rail keep reporting and are wound down there with `warmup disable`; they are never silently moved to the current managed rail.")
|
|
13444
|
-
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/
|
|
13444
|
+
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
13445
13445
|
.addCommand(new Command("enable")
|
|
13446
13446
|
.description("STANDALONE ENROLLMENT: enroll BYOK/imported mailboxes, a managed order that opted out, or mailboxes being added to warmup later. Fresh managed InboxKit Google/Microsoft/Azure orders whose approved quote included warmup activate automatically after provisioning and do not need this command. Targets the whole pool unless --mailboxes is given; one synchronous preview/approval is capped at 220 mailboxes, so split larger pools into explicit batches. Without --approved this is a 0-credit, no-provider-write priced preview: nothing is exported, enrolled, or billed. Preview one inbox with `oxygen mailboxes warmup enable --mailboxes sender@example.com --json`. Execute by echoing its --plan and --max-credits with --approved. Eligible managed InboxKit targets still use InboxKit's native Sequencer export with exact UIDs; auto-export remains off, and any non-cancelled InboxKit warmup must be cancelled before export (pausing is not enough). Only eligible non-InboxKit Microsoft mailboxes need `mailboxes warmup microsoft`. OXYGEN Warm-up retains no campaign authority — OXYGEN Sequences own enrollment and dispatch. New enrollments only ever land on the managed OXYGEN rail; the retired TrulyInbox rail is refused here and only accepts teardown.")
|
|
13447
|
-
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/
|
|
13447
|
+
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
13448
13448
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
|
|
13449
13449
|
.option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
|
|
13450
13450
|
.option("--plan <hash>", "Hash from the fresh preview (required with --approved).")
|
|
@@ -13489,7 +13489,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13489
13489
|
}))
|
|
13490
13490
|
.addCommand(new Command("microsoft")
|
|
13491
13491
|
.description("NON-INBOXKIT FALLBACK ONLY: authorize eligible Microsoft/Outlook mailboxes for OXYGEN Warm-up with ONE browser consent PER MAILBOX, never a password or app password. Do not run this for managed InboxKit mailboxes: fresh Microsoft/Azure orders activate native InboxKit warm-up exports automatically when the approved order includes warmup; opted-out or later separate managed enrollment uses standalone `warmup enable`. For the non-InboxKit fallback there is no tenant-admin shortcut: a 40-inbox Microsoft pool means 40 separate authorizations, each opened by whoever can sign in to that mailbox. The preview names the exact fallback mailboxes and outstanding links at 0 credits. Approve that fresh hash with --max-credits 0, open every returned consent_url, then re-run with --status. Warmup itself stays OFF until standalone `warmup enable`.")
|
|
13492
|
-
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/
|
|
13492
|
+
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
13493
13493
|
.option("--mailboxes <list>", "Comma-separated eligible non-InboxKit Microsoft mailbox ids or addresses. Omit to select the pool; the command refuses a scope containing managed InboxKit rows and points to warmup enable.")
|
|
13494
13494
|
.option("--tenant <id>", "Deprecated and ignored: OXYGEN Warm-up consent is per mailbox, so there is no tenant-wide scope to bind. Still accepted so older scripts keep running.")
|
|
13495
13495
|
.option("--approved", "Mint the per-mailbox consent links for the exact previewed mailbox scope. Still 0 credits — a human then has to open each link.")
|
|
@@ -14125,6 +14125,13 @@ Authoring guide:
|
|
|
14125
14125
|
Install or refresh the focused product skill:
|
|
14126
14126
|
oxygen skills install --skill oxygen-workflow-authoring
|
|
14127
14127
|
|
|
14128
|
+
Web creation chooser:
|
|
14129
|
+
Open /workspaces/<workspace-id>/workflows and click Create workflow.
|
|
14130
|
+
Choose Blank workflow, Workflow template (one workflow), Blueprint
|
|
14131
|
+
(a multi-resource motion), or Import definition. The chooser opens on the
|
|
14132
|
+
collection page; it has no separate public URL.
|
|
14133
|
+
Guide: https://oxygen-agent.com/docs/execution/workflows
|
|
14134
|
+
|
|
14128
14135
|
Fastest editable graph:
|
|
14129
14136
|
1. oxygen workflows events list --search "<outcome>" --kind builtin --json
|
|
14130
14137
|
2. oxygen workflows init --id my-workflow
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
* under `worker_fields`. New instrumentation can then be added freely without
|
|
20
20
|
* anyone having to remember that a new key can take down log shipping globally.
|
|
21
21
|
*
|
|
22
|
+
* WITH ONE HOLE, found the hard way on 2026-08-15. Packing protects the dataset
|
|
23
|
+
* from the CONTENTS, not from the CONTAINER: `worker_fields` is itself a column,
|
|
24
|
+
* and a dataset with no free column cannot have it. Registering the map field on
|
|
25
|
+
* the already-full `oxygen-logs` therefore turned a slow leak into a total ingest
|
|
26
|
+
* outage — 41,372 records/day, ~60x worse than the leak it replaced. `AxiomOverflowMode`
|
|
27
|
+
* below is the escape hatch; freeing a column slot is the actual fix.
|
|
28
|
+
*
|
|
22
29
|
* THE ALLOWLIST IS A LOAD-BEARING CONTRACT IN BOTH DIRECTIONS.
|
|
23
30
|
*
|
|
24
31
|
* - A field that a monitor, dashboard, or checked-in query reads **flat** must be
|
|
@@ -34,6 +41,39 @@
|
|
|
34
41
|
* Both directions are asserted by `scripts/ci/log-field-budget.mjs` against the
|
|
35
42
|
* real seed and dashboard JSON, so a change to either side fails loudly instead
|
|
36
43
|
* of quietly blinding a monitor.
|
|
44
|
+
*
|
|
45
|
+
* THERE IS A THIRD DIRECTION, and it is the one that took production down: a
|
|
46
|
+
* name in this set that the DATASET has no column for. Allowlisted means flat by
|
|
47
|
+
* definition, so such a name cannot fall back into the map — it demands a
|
|
48
|
+
* brand-new column, and a dataset with no slot to allocate one rejects the whole
|
|
49
|
+
* ingest batch with HTTP 400. So membership here is not "a field we read flat";
|
|
50
|
+
* it is "a field we read flat AND the dataset can actually hold". The live half
|
|
51
|
+
* of `scripts/ci/log-field-budget.mjs` checks that second half against Axiom.
|
|
52
|
+
*
|
|
53
|
+
* REMOVED 2026-08-15 — `chunk_id`, `item_id`, `digest`. Prod `oxygen-logs` had no
|
|
54
|
+
* column for any of the three and no slot left to create one, so every record
|
|
55
|
+
* carrying one was rejected 400 and DROPPED. That was still ~116 ship failures
|
|
56
|
+
* per 25 minutes after the `worker_fields` map field itself had been repaired on
|
|
57
|
+
* the Axiom side, and it hit the Next.js server-error path hardest, through
|
|
58
|
+
* `digest`.
|
|
59
|
+
*
|
|
60
|
+
* The warning above — that dropping a name silently moves it into the overflow
|
|
61
|
+
* and can zero a query that reads it flat — applies to exactly this removal, so
|
|
62
|
+
* it was CHECKED rather than assumed, in both directions:
|
|
63
|
+
*
|
|
64
|
+
* - Nothing reads them FLAT. Every `aplQuery` in docs/observability/monitors.json
|
|
65
|
+
* was parsed and all dashboard JSON grepped: zero matches for the three as flat
|
|
66
|
+
* fields (the apparent hits are the English word "digest" in description prose).
|
|
67
|
+
* - Nothing reads them out of `worker_fields` either, so the move breaks no query
|
|
68
|
+
* in the opposite direction.
|
|
69
|
+
*
|
|
70
|
+
* Packed, they stay queryable as `['worker_fields']['digest']` — a longer access
|
|
71
|
+
* path, the same value, still typed. `digest` matters most: it is the only join
|
|
72
|
+
* key from a browser error report back to its `next.request_error` row (see the
|
|
73
|
+
* error-shape group below), and that correlation still works through the map
|
|
74
|
+
* field. The trade is explicit and worth stating plainly: keeping them flat meant
|
|
75
|
+
* the ENTIRE record was dropped, which loses the digest anyway plus everything
|
|
76
|
+
* travelling with it. Packed is strictly better than dropped.
|
|
37
77
|
*/
|
|
38
78
|
/**
|
|
39
79
|
* Fields that stay top-level in `oxygen-logs`.
|
|
@@ -64,8 +104,67 @@ export declare const AXIOM_STABLE_FIELDS: ReadonlySet<string>;
|
|
|
64
104
|
* ordinary flat fields and the budget is defeated silently. Create it with:
|
|
65
105
|
* POST /v2/datasets/{dataset}/mapfields {"name": "worker_fields"}
|
|
66
106
|
* `scripts/ops/axiom-mapfields.mjs` does this idempotently for every log dataset.
|
|
107
|
+
*
|
|
108
|
+
* AND IT MUST BE ABLE TO EXIST — see `AxiomOverflowMode` below. A map field still
|
|
109
|
+
* occupies ONE column slot, so registering it on an already-full dataset does not
|
|
110
|
+
* create it; Axiom rejects every ingest that carries it instead.
|
|
67
111
|
*/
|
|
68
112
|
export declare const AXIOM_OVERFLOW_MAP_FIELD = "worker_fields";
|
|
113
|
+
/**
|
|
114
|
+
* How overflow is attached to a record.
|
|
115
|
+
*
|
|
116
|
+
* `map` is correct and is the default. `message` exists because of a failure mode
|
|
117
|
+
* the map-field design did not anticipate, measured in production 2026-08-15:
|
|
118
|
+
*
|
|
119
|
+
* {"code":400,"message":"adding 'worker_fields' to dataset fields would
|
|
120
|
+
* exceed the column limit of 1025"}
|
|
121
|
+
*
|
|
122
|
+
* `oxygen-logs` was at 1,026 columns. A map field's CONTENTS are free, but the map
|
|
123
|
+
* field itself is still a column, and there was no slot for it. So Axiom refused to
|
|
124
|
+
* create it and rejected every batch carrying it — 19,029 rejected batches and
|
|
125
|
+
* 41,372 lost records in 24h, roughly 60x the 412-batches-per-7-days leak the map
|
|
126
|
+
* field was introduced to fix. Registering the map field on a full dataset had
|
|
127
|
+
* converted "silently absorb overflow as new columns" into "drop everything".
|
|
128
|
+
*
|
|
129
|
+
* The degraded mode restores the shape this module shipped BEFORE the map field:
|
|
130
|
+
* a JSON string in `message` under the same `worker_fields` key, so
|
|
131
|
+
* `parse_json(tostring(message)).worker_fields.x` — the access path every
|
|
132
|
+
* pre-2026-08-08 row and every legacy query already uses — keeps working.
|
|
133
|
+
*
|
|
134
|
+
* Deliberately process-level and one-way. A shipper flips it on the first
|
|
135
|
+
* column-limit rejection and every later record in that process packs the same
|
|
136
|
+
* way; nothing flips it back, because a dataset does not grow a free column while
|
|
137
|
+
* the process is running. Fresh processes start in `map` again, so the moment the
|
|
138
|
+
* dataset has headroom the next deploy silently returns to the better shape with
|
|
139
|
+
* no code change.
|
|
140
|
+
*
|
|
141
|
+
* IT IS A LAST RESORT, NOT A HOME. While degraded, every monitor reading
|
|
142
|
+
* `['worker_fields']['x']` resolves to null — the silent-blinding failure this
|
|
143
|
+
* module's header warns about. Readers that must survive both shapes coalesce
|
|
144
|
+
* them; see `docs/observability/queries.apl.md`. Losing the flat shape beats
|
|
145
|
+
* losing the record.
|
|
146
|
+
*/
|
|
147
|
+
export type AxiomOverflowMode = "map" | "message";
|
|
148
|
+
/** The mode the next `packAxiomLogRecord` call will use. */
|
|
149
|
+
export declare function getAxiomOverflowMode(): AxiomOverflowMode;
|
|
150
|
+
/**
|
|
151
|
+
* Does this Axiom 400 body mean "the map field will not fit"?
|
|
152
|
+
*
|
|
153
|
+
* Matched on the stable half of Axiom's message rather than the whole string: the
|
|
154
|
+
* field name and the limit number both vary, and a shipper that only recognised
|
|
155
|
+
* `worker_fields`/`1025` verbatim would go back to dropping everything the day
|
|
156
|
+
* either changed.
|
|
157
|
+
*/
|
|
158
|
+
export declare function isColumnLimitRejection(body: string | null | undefined): boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Switches this process to `message` packing.
|
|
161
|
+
*
|
|
162
|
+
* Returns true only on the transition, so a caller can retry the rejected batch
|
|
163
|
+
* and log the degradation exactly once instead of once per batch forever.
|
|
164
|
+
*/
|
|
165
|
+
export declare function degradeAxiomOverflowToMessage(): boolean;
|
|
166
|
+
/** Test-only: restores the default so cases cannot leak mode into each other. */
|
|
167
|
+
export declare function resetAxiomOverflowMode(): void;
|
|
69
168
|
/**
|
|
70
169
|
* Bound on the serialized overflow, so one pathological record cannot push a whole
|
|
71
170
|
* batch past Axiom's request limits. 64 KB is far above any real log line and far
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
* under `worker_fields`. New instrumentation can then be added freely without
|
|
20
20
|
* anyone having to remember that a new key can take down log shipping globally.
|
|
21
21
|
*
|
|
22
|
+
* WITH ONE HOLE, found the hard way on 2026-08-15. Packing protects the dataset
|
|
23
|
+
* from the CONTENTS, not from the CONTAINER: `worker_fields` is itself a column,
|
|
24
|
+
* and a dataset with no free column cannot have it. Registering the map field on
|
|
25
|
+
* the already-full `oxygen-logs` therefore turned a slow leak into a total ingest
|
|
26
|
+
* outage — 41,372 records/day, ~60x worse than the leak it replaced. `AxiomOverflowMode`
|
|
27
|
+
* below is the escape hatch; freeing a column slot is the actual fix.
|
|
28
|
+
*
|
|
22
29
|
* THE ALLOWLIST IS A LOAD-BEARING CONTRACT IN BOTH DIRECTIONS.
|
|
23
30
|
*
|
|
24
31
|
* - A field that a monitor, dashboard, or checked-in query reads **flat** must be
|
|
@@ -34,6 +41,39 @@
|
|
|
34
41
|
* Both directions are asserted by `scripts/ci/log-field-budget.mjs` against the
|
|
35
42
|
* real seed and dashboard JSON, so a change to either side fails loudly instead
|
|
36
43
|
* of quietly blinding a monitor.
|
|
44
|
+
*
|
|
45
|
+
* THERE IS A THIRD DIRECTION, and it is the one that took production down: a
|
|
46
|
+
* name in this set that the DATASET has no column for. Allowlisted means flat by
|
|
47
|
+
* definition, so such a name cannot fall back into the map — it demands a
|
|
48
|
+
* brand-new column, and a dataset with no slot to allocate one rejects the whole
|
|
49
|
+
* ingest batch with HTTP 400. So membership here is not "a field we read flat";
|
|
50
|
+
* it is "a field we read flat AND the dataset can actually hold". The live half
|
|
51
|
+
* of `scripts/ci/log-field-budget.mjs` checks that second half against Axiom.
|
|
52
|
+
*
|
|
53
|
+
* REMOVED 2026-08-15 — `chunk_id`, `item_id`, `digest`. Prod `oxygen-logs` had no
|
|
54
|
+
* column for any of the three and no slot left to create one, so every record
|
|
55
|
+
* carrying one was rejected 400 and DROPPED. That was still ~116 ship failures
|
|
56
|
+
* per 25 minutes after the `worker_fields` map field itself had been repaired on
|
|
57
|
+
* the Axiom side, and it hit the Next.js server-error path hardest, through
|
|
58
|
+
* `digest`.
|
|
59
|
+
*
|
|
60
|
+
* The warning above — that dropping a name silently moves it into the overflow
|
|
61
|
+
* and can zero a query that reads it flat — applies to exactly this removal, so
|
|
62
|
+
* it was CHECKED rather than assumed, in both directions:
|
|
63
|
+
*
|
|
64
|
+
* - Nothing reads them FLAT. Every `aplQuery` in docs/observability/monitors.json
|
|
65
|
+
* was parsed and all dashboard JSON grepped: zero matches for the three as flat
|
|
66
|
+
* fields (the apparent hits are the English word "digest" in description prose).
|
|
67
|
+
* - Nothing reads them out of `worker_fields` either, so the move breaks no query
|
|
68
|
+
* in the opposite direction.
|
|
69
|
+
*
|
|
70
|
+
* Packed, they stay queryable as `['worker_fields']['digest']` — a longer access
|
|
71
|
+
* path, the same value, still typed. `digest` matters most: it is the only join
|
|
72
|
+
* key from a browser error report back to its `next.request_error` row (see the
|
|
73
|
+
* error-shape group below), and that correlation still works through the map
|
|
74
|
+
* field. The trade is explicit and worth stating plainly: keeping them flat meant
|
|
75
|
+
* the ENTIRE record was dropped, which loses the digest anyway plus everything
|
|
76
|
+
* travelling with it. Packed is strictly better than dropped.
|
|
37
77
|
*/
|
|
38
78
|
/**
|
|
39
79
|
* Fields that stay top-level in `oxygen-logs`.
|
|
@@ -66,8 +106,9 @@ export const AXIOM_STABLE_FIELDS = new Set([
|
|
|
66
106
|
"operation",
|
|
67
107
|
"tenant_database_id",
|
|
68
108
|
"run_id",
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
// `chunk_id` and `item_id` used to sit here. Removed 2026-08-15 because prod
|
|
110
|
+
// had no column for either and no slot to make one — see the header. They now
|
|
111
|
+
// ride in `worker_fields`, where they cost no column at all.
|
|
71
112
|
"worker_id",
|
|
72
113
|
"conversation_id",
|
|
73
114
|
// --- Error shape (errorFields) -------------------------------------------
|
|
@@ -75,9 +116,11 @@ export const AXIOM_STABLE_FIELDS = new Set([
|
|
|
75
116
|
"error_name",
|
|
76
117
|
"error_message",
|
|
77
118
|
"error_code",
|
|
78
|
-
// Next.js stamps
|
|
79
|
-
// that joins a browser error report back to its
|
|
80
|
-
|
|
119
|
+
// `digest` used to sit here. Next.js stamps it on a serialized server error and
|
|
120
|
+
// it is still the only value that joins a browser error report back to its
|
|
121
|
+
// `next.request_error` row — but prod could not allocate a column for it, so a
|
|
122
|
+
// flat `digest` meant the whole record died and the join key with it. Removed
|
|
123
|
+
// 2026-08-15; the join now reads `tostring(['worker_fields']['digest'])`.
|
|
81
124
|
// --- Primitive identity --------------------------------------------------
|
|
82
125
|
"workflow_id",
|
|
83
126
|
"workflow_run_id",
|
|
@@ -195,8 +238,51 @@ export const AXIOM_STABLE_FIELDS = new Set([
|
|
|
195
238
|
* ordinary flat fields and the budget is defeated silently. Create it with:
|
|
196
239
|
* POST /v2/datasets/{dataset}/mapfields {"name": "worker_fields"}
|
|
197
240
|
* `scripts/ops/axiom-mapfields.mjs` does this idempotently for every log dataset.
|
|
241
|
+
*
|
|
242
|
+
* AND IT MUST BE ABLE TO EXIST — see `AxiomOverflowMode` below. A map field still
|
|
243
|
+
* occupies ONE column slot, so registering it on an already-full dataset does not
|
|
244
|
+
* create it; Axiom rejects every ingest that carries it instead.
|
|
198
245
|
*/
|
|
199
246
|
export const AXIOM_OVERFLOW_MAP_FIELD = "worker_fields";
|
|
247
|
+
/**
|
|
248
|
+
* The pre-existing column the overflow degrades into when the map field cannot be
|
|
249
|
+
* created. Owned by the Vercel drain (it is where Lane B parks the whole app JSON),
|
|
250
|
+
* which is exactly why it is safe here: it already exists on every log dataset, so
|
|
251
|
+
* writing it costs no new column on a dataset that has none left to give.
|
|
252
|
+
*/
|
|
253
|
+
const AXIOM_OVERFLOW_MESSAGE_FIELD = "message";
|
|
254
|
+
let overflowMode = "map";
|
|
255
|
+
/** The mode the next `packAxiomLogRecord` call will use. */
|
|
256
|
+
export function getAxiomOverflowMode() {
|
|
257
|
+
return overflowMode;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Does this Axiom 400 body mean "the map field will not fit"?
|
|
261
|
+
*
|
|
262
|
+
* Matched on the stable half of Axiom's message rather than the whole string: the
|
|
263
|
+
* field name and the limit number both vary, and a shipper that only recognised
|
|
264
|
+
* `worker_fields`/`1025` verbatim would go back to dropping everything the day
|
|
265
|
+
* either changed.
|
|
266
|
+
*/
|
|
267
|
+
export function isColumnLimitRejection(body) {
|
|
268
|
+
return typeof body === "string" && /would exceed the column limit/i.test(body);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Switches this process to `message` packing.
|
|
272
|
+
*
|
|
273
|
+
* Returns true only on the transition, so a caller can retry the rejected batch
|
|
274
|
+
* and log the degradation exactly once instead of once per batch forever.
|
|
275
|
+
*/
|
|
276
|
+
export function degradeAxiomOverflowToMessage() {
|
|
277
|
+
if (overflowMode === "message")
|
|
278
|
+
return false;
|
|
279
|
+
overflowMode = "message";
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
/** Test-only: restores the default so cases cannot leak mode into each other. */
|
|
283
|
+
export function resetAxiomOverflowMode() {
|
|
284
|
+
overflowMode = "map";
|
|
285
|
+
}
|
|
200
286
|
/**
|
|
201
287
|
* Bound on the serialized overflow, so one pathological record cannot push a whole
|
|
202
288
|
* batch past Axiom's request limits. 64 KB is far above any real log line and far
|
|
@@ -246,17 +332,26 @@ export function packAxiomLogRecord(record) {
|
|
|
246
332
|
}
|
|
247
333
|
if (Object.keys(overflow).length === 0)
|
|
248
334
|
return stable;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
335
|
+
const bounded = JSON.stringify(overflow).length <= MAX_PACKED_MESSAGE_LENGTH
|
|
336
|
+
? overflow
|
|
337
|
+
: {
|
|
338
|
+
// Never silently truncate to nothing: the key NAMES alone are usually
|
|
339
|
+
// enough to identify which call site produced the oversized record.
|
|
340
|
+
_truncated: true,
|
|
341
|
+
_keys: Object.keys(overflow),
|
|
342
|
+
};
|
|
343
|
+
if (overflowMode === "map") {
|
|
344
|
+
// Nested under the map field as a real object — NOT stringified. Axiom stores
|
|
345
|
+
// map contents without allocating a column per key, and keeps them queryable
|
|
346
|
+
// and typed.
|
|
347
|
+
stable[AXIOM_OVERFLOW_MAP_FIELD] = bounded;
|
|
348
|
+
return stable;
|
|
349
|
+
}
|
|
350
|
+
// Degraded: the map field has no column to live in. Same payload, same inner
|
|
351
|
+
// key, stringified into a column that already exists. A caller's own `message`
|
|
352
|
+
// field is not lost — it is non-allowlisted, so it is inside `bounded` already.
|
|
353
|
+
stable[AXIOM_OVERFLOW_MESSAGE_FIELD] = JSON.stringify({
|
|
354
|
+
[AXIOM_OVERFLOW_MAP_FIELD]: bounded,
|
|
355
|
+
});
|
|
261
356
|
return stable;
|
|
262
357
|
}
|
|
@@ -51,7 +51,7 @@ export * from "./dnc-identities.js";
|
|
|
51
51
|
export * from "./table-limits.js";
|
|
52
52
|
export * from "./log.js";
|
|
53
53
|
export * from "./axiom-field-budget.js";
|
|
54
|
-
export { sanitizeLogFields } from "./redaction.js";
|
|
54
|
+
export { redactSecretsInString, sanitizeLogFields } from "./redaction.js";
|
|
55
55
|
export * from "./provider-request-outcomes.js";
|
|
56
56
|
export * from "./schedule-label.js";
|
|
57
57
|
export * from "./social-capabilities.js";
|
|
@@ -55,7 +55,14 @@ export * from "./axiom-field-budget.js";
|
|
|
55
55
|
// their field names against the REAL log sanitizer — the unanchored
|
|
56
56
|
// SECRET_KEY_PATTERN redacts any name containing "token", which mocked loggers
|
|
57
57
|
// cannot catch. Not a license to pre-sanitize outside log().
|
|
58
|
-
|
|
58
|
+
// `redactSecretsInString` joins it for the MCP `$mcp_parameters`/`$mcp_response`
|
|
59
|
+
// capture (apps/web/src/lib/analytics/mcp-analytics.ts): that path must scrub
|
|
60
|
+
// credential SUBSTRINGS out of tool arguments while KEEPING the business
|
|
61
|
+
// payload, so it cannot reuse sanitizeLogFields — whose OMITTED_KEY_PATTERN
|
|
62
|
+
// blanks `rows`/`input`/`body` wholesale. There is exactly one implementation of
|
|
63
|
+
// the Bearer / sk- / DB-URL scrub and this keeps it that way; duplicating those
|
|
64
|
+
// three regexes in apps/web is what this export exists to prevent.
|
|
65
|
+
export { redactSecretsInString, sanitizeLogFields } from "./redaction.js";
|
|
59
66
|
export * from "./provider-request-outcomes.js";
|
|
60
67
|
export * from "./schedule-label.js";
|
|
61
68
|
export * from "./social-capabilities.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.720.7";
|
|
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:
|