@oxygen-agent/cli 1.1014.135 → 1.1014.618
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.js +27 -19
- package/dist/cli-output.d.ts +12 -0
- package/dist/cli-output.js +395 -0
- package/dist/cli-paths.d.ts +1 -0
- package/dist/cli-paths.js +3 -0
- package/dist/cli-profile-context.d.ts +3 -0
- package/dist/cli-profile-context.js +13 -0
- package/dist/command-manifest.d.ts +15 -0
- package/dist/command-manifest.js +23 -1
- package/dist/command-support.d.ts +438 -0
- package/dist/command-support.js +1610 -0
- package/dist/commands/action-column.d.ts +3 -0
- package/dist/commands/action-column.js +43 -0
- package/dist/commands/activation.d.ts +3 -0
- package/dist/commands/activation.js +24 -0
- package/dist/commands/admin.d.ts +3 -0
- package/dist/commands/admin.js +347 -0
- package/dist/commands/ads.d.ts +3 -0
- package/dist/commands/ads.js +62 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +274 -0
- package/dist/commands/api-keys.d.ts +3 -0
- package/dist/commands/api-keys.js +77 -0
- package/dist/commands/approvals.d.ts +3 -0
- package/dist/commands/approvals.js +518 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +230 -0
- package/dist/commands/billing.d.ts +3 -0
- package/dist/commands/billing.js +357 -0
- package/dist/commands/blueprints.d.ts +3 -0
- package/dist/commands/blueprints.js +312 -0
- package/dist/commands/budget.d.ts +3 -0
- package/dist/commands/budget.js +55 -0
- package/dist/commands/callables.d.ts +3 -0
- package/dist/commands/callables.js +111 -0
- package/dist/commands/capabilities.d.ts +3 -0
- package/dist/commands/capabilities.js +44 -0
- package/dist/commands/cells.d.ts +3 -0
- package/dist/commands/cells.js +94 -0
- package/dist/commands/columns.d.ts +3 -0
- package/dist/commands/columns.js +1628 -0
- package/dist/commands/commands.d.ts +3 -0
- package/dist/commands/commands.js +45 -0
- package/dist/commands/comments.d.ts +3 -0
- package/dist/commands/comments.js +188 -0
- package/dist/commands/companies.d.ts +3 -0
- package/dist/commands/companies.js +312 -0
- package/dist/commands/connections.d.ts +3 -0
- package/dist/commands/connections.js +39 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +170 -0
- package/dist/commands/copilot.d.ts +3 -0
- package/dist/commands/copilot.js +565 -0
- package/dist/commands/crm.d.ts +3 -0
- package/dist/commands/crm.js +1669 -0
- package/dist/commands/custom-integrations.d.ts +3 -0
- package/dist/commands/custom-integrations.js +140 -0
- package/dist/commands/dashboards.d.ts +3 -0
- package/dist/commands/dashboards.js +240 -0
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.js +190 -0
- package/dist/commands/deliverability.d.ts +3 -0
- package/dist/commands/deliverability.js +90 -0
- package/dist/commands/directory.d.ts +3 -0
- package/dist/commands/directory.js +101 -0
- package/dist/commands/domains.d.ts +3 -0
- package/dist/commands/domains.js +604 -0
- package/dist/commands/egress.d.ts +3 -0
- package/dist/commands/egress.js +130 -0
- package/dist/commands/email.d.ts +3 -0
- package/dist/commands/email.js +49 -0
- package/dist/commands/engagement.d.ts +3 -0
- package/dist/commands/engagement.js +189 -0
- package/dist/commands/enrich-column.d.ts +3 -0
- package/dist/commands/enrich-column.js +149 -0
- package/dist/commands/enrichment.d.ts +3 -0
- package/dist/commands/enrichment.js +49 -0
- package/dist/commands/feedback.d.ts +3 -0
- package/dist/commands/feedback.js +96 -0
- package/dist/commands/feeds.d.ts +3 -0
- package/dist/commands/feeds.js +486 -0
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.js +159 -0
- package/dist/commands/followers.d.ts +3 -0
- package/dist/commands/followers.js +39 -0
- package/dist/commands/formulas.d.ts +3 -0
- package/dist/commands/formulas.js +43 -0
- package/dist/commands/home.d.ts +3 -0
- package/dist/commands/home.js +22 -0
- package/dist/commands/inbox.d.ts +3 -0
- package/dist/commands/inbox.js +779 -0
- package/dist/commands/integrations.d.ts +3 -0
- package/dist/commands/integrations.js +290 -0
- package/dist/commands/knowledge.d.ts +3 -0
- package/dist/commands/knowledge.js +874 -0
- package/dist/commands/lead-sourcing.d.ts +3 -0
- package/dist/commands/lead-sourcing.js +52 -0
- package/dist/commands/limits.d.ts +3 -0
- package/dist/commands/limits.js +54 -0
- package/dist/commands/linkedin.d.ts +3 -0
- package/dist/commands/linkedin.js +275 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +347 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/mailboxes.d.ts +3 -0
- package/dist/commands/mailboxes.js +1290 -0
- package/dist/commands/managed-inboxes.d.ts +3 -0
- package/dist/commands/managed-inboxes.js +361 -0
- package/dist/commands/messages.d.ts +3 -0
- package/dist/commands/messages.js +78 -0
- package/dist/commands/notetaker.d.ts +3 -0
- package/dist/commands/notetaker.js +79 -0
- package/dist/commands/observability.d.ts +3 -0
- package/dist/commands/observability.js +125 -0
- package/dist/commands/orgs.d.ts +3 -0
- package/dist/commands/orgs.js +237 -0
- package/dist/commands/people.d.ts +3 -0
- package/dist/commands/people.js +353 -0
- package/dist/commands/posts.d.ts +3 -0
- package/dist/commands/posts.js +139 -0
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +250 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +94 -0
- package/dist/commands/prompts.d.ts +3 -0
- package/dist/commands/prompts.js +84 -0
- package/dist/commands/publishing.d.ts +3 -0
- package/dist/commands/publishing.js +1419 -0
- package/dist/commands/recipes.d.ts +3 -0
- package/dist/commands/recipes.js +120 -0
- package/dist/commands/reviews.d.ts +3 -0
- package/dist/commands/reviews.js +75 -0
- package/dist/commands/rows.d.ts +3 -0
- package/dist/commands/rows.js +27 -0
- package/dist/commands/runs.d.ts +3 -0
- package/dist/commands/runs.js +37 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.js +182 -0
- package/dist/commands/senders.d.ts +3 -0
- package/dist/commands/senders.js +393 -0
- package/dist/commands/sequences.d.ts +3 -0
- package/dist/commands/sequences.js +2322 -0
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.js +109 -0
- package/dist/commands/signals.d.ts +3 -0
- package/dist/commands/signals.js +289 -0
- package/dist/commands/signup-leads.d.ts +3 -0
- package/dist/commands/signup-leads.js +46 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.js +50 -0
- package/dist/commands/sourcing.d.ts +3 -0
- package/dist/commands/sourcing.js +27 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +51 -0
- package/dist/commands/supabase.d.ts +3 -0
- package/dist/commands/supabase.js +128 -0
- package/dist/commands/support.d.ts +3 -0
- package/dist/commands/support.js +651 -0
- package/dist/commands/suppressions.d.ts +3 -0
- package/dist/commands/suppressions.js +577 -0
- package/dist/commands/table-ingestions.d.ts +3 -0
- package/dist/commands/table-ingestions.js +145 -0
- package/dist/commands/table-runs.d.ts +3 -0
- package/dist/commands/table-runs.js +277 -0
- package/dist/commands/tables.d.ts +3 -0
- package/dist/commands/tables.js +3492 -0
- package/dist/commands/tags.d.ts +3 -0
- package/dist/commands/tags.js +147 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.js +246 -0
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +106 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.js +27 -0
- package/dist/commands/viewers.d.ts +3 -0
- package/dist/commands/viewers.js +72 -0
- package/dist/commands/voice.d.ts +3 -0
- package/dist/commands/voice.js +351 -0
- package/dist/commands/whatsapp.d.ts +3 -0
- package/dist/commands/whatsapp.js +226 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.js +110 -0
- package/dist/commands/worker.d.ts +3 -0
- package/dist/commands/worker.js +60 -0
- package/dist/commands/workflows.d.ts +3 -0
- package/dist/commands/workflows.js +1752 -0
- package/dist/commands/workspace.d.ts +3 -0
- package/dist/commands/workspace.js +94 -0
- package/dist/compact-list-flags.d.ts +7 -0
- package/dist/compact-list-flags.js +12 -0
- package/dist/crm-support-flags.js +14 -5
- package/dist/customer-operation-commands.js +8 -6
- package/dist/diagnostic-uploader.d.ts +3 -1
- package/dist/diagnostic-uploader.js +29 -8
- package/dist/help.d.ts +5 -0
- package/dist/help.js +22 -0
- package/dist/index.d.ts +98 -1
- package/dist/index.js +331 -30719
- package/dist/local-module-loader.d.ts +4 -0
- package/dist/local-module-loader.js +156 -0
- package/dist/visual-commands.js +9 -5
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.js +69 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +14 -5
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +14 -3
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +175 -14
- package/node_modules/@oxygen/shared/dist/compact-list-result.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/compact-list-result.js +27 -0
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +12 -1
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +5 -2
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.js +23 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -2
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +17 -14
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.d.ts +49 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.js +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +60 -7
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +13 -5
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/teammates.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +115 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +21 -3
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +31 -1
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +56 -15
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +71 -44
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +348 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.js +42 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +59 -213
- package/node_modules/@oxygen/shared/dist/diagnostics.js +3 -2
- package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +16 -5
- package/node_modules/@oxygen/shared/dist/email-dsn.js +97 -1
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +5 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +4 -5
- package/node_modules/@oxygen/shared/dist/file-import.js +5 -2
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +20 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -2
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +17 -7
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +81 -17
- package/node_modules/@oxygen/shared/dist/langfuse.js +8 -4
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +6 -3
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +9 -9
- package/node_modules/@oxygen/shared/dist/mcp-server-info.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/mcp-server-info.js +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +14 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +17 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
- package/node_modules/@oxygen/shared/dist/provider-selection.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/provider-selection.js +3 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/repricing.js +2 -2
- package/node_modules/@oxygen/shared/dist/schedule-label.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/schedule-label.js +12 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +20 -6
- package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +55 -0
- package/node_modules/@oxygen/shared/dist/teammate-groups.js +74 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.js +88 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.js +73 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.d.ts +95 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.js +266 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.d.ts +56 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.js +114 -0
- package/node_modules/@oxygen/shared/dist/teammates.d.ts +78 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +140 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +120 -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/visual-render.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/visual-render.js +12 -5
- package/node_modules/@oxygen/shared/dist/warmup-metrics.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/warmup-metrics.js +44 -0
- package/package.json +1 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, readdir, rename, stat, unlink, utimes } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { DIAGNOSTIC_MAX_BYTES, diagnosticId } from "./diagnostics.js";
|
|
5
|
+
import { acquireDiagnosticStorageLock } from "./diagnostic-storage-lock.js";
|
|
6
|
+
import { diagnosticRole, StorageFailure, storageFailureFields, storageStage } from "./diagnostic-storage-failure.js";
|
|
7
|
+
import { log } from "./log.js";
|
|
8
|
+
import { recordTelemetryCounter } from "./telemetry.js";
|
|
9
|
+
const admissions = new Map();
|
|
10
|
+
const RECEIPT_MAX_BYTES = 512;
|
|
11
|
+
/** Admit durable content plus bounded receipt bytes; acknowledge only after publication. */
|
|
12
|
+
export async function admitDiagnosticSpool(directory, record, compressed) {
|
|
13
|
+
await storageStage("mkdir", () => mkdir(directory, { recursive: true, mode: 0o700 }));
|
|
14
|
+
const path = await admitDiagnostic(directory, record, { compressed });
|
|
15
|
+
return async () => { await unlink(path).catch(() => undefined); };
|
|
16
|
+
}
|
|
17
|
+
/** Receipt writes share the admission owner's protected byte count and retry reservation. */
|
|
18
|
+
export async function saveDiagnosticUploadProgress(directory, record, progress, reservation) {
|
|
19
|
+
await admitDiagnostic(directory, record, { compressed: Buffer.alloc(0) }, progress, reservation);
|
|
20
|
+
}
|
|
21
|
+
/** Coalesce local contenders; the kernel mutex still owns cross-process admission. */
|
|
22
|
+
function admitDiagnostic(directory, record, encoded, progress, reservation) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const pending = admissions.get(directory);
|
|
25
|
+
if (progress && pending?.used !== undefined) {
|
|
26
|
+
// A retry must not queue behind a new writer's stalled fsync (#5403).
|
|
27
|
+
// Reserve against the active owner's protected count synchronously; it
|
|
28
|
+
// keeps the kernel admission lock until every such write has settled.
|
|
29
|
+
const required = reservation ? 0 : RECEIPT_MAX_BYTES;
|
|
30
|
+
if (pending.used + required > DIAGNOSTIC_MAX_BYTES) {
|
|
31
|
+
reject(new StorageFailure("capacity", { code: "spool_full" }));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
pending.used += required;
|
|
35
|
+
const write = writeUploadProgress(directory, record, progress, reservation);
|
|
36
|
+
pending.progressWrites.add(write);
|
|
37
|
+
void write.then(() => resolve(join(directory, `${record.id}.receipt.json`)), reject).finally(() => { pending.progressWrites.delete(write); });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const admission = { record, encoded, ...(progress ? { progress } : {}), ...(reservation ? { reservation } : {}), enqueuedAt: performance.now(), resolve, reject };
|
|
41
|
+
if (pending)
|
|
42
|
+
pending.pending.push(admission);
|
|
43
|
+
else {
|
|
44
|
+
const queue = { pending: [admission], progressWrites: new Set() };
|
|
45
|
+
admissions.set(directory, queue);
|
|
46
|
+
queueMicrotask(() => { void drainAdmissions(directory, queue); });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function drainAdmissions(directory, queue) {
|
|
51
|
+
try {
|
|
52
|
+
while (queue.pending.length) {
|
|
53
|
+
const batch = queue.pending.splice(0, 32);
|
|
54
|
+
let release = null;
|
|
55
|
+
try {
|
|
56
|
+
// Bound actual cross-process contention, not local queue/inspection time (#5810).
|
|
57
|
+
release = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, ".admission.lock"), 5_000));
|
|
58
|
+
if (!release)
|
|
59
|
+
throw new StorageFailure("lock", { code: "lock_timeout" });
|
|
60
|
+
const started = performance.now();
|
|
61
|
+
const reservedOnly = batch.every(admission => admission.progress && admission.reservation);
|
|
62
|
+
let files = queue.files ?? [];
|
|
63
|
+
if (!reservedOnly) {
|
|
64
|
+
const inspected = await storageStage("inspect", () => inspectDiagnosticSpool(directory));
|
|
65
|
+
queue.used = inspected.used;
|
|
66
|
+
queue.files = files = inspected.files;
|
|
67
|
+
}
|
|
68
|
+
const scanMs = performance.now() - started;
|
|
69
|
+
// Captures arriving during inspection share its protected byte count,
|
|
70
|
+
// avoiding another scan while keeping the 32-record batch bound.
|
|
71
|
+
if (!reservedOnly)
|
|
72
|
+
batch.push(...queue.pending.splice(0, 32 - batch.length));
|
|
73
|
+
for (const admission of batch) {
|
|
74
|
+
try {
|
|
75
|
+
// Include an upload receipt and its atomic replacement in admission.
|
|
76
|
+
const required = admission.reservation ? 0 : admission.progress ? RECEIPT_MAX_BYTES : admission.encoded.compressed.length + 2 * RECEIPT_MAX_BYTES;
|
|
77
|
+
if (required && (queue.used === undefined || queue.used + required > DIAGNOSTIC_MAX_BYTES))
|
|
78
|
+
throw new StorageFailure("capacity", { code: "spool_full" });
|
|
79
|
+
// Reserve conservatively even if cleanup of a failed partial write fails.
|
|
80
|
+
if (queue.used !== undefined)
|
|
81
|
+
queue.used += required;
|
|
82
|
+
if (admission.progress) {
|
|
83
|
+
await writeUploadProgress(directory, admission.record, admission.progress, admission.reservation);
|
|
84
|
+
admission.resolve(join(directory, `${admission.record.id}.receipt.json`));
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
admission.resolve(await writeDiagnosticSpool(directory, admission.record.id, admission.encoded.compressed));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
admission.reject(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!reservedOnly)
|
|
94
|
+
log("info", "diagnostics.spool_admitted", { queue_role: diagnosticRole(), batch_records: batch.length, spool_files: files.length,
|
|
95
|
+
spool_bytes: queue.used, scan_ms: scanMs, admission_wait_ms: performance.now() - batch[0].enqueuedAt });
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
for (const admission of batch)
|
|
99
|
+
admission.reject(error);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
delete queue.used;
|
|
103
|
+
await Promise.allSettled([...queue.progressWrites]);
|
|
104
|
+
await release?.();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
for (const admission of queue.pending.splice(0))
|
|
110
|
+
admission.reject(error);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
admissions.delete(directory);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function writeDiagnosticSpool(directory, id, compressed) {
|
|
117
|
+
const path = join(directory, `${id}.json.gz`);
|
|
118
|
+
const temporary = join(directory, `${id}.${randomUUID()}.tmp`);
|
|
119
|
+
let handle;
|
|
120
|
+
try {
|
|
121
|
+
handle = await storageStage("open", () => open(temporary, "wx", 0o600));
|
|
122
|
+
await storageStage("write", () => handle.writeFile(compressed));
|
|
123
|
+
await storageStage("sync", () => handle.sync());
|
|
124
|
+
await handle.close();
|
|
125
|
+
handle = undefined;
|
|
126
|
+
await storageStage("rename", () => rename(temporary, path));
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
try {
|
|
131
|
+
await handle?.close();
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await unlink(temporary).catch(() => undefined);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** A fixed set of stripes bounds SQLite lock files while coordinating direct and retry owners. */
|
|
139
|
+
export async function lockDiagnosticUpload(directory, id, waitMs = 0) {
|
|
140
|
+
const release = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, `.upload-${id.slice(0, 2).toLowerCase()}.lock`), waitMs));
|
|
141
|
+
if (!release)
|
|
142
|
+
throw new StorageFailure("lock", { code: "lock_timeout" });
|
|
143
|
+
return release;
|
|
144
|
+
}
|
|
145
|
+
export async function readUploadProgress(directory, id, storeFingerprint) {
|
|
146
|
+
const path = join(directory, `${id}.receipt.json`);
|
|
147
|
+
const info = await stat(path).catch(error => { if (error?.code === "ENOENT")
|
|
148
|
+
return null; throw error; });
|
|
149
|
+
if (!info)
|
|
150
|
+
return null;
|
|
151
|
+
if (info.size > RECEIPT_MAX_BYTES)
|
|
152
|
+
throw new Error("Diagnostic upload receipt exceeds its bound");
|
|
153
|
+
const bytes = await readFile(path).catch(error => { if (error?.code === "ENOENT")
|
|
154
|
+
return null; throw error; });
|
|
155
|
+
if (!bytes)
|
|
156
|
+
return null;
|
|
157
|
+
if (bytes.length > RECEIPT_MAX_BYTES)
|
|
158
|
+
throw new Error("Diagnostic upload receipt exceeds its bound");
|
|
159
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
160
|
+
if (!value || !/^[0-9a-f]{64}$/.test(value.sha256) || value.store !== storeFingerprint || typeof value.published !== "boolean")
|
|
161
|
+
throw new Error("Diagnostic upload receipt integrity mismatch");
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
async function writeUploadProgress(directory, record, progress, reservation) {
|
|
165
|
+
const bytes = Buffer.from(JSON.stringify(progress));
|
|
166
|
+
if (bytes.length > RECEIPT_MAX_BYTES)
|
|
167
|
+
throw new Error("Diagnostic upload receipt exceeds its bound");
|
|
168
|
+
if (reservation) {
|
|
169
|
+
// Caller owns the admission mutex. Exchange already-counted bytes for the
|
|
170
|
+
// new atomic receipt; concurrent inspections cannot miss a rename or grow.
|
|
171
|
+
const quota = await open(reservation.path, "r+");
|
|
172
|
+
try {
|
|
173
|
+
const info = await quota.stat();
|
|
174
|
+
if (!info.isFile() || info.size < RECEIPT_MAX_BYTES || info.size > 40 * RECEIPT_MAX_BYTES || info.size % RECEIPT_MAX_BYTES)
|
|
175
|
+
throw new Error("Diagnostic receipt reservation exhausted");
|
|
176
|
+
await quota.truncate(info.size - RECEIPT_MAX_BYTES);
|
|
177
|
+
await quota.sync();
|
|
178
|
+
await utimes(reservation.path, info.atime, info.mtime);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await quota.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const path = join(directory, `${record.id}.receipt.json`);
|
|
185
|
+
const temporary = join(directory, `${record.id}.${randomUUID()}.tmp`);
|
|
186
|
+
let handle;
|
|
187
|
+
try {
|
|
188
|
+
handle = await open(temporary, "wx", 0o600);
|
|
189
|
+
await handle.writeFile(bytes);
|
|
190
|
+
await handle.sync();
|
|
191
|
+
await handle.close();
|
|
192
|
+
handle = undefined;
|
|
193
|
+
// Retry progress does not renew the captured record's 72-hour lifetime.
|
|
194
|
+
const captured = new Date(record.started_at);
|
|
195
|
+
await utimes(temporary, captured, captured);
|
|
196
|
+
await rename(temporary, path);
|
|
197
|
+
const directoryHandle = await open(directory, "r");
|
|
198
|
+
try {
|
|
199
|
+
await directoryHandle.sync();
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
await directoryHandle.close();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
await handle?.close();
|
|
207
|
+
await unlink(temporary).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Caller owns the admission mutex, directly or through an active local writer. */
|
|
211
|
+
async function reserveRetryBatch(directory, files, used, local) {
|
|
212
|
+
if (!files.length)
|
|
213
|
+
return { files };
|
|
214
|
+
const count = Math.min(files.length, Math.floor((DIAGNOSTIC_MAX_BYTES - used) / (2 * RECEIPT_MAX_BYTES)));
|
|
215
|
+
if (count < 1)
|
|
216
|
+
throw new StorageFailure("capacity", { code: "spool_full" });
|
|
217
|
+
const bytes = count * 2 * RECEIPT_MAX_BYTES;
|
|
218
|
+
// This fixed metadata prefix cannot be a UUID content temp. Its ordinary
|
|
219
|
+
// .tmp shape remains visible to older admission owners during a rollout.
|
|
220
|
+
const reservation = { path: join(directory, `f.${randomUUID()}.tmp`) };
|
|
221
|
+
if (local)
|
|
222
|
+
local.used = used + bytes;
|
|
223
|
+
const write = storageStage("receipt", async () => {
|
|
224
|
+
let handle;
|
|
225
|
+
try {
|
|
226
|
+
handle = await open(reservation.path, "wx", 0o600);
|
|
227
|
+
await handle.writeFile(Buffer.alloc(bytes));
|
|
228
|
+
await handle.sync();
|
|
229
|
+
await handle.close();
|
|
230
|
+
handle = undefined;
|
|
231
|
+
const parent = await open(directory, "r");
|
|
232
|
+
try {
|
|
233
|
+
await parent.sync();
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
await parent.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
await unlink(reservation.path).catch(() => undefined);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
await handle?.close();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
local?.progressWrites.add(write);
|
|
248
|
+
try {
|
|
249
|
+
await write;
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
local?.progressWrites.delete(write);
|
|
253
|
+
}
|
|
254
|
+
return { files: files.slice(0, count), reservation };
|
|
255
|
+
}
|
|
256
|
+
/** Replay retained bytes under their stripe; publication owns the upload acknowledgment. */
|
|
257
|
+
export async function retryDiagnosticSpool(directory, storeFingerprint, upload) {
|
|
258
|
+
const release = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, ".upload.lock")));
|
|
259
|
+
if (!release)
|
|
260
|
+
return;
|
|
261
|
+
let reservation;
|
|
262
|
+
try {
|
|
263
|
+
// Idle workers enforce the same expiry as new admissions. The admission
|
|
264
|
+
// lock protects cleanup against a writer replacing an old record path.
|
|
265
|
+
let files;
|
|
266
|
+
const local = admissions.get(directory);
|
|
267
|
+
if (local) {
|
|
268
|
+
// Reuse the active writer's protected inspection. Waiting for local idle
|
|
269
|
+
// can starve retained evidence indefinitely under sustained capture.
|
|
270
|
+
const retained = local.files?.slice(0, 20) ?? [];
|
|
271
|
+
({ files, reservation } = local.used === undefined ? { files: [] } : await reserveRetryBatch(directory, retained, local.used, local));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const admission = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, ".admission.lock")));
|
|
275
|
+
if (!admission)
|
|
276
|
+
return;
|
|
277
|
+
try {
|
|
278
|
+
const inspected = await storageStage("inspect", () => inspectDiagnosticSpool(directory));
|
|
279
|
+
({ files, reservation } = await reserveRetryBatch(directory, inspected.files.slice(0, 20), inspected.used));
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
await admission();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const name of files) {
|
|
286
|
+
const path = join(directory, name);
|
|
287
|
+
try {
|
|
288
|
+
const release = await lockDiagnosticUpload(directory, name.slice(0, -8));
|
|
289
|
+
try {
|
|
290
|
+
const compressed = await storageStage("read", () => readFile(path));
|
|
291
|
+
await upload(compressed, reservation);
|
|
292
|
+
await unlink(path).catch(() => undefined);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
await release();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
// A completed owner can remove the path after this sweep's snapshot.
|
|
300
|
+
// The private acknowledgement proves this is success, rather than loss.
|
|
301
|
+
if (error instanceof StorageFailure && error.stage === "read" && error.failure?.code === "ENOENT") {
|
|
302
|
+
const progress = await readUploadProgress(directory, name.slice(0, -8), storeFingerprint).then(value => value?.published === true).catch(() => false);
|
|
303
|
+
if (progress)
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
log("warn", "diagnostics.spool_upload_deferred", { diagnostic_id: name.slice(0, -8), ...storageFailureFields(error) });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
if (reservation)
|
|
312
|
+
await unlink(reservation.path).catch(() => undefined);
|
|
313
|
+
await release();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** Caller holds admission lock; count retained bytes and expire actual filenames. */
|
|
317
|
+
async function inspectDiagnosticSpool(directory) {
|
|
318
|
+
let used = 0;
|
|
319
|
+
const retained = [];
|
|
320
|
+
const names = (await readdir(directory)).filter(name => /^[0-9a-f-]+\.(?:json\.gz|receipt\.json)$/.test(name) || /^[0-9a-f-]+(?:\.[0-9a-f-]+)?\.tmp$/.test(name));
|
|
321
|
+
for (let offset = 0; offset < names.length; offset += 64) {
|
|
322
|
+
const inspected = await Promise.all(names.slice(offset, offset + 64).map(async (name) => {
|
|
323
|
+
const path = join(directory, name);
|
|
324
|
+
const info = await stat(path).catch(error => { if (error?.code === "ENOENT")
|
|
325
|
+
return null; throw error; });
|
|
326
|
+
return { name, path, info };
|
|
327
|
+
}));
|
|
328
|
+
for (const { name, path, info } of inspected) {
|
|
329
|
+
const record = /^[0-9a-f-]+\.json\.gz$/.test(name);
|
|
330
|
+
if (!info)
|
|
331
|
+
continue;
|
|
332
|
+
if (Date.now() - info.mtimeMs >= 72 * 60 * 60 * 1000) {
|
|
333
|
+
const removed = await unlink(path).then(() => true).catch(error => { if (error?.code === "ENOENT")
|
|
334
|
+
return false; throw error; });
|
|
335
|
+
if (removed && !name.endsWith(".receipt.json") && !/^f\.[0-9a-f-]+\.tmp$/.test(name)) {
|
|
336
|
+
log("warn", "diagnostics.capture_lost", { reason: "spool_expired", ...(record && diagnosticId(name.slice(0, -8)) ? { diagnostic_id: name.slice(0, -8) } : {}), bytes: info.size, queue_role: diagnosticRole() });
|
|
337
|
+
recordTelemetryCounter("oxygen.diagnostics.lost", 1, { reason: "spool_expired" });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
used += info.size;
|
|
342
|
+
if (record)
|
|
343
|
+
retained.push({ name, mtimeMs: info.mtimeMs });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { used, files: retained.sort((a, b) => a.mtimeMs - b.mtimeMs || a.name.localeCompare(b.name)).map(file => file.name) };
|
|
348
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type StorageStage = "encode" | "mkdir" | "lock" | "inspect" | "capacity" | "open" | "write" | "sync" | "rename" | "read" | "decode" | "upload" | "receipt" | "observer";
|
|
2
|
+
export declare class StorageFailure extends Error {
|
|
3
|
+
readonly stage: StorageStage;
|
|
4
|
+
readonly failure: unknown;
|
|
5
|
+
constructor(stage: StorageStage, failure: unknown);
|
|
6
|
+
}
|
|
7
|
+
export declare function storageStage<T>(stage: StorageStage, action: () => Promise<T>): Promise<T>;
|
|
8
|
+
/** Safe storage metadata shared by capture and the chunk transport; never upstream prose. */
|
|
9
|
+
export declare function storageFailureFields(error: unknown, fallbackStage?: StorageStage): Record<string, string | number | null>;
|
|
10
|
+
export declare function diagnosticRole(): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class StorageFailure extends Error {
|
|
2
|
+
stage;
|
|
3
|
+
failure;
|
|
4
|
+
constructor(stage, failure) {
|
|
5
|
+
super("Diagnostic storage failed");
|
|
6
|
+
this.stage = stage;
|
|
7
|
+
this.failure = failure;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export async function storageStage(stage, action) {
|
|
11
|
+
try {
|
|
12
|
+
return await action();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
throw error instanceof StorageFailure ? error : new StorageFailure(stage, error);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Safe storage metadata shared by capture and the chunk transport; never upstream prose. */
|
|
19
|
+
export function storageFailureFields(error, fallbackStage = "encode") {
|
|
20
|
+
const stage = error instanceof StorageFailure ? error.stage : fallbackStage;
|
|
21
|
+
const failure = error instanceof StorageFailure ? error.failure : error;
|
|
22
|
+
const value = failure && typeof failure === "object" ? failure : {};
|
|
23
|
+
const code = typeof value.code === "string" && /^(?:E[A-Z0-9_]{1,30}|SQLITE_[A-Z_]{1,30}|spool_full|lock_timeout)$/.test(value.code) ? value.code : null;
|
|
24
|
+
const sqlite = typeof value.errcode === "number" ? value.errcode : null;
|
|
25
|
+
const status = value.$metadata?.httpStatusCode ?? value.status;
|
|
26
|
+
const httpStatus = typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 ? status : null;
|
|
27
|
+
const timeout = [value.name, value.cause?.name].some(name => typeof name === "string" && ["AbortError", "TimeoutError", "RequestTimeout"].includes(name))
|
|
28
|
+
|| [value.code, value.cause?.code].some(code => typeof code === "string" && ["ETIMEDOUT", "ESOCKETTIMEDOUT", "ABORT_ERR"].includes(code));
|
|
29
|
+
const cause = code === "lock_timeout" ? "lock_timeout" : code === "spool_full" ? "spool_limit"
|
|
30
|
+
: ["EACCES", "EPERM", "EROFS"].includes(code ?? "") || (sqlite !== null && [3, 8, 23].includes(sqlite & 255)) ? "permission"
|
|
31
|
+
: ["ENOSPC", "EDQUOT"].includes(code ?? "") || (sqlite !== null && (sqlite & 255) === 13) ? "filesystem_capacity"
|
|
32
|
+
: httpStatus === 507 && stage === "upload" ? "object_storage_capacity" : httpStatus === 429 ? "rate_limit"
|
|
33
|
+
: timeout ? "network_timeout" : [code, value.cause?.code].some(code => typeof code === "string" && ["ECONNRESET", "ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH", "EPIPE", "ENOTFOUND", "EAI_AGAIN"].includes(code)) ? "network"
|
|
34
|
+
: httpStatus !== null && httpStatus >= 500 ? "http_server" : httpStatus !== null && httpStatus >= 400 ? "http_client" : "other";
|
|
35
|
+
return { cause, stage, error_code: code, sqlite_code: sqlite, errno: typeof value.errno === "number" ? value.errno : null,
|
|
36
|
+
http_status: httpStatus, queue_role: diagnosticRole() };
|
|
37
|
+
}
|
|
38
|
+
export function diagnosticRole() {
|
|
39
|
+
const role = process.env.OXYGEN_WORKER_QUEUE_ROLE;
|
|
40
|
+
return role && ["relay", "bulk", "maintenance", "interactive", "provider", "outbound"].includes(role) ? role
|
|
41
|
+
: process.env.OXYGEN_PROCESS_ROLE === "worker" ? "legacy" : "web";
|
|
42
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
2
|
import { type DiagnosticRecord } from "./diagnostics.js";
|
|
3
|
+
export { storageFailureFields as diagnosticStorageFailureFields } from "./diagnostic-storage-failure.js";
|
|
3
4
|
export type DiagnosticStoredReceipt = {
|
|
4
5
|
key: string;
|
|
5
6
|
sha256: string;
|
|
@@ -28,4 +29,3 @@ export declare function storeDiagnostic(record: DiagnosticRecord): Promise<Diagn
|
|
|
28
29
|
export declare function flushDiagnosticSpool(): Promise<void>;
|
|
29
30
|
/** A metadata-only fallback for an unsupported value never claims full capture. */
|
|
30
31
|
export declare function partialDiagnostic(record: DiagnosticRecord, reason: string): DiagnosticRecord;
|
|
31
|
-
export {};
|