@oxygen-agent/cli 1.164.30 → 1.177.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +1195 -507
- package/dist/run-wait.d.ts +23 -0
- package/dist/run-wait.js +57 -0
- package/node_modules/@oxygen/shared/dist/cell-format.d.ts +2 -14
- package/node_modules/@oxygen/shared/dist/cell-format.js +3 -10
- package/node_modules/@oxygen/shared/dist/cli-envelope.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/cli-result.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +52 -0
- package/node_modules/@oxygen/shared/dist/credit-guidance.d.ts +0 -1
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +1 -1
- package/node_modules/@oxygen/shared/dist/file-import.js +1 -1
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -39
- package/node_modules/@oxygen/shared/dist/index.js +2 -44
- package/node_modules/@oxygen/shared/dist/log.d.ts +0 -1
- package/node_modules/@oxygen/shared/dist/log.js +8 -3
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +0 -3
- package/node_modules/@oxygen/shared/dist/object-storage.js +1 -24
- package/node_modules/@oxygen/shared/dist/search-vocab.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/search-vocab.js +151 -0
- package/node_modules/@oxygen/shared/dist/select-options.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/select-options.js +121 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +1 -1
- package/node_modules/@oxygen/shared/dist/sql-error.d.ts +0 -6
- package/node_modules/@oxygen/shared/dist/sql-error.js +67 -58
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +0 -1
- package/node_modules/@oxygen/shared/dist/telemetry.js +23 -18
- 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/worker-failures-queue.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/worker-failures-queue.js +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Shared search-planner vocabulary + stateless text helpers.
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for the constraint vocabulary and pure text helpers
|
|
4
|
+
// consumed by BOTH the legacy web search planners
|
|
5
|
+
// (apps/web/src/lib/company-search-plan.ts, people-search-plan.ts) and the
|
|
6
|
+
// @oxygen/routing classifier/compiler (via packages/routing/src/vocab.ts,
|
|
7
|
+
// which layers its named tuning deltas on top of these bases). Keeping one copy
|
|
8
|
+
// here is what stops the two surfaces from silently drifting (OXY-414).
|
|
9
|
+
//
|
|
10
|
+
// Everything here is a pure function or a static table: no Date.now, no
|
|
11
|
+
// randomness, no I/O — safe to import from any layer.
|
|
12
|
+
export function normalizeText(value) {
|
|
13
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
14
|
+
}
|
|
15
|
+
export function escapeRegExp(value) {
|
|
16
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
|
+
}
|
|
18
|
+
export function extractUrls(value) {
|
|
19
|
+
return [...new Set((value.match(/https?:\/\/[^\s)]+/gi) ?? []).map((url) => url.replace(/[.,;]+$/, "")))];
|
|
20
|
+
}
|
|
21
|
+
export function extractDomains(value) {
|
|
22
|
+
const domains = value.match(/\b(?:[a-z0-9-]+\.)+[a-z]{2,}\b/gi) ?? [];
|
|
23
|
+
return [...new Set(domains.map((domain) => domain.toLowerCase()).filter((domain) => !domain.includes("@")))];
|
|
24
|
+
}
|
|
25
|
+
export function parseMoney(amount, unit) {
|
|
26
|
+
const value = Number(amount);
|
|
27
|
+
const normalized = (unit ?? "").toLowerCase();
|
|
28
|
+
if (normalized.startsWith("b"))
|
|
29
|
+
return Math.round(value * 1_000_000_000);
|
|
30
|
+
if (normalized.startsWith("m"))
|
|
31
|
+
return Math.round(value * 1_000_000);
|
|
32
|
+
if (normalized.startsWith("k"))
|
|
33
|
+
return Math.round(value * 1_000);
|
|
34
|
+
return Math.round(value);
|
|
35
|
+
}
|
|
36
|
+
// ISO 3166-1 alpha-2 handling is backed by the platform's Intl region data instead of
|
|
37
|
+
// a hardcoded country table, so every assigned code validates and renders a display name.
|
|
38
|
+
const REGION_DISPLAY = new Intl.DisplayNames(["en"], { type: "region" });
|
|
39
|
+
export function regionDisplayName(code) {
|
|
40
|
+
if (!/^[A-Z]{2}$/.test(code))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
const name = REGION_DISPLAY.of(code);
|
|
44
|
+
return name && name !== code ? name : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Lowercased country display name -> ISO code, generated from Intl region data at module load.
|
|
51
|
+
export const COUNTRY_NAME_TO_ISO = (() => {
|
|
52
|
+
const map = new Map();
|
|
53
|
+
for (let first = 65; first <= 90; first += 1) {
|
|
54
|
+
for (let second = 65; second <= 90; second += 1) {
|
|
55
|
+
const code = String.fromCharCode(first) + String.fromCharCode(second);
|
|
56
|
+
const name = regionDisplayName(code);
|
|
57
|
+
if (name)
|
|
58
|
+
map.set(name.toLowerCase(), code);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return map;
|
|
62
|
+
})();
|
|
63
|
+
// Common aliases and demonyms that Intl display names do not cover. Keys are lowercase prompt phrases.
|
|
64
|
+
export const COUNTRY_ALIAS_TO_ISO = {
|
|
65
|
+
"usa": "US", "u.s.": "US", "u.s.a.": "US", "us": "US", "america": "US", "american": "US", "the states": "US",
|
|
66
|
+
"uk": "GB", "u.k.": "GB", "britain": "GB", "great britain": "GB", "british": "GB", "england": "GB",
|
|
67
|
+
"german": "DE", "french": "FR", "dutch": "NL", "the netherlands": "NL", "holland": "NL",
|
|
68
|
+
"canadian": "CA", "australian": "AU", "austrian": "AT", "swiss": "CH", "swedish": "SE", "danish": "DK",
|
|
69
|
+
"norwegian": "NO", "finnish": "FI", "spanish": "ES", "italian": "IT", "portuguese": "PT", "polish": "PL",
|
|
70
|
+
"irish": "IE", "belgian": "BE", "brazilian": "BR", "mexican": "MX", "indian": "IN", "japanese": "JP",
|
|
71
|
+
"korean": "KR", "south korean": "KR", "chinese": "CN", "singaporean": "SG", "israeli": "IL",
|
|
72
|
+
"emirati": "AE", "uae": "AE", "saudi": "SA", "south african": "ZA", "nordic": "SE", "scandinavian": "SE",
|
|
73
|
+
};
|
|
74
|
+
// Major business-hub cities -> country code, used as geo hints when no country is named.
|
|
75
|
+
export const CITY_TO_ISO = {
|
|
76
|
+
"san francisco": "US", "new york": "US", "nyc": "US", "boston": "US", "austin": "US", "seattle": "US",
|
|
77
|
+
"los angeles": "US", "chicago": "US", "miami": "US", "denver": "US", "atlanta": "US",
|
|
78
|
+
"london": "GB", "manchester": "GB", "berlin": "DE", "munich": "DE", "hamburg": "DE", "cologne": "DE",
|
|
79
|
+
"paris": "FR", "amsterdam": "NL", "rotterdam": "NL", "stockholm": "SE", "copenhagen": "DK", "oslo": "NO",
|
|
80
|
+
"helsinki": "FI", "zurich": "CH", "geneva": "CH", "vienna": "AT", "madrid": "ES", "barcelona": "ES",
|
|
81
|
+
"milan": "IT", "lisbon": "PT", "dublin": "IE", "brussels": "BE", "warsaw": "PL", "toronto": "CA",
|
|
82
|
+
"vancouver": "CA", "montreal": "CA", "sydney": "AU", "melbourne": "AU", "singapore": "SG",
|
|
83
|
+
"tokyo": "JP", "tel aviv": "IL", "dubai": "AE", "bangalore": "IN", "mumbai": "IN", "sao paulo": "BR",
|
|
84
|
+
};
|
|
85
|
+
// Common B2B technologies recognized at query time. Unrecognized "using <X>" phrases become unparsed hints.
|
|
86
|
+
export const KNOWN_TECHNOLOGIES = [
|
|
87
|
+
"salesforce", "hubspot", "shopify", "stripe", "aws", "azure", "gcp", "google cloud", "snowflake",
|
|
88
|
+
"databricks", "postgres", "postgresql", "mysql", "mongodb", "redis", "kafka", "kubernetes", "docker",
|
|
89
|
+
"react", "nextjs", "next.js", "vue", "angular", "node", "python", "ruby on rails", "django",
|
|
90
|
+
"segment", "amplitude", "mixpanel", "intercom", "zendesk", "marketo", "pardot", "outreach",
|
|
91
|
+
"salesloft", "gong", "clari", "netsuite", "workday", "sap", "oracle", "twilio", "klaviyo", "webflow",
|
|
92
|
+
"wordpress", "magento", "bigcommerce", "woocommerce",
|
|
93
|
+
];
|
|
94
|
+
// Industry-ish terms that go to both keywords and industries (provider enum resolution is reported separately).
|
|
95
|
+
export const INDUSTRY_TERMS = [
|
|
96
|
+
"saas", "b2b saas", "fintech", "healthtech", "healthcare", "edtech", "martech", "adtech", "proptech",
|
|
97
|
+
"insurtech", "legaltech", "hrtech", "regtech", "biotech", "medtech", "cleantech", "climate tech",
|
|
98
|
+
"cybersecurity", "security", "devtools", "developer tools", "e-commerce", "ecommerce", "logistics",
|
|
99
|
+
"supply chain", "manufacturing", "construction", "real estate", "hospitality", "retail", "gaming",
|
|
100
|
+
"media", "telecom", "automotive", "aerospace", "agritech", "foodtech", "ai", "artificial intelligence",
|
|
101
|
+
"machine learning", "data analytics", "consulting", "staffing", "recruiting",
|
|
102
|
+
];
|
|
103
|
+
export const EMPLOYEE_SEGMENTS = {
|
|
104
|
+
"smb": { min: 1, max: 200 },
|
|
105
|
+
"small business": { min: 1, max: 200 },
|
|
106
|
+
"small businesses": { min: 1, max: 200 },
|
|
107
|
+
"mid-market": { min: 50, max: 1000 },
|
|
108
|
+
"mid market": { min: 50, max: 1000 },
|
|
109
|
+
"midmarket": { min: 50, max: 1000 },
|
|
110
|
+
"enterprise": { min: 1000 },
|
|
111
|
+
"enterprises": { min: 1000 },
|
|
112
|
+
"startup": { min: 1, max: 50 },
|
|
113
|
+
"startups": { min: 1, max: 50 },
|
|
114
|
+
"scaleup": { min: 50, max: 500 },
|
|
115
|
+
"scaleups": { min: 50, max: 500 },
|
|
116
|
+
};
|
|
117
|
+
// Words to drop when harvesting free-text keyword phrases (geo/size words are captured by dedicated extractors).
|
|
118
|
+
export const KEYWORD_STOPWORDS = new Set([
|
|
119
|
+
"find", "source", "build", "list", "of", "for", "the", "a", "an", "top", "best", "new", "all", "some",
|
|
120
|
+
"more", "with", "and", "or", "in", "at", "us", "uk", "eu", "based", "headquartered", "located",
|
|
121
|
+
"small", "large", "mid-market", "enterprise", "smb", "startup", "early-stage", "growth-stage",
|
|
122
|
+
]);
|
|
123
|
+
// Department phrase -> canonical department label (people search).
|
|
124
|
+
export const DEPARTMENT_TERMS = {
|
|
125
|
+
sales: "Sales",
|
|
126
|
+
marketing: "Marketing",
|
|
127
|
+
engineering: "Engineering",
|
|
128
|
+
product: "Product",
|
|
129
|
+
finance: "Finance",
|
|
130
|
+
"human resources": "Human Resources",
|
|
131
|
+
"people ops": "Human Resources",
|
|
132
|
+
recruiting: "Human Resources",
|
|
133
|
+
operations: "Operations",
|
|
134
|
+
revops: "Revenue Operations",
|
|
135
|
+
"revenue operations": "Revenue Operations",
|
|
136
|
+
"customer success": "Customer Success",
|
|
137
|
+
support: "Customer Success",
|
|
138
|
+
legal: "Legal",
|
|
139
|
+
it: "Information Technology",
|
|
140
|
+
security: "Information Technology",
|
|
141
|
+
data: "Data",
|
|
142
|
+
design: "Design",
|
|
143
|
+
};
|
|
144
|
+
// Seniority phrase -> canonical seniority band (people search).
|
|
145
|
+
export const SENIORITY_TERMS = {
|
|
146
|
+
"c-suite": "C-Suite", "c-level": "C-Suite", chief: "C-Suite", founder: "C-Suite", ceo: "C-Suite",
|
|
147
|
+
cto: "C-Suite", cfo: "C-Suite", coo: "C-Suite", cmo: "C-Suite", cro: "C-Suite", president: "C-Suite",
|
|
148
|
+
vp: "VP", "vice president": "VP", svp: "VP", evp: "VP",
|
|
149
|
+
director: "Director", "head of": "Director",
|
|
150
|
+
manager: "Manager", lead: "Manager", principal: "Manager",
|
|
151
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const TAG_COLORS: readonly ["gray", "red", "orange", "amber", "yellow", "green", "teal", "blue", "indigo", "purple", "pink"];
|
|
2
|
+
export type TagColor = (typeof TAG_COLORS)[number];
|
|
3
|
+
export declare const DEFAULT_TAG_COLOR: TagColor;
|
|
4
|
+
export declare function isTagColor(value: unknown): value is TagColor;
|
|
5
|
+
export declare function tagColorForIndex(index: number): TagColor;
|
|
6
|
+
export declare const SELECT_COLUMN_SEMANTICS: readonly ["select", "single_select", "multi_select", "tags", "status"];
|
|
7
|
+
export type SelectColumnSemantic = (typeof SELECT_COLUMN_SEMANTICS)[number];
|
|
8
|
+
export declare function isSelectColumnSemantic(semanticType: string | null | undefined): boolean;
|
|
9
|
+
export declare function isMultiSelectSemantic(semanticType: string | null | undefined): boolean;
|
|
10
|
+
export declare function isStatusSemantic(semanticType: string | null | undefined): boolean;
|
|
11
|
+
export type SelectOption = {
|
|
12
|
+
id: string;
|
|
13
|
+
value: string;
|
|
14
|
+
label: string;
|
|
15
|
+
color: TagColor;
|
|
16
|
+
order: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function normalizeSelectOptions(raw: unknown): SelectOption[];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Canonical colored option model for select / multi-select / status columns.
|
|
2
|
+
//
|
|
3
|
+
// Shared across server validation, the web option editor, colored-pill
|
|
4
|
+
// rendering, filters, and the kanban view so every surface agrees. Colors are
|
|
5
|
+
// NAMED (theme-safe), stored once on the option — never raw hex — mirroring how
|
|
6
|
+
// Attio/Notion model status & select option colors.
|
|
7
|
+
export const TAG_COLORS = [
|
|
8
|
+
"gray",
|
|
9
|
+
"red",
|
|
10
|
+
"orange",
|
|
11
|
+
"amber",
|
|
12
|
+
"yellow",
|
|
13
|
+
"green",
|
|
14
|
+
"teal",
|
|
15
|
+
"blue",
|
|
16
|
+
"indigo",
|
|
17
|
+
"purple",
|
|
18
|
+
"pink",
|
|
19
|
+
];
|
|
20
|
+
export const DEFAULT_TAG_COLOR = "gray";
|
|
21
|
+
export function isTagColor(value) {
|
|
22
|
+
return typeof value === "string" && TAG_COLORS.includes(value);
|
|
23
|
+
}
|
|
24
|
+
// Auto-assign colors for fresh options, skipping gray so a new column looks
|
|
25
|
+
// colorful out of the box; gray stays a deliberate manual choice.
|
|
26
|
+
export function tagColorForIndex(index) {
|
|
27
|
+
const palette = TAG_COLORS.filter((color) => color !== "gray");
|
|
28
|
+
return palette[Math.abs(index) % palette.length] ?? DEFAULT_TAG_COLOR;
|
|
29
|
+
}
|
|
30
|
+
// Semantic types (the column's `semanticType`) that present a fixed option set.
|
|
31
|
+
// `select`/`status` are single-value; `multi_select`/`tags` are array-valued.
|
|
32
|
+
// `single_select` is accepted as an alias of `select`.
|
|
33
|
+
export const SELECT_COLUMN_SEMANTICS = [
|
|
34
|
+
"select",
|
|
35
|
+
"single_select",
|
|
36
|
+
"multi_select",
|
|
37
|
+
"tags",
|
|
38
|
+
"status",
|
|
39
|
+
];
|
|
40
|
+
export function isSelectColumnSemantic(semanticType) {
|
|
41
|
+
return Boolean(semanticType) && SELECT_COLUMN_SEMANTICS.includes(semanticType);
|
|
42
|
+
}
|
|
43
|
+
export function isMultiSelectSemantic(semanticType) {
|
|
44
|
+
return semanticType === "multi_select" || semanticType === "tags";
|
|
45
|
+
}
|
|
46
|
+
export function isStatusSemantic(semanticType) {
|
|
47
|
+
return semanticType === "status";
|
|
48
|
+
}
|
|
49
|
+
const MAX_OPTIONS = 200;
|
|
50
|
+
// Normalize a raw options array (from a column `definition.options`, CLI/MCP, or
|
|
51
|
+
// the editor) into canonical SelectOptions: dedupe by value, default colors,
|
|
52
|
+
// canonical 0..n ordering. Accepts strings or {value,label,color,order} objects.
|
|
53
|
+
export function normalizeSelectOptions(raw) {
|
|
54
|
+
if (!Array.isArray(raw))
|
|
55
|
+
return [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const collected = [];
|
|
58
|
+
for (const entry of raw) {
|
|
59
|
+
const option = normalizeSelectOption(entry, collected.length);
|
|
60
|
+
if (!option)
|
|
61
|
+
continue;
|
|
62
|
+
const dedupeKey = option.value.toLowerCase();
|
|
63
|
+
if (seen.has(dedupeKey))
|
|
64
|
+
continue;
|
|
65
|
+
seen.add(dedupeKey);
|
|
66
|
+
collected.push(option);
|
|
67
|
+
if (collected.length >= MAX_OPTIONS)
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
collected.sort((a, b) => a.order - b.order);
|
|
71
|
+
return collected.map((option, index) => ({ ...option, order: index }));
|
|
72
|
+
}
|
|
73
|
+
function normalizeSelectOption(entry, fallbackIndex) {
|
|
74
|
+
if (typeof entry === "string")
|
|
75
|
+
return optionFromString(entry, fallbackIndex);
|
|
76
|
+
if (!entry || typeof entry !== "object")
|
|
77
|
+
return null;
|
|
78
|
+
const record = entry;
|
|
79
|
+
const rawValue = readOptionValue(record);
|
|
80
|
+
if (!rawValue)
|
|
81
|
+
return null;
|
|
82
|
+
const label = readTrimmedString(record.label) ?? rawValue;
|
|
83
|
+
const id = readTrimmedString(record.id) ?? slugifyOption(rawValue);
|
|
84
|
+
const color = isTagColor(record.color) ? record.color : tagColorForIndex(fallbackIndex);
|
|
85
|
+
const order = typeof record.order === "number" && Number.isFinite(record.order) ? record.order : fallbackIndex;
|
|
86
|
+
return { id, value: rawValue, label, color, order };
|
|
87
|
+
}
|
|
88
|
+
// A bare string entry becomes a fully-defaulted option (value == label == the
|
|
89
|
+
// trimmed string); blank strings are dropped.
|
|
90
|
+
function optionFromString(entry, fallbackIndex) {
|
|
91
|
+
const value = entry.trim();
|
|
92
|
+
if (!value)
|
|
93
|
+
return null;
|
|
94
|
+
return {
|
|
95
|
+
id: slugifyOption(value),
|
|
96
|
+
value,
|
|
97
|
+
label: value,
|
|
98
|
+
color: tagColorForIndex(fallbackIndex),
|
|
99
|
+
order: fallbackIndex,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// The cell value comes from `value`, falling back to `label`; "" when neither is
|
|
103
|
+
// a non-blank string (so the caller drops the option).
|
|
104
|
+
function readOptionValue(record) {
|
|
105
|
+
return readTrimmedString(record.value) ?? readTrimmedString(record.label) ?? "";
|
|
106
|
+
}
|
|
107
|
+
// Returns the trimmed string when the field is a non-blank string, else null.
|
|
108
|
+
function readTrimmedString(value) {
|
|
109
|
+
if (typeof value !== "string")
|
|
110
|
+
return null;
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
return trimmed ? trimmed : null;
|
|
113
|
+
}
|
|
114
|
+
function slugifyOption(value) {
|
|
115
|
+
const slug = value
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
118
|
+
.replace(/^_+|_+$/g, "")
|
|
119
|
+
.slice(0, 60);
|
|
120
|
+
return slug || "option";
|
|
121
|
+
}
|
|
@@ -21,12 +21,6 @@ export type SqlErrorAttribution = {
|
|
|
21
21
|
/** Neon/HTTP transport status code (numeric) when the failure carries one. */
|
|
22
22
|
statusCode: number | null;
|
|
23
23
|
};
|
|
24
|
-
/**
|
|
25
|
-
* Classifies a pg/drizzle error into structured, non-secret attribution.
|
|
26
|
-
* Returns null when the error carries no SQL/connection signal at all (so
|
|
27
|
-
* non-DB errors never gain spurious `db.*` attributes). Never throws.
|
|
28
|
-
*/
|
|
29
|
-
export declare function describeSqlError(error: unknown): SqlErrorAttribution | null;
|
|
30
24
|
/** Log-field shape (snake_case) for merging into `errorFields(error)` payloads. */
|
|
31
25
|
export declare function sqlErrorFields(error: unknown): Record<string, unknown>;
|
|
32
26
|
/**
|
|
@@ -68,23 +68,14 @@ const TRANSIENT_CAUSES = new Set([
|
|
|
68
68
|
* Returns null when the error carries no SQL/connection signal at all (so
|
|
69
69
|
* non-DB errors never gain spurious `db.*` attributes). Never throws.
|
|
70
70
|
*/
|
|
71
|
-
|
|
71
|
+
function describeSqlError(error) {
|
|
72
72
|
try {
|
|
73
73
|
const sqlstate = readSqlstate(error);
|
|
74
74
|
const errno = readErrno(error);
|
|
75
75
|
const message = readMessage(error);
|
|
76
76
|
const drizzle = isDrizzleQueryError(error, message);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (errno && CONNECT_TIMEOUT_ERRNOS.has(errno))
|
|
80
|
-
cause = "connect_timeout";
|
|
81
|
-
else if (errno && CONNECTION_ERRNOS.has(errno))
|
|
82
|
-
cause = "connection";
|
|
83
|
-
else if (CONNECT_TIMEOUT_MESSAGE.test(message))
|
|
84
|
-
cause = "connect_timeout";
|
|
85
|
-
else if (CONNECTION_MESSAGE.test(message))
|
|
86
|
-
cause = "connection";
|
|
87
|
-
}
|
|
77
|
+
const sqlstateCause = sqlstate ? categoryFromSqlstate(sqlstate) ?? "unknown" : "unknown";
|
|
78
|
+
const cause = sqlstateCause !== "unknown" ? sqlstateCause : causeFromErrnoOrMessage(errno, message);
|
|
88
79
|
const hasSignal = Boolean(sqlstate) || Boolean(errno) || cause !== "unknown" || drizzle;
|
|
89
80
|
if (!hasSignal)
|
|
90
81
|
return null;
|
|
@@ -110,22 +101,35 @@ export function describeSqlError(error) {
|
|
|
110
101
|
return null;
|
|
111
102
|
}
|
|
112
103
|
}
|
|
104
|
+
// Connection-level fallback when no SQLSTATE resolved a cause: prefer the
|
|
105
|
+
// errno (set before/around the TCP+TLS handshake), then the message text.
|
|
106
|
+
function causeFromErrnoOrMessage(errno, message) {
|
|
107
|
+
if (errno && CONNECT_TIMEOUT_ERRNOS.has(errno))
|
|
108
|
+
return "connect_timeout";
|
|
109
|
+
if (errno && CONNECTION_ERRNOS.has(errno))
|
|
110
|
+
return "connection";
|
|
111
|
+
if (CONNECT_TIMEOUT_MESSAGE.test(message))
|
|
112
|
+
return "connect_timeout";
|
|
113
|
+
if (CONNECTION_MESSAGE.test(message))
|
|
114
|
+
return "connection";
|
|
115
|
+
return "unknown";
|
|
116
|
+
}
|
|
113
117
|
/** Log-field shape (snake_case) for merging into `errorFields(error)` payloads. */
|
|
114
118
|
export function sqlErrorFields(error) {
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
119
|
+
const attribution = describeSqlError(error);
|
|
120
|
+
if (!attribution)
|
|
117
121
|
return {};
|
|
118
122
|
return {
|
|
119
|
-
...(
|
|
120
|
-
sql_error_cause:
|
|
121
|
-
...(
|
|
122
|
-
...(
|
|
123
|
-
sql_error_transient:
|
|
124
|
-
...(
|
|
125
|
-
...(
|
|
126
|
-
...(
|
|
127
|
-
...(
|
|
128
|
-
...(
|
|
123
|
+
...(attribution.pgCode ? { sql_error_pg_code: attribution.pgCode } : {}),
|
|
124
|
+
sql_error_cause: attribution.cause,
|
|
125
|
+
...(attribution.queryKind ? { sql_error_query_kind: attribution.queryKind } : {}),
|
|
126
|
+
...(attribution.table ? { sql_error_table: attribution.table } : {}),
|
|
127
|
+
sql_error_transient: attribution.transient,
|
|
128
|
+
...(attribution.severity ? { sql_error_severity: attribution.severity } : {}),
|
|
129
|
+
...(attribution.schema ? { sql_error_schema: attribution.schema } : {}),
|
|
130
|
+
...(attribution.column ? { sql_error_column: attribution.column } : {}),
|
|
131
|
+
...(attribution.constraint ? { sql_error_constraint: attribution.constraint } : {}),
|
|
132
|
+
...(attribution.statusCode !== null ? { sql_error_status: attribution.statusCode } : {}),
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
135
|
/**
|
|
@@ -144,47 +148,52 @@ export function redactSqlParameters(text) {
|
|
|
144
148
|
}
|
|
145
149
|
/** Telemetry-attribute shape (dotted keys) for span error attribution. */
|
|
146
150
|
export function sqlErrorTelemetryAttributes(error) {
|
|
147
|
-
const
|
|
148
|
-
if (!
|
|
151
|
+
const attribution = describeSqlError(error);
|
|
152
|
+
if (!attribution)
|
|
149
153
|
return {};
|
|
150
154
|
return {
|
|
151
|
-
...(
|
|
152
|
-
"db.error.cause":
|
|
153
|
-
...(
|
|
154
|
-
...(
|
|
155
|
-
"db.error.transient":
|
|
156
|
-
...(
|
|
157
|
-
...(
|
|
158
|
-
...(
|
|
159
|
-
...(
|
|
160
|
-
...(
|
|
155
|
+
...(attribution.pgCode ? { "db.pg_code": attribution.pgCode } : {}),
|
|
156
|
+
"db.error.cause": attribution.cause,
|
|
157
|
+
...(attribution.queryKind ? { "db.error.query_kind": attribution.queryKind } : {}),
|
|
158
|
+
...(attribution.table ? { "db.error.table": attribution.table } : {}),
|
|
159
|
+
"db.error.transient": attribution.transient,
|
|
160
|
+
...(attribution.severity ? { "db.error.severity": attribution.severity } : {}),
|
|
161
|
+
...(attribution.schema ? { "db.error.schema": attribution.schema } : {}),
|
|
162
|
+
...(attribution.column ? { "db.error.column": attribution.column } : {}),
|
|
163
|
+
...(attribution.constraint ? { "db.error.constraint": attribution.constraint } : {}),
|
|
164
|
+
...(attribution.statusCode !== null ? { "db.error.status": attribution.statusCode } : {}),
|
|
161
165
|
};
|
|
162
166
|
}
|
|
167
|
+
// Specific SQLSTATEs that must resolve before the coarser class-prefix lookup
|
|
168
|
+
// (e.g. 40001 serialization / 40P01 deadlock are more precise than class 40).
|
|
169
|
+
const EXACT_SQLSTATE_CAUSES = new Map([
|
|
170
|
+
["57014", "statement_timeout"],
|
|
171
|
+
["53300", "too_many_connections"],
|
|
172
|
+
["40001", "serialization"],
|
|
173
|
+
["40P01", "deadlock"],
|
|
174
|
+
]);
|
|
175
|
+
// SQLSTATE class (first two chars) → cause, applied only after the exact and
|
|
176
|
+
// schema-drift lookups above so the precise codes keep their dedicated cause.
|
|
177
|
+
const SQLSTATE_CLASS_CAUSES = new Map([
|
|
178
|
+
["08", "connection"],
|
|
179
|
+
["28", "auth"],
|
|
180
|
+
["22", "data_exception"],
|
|
181
|
+
["23", "integrity_constraint"],
|
|
182
|
+
["40", "transaction_rollback"],
|
|
183
|
+
["42", "syntax_or_access"],
|
|
184
|
+
["53", "insufficient_resources"],
|
|
185
|
+
["57", "admin_shutdown"],
|
|
186
|
+
["58", "internal"],
|
|
187
|
+
["XX", "internal"],
|
|
188
|
+
["3F", "schema_drift"],
|
|
189
|
+
]);
|
|
163
190
|
function categoryFromSqlstate(code) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return "too_many_connections";
|
|
168
|
-
if (code === "40001")
|
|
169
|
-
return "serialization";
|
|
170
|
-
if (code === "40P01")
|
|
171
|
-
return "deadlock";
|
|
191
|
+
const exact = EXACT_SQLSTATE_CAUSES.get(code);
|
|
192
|
+
if (exact)
|
|
193
|
+
return exact;
|
|
172
194
|
if (SCHEMA_DRIFT_SQLSTATES.has(code))
|
|
173
195
|
return "schema_drift";
|
|
174
|
-
|
|
175
|
-
case "08": return "connection";
|
|
176
|
-
case "28": return "auth";
|
|
177
|
-
case "22": return "data_exception";
|
|
178
|
-
case "23": return "integrity_constraint";
|
|
179
|
-
case "40": return "transaction_rollback";
|
|
180
|
-
case "42": return "syntax_or_access";
|
|
181
|
-
case "53": return "insufficient_resources";
|
|
182
|
-
case "57": return "admin_shutdown";
|
|
183
|
-
case "58": return "internal";
|
|
184
|
-
case "XX": return "internal";
|
|
185
|
-
case "3F": return "schema_drift";
|
|
186
|
-
default: return null;
|
|
187
|
-
}
|
|
196
|
+
return SQLSTATE_CLASS_CAUSES.get(code.slice(0, 2)) ?? null;
|
|
188
197
|
}
|
|
189
198
|
// Walk the Error.cause chain (bounded) and return the first non-null value the
|
|
190
199
|
// reader extracts. drizzle wraps the pg error on `.cause`, and custom wrappers
|
|
@@ -5,7 +5,6 @@ export type WithTelemetrySpanOptions = {
|
|
|
5
5
|
export declare function withTelemetrySpan<T>(tracerName: string, name: string, attributes: TelemetryAttributes | undefined, fn: () => Promise<T>, options?: WithTelemetrySpanOptions): Promise<T>;
|
|
6
6
|
export declare function setActiveTelemetryAttributes(attributes: TelemetryAttributes): void;
|
|
7
7
|
export declare function markActiveTelemetryError(message: string, attributes?: TelemetryAttributes): void;
|
|
8
|
-
export declare function addTelemetryEvent(name: string, attributes?: TelemetryAttributes): void;
|
|
9
8
|
export declare function recordTelemetryCounter(name: string, value?: number, attributes?: TelemetryAttributes): void;
|
|
10
9
|
export declare function recordTelemetryHistogram(name: string, value: number, attributes?: TelemetryAttributes): void;
|
|
11
10
|
export declare function commonTelemetryAttributes(attributes?: TelemetryAttributes): TelemetryAttributes;
|
|
@@ -5,6 +5,8 @@ import { redactSqlParameters, sqlErrorTelemetryAttributes } from "./sql-error.js
|
|
|
5
5
|
import { OXYGEN_VERSION } from "./version.js";
|
|
6
6
|
const counterCache = new Map();
|
|
7
7
|
const histogramCache = new Map();
|
|
8
|
+
// skipcq: JS-0116 — `async` keeps synchronous setup throws (getTracer / attribute
|
|
9
|
+
// normalization) as rejections so the Promise<T> contract holds for all callers.
|
|
8
10
|
export async function withTelemetrySpan(tracerName, name, attributes, fn, options) {
|
|
9
11
|
const tracer = trace.getTracer(tracerName, OXYGEN_VERSION);
|
|
10
12
|
return tracer.startActiveSpan(name, { attributes: normalizeTelemetryAttributes(commonTelemetryAttributes(attributes)) }, async (span) => {
|
|
@@ -51,12 +53,6 @@ export function markActiveTelemetryError(message, attributes) {
|
|
|
51
53
|
span.setAttributes(normalizeTelemetryAttributes(attributes));
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
|
-
export function addTelemetryEvent(name, attributes) {
|
|
55
|
-
const span = trace.getActiveSpan();
|
|
56
|
-
if (!span)
|
|
57
|
-
return;
|
|
58
|
-
span.addEvent(name, normalizeTelemetryAttributes(attributes));
|
|
59
|
-
}
|
|
60
56
|
export function recordTelemetryCounter(name, value = 1, attributes) {
|
|
61
57
|
if (!Number.isFinite(value))
|
|
62
58
|
return;
|
|
@@ -71,28 +67,37 @@ export function recordTelemetryHistogram(name, value, attributes) {
|
|
|
71
67
|
}
|
|
72
68
|
export function commonTelemetryAttributes(attributes) {
|
|
73
69
|
const workerProcess = isWorkerProcess();
|
|
70
|
+
// Each attribute reads an ordered env-var fallback chain that differs by
|
|
71
|
+
// surface: the worker prefers its Fly vars first, the web its Vercel vars.
|
|
72
|
+
// pickEnvBySurface picks the surface's list, then the first defined value.
|
|
73
|
+
const pick = (workerKeys, webKeys) => pickEnvBySurface(workerProcess, workerKeys, webKeys);
|
|
74
74
|
return {
|
|
75
75
|
"oxygen.version": OXYGEN_VERSION,
|
|
76
|
-
"deployment.environment":
|
|
77
|
-
? process.env.FLY_ENVIRONMENT ?? process.env.NODE_ENV ?? null
|
|
78
|
-
: process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? null,
|
|
76
|
+
"deployment.environment": pick(["FLY_ENVIRONMENT", "NODE_ENV"], ["VERCEL_ENV", "NODE_ENV"]),
|
|
79
77
|
// deployment.sha stays the deploy artifact id: the Fly registry image ref on
|
|
80
78
|
// the worker (release cross-reference), VERCEL_GIT_COMMIT_SHA on the web. The
|
|
81
79
|
// worker's image ref is NOT a git commit, so deployment.git_sha carries the
|
|
82
80
|
// resolved commit separately and is the single field that means "the commit"
|
|
83
81
|
// on both surfaces (OXY-61). On the web both happen to equal the Vercel SHA.
|
|
84
|
-
"deployment.sha":
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"deployment.git_sha": workerProcess
|
|
88
|
-
? process.env.OXYGEN_GIT_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA ?? null
|
|
89
|
-
: process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.OXYGEN_GIT_SHA ?? null,
|
|
90
|
-
"cloud.region": workerProcess
|
|
91
|
-
? process.env.FLY_REGION ?? process.env.VERCEL_REGION ?? null
|
|
92
|
-
: process.env.VERCEL_REGION ?? process.env.FLY_REGION ?? null,
|
|
82
|
+
"deployment.sha": pick(["FLY_IMAGE_REF", "VERCEL_GIT_COMMIT_SHA"], ["VERCEL_GIT_COMMIT_SHA", "FLY_IMAGE_REF"]),
|
|
83
|
+
"deployment.git_sha": pick(["OXYGEN_GIT_SHA", "VERCEL_GIT_COMMIT_SHA"], ["VERCEL_GIT_COMMIT_SHA", "OXYGEN_GIT_SHA"]),
|
|
84
|
+
"cloud.region": pick(["FLY_REGION", "VERCEL_REGION"], ["VERCEL_REGION", "FLY_REGION"]),
|
|
93
85
|
...attributes,
|
|
94
86
|
};
|
|
95
87
|
}
|
|
88
|
+
// Picks the env-var fallback list for the active surface (worker vs web), then
|
|
89
|
+
// returns the first env var in that list that is set, else null. Preserves the
|
|
90
|
+
// original `a ?? b ?? null` semantics exactly: only an unset (`undefined`) env
|
|
91
|
+
// var falls through to the next key — an explicit empty string is kept.
|
|
92
|
+
function pickEnvBySurface(workerProcess, workerKeys, webKeys) {
|
|
93
|
+
const keys = workerProcess ? workerKeys : webKeys;
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
const value = process.env[key];
|
|
96
|
+
if (value !== undefined)
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
96
101
|
function isWorkerProcess() {
|
|
97
102
|
if (process.env.OXYGEN_PROCESS_ROLE === "worker")
|
|
98
103
|
return true;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.177.1";
|
|
2
2
|
export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.154.0";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.177.1";
|
|
2
2
|
// Bump this only when deployed CLI/API contracts require a newer CLI.
|
|
3
3
|
// 1.154.0: LinkedIn → Sequencer rename moved the CLI/API/MCP surface
|
|
4
4
|
// (oxygen sequences|inbox|senders, /api/cli/{sequences,inbox,senders}) and
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical queue selector for the worker-failures surface, shared by:
|
|
3
|
+
* - CLI `oxygen worker failures --queue <queue>` (free-text, passed through),
|
|
4
|
+
* - MCP `oxygen_worker_failures({ queue })` (enum-constrained), and
|
|
5
|
+
* - the `/api/cli/worker/failures` route that actually parses the value.
|
|
6
|
+
*
|
|
7
|
+
* Single-sourced so a token documented by one surface can never silently fall
|
|
8
|
+
* back to the `all` aggregate on another — the exact drift OXY-337 fixed, where
|
|
9
|
+
* the CLI documented `postgres_jobs` but the route only accepted `postgres_queue`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Public, documented queue tokens in display order. `postgres_queue` is the
|
|
13
|
+
* canonical name for the Postgres-backed background job queue. The MCP enum is
|
|
14
|
+
* built from this list so the constraint can never drift from the parser.
|
|
15
|
+
*/
|
|
16
|
+
export declare const WORKER_FAILURES_QUEUE_VALUES: readonly ["all", "actions", "ingestions", "postgres_queue"];
|
|
17
|
+
export type WorkerFailuresQueue = (typeof WORKER_FAILURES_QUEUE_VALUES)[number];
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a raw `--queue` / `queue` value to its canonical token. Empty,
|
|
20
|
+
* missing, or unrecognized values resolve to `all` (the aggregate default).
|
|
21
|
+
*/
|
|
22
|
+
export declare function normalizeWorkerFailuresQueue(value: string | null | undefined): WorkerFailuresQueue;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical queue selector for the worker-failures surface, shared by:
|
|
3
|
+
* - CLI `oxygen worker failures --queue <queue>` (free-text, passed through),
|
|
4
|
+
* - MCP `oxygen_worker_failures({ queue })` (enum-constrained), and
|
|
5
|
+
* - the `/api/cli/worker/failures` route that actually parses the value.
|
|
6
|
+
*
|
|
7
|
+
* Single-sourced so a token documented by one surface can never silently fall
|
|
8
|
+
* back to the `all` aggregate on another — the exact drift OXY-337 fixed, where
|
|
9
|
+
* the CLI documented `postgres_jobs` but the route only accepted `postgres_queue`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Public, documented queue tokens in display order. `postgres_queue` is the
|
|
13
|
+
* canonical name for the Postgres-backed background job queue. The MCP enum is
|
|
14
|
+
* built from this list so the constraint can never drift from the parser.
|
|
15
|
+
*/
|
|
16
|
+
export const WORKER_FAILURES_QUEUE_VALUES = [
|
|
17
|
+
"all",
|
|
18
|
+
"actions",
|
|
19
|
+
"ingestions",
|
|
20
|
+
"postgres_queue",
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Every accepted token mapped to its canonical value. Covers the canonical
|
|
24
|
+
* tokens themselves plus legacy/compat aliases:
|
|
25
|
+
* - `postgres_jobs` was the CLI-documented token before this unification.
|
|
26
|
+
* - `bullmq` / `redis` / `jobs` predate the Postgres-queue migration.
|
|
27
|
+
* - `postgres` / `pg` and the singular `action` / `ingestion` are shorthands.
|
|
28
|
+
*/
|
|
29
|
+
const QUEUE_TOKEN_TO_CANONICAL = {
|
|
30
|
+
all: "all",
|
|
31
|
+
action: "actions",
|
|
32
|
+
actions: "actions",
|
|
33
|
+
table_actions: "actions",
|
|
34
|
+
ingestion: "ingestions",
|
|
35
|
+
ingestions: "ingestions",
|
|
36
|
+
table_ingestions: "ingestions",
|
|
37
|
+
postgres: "postgres_queue",
|
|
38
|
+
postgres_queue: "postgres_queue",
|
|
39
|
+
postgres_jobs: "postgres_queue",
|
|
40
|
+
pg: "postgres_queue",
|
|
41
|
+
bullmq: "postgres_queue",
|
|
42
|
+
redis: "postgres_queue",
|
|
43
|
+
jobs: "postgres_queue",
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Normalize a raw `--queue` / `queue` value to its canonical token. Empty,
|
|
47
|
+
* missing, or unrecognized values resolve to `all` (the aggregate default).
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeWorkerFailuresQueue(value) {
|
|
50
|
+
if (!value)
|
|
51
|
+
return "all";
|
|
52
|
+
const normalized = value.trim().toLowerCase();
|
|
53
|
+
if (!normalized)
|
|
54
|
+
return "all";
|
|
55
|
+
return QUEUE_TOKEN_TO_CANONICAL[normalized] ?? "all";
|
|
56
|
+
}
|