@oxygen-agent/cli 1.717.11 → 1.739.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-manifest.js +27 -12
- package/dist/index.js +243 -72
- package/dist/skills.js +2 -2
- 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/cli-result.js +2 -0
- package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/crm-activity-events.js +5 -0
- package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +27 -0
- package/node_modules/@oxygen/shared/dist/future-signup-events.js +90 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +320 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1233 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/index.js +14 -1
- package/node_modules/@oxygen/shared/dist/notetaker-events.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/notetaker-events.js +23 -0
- package/node_modules/@oxygen/shared/dist/plain-support-events.d.ts +40 -0
- package/node_modules/@oxygen/shared/dist/plain-support-events.js +43 -0
- package/node_modules/@oxygen/shared/dist/redaction.js +6 -1
- package/node_modules/@oxygen/shared/dist/user-capability-routing.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +23 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/webhook-headers.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/webhook-headers.js +48 -0
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +31 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +112 -1
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +7 -5
- package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +9 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.js +409 -7
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +513 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +104 -1
- package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +2 -0
- package/node_modules/@oxygen/workflows/dist/graph/params.js +15 -0
- package/node_modules/@oxygen/workflows/dist/graph/remap.js +12 -3
- package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +9 -0
- package/node_modules/@oxygen/workflows/dist/graph/topology.js +33 -1
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +123 -16
- package/node_modules/@oxygen/workflows/dist/graph/types.js +45 -0
- package/node_modules/@oxygen/workflows/dist/index.d.ts +94 -2
- package/node_modules/@oxygen/workflows/dist/index.js +169 -26
- package/node_modules/@oxygen/workflows/dist/portable.js +3 -1
- package/package.json +1 -1
package/dist/skills.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { OxygenError,
|
|
5
|
+
import { OxygenError, inferUserCapabilityRoute, serializeCapabilityRoute, toFailure, } from "@oxygen/shared";
|
|
6
6
|
import { defaultApiUrl, loadCredentials, normalizeApiUrl } from "./credentials.js";
|
|
7
7
|
import { requestOxygen } from "./http-client.js";
|
|
8
8
|
import { isRecord, readOption } from "./util.js";
|
|
@@ -98,7 +98,7 @@ export async function searchAgentSkills(query, options, runtime = {}) {
|
|
|
98
98
|
}
|
|
99
99
|
const normalized = query.trim();
|
|
100
100
|
const tokens = tokenizeSkillQuery(normalized);
|
|
101
|
-
const route =
|
|
101
|
+
const route = inferUserCapabilityRoute(normalized);
|
|
102
102
|
const requestedLimit = Number.parseInt(readOption(options.limit) ?? "5", 10);
|
|
103
103
|
const limit = Number.isFinite(requestedLimit) ? Math.min(10, Math.max(1, requestedLimit)) : 5;
|
|
104
104
|
const matches = index.skills
|
|
@@ -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
|
}
|
|
@@ -70,6 +70,8 @@ export function exitCodeForErrorCode(code) {
|
|
|
70
70
|
}
|
|
71
71
|
if (code === "not_found" || code.endsWith("_not_found"))
|
|
72
72
|
return 4;
|
|
73
|
+
if (code.endsWith("_requires_approval"))
|
|
74
|
+
return 7;
|
|
73
75
|
if (code === "timeout" || code.endsWith("_timeout"))
|
|
74
76
|
return 8;
|
|
75
77
|
if (code.startsWith("invalid_")
|
|
@@ -74,6 +74,26 @@ export declare const CRM_ACTIVITY_EVENT_DEFINITIONS: readonly [{
|
|
|
74
74
|
readonly key: "signup";
|
|
75
75
|
readonly label: "Signup";
|
|
76
76
|
readonly group: "Signals";
|
|
77
|
+
}, {
|
|
78
|
+
readonly key: "product_activated";
|
|
79
|
+
readonly label: "Product activated";
|
|
80
|
+
readonly group: "Product lifecycle";
|
|
81
|
+
}, {
|
|
82
|
+
readonly key: "trial_started";
|
|
83
|
+
readonly label: "Free trial started";
|
|
84
|
+
readonly group: "Product lifecycle";
|
|
85
|
+
}, {
|
|
86
|
+
readonly key: "plan_paid";
|
|
87
|
+
readonly label: "Paid plan started";
|
|
88
|
+
readonly group: "Product lifecycle";
|
|
89
|
+
}, {
|
|
90
|
+
readonly key: "customer_lost";
|
|
91
|
+
readonly label: "Customer lost";
|
|
92
|
+
readonly group: "Product lifecycle";
|
|
93
|
+
}, {
|
|
94
|
+
readonly key: "workspace_deleted";
|
|
95
|
+
readonly label: "Workspace deleted";
|
|
96
|
+
readonly group: "Product lifecycle";
|
|
77
97
|
}, {
|
|
78
98
|
readonly key: "website_visit";
|
|
79
99
|
readonly label: "Website visit";
|
|
@@ -24,6 +24,11 @@ export const CRM_ACTIVITY_EVENT_DEFINITIONS = [
|
|
|
24
24
|
{ key: "sync_pushed", label: "CRM sync pushed", group: "CRM operations" },
|
|
25
25
|
{ key: "sync_pulled", label: "CRM sync pulled", group: "CRM operations" },
|
|
26
26
|
{ key: "signup", label: "Signup", group: "Signals" },
|
|
27
|
+
{ key: "product_activated", label: "Product activated", group: "Product lifecycle" },
|
|
28
|
+
{ key: "trial_started", label: "Free trial started", group: "Product lifecycle" },
|
|
29
|
+
{ key: "plan_paid", label: "Paid plan started", group: "Product lifecycle" },
|
|
30
|
+
{ key: "customer_lost", label: "Customer lost", group: "Product lifecycle" },
|
|
31
|
+
{ key: "workspace_deleted", label: "Workspace deleted", group: "Product lifecycle" },
|
|
27
32
|
{ key: "website_visit", label: "Website visit", group: "Signals" },
|
|
28
33
|
{ key: "profile_view", label: "Profile view", group: "Signals" },
|
|
29
34
|
{ key: "post_reaction", label: "Post reaction", group: "Signals" },
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const FUTURE_SIGNUP_CONTRACT_VERSION = "future_signup_crm_activation.v1";
|
|
2
|
+
export declare const FUTURE_SIGNUP_ACTIVATION_TAXONOMY_VERSION = "activation_commands.v1";
|
|
3
|
+
export declare const FUTURE_SIGNUP_LIFECYCLE_CONTRACT_VERSION = "future_signup_lifecycle.v1";
|
|
4
|
+
export declare const FUTURE_SIGNUP_LIFECYCLE_PROJECTION_CONTRACT_VERSION = "future_signup_lifecycle_projection.v1";
|
|
5
|
+
export declare const FUTURE_SIGNUP_CRM_DESTINATION = "oxygen_company_crm";
|
|
6
|
+
export declare const FUTURE_SIGNUP_ACTIVATION_COMMANDS: readonly ["agent create", "crm activity log", "crm assert", "crm object add-attr", "crm object create", "crm relationships define", "crm relationships upsert", "knowledge page upsert", "publishing posts create", "sequences create", "tables create", "tables duplicate", "tables import-csv", "tables import-file", "tables import-staged", "tables import-url", "tables insert", "tables relate", "tables update", "tables upsert", "workflows apply"];
|
|
7
|
+
export declare function isFutureSignupActivationCommand(command: string): boolean;
|
|
8
|
+
export declare function futureSignupJourneyId(clerkUserId: string): string;
|
|
9
|
+
export declare function futureSignupFactId(clerkUserId: string): string;
|
|
10
|
+
export declare function futureSignupOperationFactId(operationEventId: string): string;
|
|
11
|
+
export declare function futureSignupActivationFactId(clerkUserId: string): string;
|
|
12
|
+
export declare function futureSignupTrialStartedFactId(stripeSubscriptionId: string): string;
|
|
13
|
+
export declare function futureSignupPaidFactId(stripeInvoiceId: string): string;
|
|
14
|
+
export declare function futureSignupLostFactId(stripeSubscriptionId: string, lossKind: "paid_churn" | "trial_non_conversion"): string;
|
|
15
|
+
export declare function futureSignupSubscriptionEndedFactId(stripeSubscriptionId: string, terminationKind: "replaced" | "duplicate" | "unqualified"): string;
|
|
16
|
+
export declare function futureSignupWorkspaceDeletedFactId(clerkOrgId: string): string;
|
|
17
|
+
/** Immutable cause for membership, completeness, or Company-identity projection changes. */
|
|
18
|
+
export declare function futureSignupLifecycleProjectionRefreshFactId(clerkOrgId: string, sourceEventId: string): string;
|
|
19
|
+
export declare function futureSignupLifecycleProjectionFactId(sourceFactId: string): string;
|
|
20
|
+
export declare function futureSignupLifecycleAggregateId(clerkOrgId: string): string;
|
|
21
|
+
export declare function futureSignupLifecycleEventId(factId: string, factRevision: number): string;
|
|
22
|
+
export declare function isAtOrAfterFutureSignupCutover(input: {
|
|
23
|
+
clerkUserId: string;
|
|
24
|
+
signupAt: Date;
|
|
25
|
+
cutoverAt: Date;
|
|
26
|
+
clerkUserIdTiebreaker?: string;
|
|
27
|
+
}): boolean;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const FUTURE_SIGNUP_CONTRACT_VERSION = "future_signup_crm_activation.v1";
|
|
2
|
+
export const FUTURE_SIGNUP_ACTIVATION_TAXONOMY_VERSION = "activation_commands.v1";
|
|
3
|
+
export const FUTURE_SIGNUP_LIFECYCLE_CONTRACT_VERSION = "future_signup_lifecycle.v1";
|
|
4
|
+
export const FUTURE_SIGNUP_LIFECYCLE_PROJECTION_CONTRACT_VERSION = "future_signup_lifecycle_projection.v1";
|
|
5
|
+
export const FUTURE_SIGNUP_CRM_DESTINATION = "oxygen_company_crm";
|
|
6
|
+
export const FUTURE_SIGNUP_ACTIVATION_COMMANDS = [
|
|
7
|
+
"agent create",
|
|
8
|
+
"crm activity log",
|
|
9
|
+
"crm assert",
|
|
10
|
+
"crm object add-attr",
|
|
11
|
+
"crm object create",
|
|
12
|
+
"crm relationships define",
|
|
13
|
+
"crm relationships upsert",
|
|
14
|
+
"knowledge page upsert",
|
|
15
|
+
"publishing posts create",
|
|
16
|
+
"sequences create",
|
|
17
|
+
"tables create",
|
|
18
|
+
"tables duplicate",
|
|
19
|
+
"tables import-csv",
|
|
20
|
+
"tables import-file",
|
|
21
|
+
"tables import-staged",
|
|
22
|
+
"tables import-url",
|
|
23
|
+
"tables insert",
|
|
24
|
+
"tables relate",
|
|
25
|
+
"tables update",
|
|
26
|
+
"tables upsert",
|
|
27
|
+
"workflows apply",
|
|
28
|
+
];
|
|
29
|
+
const ACTIVATION_COMMAND_SET = new Set(FUTURE_SIGNUP_ACTIVATION_COMMANDS);
|
|
30
|
+
export function isFutureSignupActivationCommand(command) {
|
|
31
|
+
return ACTIVATION_COMMAND_SET.has(command.trim().toLowerCase());
|
|
32
|
+
}
|
|
33
|
+
export function futureSignupJourneyId(clerkUserId) {
|
|
34
|
+
return `future_signup:v1:clerk_user:${requiredId(clerkUserId, "clerkUserId")}`;
|
|
35
|
+
}
|
|
36
|
+
export function futureSignupFactId(clerkUserId) {
|
|
37
|
+
return futureSignupJourneyId(clerkUserId);
|
|
38
|
+
}
|
|
39
|
+
export function futureSignupOperationFactId(operationEventId) {
|
|
40
|
+
return `product_operation:v1:${requiredId(operationEventId, "operationEventId")}`;
|
|
41
|
+
}
|
|
42
|
+
export function futureSignupActivationFactId(clerkUserId) {
|
|
43
|
+
return `product_activated:v1:${futureSignupFactId(clerkUserId)}`;
|
|
44
|
+
}
|
|
45
|
+
export function futureSignupTrialStartedFactId(stripeSubscriptionId) {
|
|
46
|
+
return `future_signup_lifecycle:trial_started:v1:${requiredId(stripeSubscriptionId, "stripeSubscriptionId")}`;
|
|
47
|
+
}
|
|
48
|
+
export function futureSignupPaidFactId(stripeInvoiceId) {
|
|
49
|
+
return `future_signup_lifecycle:paid:v1:${requiredId(stripeInvoiceId, "stripeInvoiceId")}`;
|
|
50
|
+
}
|
|
51
|
+
export function futureSignupLostFactId(stripeSubscriptionId, lossKind) {
|
|
52
|
+
return `future_signup_lifecycle:lost:v1:${requiredId(stripeSubscriptionId, "stripeSubscriptionId")}:${lossKind}`;
|
|
53
|
+
}
|
|
54
|
+
export function futureSignupSubscriptionEndedFactId(stripeSubscriptionId, terminationKind) {
|
|
55
|
+
return `future_signup_lifecycle:subscription_ended:v1:${requiredId(stripeSubscriptionId, "stripeSubscriptionId")}:${terminationKind}`;
|
|
56
|
+
}
|
|
57
|
+
export function futureSignupWorkspaceDeletedFactId(clerkOrgId) {
|
|
58
|
+
return `future_signup_lifecycle:workspace_deleted:v1:${requiredId(clerkOrgId, "clerkOrgId")}`;
|
|
59
|
+
}
|
|
60
|
+
/** Immutable cause for membership, completeness, or Company-identity projection changes. */
|
|
61
|
+
export function futureSignupLifecycleProjectionRefreshFactId(clerkOrgId, sourceEventId) {
|
|
62
|
+
return `future_signup_lifecycle:projection_refreshed:v1:${requiredId(clerkOrgId, "clerkOrgId")}:${requiredId(sourceEventId, "sourceEventId")}`;
|
|
63
|
+
}
|
|
64
|
+
export function futureSignupLifecycleProjectionFactId(sourceFactId) {
|
|
65
|
+
return `lifecycle_projection:v1:${requiredId(sourceFactId, "sourceFactId")}`;
|
|
66
|
+
}
|
|
67
|
+
export function futureSignupLifecycleAggregateId(clerkOrgId) {
|
|
68
|
+
return `lifecycle_aggregate:v1:clerk_org:${requiredId(clerkOrgId, "clerkOrgId")}`;
|
|
69
|
+
}
|
|
70
|
+
export function futureSignupLifecycleEventId(factId, factRevision) {
|
|
71
|
+
if (!Number.isInteger(factRevision) || factRevision < 1) {
|
|
72
|
+
throw new TypeError("factRevision must be a positive integer.");
|
|
73
|
+
}
|
|
74
|
+
return `${requiredId(factId, "factId")}:r${factRevision}`;
|
|
75
|
+
}
|
|
76
|
+
export function isAtOrAfterFutureSignupCutover(input) {
|
|
77
|
+
const signupMs = input.signupAt.getTime();
|
|
78
|
+
const cutoverMs = input.cutoverAt.getTime();
|
|
79
|
+
if (!Number.isFinite(signupMs) || !Number.isFinite(cutoverMs))
|
|
80
|
+
return false;
|
|
81
|
+
if (signupMs !== cutoverMs)
|
|
82
|
+
return signupMs > cutoverMs;
|
|
83
|
+
return input.clerkUserId.localeCompare(input.clerkUserIdTiebreaker ?? "") >= 0;
|
|
84
|
+
}
|
|
85
|
+
function requiredId(value, field) {
|
|
86
|
+
const normalized = value.trim();
|
|
87
|
+
if (!normalized)
|
|
88
|
+
throw new TypeError(`${field} is required.`);
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|