@oxygen-agent/cli 1.717.11 → 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 CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.717.11
37
+ Version: 1.720.7
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/mailbox-compatibility.`, { exitCode: 1 });
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. Creates the canonical Plain support Thread; transcripts are never attached unless explicitly requested.")
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/mailbox-compatibility",
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/mailbox-compatibility\n")
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/mailbox-compatibility\n")
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/mailbox-compatibility\n")
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/mailbox-compatibility\n")
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
- "chunk_id",
70
- "item_id",
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 `digest` on a serialized server error; it is the only value
79
- // that joins a browser error report back to its `next.request_error` row.
80
- "digest",
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
- // Nested under the map field as a real object — NOT stringified. Axiom stores
250
- // map contents without allocating a column per key, and keeps them queryable
251
- // and typed.
252
- stable[AXIOM_OVERFLOW_MAP_FIELD] =
253
- JSON.stringify(overflow).length <= MAX_PACKED_MESSAGE_LENGTH
254
- ? overflow
255
- : {
256
- // Never silently truncate to nothing: the key NAMES alone are usually
257
- // enough to identify which call site produced the oversized record.
258
- _truncated: true,
259
- _keys: Object.keys(overflow),
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
- export { sanitizeLogFields } from "./redaction.js";
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,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.717.11";
1
+ export declare const OXYGEN_VERSION = "1.720.7";
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";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.717.11";
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.717.11",
3
+ "version": "1.720.7",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",