@oxygen-agent/cli 1.1012.2 → 1.1013.15
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/customer-operation-commands.js +6 -2
- package/dist/diagnostic-upload-main.d.ts +1 -0
- package/dist/diagnostic-upload-main.js +2 -0
- package/dist/diagnostic-uploader.d.ts +3 -0
- package/dist/diagnostic-uploader.js +68 -0
- package/dist/diagnostics.d.ts +22 -0
- package/dist/diagnostics.js +219 -0
- package/dist/http-client.d.ts +7 -0
- package/dist/http-client.js +12 -2
- package/dist/index.js +15 -3
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +4 -4
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/geo.js +30 -8
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-context.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-context.js +17 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +12 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +60 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-receipt.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-receipt.js +22 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +39 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +233 -0
- package/node_modules/@oxygen/shared/dist/diagnostics.d.ts +49 -0
- package/node_modules/@oxygen/shared/dist/diagnostics.js +253 -0
- package/node_modules/@oxygen/shared/dist/geo.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/geo.js +31 -3
- package/node_modules/@oxygen/shared/dist/onboarding-first-leads.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/onboarding-first-leads.js +25 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +4 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +2 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { credentialBearingOperation } from "./diagnostic-credentials.js";
|
|
3
|
+
import { redactSecretsInString } from "./redaction.js";
|
|
4
|
+
/** Internal diagnostic evidence, distinct from customer run state and usage counts. */
|
|
5
|
+
export const DIAGNOSTIC_VERSION = 1;
|
|
6
|
+
export const DIAGNOSTIC_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
7
|
+
export const DIAGNOSTIC_INVOCATION_HEADER = "X-Oxygen-Invocation-Id";
|
|
8
|
+
export const DIAGNOSTIC_UPLOAD_CHUNK_BYTES = 1024 * 1024;
|
|
9
|
+
export const DIAGNOSTIC_MAX_BYTES = 512 * 1024 * 1024;
|
|
10
|
+
// Total spool capacity is separate from the bounded JSON materialization of one
|
|
11
|
+
// record. Larger contents explicitly become partial evidence, never a full claim.
|
|
12
|
+
export const DIAGNOSTIC_RECORD_MAX_BYTES = 16 * 1024 * 1024;
|
|
13
|
+
export const DIAGNOSTIC_UPLOAD_MAX_CHUNKS = 17; // Includes gzip/envelope overhead.
|
|
14
|
+
export function diagnosticId(value) {
|
|
15
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? value.toLowerCase() : null;
|
|
16
|
+
}
|
|
17
|
+
export function diagnosticOutcome(ok, status, code) {
|
|
18
|
+
if (ok)
|
|
19
|
+
return "ok";
|
|
20
|
+
if (code && /^(network_|.*timeout$)/.test(code))
|
|
21
|
+
return "network_error";
|
|
22
|
+
if (code && /^(cancelled|aborted|interrupted)$/.test(code))
|
|
23
|
+
return "cancelled";
|
|
24
|
+
if (status === 401 || status === 403 || status === 402 || status === 409 || status === 429
|
|
25
|
+
|| (code && /approval|budget|credit|quota|rate_limit|permission|forbidden|not_logged_in|unauthorized|update_required|read_only/.test(code)))
|
|
26
|
+
return "refused";
|
|
27
|
+
if ((status && status >= 400 && status < 500) || (code && /invalid|missing|unknown_(command|option|tool)|not_found|commander\./.test(code)))
|
|
28
|
+
return "invalid_input";
|
|
29
|
+
return "platform_error";
|
|
30
|
+
}
|
|
31
|
+
export function createDiagnosticRecord(input) {
|
|
32
|
+
const id = diagnosticId(input.id) ?? randomUUID();
|
|
33
|
+
return {
|
|
34
|
+
version: DIAGNOSTIC_VERSION, id, invocation_id: diagnosticId(input.invocation_id) ?? id,
|
|
35
|
+
parent_id: input.parent_id ?? null, layer: input.layer ?? "invocation", surface: input.surface,
|
|
36
|
+
operation: input.operation, started_at: input.started_at ?? new Date().toISOString(),
|
|
37
|
+
ended_at: input.ended_at ?? null, outcome: input.outcome ?? "pending", error_code: input.error_code ?? null,
|
|
38
|
+
status: input.status ?? null, organization_id: input.organization_id ?? null, user_id: input.user_id ?? null,
|
|
39
|
+
identity_verified: input.identity_verified ?? false, client_version: input.client_version ?? null,
|
|
40
|
+
trace_id: input.trace_id ?? null, traceparent: input.traceparent ?? null,
|
|
41
|
+
capture_state: input.capture_state ?? "complete", input: input.input ?? null, output: input.output ?? null,
|
|
42
|
+
metadata: input.metadata ?? {},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const SECRET_FIELD = /(?:^|_)(?:authorization|cookie|cookies|password|passwd|passphrase|secret|api_key|access_key|private_key|token|otp|security_code|connection_string|ciphertext|dsn|connection_uri|database_url|x_key|signature)(?:$|_)/i;
|
|
46
|
+
function credentialField(key) {
|
|
47
|
+
return SECRET_FIELD.test(key.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[- .]/g, "_"));
|
|
48
|
+
}
|
|
49
|
+
function binaryField(key) {
|
|
50
|
+
return /(?:^|_)(?:base64|b64)(?:$|_)/i.test(key.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[- .]/g, "_"));
|
|
51
|
+
}
|
|
52
|
+
function binaryMetadata(value) {
|
|
53
|
+
return { withheld: "binary", bytes: Buffer.byteLength(value, "base64") };
|
|
54
|
+
}
|
|
55
|
+
export function sanitizeDiagnosticText(value, knownSecrets = []) {
|
|
56
|
+
// CLI stdout and MCP text often contain serialized JSON rather than objects.
|
|
57
|
+
// Filter its credential fields without changing the surrounding business text.
|
|
58
|
+
for (const secret of knownSecrets)
|
|
59
|
+
if (secret)
|
|
60
|
+
value = value.split(secret).join("[credential excluded]");
|
|
61
|
+
// Text blocks may contain serialized tool results, including binary blocks.
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(value);
|
|
64
|
+
if (parsed && typeof parsed === "object") {
|
|
65
|
+
const sanitized = sanitizeDiagnosticContent(parsed, knownSecrets);
|
|
66
|
+
if (JSON.stringify(parsed) !== JSON.stringify(sanitized))
|
|
67
|
+
value = JSON.stringify(sanitized);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { /* preserve ordinary business text */ }
|
|
71
|
+
value = value.replace(/data:[\w/+.-]+(?:;[\w=.-]+)*;base64,[A-Za-z0-9+/=]+/g, match => JSON.stringify(binaryMetadata(match.slice(match.indexOf(",") + 1))));
|
|
72
|
+
const redacted = redactSecretsInString(value)
|
|
73
|
+
.replace(/([?&#][\w.-]*(?:api[_-]?key|token|secret|password|signature|credential|code|auth)[\w.-]*=)[^&#\s"'<>]+/gi, "$1[credential excluded]")
|
|
74
|
+
.replace(/\b(?:oxy_(?:live|sess|user)_|gh[pousr]_|xox[baprs]-)[A-Za-z0-9_.-]+/g, "[credential excluded]");
|
|
75
|
+
return redacted.replace(/("(?:[^"\\]|\\.)*"\s*:\s*)("(?:[^"\\]|\\.)*")/g, (match, prefix) => {
|
|
76
|
+
try {
|
|
77
|
+
return credentialField(JSON.parse(prefix.slice(0, prefix.lastIndexOf(":"))).trim()) ? `${prefix}"[credential excluded]"` : match;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return match;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** No business-content, array, string, field-count or depth truncation. */
|
|
85
|
+
export function sanitizeDiagnosticContent(value, knownSecrets = []) {
|
|
86
|
+
const ancestors = new Set();
|
|
87
|
+
const walk = (item) => {
|
|
88
|
+
if (typeof item === "string")
|
|
89
|
+
return sanitizeDiagnosticText(item, knownSecrets);
|
|
90
|
+
if (item === null || typeof item === "number" || typeof item === "boolean")
|
|
91
|
+
return item;
|
|
92
|
+
if (item === undefined)
|
|
93
|
+
return null;
|
|
94
|
+
if (typeof item !== "object")
|
|
95
|
+
throw new Error("Non-JSON diagnostic value");
|
|
96
|
+
if (ancestors.has(item))
|
|
97
|
+
throw new Error("Cyclic diagnostic value");
|
|
98
|
+
ancestors.add(item);
|
|
99
|
+
try {
|
|
100
|
+
if (item instanceof Date)
|
|
101
|
+
return item.toISOString();
|
|
102
|
+
if (item instanceof Error)
|
|
103
|
+
return walk({ name: item.name, message: item.message, stack: item.stack });
|
|
104
|
+
if (ArrayBuffer.isView(item) || item instanceof ArrayBuffer)
|
|
105
|
+
return { withheld: "binary", bytes: item.byteLength };
|
|
106
|
+
if (Array.isArray(item))
|
|
107
|
+
return item.map(walk);
|
|
108
|
+
if (("type" in item && (item.type === "image" || item.type === "audio")) && "data" in item && typeof item.data === "string") {
|
|
109
|
+
return { type: item.type, withheld: "binary", bytes: Buffer.byteLength(item.data, "base64"), ...("mimeType" in item ? { mimeType: item.mimeType } : {}) };
|
|
110
|
+
}
|
|
111
|
+
const result = Object.create(null);
|
|
112
|
+
for (const [key, entry] of Object.entries(item)) {
|
|
113
|
+
const encoded = typeof entry === "string" && (binaryField(key)
|
|
114
|
+
|| ((key === "data" || key === "content") && "encoding" in item && item.encoding === "base64")
|
|
115
|
+
|| (key === "content" && "filename" in item && "content_type" in item)
|
|
116
|
+
|| (key === "blob" && "uri" in item && typeof item.uri === "string"));
|
|
117
|
+
result[key] = credentialField(key) ? "[credential excluded]" : encoded ? binaryMetadata(entry) : walk(entry);
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
ancestors.delete(item);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
return walk(value);
|
|
126
|
+
}
|
|
127
|
+
export function sanitizeDiagnosticArgv(argv) {
|
|
128
|
+
let credentialValue = false;
|
|
129
|
+
let binaryValue = false;
|
|
130
|
+
return argv.map((argument) => {
|
|
131
|
+
if (credentialValue) {
|
|
132
|
+
credentialValue = false;
|
|
133
|
+
return "[credential excluded]";
|
|
134
|
+
}
|
|
135
|
+
if (binaryValue) {
|
|
136
|
+
binaryValue = false;
|
|
137
|
+
return JSON.stringify(binaryMetadata(argument));
|
|
138
|
+
}
|
|
139
|
+
if (argument.startsWith("--")) {
|
|
140
|
+
const [flag] = argument.slice(2).split("=");
|
|
141
|
+
if (credentialField(flag)) {
|
|
142
|
+
if (argument.includes("="))
|
|
143
|
+
return argument.slice(0, argument.indexOf("=") + 1) + "[credential excluded]";
|
|
144
|
+
credentialValue = true;
|
|
145
|
+
}
|
|
146
|
+
else if (binaryField(flag)) {
|
|
147
|
+
if (argument.includes("="))
|
|
148
|
+
return argument.slice(0, argument.indexOf("=") + 1) + JSON.stringify(binaryMetadata(argument.slice(argument.indexOf("=") + 1)));
|
|
149
|
+
binaryValue = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// JSON-valued arguments need keyed filtering as well as token scanning.
|
|
153
|
+
if (/^[\[{]/.test(argument)) {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.stringify(sanitizeDiagnosticContent(JSON.parse(argument)));
|
|
156
|
+
}
|
|
157
|
+
catch { /* ordinary text */ }
|
|
158
|
+
}
|
|
159
|
+
return sanitizeDiagnosticText(argument);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export function parseDiagnosticRecord(value) {
|
|
163
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
164
|
+
throw new Error("Invalid diagnostic record");
|
|
165
|
+
const row = value;
|
|
166
|
+
if (row.version !== 1 || !diagnosticId(row.id) || !diagnosticId(row.invocation_id)
|
|
167
|
+
|| !["cli", "mcp", "web", "worker"].includes(row.surface)
|
|
168
|
+
|| !["invocation", "api", "worker", "protocol"].includes(row.layer)
|
|
169
|
+
|| !["ok", "invalid_input", "refused", "platform_error", "network_error", "cancelled", "pending"].includes(row.outcome)
|
|
170
|
+
|| !["complete", "partial"].includes(row.capture_state)
|
|
171
|
+
|| typeof row.operation !== "string" || row.operation.length > 512
|
|
172
|
+
|| !Number.isFinite(Date.parse(row.started_at))
|
|
173
|
+
|| (row.ended_at !== null && !Number.isFinite(Date.parse(row.ended_at)))
|
|
174
|
+
|| typeof row.identity_verified !== "boolean"
|
|
175
|
+
|| (row.organization_id !== null && !diagnosticId(row.organization_id))
|
|
176
|
+
|| (row.user_id !== null && typeof row.user_id !== "string")
|
|
177
|
+
|| (row.parent_id !== null && !diagnosticId(row.parent_id))
|
|
178
|
+
|| (row.status !== null && (!Number.isInteger(row.status) || row.status < 0 || row.status > 599))
|
|
179
|
+
|| (row.error_code !== null && (typeof row.error_code !== "string" || row.error_code.length > 512))
|
|
180
|
+
|| !row.metadata || typeof row.metadata !== "object" || Array.isArray(row.metadata))
|
|
181
|
+
throw new Error("Invalid diagnostic record");
|
|
182
|
+
return row;
|
|
183
|
+
}
|
|
184
|
+
export function diagnosticSummary(record) {
|
|
185
|
+
return {
|
|
186
|
+
diagnostic_id: record.id, invocation_id: record.invocation_id, parent_id: record.parent_id,
|
|
187
|
+
layer: record.layer, source: record.surface, operation: record.operation, outcome: record.outcome,
|
|
188
|
+
error_code: record.error_code, status: record.status, capture_state: record.capture_state,
|
|
189
|
+
identity_verified: record.identity_verified, client_version: record.client_version, trace_id: record.trace_id,
|
|
190
|
+
duration_ms: record.ended_at ? Math.max(0, Date.parse(record.ended_at) - Date.parse(record.started_at)) : null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export function credentialSafeDiagnostic(record) {
|
|
194
|
+
const input = record.input;
|
|
195
|
+
if (Array.isArray(input)) {
|
|
196
|
+
const members = input.map(member => credentialSafeDiagnostic({ ...record, input: member }));
|
|
197
|
+
const withheld = (value) => Boolean(value && typeof value === "object" && "withheld" in value && value.withheld === "credential_bearing_operation");
|
|
198
|
+
return { ...record,
|
|
199
|
+
input: members.some(member => withheld(member.input)) ? { withheld: "credential_bearing_operation" } : input,
|
|
200
|
+
output: members.some(member => withheld(member.output)) ? { withheld: "credential_bearing_operation" } : record.output };
|
|
201
|
+
}
|
|
202
|
+
const params = input && typeof input === "object" && "params" in input ? input.params : null;
|
|
203
|
+
const toolName = params && typeof params.name === "string" ? params.name : record.operation;
|
|
204
|
+
const args = params?.arguments ?? input;
|
|
205
|
+
const selectedName = ["oxygen_capabilities_read", "oxygen_capabilities_call"].includes(toolName) && args && typeof args === "object" && "tool" in args
|
|
206
|
+
? String(args.tool) : toolName;
|
|
207
|
+
return { ...record, input: credentialBearingOperation(selectedName, "input") ? { withheld: "credential_bearing_operation" } : record.input,
|
|
208
|
+
output: credentialBearingOperation(selectedName, "output") ? { withheld: "credential_bearing_operation" } : record.output };
|
|
209
|
+
}
|
|
210
|
+
/** Bound the diagnostic branch of a response tee without delaying its consumer. */
|
|
211
|
+
export async function readDiagnosticProtocolText(body) {
|
|
212
|
+
if (!body)
|
|
213
|
+
return null;
|
|
214
|
+
const reader = body.getReader();
|
|
215
|
+
const chunks = [];
|
|
216
|
+
let bytes = 0;
|
|
217
|
+
try {
|
|
218
|
+
while (true) {
|
|
219
|
+
const part = await reader.read();
|
|
220
|
+
if (part.done)
|
|
221
|
+
return Buffer.concat(chunks, bytes).toString("utf8");
|
|
222
|
+
bytes += part.value.byteLength;
|
|
223
|
+
if (bytes > DIAGNOSTIC_RECORD_MAX_BYTES) {
|
|
224
|
+
void reader.cancel().catch(() => undefined);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
chunks.push(part.value);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
reader.releaseLock();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Finite POST SSE is decoded before credential/binary filtering, like JSON. */
|
|
238
|
+
export function parseDiagnosticProtocolBody(text, contentType) {
|
|
239
|
+
if (text === null)
|
|
240
|
+
return null;
|
|
241
|
+
if (contentType?.includes("text/event-stream")) {
|
|
242
|
+
return text.split(/\r?\n\r?\n/).filter(Boolean).map(event => ({
|
|
243
|
+
event: event.split(/\r?\n/).find(line => line.startsWith("event:"))?.slice(6).trim() ?? null,
|
|
244
|
+
data: parseDiagnosticProtocolBody(event.split(/\r?\n/).filter(line => line.startsWith("data:")).map(line => line.slice(5).trimStart()).join("\n")),
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(text);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return text;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -70,6 +70,14 @@ export type GeoAnswer = {
|
|
|
70
70
|
prompt_revision?: number;
|
|
71
71
|
topic?: string;
|
|
72
72
|
};
|
|
73
|
+
export type GeoExportPagination = {
|
|
74
|
+
returned: number;
|
|
75
|
+
total: number;
|
|
76
|
+
next_cursor: string | null;
|
|
77
|
+
summary_scope: "all_filtered_answers";
|
|
78
|
+
};
|
|
79
|
+
/** Binary exports carry this metadata to web, CLI and MCP, even for CSV/PDF. */
|
|
80
|
+
export declare function readGeoExportPagination(headers: Pick<Headers, "get">): GeoExportPagination | undefined;
|
|
73
81
|
export type GeoDocumentKind = "prompt" | "competitor" | "answer" | "evidence" | "opportunity" | "experiment" | "audit";
|
|
74
82
|
export type GeoDocument = {
|
|
75
83
|
id: string;
|
|
@@ -96,6 +104,7 @@ export type GeoRunItem = {
|
|
|
96
104
|
error: string | null;
|
|
97
105
|
attempt: number;
|
|
98
106
|
terminal_before_dispatch?: boolean;
|
|
107
|
+
submission_rejected?: boolean;
|
|
99
108
|
submission_cost_micros?: number | null;
|
|
100
109
|
};
|
|
101
110
|
export declare const GEO_PILOT_CAP_MICROS = 10000000;
|
|
@@ -110,10 +119,14 @@ export declare function analyzeGeoAnswer(text: string, brand: GeoCompetitor, com
|
|
|
110
119
|
};
|
|
111
120
|
/** Exact capture-time configuration, canonicalized for matched comparisons. */
|
|
112
121
|
export declare function geoMeasurementKey(answer: Pick<GeoAnswer, "measurement">): string;
|
|
122
|
+
/** A scraper's question heading is not answer prose, including old stored captures. */
|
|
123
|
+
export declare function isGeoPromptEcho(text: string, prompt: string): boolean;
|
|
124
|
+
export declare function isGeoControlledAnswer(answer: GeoAnswer): boolean;
|
|
113
125
|
export declare function summarizeGeoAnswers(answers: GeoAnswer[], brand: string, failedSamples?: number): {
|
|
114
126
|
successful_samples: number;
|
|
115
127
|
measurement_contexts: number;
|
|
116
128
|
failed_samples: number;
|
|
129
|
+
invalid_answer_samples: number;
|
|
117
130
|
imported_samples: number;
|
|
118
131
|
no_overview_samples: number;
|
|
119
132
|
mentioned_samples: number;
|
|
@@ -149,6 +162,7 @@ export declare function compareGeoExperiment(before: GeoAnswer[], after: GeoAnsw
|
|
|
149
162
|
successful_samples: number;
|
|
150
163
|
measurement_contexts: number;
|
|
151
164
|
failed_samples: number;
|
|
165
|
+
invalid_answer_samples: number;
|
|
152
166
|
imported_samples: number;
|
|
153
167
|
no_overview_samples: number;
|
|
154
168
|
mentioned_samples: number;
|
|
@@ -172,6 +186,7 @@ export declare function compareGeoExperiment(before: GeoAnswer[], after: GeoAnsw
|
|
|
172
186
|
successful_samples: number;
|
|
173
187
|
measurement_contexts: number;
|
|
174
188
|
failed_samples: number;
|
|
189
|
+
invalid_answer_samples: number;
|
|
175
190
|
imported_samples: number;
|
|
176
191
|
no_overview_samples: number;
|
|
177
192
|
mentioned_samples: number;
|
|
@@ -200,6 +215,7 @@ export declare function compareGeoExperiment(before: GeoAnswer[], after: GeoAnsw
|
|
|
200
215
|
successful_samples: number;
|
|
201
216
|
measurement_contexts: number;
|
|
202
217
|
failed_samples: number;
|
|
218
|
+
invalid_answer_samples: number;
|
|
203
219
|
imported_samples: number;
|
|
204
220
|
no_overview_samples: number;
|
|
205
221
|
mentioned_samples: number;
|
|
@@ -223,6 +239,7 @@ export declare function compareGeoExperiment(before: GeoAnswer[], after: GeoAnsw
|
|
|
223
239
|
successful_samples: number;
|
|
224
240
|
measurement_contexts: number;
|
|
225
241
|
failed_samples: number;
|
|
242
|
+
invalid_answer_samples: number;
|
|
226
243
|
imported_samples: number;
|
|
227
244
|
no_overview_samples: number;
|
|
228
245
|
mentioned_samples: number;
|
|
@@ -9,6 +9,24 @@ export const GEO_ENGINES = [
|
|
|
9
9
|
"copilot",
|
|
10
10
|
"claude",
|
|
11
11
|
];
|
|
12
|
+
/** Binary exports carry this metadata to web, CLI and MCP, even for CSV/PDF. */
|
|
13
|
+
export function readGeoExportPagination(headers) {
|
|
14
|
+
const value = headers.get("x-oxygen-export-pagination");
|
|
15
|
+
if (!value || value.length > 2048)
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(value);
|
|
19
|
+
if (!Number.isSafeInteger(data.returned) || data.returned < 0 ||
|
|
20
|
+
!Number.isSafeInteger(data.total) || data.total < data.returned ||
|
|
21
|
+
!(data.next_cursor === null || typeof data.next_cursor === "string" && /^[0-9a-f-]{36}$/i.test(data.next_cursor)) ||
|
|
22
|
+
data.summary_scope !== "all_filtered_answers")
|
|
23
|
+
return undefined;
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
12
30
|
export const GEO_PILOT_CAP_MICROS = 10_000_000;
|
|
13
31
|
/** The initial release gate is independent of NODE_ENV and protects shadow data. */
|
|
14
32
|
export function isGeoEnabled(env = process.env) {
|
|
@@ -137,8 +155,18 @@ export function geoMeasurementKey(answer) {
|
|
|
137
155
|
.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
|
|
138
156
|
});
|
|
139
157
|
}
|
|
158
|
+
/** A scraper's question heading is not answer prose, including old stored captures. */
|
|
159
|
+
export function isGeoPromptEcho(text, prompt) {
|
|
160
|
+
const normalized = (value) => value.replace(/^[#>\s]+/gm, "").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
161
|
+
return Boolean(normalized(prompt)) && normalized(text) === normalized(prompt);
|
|
162
|
+
}
|
|
163
|
+
export function isGeoControlledAnswer(answer) {
|
|
164
|
+
return answer.collection === "consumer" && answer.outcome === "answer" &&
|
|
165
|
+
typeof answer.text === "string" && Boolean(answer.text.trim()) &&
|
|
166
|
+
!isGeoPromptEcho(answer.text, answer.prompt ?? "");
|
|
167
|
+
}
|
|
140
168
|
export function summarizeGeoAnswers(answers, brand, failedSamples = 0) {
|
|
141
|
-
const samples = answers.filter(
|
|
169
|
+
const samples = answers.filter(isGeoControlledAnswer);
|
|
142
170
|
const measurementContexts = new Set(samples.map(geoMeasurementKey)).size;
|
|
143
171
|
const comparable = measurementContexts <= 1;
|
|
144
172
|
const brands = new Map();
|
|
@@ -170,6 +198,7 @@ export function summarizeGeoAnswers(answers, brand, failedSamples = 0) {
|
|
|
170
198
|
successful_samples: samples.length,
|
|
171
199
|
measurement_contexts: measurementContexts,
|
|
172
200
|
failed_samples: failedSamples,
|
|
201
|
+
invalid_answer_samples: answers.filter((a) => a.collection === "consumer" && a.outcome === "answer" && !isGeoControlledAnswer(a)).length,
|
|
173
202
|
imported_samples: answers.filter((a) => a.collection === "import").length,
|
|
174
203
|
no_overview_samples: answers.filter((a) => a.outcome === "no_overview")
|
|
175
204
|
.length,
|
|
@@ -279,8 +308,7 @@ export function parseGeoTranscript(input, redactions = []) {
|
|
|
279
308
|
}
|
|
280
309
|
/** Compare only common collection cohorts; imports and missing AIOs are excluded. */
|
|
281
310
|
export function compareGeoExperiment(before, after, brand) {
|
|
282
|
-
const eligible = (answers) => answers.filter((a) => a
|
|
283
|
-
a.outcome === "answer" &&
|
|
311
|
+
const eligible = (answers) => answers.filter((a) => isGeoControlledAnswer(a) &&
|
|
284
312
|
a.measurement !== undefined);
|
|
285
313
|
const key = (a) => JSON.stringify([
|
|
286
314
|
a.prompt_id,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The table the onboarding consultant saves its ranked first leads into (founder, current
|
|
3
|
+
* chat, 2026-10-09), so a first session leaves a durable record the user can open, enrich
|
|
4
|
+
* and enroll instead of a chat transcript. The onboarding read (web) and the ChatGPT
|
|
5
|
+
* connector's onboarding hint (MCP) both build it from here; the served onboarding skill
|
|
6
|
+
* shows the same shape, and `apps/web/src/lib/agent-skills.test.ts` holds it to this one.
|
|
7
|
+
*
|
|
8
|
+
* The upsert refuses a row without its key, so every row carries `lead_key`: the LinkedIn
|
|
9
|
+
* profile URL, or the email when there is no profile. The key also keeps a re-run from
|
|
10
|
+
* adding anyone twice. Creating the table and writing its rows cost 0 credits.
|
|
11
|
+
*/
|
|
12
|
+
export declare const FIRST_LEADS_TABLE = "First leads";
|
|
13
|
+
export declare const FIRST_LEADS_UPSERT_KEY = "lead_key";
|
|
14
|
+
export type FirstLeadsColumn = {
|
|
15
|
+
key: string;
|
|
16
|
+
label: string;
|
|
17
|
+
dataType: "text";
|
|
18
|
+
};
|
|
19
|
+
export declare const FIRST_LEADS_COLUMNS: readonly FirstLeadsColumn[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The table the onboarding consultant saves its ranked first leads into (founder, current
|
|
3
|
+
* chat, 2026-10-09), so a first session leaves a durable record the user can open, enrich
|
|
4
|
+
* and enroll instead of a chat transcript. The onboarding read (web) and the ChatGPT
|
|
5
|
+
* connector's onboarding hint (MCP) both build it from here; the served onboarding skill
|
|
6
|
+
* shows the same shape, and `apps/web/src/lib/agent-skills.test.ts` holds it to this one.
|
|
7
|
+
*
|
|
8
|
+
* The upsert refuses a row without its key, so every row carries `lead_key`: the LinkedIn
|
|
9
|
+
* profile URL, or the email when there is no profile. The key also keeps a re-run from
|
|
10
|
+
* adding anyone twice. Creating the table and writing its rows cost 0 credits.
|
|
11
|
+
*/
|
|
12
|
+
export const FIRST_LEADS_TABLE = "First leads";
|
|
13
|
+
export const FIRST_LEADS_UPSERT_KEY = "lead_key";
|
|
14
|
+
export const FIRST_LEADS_COLUMNS = [
|
|
15
|
+
["name", "Name"],
|
|
16
|
+
["title", "Title"],
|
|
17
|
+
["company", "Company"],
|
|
18
|
+
["linkedin_url", "LinkedIn URL"],
|
|
19
|
+
["email", "Email"],
|
|
20
|
+
[FIRST_LEADS_UPSERT_KEY, "Lead key"],
|
|
21
|
+
["why_warm", "Why warm"],
|
|
22
|
+
["icp_fit", "ICP fit"],
|
|
23
|
+
["next_step", "Next step"],
|
|
24
|
+
["draft_message", "Draft message"],
|
|
25
|
+
].map(([key, label]) => ({ key, label, dataType: "text" }));
|
|
@@ -60,7 +60,7 @@ export type OtlpLogSinkOptions = {
|
|
|
60
60
|
* `worker.log_shipper.stats` is the Axiom shipper's once-a-minute loss accounting,
|
|
61
61
|
* which by design never reaches Axiom (OXP-1.6.126).
|
|
62
62
|
*/
|
|
63
|
-
export declare const OTLP_MONITOR_INFO_EVENTS: readonly ["tenant.migrations.cron.done", "outbound_send.sent", "worker_workflow_cron_overlap_skipped", "worker_workflow_cron_enqueued", "copilot.turn.finished", "worker.queue.heartbeat", "worker.queue.ledger", "worker.queue.redis", "worker.queue.unregistered_kind", "automation_usage.reconcile_completed", "provider_health.cron_completed", "provider_balances.cron_completed", "cost_snapshot.cron_completed", "worker.egress_order_reconcile_sweep", "posthog_canary.round_trip_ok", "web.log_volume", "worker.log_shipper.stats"];
|
|
63
|
+
export declare const OTLP_MONITOR_INFO_EVENTS: readonly ["diagnostics.capture_stored", "diagnostics.invocation_completed", "diagnostics.canary_completed", "diagnostics.digest_completed", "tenant.migrations.cron.done", "outbound_send.sent", "worker_workflow_cron_overlap_skipped", "worker_workflow_cron_enqueued", "copilot.turn.finished", "worker.queue.heartbeat", "worker.queue.ledger", "worker.queue.redis", "worker.queue.unregistered_kind", "automation_usage.reconcile_completed", "provider_health.cron_completed", "provider_balances.cron_completed", "cost_snapshot.cron_completed", "worker.egress_order_reconcile_sweep", "posthog_canary.round_trip_ok", "web.log_volume", "worker.log_shipper.stats"];
|
|
64
64
|
export type OtlpLogSinkHandle = {
|
|
65
65
|
sink: LogSink;
|
|
66
66
|
flush: () => Promise<void>;
|
|
@@ -12,6 +12,10 @@ import { telemetryEndpointForLog } from "./telemetry-export-observer.js";
|
|
|
12
12
|
* which by design never reaches Axiom (OXP-1.6.126).
|
|
13
13
|
*/
|
|
14
14
|
export const OTLP_MONITOR_INFO_EVENTS = [
|
|
15
|
+
"diagnostics.capture_stored",
|
|
16
|
+
"diagnostics.invocation_completed",
|
|
17
|
+
"diagnostics.canary_completed",
|
|
18
|
+
"diagnostics.digest_completed",
|
|
15
19
|
"tenant.migrations.cron.done",
|
|
16
20
|
"outbound_send.sent",
|
|
17
21
|
"worker_workflow_cron_overlap_skipped",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* set, so adding one is a deliberate edit there too.
|
|
16
16
|
*/
|
|
17
17
|
export declare const PRODUCT_EVENTS: {
|
|
18
|
+
/** Internal diagnostic receipt, never counted as an additional customer action. */
|
|
19
|
+
readonly DIAGNOSTIC_CAPTURE: "diagnostic_capture";
|
|
18
20
|
/** Acquisition. */
|
|
19
21
|
readonly USER_SIGNED_UP: "user_signed_up";
|
|
20
22
|
readonly WORKSPACE_CREATED: "workspace_created";
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* set, so adding one is a deliberate edit there too.
|
|
16
16
|
*/
|
|
17
17
|
export const PRODUCT_EVENTS = {
|
|
18
|
+
/** Internal diagnostic receipt, never counted as an additional customer action. */
|
|
19
|
+
DIAGNOSTIC_CAPTURE: "diagnostic_capture",
|
|
18
20
|
/** Acquisition. */
|
|
19
21
|
USER_SIGNED_UP: "user_signed_up",
|
|
20
22
|
WORKSPACE_CREATED: "workspace_created",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OXYGEN_BUILD_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_BUILD_VERSION = "1.1013.15";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
|
|
2
|
-
export const OXYGEN_BUILD_VERSION = "1.
|
|
2
|
+
export const OXYGEN_BUILD_VERSION = "1.1013.15";
|
|
@@ -50,3 +50,6 @@ export const OXYGEN_VERSION = OXYGEN_BUILD_VERSION;
|
|
|
50
50
|
// highest CLI version published on npm for at least 30 days. It never lowers
|
|
51
51
|
// the floor. The source change lands on dev before the next production release.
|
|
52
52
|
export const OXYGEN_MINIMUM_CLI_VERSION = "1.922.14";
|
|
53
|
+
// The internal GEO export first gains required pagination in this dev version.
|
|
54
|
+
// Earlier pilot clients would save a partial file without its continuation.
|
|
55
|
+
export const GEO_EXPORT_MINIMUM_CLI_VERSION = "1.1013.0";
|