@oxygen-agent/cli 1.922.14 → 1.936.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/admin-primary-providers-render.d.ts +18 -0
- package/dist/admin-primary-providers-render.js +371 -0
- package/dist/command-manifest.js +6 -0
- package/dist/functions-commands.d.ts +6 -0
- package/dist/functions-commands.js +56 -0
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +49 -2
- package/dist/index.js +175 -40
- package/dist/ugc-commands.d.ts +6 -0
- package/dist/ugc-commands.js +748 -0
- package/dist/visual-commands.d.ts +6 -0
- package/dist/visual-commands.js +57 -0
- package/dist/visual-render-wait.d.ts +3 -0
- package/dist/visual-render-wait.js +56 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +84 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +26 -10
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/index.js +4 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/knowledge-constants.js +3 -2
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +1 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +17 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +5 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.d.ts +113 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +158 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +24 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +54 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +113 -0
- package/node_modules/@oxygen/shared/dist/ugc.js +2 -0
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +33 -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/visual-render.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/visual-render.js +55 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +126 -0
- package/node_modules/@oxygen/shared/package.json +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { OxygenError } from "@oxygen/shared";
|
|
5
|
+
import { requestOxygen } from "./http-client.js";
|
|
6
|
+
import { waitForVisualRender } from "./visual-render-wait.js";
|
|
7
|
+
export function registerVisualCommands(program, handle) {
|
|
8
|
+
const visuals = program.command("visuals").description("Author, render, inspect and download GTM infographics using tenant workspace files and governed render jobs.");
|
|
9
|
+
visuals.command("source-create").description("Save an immutable HTML/CSS/SVG source revision; no code executes.")
|
|
10
|
+
.requiredOption("--name <name>", "Workspace filename.")
|
|
11
|
+
.requiredOption("--file <path>", "Local HTML source file (max 1.5 MB).")
|
|
12
|
+
.option("--parent-file-id <id>", "Previous source revision UUID.").option("--json", "Print a JSON envelope.")
|
|
13
|
+
.action((options) => handle("visuals source create", options, async () => {
|
|
14
|
+
if (statSync(options.file).size > 1_500_000)
|
|
15
|
+
throw new OxygenError("visual_source_limit", "HTML source must fit within 1.5 MB.");
|
|
16
|
+
const bytes = readFileSync(options.file);
|
|
17
|
+
if (bytes.length > 1_500_000)
|
|
18
|
+
throw new OxygenError("visual_source_limit", "HTML source must fit within 1.5 MB.");
|
|
19
|
+
return requestOxygen("/api/cli/visuals/sources", { method: "POST", body: { name: options.name, html: bytes.toString("utf8"), ...(options.parentFileId ? { parent_file_id: options.parentFileId } : {}) } });
|
|
20
|
+
}));
|
|
21
|
+
visuals.command("file-get <file-id>").description("Read source, file metadata, provenance and web/download links.").option("--json", "Print a JSON envelope.")
|
|
22
|
+
.action((id, options) => handle("visuals file get", options, () => requestOxygen(`/api/cli/visuals/files/${encodeURIComponent(id)}`)));
|
|
23
|
+
visuals.command("download <file-id>").description("Download a file with SHA-256 verification; refuses to overwrite existing local files.")
|
|
24
|
+
.requiredOption("--out <path>", "New local destination filename.").option("--json", "Print a JSON envelope.")
|
|
25
|
+
.action((id, options) => handle("visuals download", options, async () => {
|
|
26
|
+
const path = `/api/cli/visuals/files/${encodeURIComponent(id)}`;
|
|
27
|
+
const data = await requestOxygen(path);
|
|
28
|
+
const bytes = await requestOxygen(`${path}/download`, { binaryMaxBytes: Math.max(1, data.file.size_bytes) });
|
|
29
|
+
if (bytes.length !== data.file.size_bytes || createHash("sha256").update(bytes).digest("hex") !== data.file.sha256)
|
|
30
|
+
throw new OxygenError("file_integrity", "Downloaded bytes do not match the workspace file hash.");
|
|
31
|
+
writeFileSync(options.out, bytes, { flag: "wx", mode: 0o600 });
|
|
32
|
+
return { path: resolve(options.out), ...data.file };
|
|
33
|
+
}));
|
|
34
|
+
visuals.command("render-preview").description("Preview rendering without starting paid compute.")
|
|
35
|
+
.requiredOption("--source-file-ids <ids>", "Comma-separated HTML file UUIDs in page order.")
|
|
36
|
+
.option("--width <pixels>", "CSS pixel width.", "1080").option("--height <pixels>", "CSS pixel height.", "1350")
|
|
37
|
+
.option("--scale <1|2>", "PNG scale.", "1").option("--formats <png,pdf>", "Output formats.", "png")
|
|
38
|
+
.option("--json", "Print a JSON envelope.")
|
|
39
|
+
.action((options) => handle("visuals render preview", options, () => requestOxygen("/api/cli/visuals/renders", { method: "POST", body: { source_file_ids: options.sourceFileIds.split(",").map((s) => s.trim()), width: Number(options.width), height: Number(options.height), scale: Number(options.scale), formats: options.formats.split(",") } })));
|
|
40
|
+
visuals.command("render-start <render-id>").description("Approve an exact preview and enqueue one idempotent render job.")
|
|
41
|
+
.requiredOption("--scope-hash <hash>", "Exact preview scope hash.").requiredOption("--max-credits <credits>", "Approved ceiling from preview.")
|
|
42
|
+
.requiredOption("--approved", "Authorize this exact render.").option("--json", "Print a JSON envelope.")
|
|
43
|
+
.action((id, options) => handle("visuals render start", options, () => requestOxygen(`/api/cli/visuals/renders/${encodeURIComponent(id)}`, { method: "POST", body: { scope_hash: options.scopeHash, max_credits: Number(options.maxCredits), approved: options.approved } })));
|
|
44
|
+
visuals.command("render-get <render-id>").description("Read render state and output downloads; optionally wait for a terminal state.")
|
|
45
|
+
.option("--wait", "Poll every 2 seconds until completed, failed, cancelled or effect_unknown; starts no render.")
|
|
46
|
+
.option("--timeout-seconds <n>", "Maximum wait with --wait, from 1 to 600 seconds. Defaults to 180.")
|
|
47
|
+
.option("--json", "Print a JSON envelope.")
|
|
48
|
+
.action((id, options) => handle("visuals render get", options, async () => {
|
|
49
|
+
if (options.wait)
|
|
50
|
+
return waitForVisualRender(id, options.timeoutSeconds);
|
|
51
|
+
if (options.timeoutSeconds !== undefined)
|
|
52
|
+
throw new OxygenError("invalid_request", "Pass --wait with --timeout-seconds.");
|
|
53
|
+
return requestOxygen(`/api/cli/visuals/renders/${encodeURIComponent(id)}`);
|
|
54
|
+
}));
|
|
55
|
+
visuals.command("render-cancel <render-id>").description("Request cancellation; read state to verify it stopped.")
|
|
56
|
+
.option("--json", "Print a JSON envelope.").action((id, options) => handle("visuals render cancel", options, () => requestOxygen(`/api/cli/visuals/renders/${encodeURIComponent(id)}`, { method: "DELETE" })));
|
|
57
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { OxygenError } from "@oxygen/shared";
|
|
2
|
+
import { readPositiveInt, readRecordString } from "./cli-values.js";
|
|
3
|
+
import { requestOxygen } from "./http-client.js";
|
|
4
|
+
import { waitForCliRun } from "./run-wait.js";
|
|
5
|
+
const TERMINAL = new Set(["completed", "failed", "cancelled", "effect_unknown"]);
|
|
6
|
+
const DEFAULT_TIMEOUT_SECONDS = 180;
|
|
7
|
+
const MAX_TIMEOUT_SECONDS = 600;
|
|
8
|
+
/** Poll only the existing job. Neither terminal failure nor timeout authorizes
|
|
9
|
+
* a start, retry, cancellation, or a new credit reservation. */
|
|
10
|
+
export async function waitForVisualRender(id, requestedTimeoutSeconds) {
|
|
11
|
+
const timeoutSeconds = readPositiveInt(requestedTimeoutSeconds) ?? DEFAULT_TIMEOUT_SECONDS;
|
|
12
|
+
if (timeoutSeconds > MAX_TIMEOUT_SECONDS)
|
|
13
|
+
throw new OxygenError("invalid_number", "Visual render wait timeout must be between 1 and 600 seconds.");
|
|
14
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
let polls = 0;
|
|
17
|
+
let latestStatus = null;
|
|
18
|
+
const timedOut = () => new OxygenError("visual_render_wait_timeout", "Timed out waiting for the visual render. Read the same render ID again to continue checking.", {
|
|
19
|
+
details: { render_id: id, status: latestStatus, timeout_seconds: timeoutSeconds, polls }, exitCode: 1,
|
|
20
|
+
});
|
|
21
|
+
let timer;
|
|
22
|
+
const timeout = new Promise((_, reject) => {
|
|
23
|
+
timer = setTimeout(() => { controller.abort(); reject(timedOut()); }, timeoutSeconds * 1000);
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
return await Promise.race([waitForCliRun({
|
|
27
|
+
runId: id, requestedTimeoutSeconds: String(timeoutSeconds),
|
|
28
|
+
defaultTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS, defaultIntervalSeconds: 2,
|
|
29
|
+
fetchRun: async () => {
|
|
30
|
+
const remainingMs = deadline - Date.now();
|
|
31
|
+
if (controller.signal.aborted || remainingMs <= 0)
|
|
32
|
+
throw timedOut();
|
|
33
|
+
polls += 1;
|
|
34
|
+
const data = await requestOxygen(`/api/cli/visuals/renders/${encodeURIComponent(id)}`, {
|
|
35
|
+
method: "GET", timeoutMs: remainingMs, signal: controller.signal,
|
|
36
|
+
});
|
|
37
|
+
latestStatus = readRecordString(data.render, "status");
|
|
38
|
+
if (latestStatus === "preview")
|
|
39
|
+
throw new OxygenError("visual_render_not_started", "This render is still a preview. Start the approved render before waiting.", {
|
|
40
|
+
details: { render_id: id, status: latestStatus },
|
|
41
|
+
});
|
|
42
|
+
if (!latestStatus || !(TERMINAL.has(latestStatus) || latestStatus === "queued" || latestStatus === "running")) {
|
|
43
|
+
throw new OxygenError("invalid_response", "The render returned an unrecognized status.", { details: { render_id: id, status: latestStatus } });
|
|
44
|
+
}
|
|
45
|
+
return { ...data, status: latestStatus };
|
|
46
|
+
},
|
|
47
|
+
isTerminal: (status) => status !== null && TERMINAL.has(status),
|
|
48
|
+
shapeTerminal: (data, status, count, elapsedMs) => ({ ...data, status, terminal: true, polls: count, elapsedMs }),
|
|
49
|
+
timeoutCode: "visual_render_wait_timeout", timeoutMessage: "Timed out waiting for the visual render.", timeoutDetailIdKey: "render_id",
|
|
50
|
+
}), timeout]);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
controller.abort();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which providers `oxygen integrations connect <id> --api-key <key>` ACTUALLY
|
|
3
|
+
* connects for native execution.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `native_provider.execution.supports_byok` says a TOOL can run on
|
|
6
|
+
* a customer's own key. It does not say the customer has any way to give us one.
|
|
7
|
+
* Those are two different facts, and 10 of the 42 byok-capable native providers
|
|
8
|
+
* have the first without the second:
|
|
9
|
+
*
|
|
10
|
+
* apify builtwith dropleads lusha openwebninja parallel peopledatalabs
|
|
11
|
+
* rocketreach theirstack wiza
|
|
12
|
+
*
|
|
13
|
+
* For those, `oxygen integrations connect <id> --api-key` does not reach
|
|
14
|
+
* `connectViaApiKey` at all. With no native catalog definition,
|
|
15
|
+
* `shouldConnectIntegrationViaComposio` is true, so the key is stored WITH COMPOSIO
|
|
16
|
+
* and no `integration_secrets` row of kind `api_key` is written. On the run path the
|
|
17
|
+
* tool is native generic-REST and `shouldRouteStaticToolThroughComposio` is false, so
|
|
18
|
+
* `resolveOptionalApiKeyCredentials` finds nothing and the call falls back to the
|
|
19
|
+
* MANAGED key — the very key that was out of credits. The command appears to work,
|
|
20
|
+
* changes nothing, and the customer is back where they started with no error.
|
|
21
|
+
*
|
|
22
|
+
* That is precisely the failure Plain T-104 would have hit: `parallel.search` was one
|
|
23
|
+
* of the refused tools, and a refusal message that told Mentcape to connect their own
|
|
24
|
+
* Parallel key would have sent them down a path that silently does nothing.
|
|
25
|
+
*
|
|
26
|
+
* So a message may only offer BYOK when BOTH halves hold, and this list is the second
|
|
27
|
+
* half. It lives in @oxygen/shared because the two sides that need it cannot see each
|
|
28
|
+
* other: `packages/integrations` writes the message, the catalog it would have to
|
|
29
|
+
* consult lives in `apps/web/src/lib/integrations/catalog.ts`, and packages never
|
|
30
|
+
* import from apps. `catalog.byok-connect.test.ts` in apps/web binds the two together
|
|
31
|
+
* so this list cannot drift from the connect forms it claims exist.
|
|
32
|
+
*
|
|
33
|
+
* ALLOWLIST, NOT DENYLIST, on purpose. A new provider that arrives without a connect
|
|
34
|
+
* form gets the alternatives-only message until someone adds it here — quieter than
|
|
35
|
+
* correct. A denylist would fail the other way: silently promising a connect form
|
|
36
|
+
* nobody built, which is the bug this fixes.
|
|
37
|
+
*/
|
|
38
|
+
export declare const NATIVE_API_KEY_CONNECT_INTEGRATION_IDS: ReadonlySet<string>;
|
|
39
|
+
/**
|
|
40
|
+
* True when a customer can hand Oxygen their own key for this provider AND the
|
|
41
|
+
* native runner will use it. Never infer this from `supports_byok` alone.
|
|
42
|
+
*/
|
|
43
|
+
export declare function hasNativeApiKeyConnect(integrationId: string | null | undefined): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which providers `oxygen integrations connect <id> --api-key <key>` ACTUALLY
|
|
3
|
+
* connects for native execution.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `native_provider.execution.supports_byok` says a TOOL can run on
|
|
6
|
+
* a customer's own key. It does not say the customer has any way to give us one.
|
|
7
|
+
* Those are two different facts, and 10 of the 42 byok-capable native providers
|
|
8
|
+
* have the first without the second:
|
|
9
|
+
*
|
|
10
|
+
* apify builtwith dropleads lusha openwebninja parallel peopledatalabs
|
|
11
|
+
* rocketreach theirstack wiza
|
|
12
|
+
*
|
|
13
|
+
* For those, `oxygen integrations connect <id> --api-key` does not reach
|
|
14
|
+
* `connectViaApiKey` at all. With no native catalog definition,
|
|
15
|
+
* `shouldConnectIntegrationViaComposio` is true, so the key is stored WITH COMPOSIO
|
|
16
|
+
* and no `integration_secrets` row of kind `api_key` is written. On the run path the
|
|
17
|
+
* tool is native generic-REST and `shouldRouteStaticToolThroughComposio` is false, so
|
|
18
|
+
* `resolveOptionalApiKeyCredentials` finds nothing and the call falls back to the
|
|
19
|
+
* MANAGED key — the very key that was out of credits. The command appears to work,
|
|
20
|
+
* changes nothing, and the customer is back where they started with no error.
|
|
21
|
+
*
|
|
22
|
+
* That is precisely the failure Plain T-104 would have hit: `parallel.search` was one
|
|
23
|
+
* of the refused tools, and a refusal message that told Mentcape to connect their own
|
|
24
|
+
* Parallel key would have sent them down a path that silently does nothing.
|
|
25
|
+
*
|
|
26
|
+
* So a message may only offer BYOK when BOTH halves hold, and this list is the second
|
|
27
|
+
* half. It lives in @oxygen/shared because the two sides that need it cannot see each
|
|
28
|
+
* other: `packages/integrations` writes the message, the catalog it would have to
|
|
29
|
+
* consult lives in `apps/web/src/lib/integrations/catalog.ts`, and packages never
|
|
30
|
+
* import from apps. `catalog.byok-connect.test.ts` in apps/web binds the two together
|
|
31
|
+
* so this list cannot drift from the connect forms it claims exist.
|
|
32
|
+
*
|
|
33
|
+
* ALLOWLIST, NOT DENYLIST, on purpose. A new provider that arrives without a connect
|
|
34
|
+
* form gets the alternatives-only message until someone adds it here — quieter than
|
|
35
|
+
* correct. A denylist would fail the other way: silently promising a connect form
|
|
36
|
+
* nobody built, which is the bug this fixes.
|
|
37
|
+
*/
|
|
38
|
+
export const NATIVE_API_KEY_CONNECT_INTEGRATION_IDS = new Set([
|
|
39
|
+
"adyntel",
|
|
40
|
+
"ai_ark",
|
|
41
|
+
"apollo",
|
|
42
|
+
"bettercontact",
|
|
43
|
+
"blitzapi",
|
|
44
|
+
"bounceban",
|
|
45
|
+
"breakcold",
|
|
46
|
+
"brightdata",
|
|
47
|
+
"clearoutphone",
|
|
48
|
+
"contactout",
|
|
49
|
+
"crustdata",
|
|
50
|
+
"discolike",
|
|
51
|
+
"exa",
|
|
52
|
+
"findymail",
|
|
53
|
+
"firecrawl",
|
|
54
|
+
"forager",
|
|
55
|
+
"fullenrich",
|
|
56
|
+
"getsales",
|
|
57
|
+
"hunter",
|
|
58
|
+
"icypeas",
|
|
59
|
+
"leadmagic",
|
|
60
|
+
"linkup",
|
|
61
|
+
"millionverifier",
|
|
62
|
+
// Connects through saveMoltsetsConnection rather than the generic save, but that
|
|
63
|
+
// service calls saveApiKeyIntegrationConnection with secretKind "api_key" like the
|
|
64
|
+
// rest — so the native runner does find the key.
|
|
65
|
+
"moltsets",
|
|
66
|
+
"ocean",
|
|
67
|
+
"predictleads",
|
|
68
|
+
"prospeo",
|
|
69
|
+
"scalelist",
|
|
70
|
+
// scraper: dev v1.920.2 added the HarvestAPI workspace-key connect (native api-key form).
|
|
71
|
+
"scraper",
|
|
72
|
+
"serper",
|
|
73
|
+
"signalbase",
|
|
74
|
+
"zapmail",
|
|
75
|
+
"zerobounce",
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* True when a customer can hand Oxygen their own key for this provider AND the
|
|
79
|
+
* native runner will use it. Never infer this from `supports_byok` alone.
|
|
80
|
+
*/
|
|
81
|
+
export function hasNativeApiKeyConnect(integrationId) {
|
|
82
|
+
return typeof integrationId === "string"
|
|
83
|
+
&& NATIVE_API_KEY_CONNECT_INTEGRATION_IDS.has(integrationId);
|
|
84
|
+
}
|
|
@@ -3,6 +3,20 @@ import { RECIPE_PRIMITIVES } from "./recipes.js";
|
|
|
3
3
|
// It is navigation, never execution: live schemas, prices, readiness, and
|
|
4
4
|
// approval enforcement remain on the exact command/tool selected from here.
|
|
5
5
|
export const OXYGEN_CAPABILITY_ROUTES = [
|
|
6
|
+
{
|
|
7
|
+
id: "visual-rendering",
|
|
8
|
+
layer: "Action",
|
|
9
|
+
primitive: null,
|
|
10
|
+
owns: "Agent-authored GTM infographics and carousel pages: editable HTML/CSS/SVG sources, isolated PNG/PDF rendering, actual visual inspection, revisions and downloads.",
|
|
11
|
+
notFor: "A separate product primitive, arbitrary package execution, image generation providers, motion/video, or publishing.",
|
|
12
|
+
execution: "Save source in workspace files; preview exact rendering scope and price; approve, poll the durable job, inspect its PNG pixels, revise and export. Renderer availability is checked by preview.",
|
|
13
|
+
posture: "mixed",
|
|
14
|
+
gatewayTools: ["oxygen_visual_source_create", "oxygen_visual_render_preview", "oxygen_visual_render_get", "oxygen_visual_image_view"],
|
|
15
|
+
gatewayCommands: ["visuals source-create", "visuals render-preview", "visuals render-get", "visuals download"],
|
|
16
|
+
skills: ["oxygen-visual-design", "oxygen-knowledge"],
|
|
17
|
+
endpointSections: ["visuals"],
|
|
18
|
+
intentTerms: ["infographic", "gtm flow image", "render html", "visual design", "carousel pages", "svg", "png", "pdf export", "graphic designer"],
|
|
19
|
+
},
|
|
6
20
|
{
|
|
7
21
|
id: "workspace-access",
|
|
8
22
|
layer: "Control",
|
|
@@ -194,20 +208,20 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
194
208
|
id: "tables",
|
|
195
209
|
layer: "Data",
|
|
196
210
|
primitive: "tables",
|
|
197
|
-
owns: "Typed working datasets, rows, formulas, AI/tool/waterfall columns, cell state, projects, and run provenance.",
|
|
211
|
+
owns: "Typed working datasets, rows, formulas, AI/tool/waterfall columns, reusable Functions with isolated drafts and published versions, cell state, projects, and run provenance.",
|
|
198
212
|
notFor: "Canonical CRM truth, message cadence, or an off-platform spreadsheet runtime.",
|
|
199
213
|
execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs.",
|
|
200
214
|
posture: "mixed",
|
|
201
|
-
gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk"],
|
|
202
|
-
gatewayCommands: ["tables create", "columns add", "enrich-column preview", "tables link"],
|
|
215
|
+
gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
|
|
216
|
+
gatewayCommands: ["tables create", "columns add", "enrich-column preview", "tables link", "functions list", "functions draft"],
|
|
203
217
|
skills: ["oxygen-gtm", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration"],
|
|
204
|
-
endpointSections: ["action-columns", "callables", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
|
|
218
|
+
endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
|
|
205
219
|
// "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
|
|
206
220
|
// blind user eval asked for exactly "link two tables" and was routed to
|
|
207
221
|
// `tables create` / `columns add` / `enrich-column preview` — none of which
|
|
208
222
|
// do it. The agent only found the right command by grepping the raw 25k-line
|
|
209
223
|
// command manifest, which is not a discovery path a customer has.
|
|
210
|
-
intentTerms: ["table", "rows", "column", "columns", "dataset", "csv", "import", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship"],
|
|
224
|
+
intentTerms: ["table", "rows", "column", "columns", "dataset", "csv", "import", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship", "function", "functions", "reusable function", "function draft", "function version", "callable"],
|
|
211
225
|
},
|
|
212
226
|
{
|
|
213
227
|
id: "messages",
|
|
@@ -320,11 +334,11 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
320
334
|
notFor: "One-to-one messaging, outreach campaigns, or general automation.",
|
|
321
335
|
execution: "Create and schedule on OXYGEN; explicit approval releases the hosted publisher.",
|
|
322
336
|
posture: "external_write",
|
|
323
|
-
gatewayTools: ["oxygen_publishing_posts_create", "oxygen_publishing_posts_list", "oxygen_publishing_posts_approve"],
|
|
324
|
-
gatewayCommands: ["publishing posts create", "publishing posts list", "publishing posts approve"],
|
|
325
|
-
skills: ["oxygen-linkedin-marketing"],
|
|
326
|
-
endpointSections: ["publishing"],
|
|
327
|
-
intentTerms: ["publish", "publishing", "schedule post", "content calendar", "posting calendar", "approve post", "social publishing"],
|
|
337
|
+
gatewayTools: ["oxygen_publishing_posts_create", "oxygen_publishing_posts_list", "oxygen_publishing_posts_approve", "oxygen_ugc_get"],
|
|
338
|
+
gatewayCommands: ["publishing posts create", "publishing posts list", "publishing posts approve", "ugc programs list"],
|
|
339
|
+
skills: ["oxygen-linkedin-marketing", "oxygen-ugc"],
|
|
340
|
+
endpointSections: ["publishing", "ugc"],
|
|
341
|
+
intentTerms: ["publish", "publishing", "schedule post", "content calendar", "posting calendar", "approve post", "social publishing", "ugc", "creator program", "user generated content", "creator voice", "sponsored creator"],
|
|
328
342
|
},
|
|
329
343
|
{
|
|
330
344
|
id: "workflows",
|
|
@@ -542,6 +556,8 @@ function normalizeIntent(query) {
|
|
|
542
556
|
return query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
543
557
|
}
|
|
544
558
|
function explicitCapabilityIntent(query) {
|
|
559
|
+
if (/\b(infographic|graphic designer|render html|carousel pages|visual design|gtm flow image)\b/.test(query))
|
|
560
|
+
return ROUTE_BY_ID.get("visual-rendering") ?? null;
|
|
545
561
|
// A unified sender profile is an owned Sequence identity, even when the ask
|
|
546
562
|
// names every attached channel (LinkedIn + WhatsApp + email). Resolve this
|
|
547
563
|
// before public LinkedIn research, whose generic "profile" wording would
|
|
@@ -61,6 +61,8 @@ export type FeatureGateResolution = {
|
|
|
61
61
|
* - OXYGEN_PUBLISHING_ENABLED — (b). Fail-closed production switch for posting
|
|
62
62
|
* to real provider accounts (`apps/web/src/app/(app)/(dashboard)/publishing/
|
|
63
63
|
* data.ts`). Wrong "on" = public posts from customer accounts.
|
|
64
|
+
* - OXYGEN_UGC_ENABLED — (b). Cross-workspace public posting and sponsored
|
|
65
|
+
* paid jobs use a local fail-closed switch independent of flag services.
|
|
64
66
|
* - OXYGEN_WORKER_AUTOSCALE — (b). Arms the worker fleet autoscaler, which STOPS
|
|
65
67
|
* production Machines (`apps/worker/src/fleet-autoscaler.ts`). Its worst wrong
|
|
66
68
|
* value takes the fleet down while work is queued, so it must never depend on a
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
* - OXYGEN_PUBLISHING_ENABLED — (b). Fail-closed production switch for posting
|
|
53
53
|
* to real provider accounts (`apps/web/src/app/(app)/(dashboard)/publishing/
|
|
54
54
|
* data.ts`). Wrong "on" = public posts from customer accounts.
|
|
55
|
+
* - OXYGEN_UGC_ENABLED — (b). Cross-workspace public posting and sponsored
|
|
56
|
+
* paid jobs use a local fail-closed switch independent of flag services.
|
|
55
57
|
* - OXYGEN_WORKER_AUTOSCALE — (b). Arms the worker fleet autoscaler, which STOPS
|
|
56
58
|
* production Machines (`apps/worker/src/fleet-autoscaler.ts`). Its worst wrong
|
|
57
59
|
* value takes the fleet down while work is queued, so it must never depend on a
|
|
@@ -67,6 +69,7 @@
|
|
|
67
69
|
export const NEVER_FLAGGABLE = [
|
|
68
70
|
"OXYGEN_WORKER_AUTOSCALE",
|
|
69
71
|
"OXYGEN_PUBLISHING_ENABLED",
|
|
72
|
+
"OXYGEN_UGC_ENABLED",
|
|
70
73
|
"OXYGEN_AGENTS_ENABLED",
|
|
71
74
|
"OXYGEN_TELEMETRY_ENABLED",
|
|
72
75
|
"OXYGEN_LOG_SHIPPING_ENABLED",
|
|
@@ -2,8 +2,10 @@ export { MANAGED_INBOX_MINIMUM_CLI_VERSION, OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_V
|
|
|
2
2
|
export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
|
|
3
3
|
export { WORKFLOW_STATUS_CHANGE_METADATA_KEY, type WorkflowStatusChange, type WorkflowStatusChangeActor, type WorkflowStatusChangeSource, describeWorkflowStatusChange, formatWorkflowStatusChangeTimestamp, parseWorkflowStatusChange, readWorkflowStatusChange, } from "./workflow-status-change.js";
|
|
4
4
|
export * from "./billing.js";
|
|
5
|
+
export * from "./visual-render.js";
|
|
5
6
|
export * from "./billing-anchors.js";
|
|
6
7
|
export * from "./budget-scopes.js";
|
|
8
|
+
export * from "./byok-connect.js";
|
|
7
9
|
export * from "./capability-discovery.js";
|
|
8
10
|
export * from "./user-capability-routing.js";
|
|
9
11
|
export * from "./plan-capabilities.js";
|
|
@@ -11,6 +13,7 @@ export * from "./plan-limits.js";
|
|
|
11
13
|
export * from "./sending-seats.js";
|
|
12
14
|
export * from "./sending-seat-capacity.js";
|
|
13
15
|
export * from "./plain-support-events.js";
|
|
16
|
+
export * from "./provider-balance-signal.js";
|
|
14
17
|
export * from "./provider-funding-errors.js";
|
|
15
18
|
export * from "./publishing-limits.js";
|
|
16
19
|
export * from "./spend-safety.js";
|
|
@@ -109,3 +112,4 @@ export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
|
|
|
109
112
|
export declare function isVersionGreater(a: string, b: string): boolean;
|
|
110
113
|
/** True when `a` is a strictly lesser semantic version than `b`. */
|
|
111
114
|
export declare function isVersionLess(a: string, b: string): boolean;
|
|
115
|
+
export * from "./ugc.js";
|
|
@@ -2,8 +2,10 @@ export { MANAGED_INBOX_MINIMUM_CLI_VERSION, OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_V
|
|
|
2
2
|
export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
|
|
3
3
|
export { WORKFLOW_STATUS_CHANGE_METADATA_KEY, describeWorkflowStatusChange, formatWorkflowStatusChangeTimestamp, parseWorkflowStatusChange, readWorkflowStatusChange, } from "./workflow-status-change.js";
|
|
4
4
|
export * from "./billing.js";
|
|
5
|
+
export * from "./visual-render.js";
|
|
5
6
|
export * from "./billing-anchors.js";
|
|
6
7
|
export * from "./budget-scopes.js";
|
|
8
|
+
export * from "./byok-connect.js";
|
|
7
9
|
export * from "./capability-discovery.js";
|
|
8
10
|
export * from "./user-capability-routing.js";
|
|
9
11
|
export * from "./plan-capabilities.js";
|
|
@@ -11,6 +13,7 @@ export * from "./plan-limits.js";
|
|
|
11
13
|
export * from "./sending-seats.js";
|
|
12
14
|
export * from "./sending-seat-capacity.js";
|
|
13
15
|
export * from "./plain-support-events.js";
|
|
16
|
+
export * from "./provider-balance-signal.js";
|
|
14
17
|
export * from "./provider-funding-errors.js";
|
|
15
18
|
export * from "./publishing-limits.js";
|
|
16
19
|
export * from "./spend-safety.js";
|
|
@@ -145,3 +148,4 @@ export function isVersionGreater(a, b) {
|
|
|
145
148
|
export function isVersionLess(a, b) {
|
|
146
149
|
return compareSemver(a, b) < 0;
|
|
147
150
|
}
|
|
151
|
+
export * from "./ugc.js";
|
|
@@ -3,7 +3,7 @@ export type KnowledgePageType = typeof KNOWLEDGE_PAGE_TYPES[number];
|
|
|
3
3
|
export declare const KNOWLEDGE_META_PAGE_TYPES: readonly ["schema", "source_summary", "report"];
|
|
4
4
|
export declare const KNOWLEDGE_PAGE_STATUSES: readonly ["draft", "active", "archived"];
|
|
5
5
|
export type KnowledgePageStatus = typeof KNOWLEDGE_PAGE_STATUSES[number];
|
|
6
|
-
export declare const RESERVED_KNOWLEDGE_SLUGS: readonly ["index", "log", "schema", "company-profile", "readme"];
|
|
6
|
+
export declare const RESERVED_KNOWLEDGE_SLUGS: readonly ["index", "log", "schema", "company-profile", "readme", "new"];
|
|
7
7
|
export declare const KNOWLEDGE_SLUG_MAX_LENGTH = 120;
|
|
8
8
|
export declare const KNOWLEDGE_SLUG_PATTERN: RegExp;
|
|
9
9
|
export declare function isValidKnowledgeSlug(value: string): boolean;
|
|
@@ -32,17 +32,18 @@ export const KNOWLEDGE_PAGE_TYPES = [
|
|
|
32
32
|
export const KNOWLEDGE_META_PAGE_TYPES = ["schema", "source_summary", "report"];
|
|
33
33
|
export const KNOWLEDGE_PAGE_STATUSES = ["draft", "active", "archived"];
|
|
34
34
|
// Slugs the write path refuses (the scaffold seeder may create `schema`): they are
|
|
35
|
-
// computed projections
|
|
35
|
+
// computed projections, virtual pages, or static UI routes, never ordinary rows.
|
|
36
36
|
export const RESERVED_KNOWLEDGE_SLUGS = [
|
|
37
37
|
"index",
|
|
38
38
|
"log",
|
|
39
39
|
"schema",
|
|
40
40
|
"company-profile",
|
|
41
41
|
"readme",
|
|
42
|
+
"new",
|
|
42
43
|
];
|
|
43
44
|
export const KNOWLEDGE_SLUG_MAX_LENGTH = 120;
|
|
44
45
|
// Flat kebab grammar: lowercase alphanumeric + dashes, no leading/trailing dash,
|
|
45
|
-
// no slashes (
|
|
46
|
+
// no slashes (slugs are a flat namespace; folders, the graph, tags, and types organize).
|
|
46
47
|
export const KNOWLEDGE_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,119}$/;
|
|
47
48
|
export function isValidKnowledgeSlug(value) {
|
|
48
49
|
return KNOWLEDGE_SLUG_PATTERN.test(value);
|
|
@@ -84,7 +84,7 @@ Index-first → open only relevant pages → answer citing slugs → file durabl
|
|
|
84
84
|
| \`schema\` | describes the wiki itself (this page) |
|
|
85
85
|
|
|
86
86
|
## Slugs
|
|
87
|
-
Flat kebab: lowercase alphanumerics + dashes, no slashes, ≤120 chars. **Immutable once created** — choose the durable name (\`competitor-clay\`, not \`clay-notes-july\`).
|
|
87
|
+
Flat kebab: lowercase alphanumerics + dashes, no slashes, ≤120 chars. **Immutable once created** — choose the durable name (\`competitor-clay\`, not \`clay-notes-july\`). Nested folders organize pages in the explorer without changing slugs or wikilinks; types and tags provide additional organization. Reserved (never create): \`index\`, \`log\`, \`schema\`, \`company-profile\`, \`readme\`, \`new\`.
|
|
88
88
|
|
|
89
89
|
## Wikilinks
|
|
90
90
|
Wrap a slug in double square brackets to link it. Three forms all resolve to the same page — bare (\`slug\`), aliased (\`slug|display text\`), and heading-anchored (\`slug#heading\`) — because the alias and the heading are stripped before lookup. **Linking to a page that doesn't exist yet is good** — the dashed node is a visible to-do. Every page should link out (avoid dead-ends) and be linked to (avoid orphans).
|
|
@@ -38,6 +38,13 @@ import { createHash } from "node:crypto";
|
|
|
38
38
|
import { log } from "./log.js";
|
|
39
39
|
const FLUSH_TIMEOUT_MS = 5_000;
|
|
40
40
|
const WARN_THROTTLE_MS = 30_000;
|
|
41
|
+
/**
|
|
42
|
+
* The OTLP exporter's own deadline, in SECONDS (the @langfuse/otel option's
|
|
43
|
+
* unit). Deliberately ABOVE FLUSH_TIMEOUT_MS: our bound protects the caller,
|
|
44
|
+
* this one protects the batch, and making them equal — the SDK's 5s default —
|
|
45
|
+
* meant every flush that ran long lost its spans instead of finishing late.
|
|
46
|
+
*/
|
|
47
|
+
const LANGFUSE_EXPORT_TIMEOUT_SECONDS = 10;
|
|
41
48
|
// Defensive per-field bound, well under Langfuse's ~1 MB event cap. Copilot
|
|
42
49
|
// transcripts max out around 150 KB; anything larger is truncated with an
|
|
43
50
|
// explicit marker rather than risking a rejected ingestion batch.
|
|
@@ -179,6 +186,16 @@ function createOtelEmitter(env, warn) {
|
|
|
179
186
|
secretKey: env.LANGFUSE_SECRET_KEY,
|
|
180
187
|
...(env.LANGFUSE_BASE_URL?.trim() ? { baseUrl: env.LANGFUSE_BASE_URL.trim() } : {}),
|
|
181
188
|
environment: resolveLlmTracingEnvironment(env),
|
|
189
|
+
// SECONDS, and the OTLP POST's own deadline. The SDK default is 5,
|
|
190
|
+
// which is exactly FLUSH_TIMEOUT_MS — zero headroom, so a batch that
|
|
191
|
+
// needed 6s was guaranteed to die on the transport and be dropped:
|
|
192
|
+
// 37 `llm_tracing.ingest_failed` warns over 30 days to 2026-09-09,
|
|
193
|
+
// every one stage='flush' / 'Request timed out', against 275,970
|
|
194
|
+
// traces Langfuse accepted in the same window. 10s gives the POST
|
|
195
|
+
// room without touching FLUSH_TIMEOUT_MS — boundedNever still
|
|
196
|
+
// releases the caller at 5s, so a slow Langfuse can never hold a
|
|
197
|
+
// worker tick.
|
|
198
|
+
timeout: LANGFUSE_EXPORT_TIMEOUT_SECONDS,
|
|
182
199
|
});
|
|
183
200
|
// PRIVATE provider. Deliberately NOT .register()ed — see the file
|
|
184
201
|
// header: the global provider carries the Axiom OTLP exporters, and a
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
1
2
|
export declare function isObjectStorageConfigured(): boolean;
|
|
3
|
+
/** Server-only storage adapter seam. Callers own tenant and object-policy checks. */
|
|
4
|
+
export declare function resolveObjectStorageClient(): {
|
|
5
|
+
client: S3Client;
|
|
6
|
+
bucket: string;
|
|
7
|
+
};
|
|
2
8
|
export declare function buildImportObjectKey(input: {
|
|
3
9
|
organizationId: string;
|
|
4
10
|
fileName?: string | null;
|
|
@@ -77,6 +77,11 @@ function resolveClient() {
|
|
|
77
77
|
}
|
|
78
78
|
// Keys are namespaced by org so a tenant can only ever be handed (and the
|
|
79
79
|
// enqueue route only accepts) keys under its own prefix.
|
|
80
|
+
/** Server-only storage adapter seam. Callers own tenant and object-policy checks. */
|
|
81
|
+
export function resolveObjectStorageClient() {
|
|
82
|
+
const { client, config } = resolveClient();
|
|
83
|
+
return { client, bucket: config.bucket };
|
|
84
|
+
}
|
|
80
85
|
export function buildImportObjectKey(input) {
|
|
81
86
|
const safeName = sanitizeFileName(input.fileName) || "import";
|
|
82
87
|
return `imports/${input.organizationId}/${randomUUID()}/${safeName}`;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The managed-provider BALANCE signal — one contract shared by the two producers
|
|
3
|
+
* that can observe it and the one Axiom monitor that alerts on it.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. Until now the only way OXYGEN learned that one of its POOLED
|
|
6
|
+
* provider accounts had run out of money was a customer's call being refused.
|
|
7
|
+
* `provider.managed_credits_exhausted` fires at that moment — after the failure,
|
|
8
|
+
* never before it. Plain T-104 (Mentcape, 2026-08-24 and again 2026-08-31) is what
|
|
9
|
+
* that costs: `serper.places`, `serper.maps` and `parallel.search` refused live
|
|
10
|
+
* calls for a workspace holding 75,347 Oxygen credits, because OUR balance with
|
|
11
|
+
* those vendors was empty. Prod Axiom over the 16 days to 2026-09-05 shows the same
|
|
12
|
+
* refusal reaching >=6 orgs on serper, >=3 on parallel and >=5 on exa.
|
|
13
|
+
*
|
|
14
|
+
* The balance snapshot cron already read most of those balances every two hours and
|
|
15
|
+
* wrote them to `provider_balance_snapshots`. It emitted no per-provider log line at
|
|
16
|
+
* all, so a balance sliding toward zero was visible only to a human who opened
|
|
17
|
+
* /admin/costs. This module is the missing signal: one structured line per managed
|
|
18
|
+
* provider per snapshot, carrying the number AND the verdict.
|
|
19
|
+
*
|
|
20
|
+
* TWO PRODUCERS, ONE VOCABULARY.
|
|
21
|
+
*
|
|
22
|
+
* - `provider_balance.snapshot` — the cron, for every provider whose balance we can
|
|
23
|
+
* actually read. Proactive: it fires while there is still money left.
|
|
24
|
+
* - `provider_balance.exhausted` — the tool runner, when a managed account actually
|
|
25
|
+
* refuses a paid call. That is the ONLY balance evidence available for a provider
|
|
26
|
+
* with no usable balance API, which is exactly the T-104 three (see
|
|
27
|
+
* PROVIDERS_WITHOUT_BALANCE_API in @oxygen/providers). Without it the new monitor
|
|
28
|
+
* would be structurally blind to the providers that caused the incident.
|
|
29
|
+
*
|
|
30
|
+
* Both carry the same `status` vocabulary so ONE monitor query covers both, and both
|
|
31
|
+
* put `status` and `provider` in flat fields because those are the two dimensions the
|
|
32
|
+
* monitor filters and groups on. Every other field rides in the `worker_fields` map
|
|
33
|
+
* at zero column cost — see AXIOM_STABLE_FIELDS in ./axiom-field-budget.ts, which is
|
|
34
|
+
* load-bearing in both directions.
|
|
35
|
+
*/
|
|
36
|
+
/** The proactive line, emitted per managed provider by the balance-snapshot cron. */
|
|
37
|
+
export declare const PROVIDER_BALANCE_SNAPSHOT_MSG = "provider_balance.snapshot";
|
|
38
|
+
/**
|
|
39
|
+
* The reactive line, emitted by the tool runner when a managed account refuses a paid
|
|
40
|
+
* call. Deliberately a SEPARATE msg from `provider.managed_credits_exhausted`: that
|
|
41
|
+
* one is the refusal event (a customer call failed), this one is the balance fact (our
|
|
42
|
+
* account is empty). The refusal monitor counts the former; conflating them would make
|
|
43
|
+
* one query mean two different things.
|
|
44
|
+
*/
|
|
45
|
+
export declare const PROVIDER_BALANCE_EXHAUSTED_MSG = "provider_balance.exhausted";
|
|
46
|
+
/**
|
|
47
|
+
* The verdict on one managed balance.
|
|
48
|
+
*
|
|
49
|
+
* `unknown` is not a synonym for `ok`, and the distinction is the whole point: the
|
|
50
|
+
* pre-existing snapshot recorded status `ok` with `balance_remaining = null` for
|
|
51
|
+
* blitzapi and contactout on all 353 rows of the last 30 days, and would have
|
|
52
|
+
* recorded `ok` at a balance of 0 too. "We asked and got no number" must never read
|
|
53
|
+
* as "there is money".
|
|
54
|
+
*/
|
|
55
|
+
export type ProviderBalanceStatus = "ok" | "low" | "exhausted" | "unknown" | "not_monitored" | "auth_error" | "rate_limited" | "error";
|
|
56
|
+
/** The statuses the P1 balance monitor alerts on. Anything else is informational. */
|
|
57
|
+
export declare const ALERTING_PROVIDER_BALANCE_STATUSES: readonly ProviderBalanceStatus[];
|
|
58
|
+
/**
|
|
59
|
+
* Default low-balance floor, in the provider's OWN unit (almost always provider
|
|
60
|
+
* credits). 1,000 is not a round number chosen for looking tidy — it is roughly
|
|
61
|
+
* three to eight days of measured burn for the credit providers OXYGEN funds,
|
|
62
|
+
* taken from `provider_balance_snapshots` in the prod control DB on 2026-09-07
|
|
63
|
+
* over the preceding 30 days:
|
|
64
|
+
*
|
|
65
|
+
* bettercontact 10,130 -> 364 (~325/day) ~3 days of head-room at 1,000
|
|
66
|
+
* leadmagic 7,702 -> 903 (~227/day) ~4 days
|
|
67
|
+
* millionverifier 41,357 -> 37,695 (~122/day) ~8 days
|
|
68
|
+
* ai_ark 10,000 -> 9,813 (~6/day) months
|
|
69
|
+
*
|
|
70
|
+
* A floor is a claim about how the account behaved when it was measured, nothing
|
|
71
|
+
* more. Re-measure it rather than nudging it when it turns out to be noisy — the
|
|
72
|
+
* monitor changelog exists for exactly that conversation.
|
|
73
|
+
*/
|
|
74
|
+
export declare const DEFAULT_MANAGED_BALANCE_FLOOR = 1000;
|
|
75
|
+
/**
|
|
76
|
+
* Per-provider floors, and the providers that are deliberately NOT balance-monitored
|
|
77
|
+
* (`null`). Only entries that the default gets wrong are listed; everything else
|
|
78
|
+
* inherits DEFAULT_MANAGED_BALANCE_FLOOR.
|
|
79
|
+
*/
|
|
80
|
+
export declare const MANAGED_BALANCE_FLOORS: Readonly<Record<string, number | null>>;
|
|
81
|
+
/** `OXYGEN_MANAGED_BALANCE_FLOOR_FIRECRAWL`, `..._AI_ARK`, ... */
|
|
82
|
+
export declare function managedBalanceFloorEnvVar(provider: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* The floor in force for one provider. `null` means "do not judge this balance".
|
|
85
|
+
*
|
|
86
|
+
* The env override is the operator's escape hatch between deploys: a top-up that
|
|
87
|
+
* changes the plan size, or a provider that turns out to be noisy at the default,
|
|
88
|
+
* is one Doppler value away from being right. `off`/`none`/`disabled` switches the
|
|
89
|
+
* provider off the monitor entirely; an unparseable or negative value is IGNORED
|
|
90
|
+
* rather than obeyed, because a typo must not silently disarm an alert.
|
|
91
|
+
*/
|
|
92
|
+
export declare function managedBalanceFloor(provider: string, env?: Record<string, string | undefined>): number | null;
|
|
93
|
+
export type ProviderBalanceVerdict = {
|
|
94
|
+
status: ProviderBalanceStatus;
|
|
95
|
+
/** The single boolean the monitor's description tells a responder to read. */
|
|
96
|
+
balanceLow: boolean;
|
|
97
|
+
/** The floor actually applied, after the env override. `null` = not monitored. */
|
|
98
|
+
floor: number | null;
|
|
99
|
+
level: "info" | "warn" | "error";
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Turn one balance reading into the verdict both producers log.
|
|
103
|
+
*
|
|
104
|
+
* Pure on purpose: the thresholds are the alerting policy, so they are unit-testable
|
|
105
|
+
* without a provider, a cron, or a network.
|
|
106
|
+
*/
|
|
107
|
+
export declare function classifyManagedBalance(input: {
|
|
108
|
+
provider: string;
|
|
109
|
+
/** The balance fetcher's own status. Anything but "ok" means we are blind. */
|
|
110
|
+
fetchStatus: "ok" | "auth_error" | "rate_limited" | "error";
|
|
111
|
+
balanceRemaining: number | null;
|
|
112
|
+
env?: Record<string, string | undefined>;
|
|
113
|
+
}): ProviderBalanceVerdict;
|