@oxygen-agent/cli 1.1014.136 → 1.1014.619
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 +1672 -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 +3505 -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 +20 -16
- 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.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +21 -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
|
@@ -1,136 +1,17 @@
|
|
|
1
|
-
import { createHash
|
|
2
|
-
import { mkdir, open, readFile, readdir, rename, stat, unlink } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
4
2
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
5
3
|
import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
6
|
-
import { createDiagnosticRecord, credentialSafeDiagnostic,
|
|
7
|
-
import {
|
|
4
|
+
import { createDiagnosticRecord, credentialSafeDiagnostic, DIAGNOSTIC_RECORD_MAX_BYTES, DIAGNOSTIC_RETENTION_MS, diagnosticId, diagnosticSummary, parseDiagnosticRecord, sanitizeDiagnosticContent } from "./diagnostics.js";
|
|
5
|
+
import { admitDiagnosticSpool, lockDiagnosticUpload, readUploadProgress, retryDiagnosticSpool, saveDiagnosticUploadProgress } from "./diagnostic-spool.js";
|
|
6
|
+
import { diagnosticRole, StorageFailure, storageFailureFields as diagnosticStorageFailureFields, storageStage } from "./diagnostic-storage-failure.js";
|
|
8
7
|
import { log } from "./log.js";
|
|
9
8
|
import { recordTelemetryCounter } from "./telemetry.js";
|
|
9
|
+
export { storageFailureFields as diagnosticStorageFailureFields } from "./diagnostic-storage-failure.js";
|
|
10
10
|
let cached;
|
|
11
11
|
let warned = false;
|
|
12
12
|
let flushing = null;
|
|
13
13
|
let timer;
|
|
14
14
|
let onStored;
|
|
15
|
-
const admissions = new Map();
|
|
16
|
-
class StorageFailure extends Error {
|
|
17
|
-
stage;
|
|
18
|
-
failure;
|
|
19
|
-
constructor(stage, failure) {
|
|
20
|
-
super("Diagnostic storage failed");
|
|
21
|
-
this.stage = stage;
|
|
22
|
-
this.failure = failure;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
async function storageStage(stage, action) {
|
|
26
|
-
try {
|
|
27
|
-
return await action();
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
throw error instanceof StorageFailure ? error : new StorageFailure(stage, error);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function storageFailureFields(error) {
|
|
34
|
-
const stage = error instanceof StorageFailure ? error.stage : "encode";
|
|
35
|
-
const failure = error instanceof StorageFailure ? error.failure : error;
|
|
36
|
-
const value = failure && typeof failure === "object" ? failure : {};
|
|
37
|
-
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;
|
|
38
|
-
const sqlite = typeof value.errcode === "number" ? value.errcode : null;
|
|
39
|
-
const cause = code === "lock_timeout" ? "lock_timeout" : code === "spool_full" ? "spool_limit"
|
|
40
|
-
: ["EACCES", "EPERM", "EROFS"].includes(code ?? "") || (sqlite !== null && [3, 8, 23].includes(sqlite & 255)) ? "permission"
|
|
41
|
-
: ["ENOSPC", "EDQUOT"].includes(code ?? "") || (sqlite !== null && (sqlite & 255) === 13) ? "filesystem_capacity" : "other";
|
|
42
|
-
const status = value.$metadata?.httpStatusCode;
|
|
43
|
-
return { cause, stage, error_code: code, sqlite_code: sqlite, errno: typeof value.errno === "number" ? value.errno : null,
|
|
44
|
-
http_status: typeof status === "number" && status >= 100 && status <= 599 ? status : null, queue_role: diagnosticRole() };
|
|
45
|
-
}
|
|
46
|
-
function diagnosticRole() {
|
|
47
|
-
const role = process.env.OXYGEN_WORKER_QUEUE_ROLE;
|
|
48
|
-
return role && ["relay", "bulk", "maintenance", "interactive", "provider", "outbound"].includes(role) ? role
|
|
49
|
-
: process.env.OXYGEN_PROCESS_ROLE === "worker" ? "legacy" : "web";
|
|
50
|
-
}
|
|
51
|
-
/** Coalesce local contenders; the kernel mutex still owns cross-process admission. */
|
|
52
|
-
function admitDiagnostic(directory, record, encoded) {
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
const pending = admissions.get(directory);
|
|
55
|
-
const admission = { record, encoded, deadline: performance.now() + 5_000, resolve, reject };
|
|
56
|
-
if (pending)
|
|
57
|
-
pending.pending.push(admission);
|
|
58
|
-
else {
|
|
59
|
-
const queue = { pending: [admission] };
|
|
60
|
-
admissions.set(directory, queue);
|
|
61
|
-
queueMicrotask(() => { void drainAdmissions(directory, queue); });
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
async function drainAdmissions(directory, queue) {
|
|
66
|
-
try {
|
|
67
|
-
while (queue.pending.length) {
|
|
68
|
-
const batch = queue.pending.splice(0, 32);
|
|
69
|
-
let release = null;
|
|
70
|
-
try {
|
|
71
|
-
release = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, ".admission.lock"), Math.max(0, batch[0].deadline - performance.now())));
|
|
72
|
-
if (!release)
|
|
73
|
-
throw new StorageFailure("lock", { code: "lock_timeout" });
|
|
74
|
-
const started = performance.now();
|
|
75
|
-
let { used, files } = await storageStage("inspect", () => inspectDiagnosticSpool(directory));
|
|
76
|
-
queue.files = files;
|
|
77
|
-
const scanMs = performance.now() - started;
|
|
78
|
-
for (const admission of batch) {
|
|
79
|
-
try {
|
|
80
|
-
if (performance.now() >= admission.deadline)
|
|
81
|
-
throw new StorageFailure("lock", { code: "lock_timeout" });
|
|
82
|
-
if (used + admission.encoded.compressed.length > DIAGNOSTIC_MAX_BYTES)
|
|
83
|
-
throw new StorageFailure("capacity", { code: "spool_full" });
|
|
84
|
-
// Reserve conservatively even if cleanup of a failed partial write fails.
|
|
85
|
-
used += admission.encoded.compressed.length;
|
|
86
|
-
admission.resolve(await writeDiagnosticSpool(directory, admission.record.id, admission.encoded.compressed));
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
admission.reject(error);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
log("info", "diagnostics.spool_admitted", { queue_role: diagnosticRole(), batch_records: batch.length, spool_files: files.length,
|
|
93
|
-
spool_bytes: used, scan_ms: scanMs, admission_wait_ms: performance.now() - (batch[0].deadline - 5_000) });
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
for (const admission of batch)
|
|
97
|
-
admission.reject(error);
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
await release?.();
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
for (const admission of queue.pending.splice(0))
|
|
106
|
-
admission.reject(error);
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
admissions.delete(directory);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
async function writeDiagnosticSpool(directory, id, compressed) {
|
|
113
|
-
const path = join(directory, `${id}.json.gz`);
|
|
114
|
-
const temporary = join(directory, `${id}.${randomUUID()}.tmp`);
|
|
115
|
-
let handle;
|
|
116
|
-
try {
|
|
117
|
-
handle = await storageStage("open", () => open(temporary, "wx", 0o600));
|
|
118
|
-
await storageStage("write", () => handle.writeFile(compressed));
|
|
119
|
-
await storageStage("sync", () => handle.sync());
|
|
120
|
-
await handle.close();
|
|
121
|
-
handle = undefined;
|
|
122
|
-
await storageStage("rename", () => rename(temporary, path));
|
|
123
|
-
return path;
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
try {
|
|
127
|
-
await handle?.close();
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
await unlink(temporary).catch(() => undefined);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
15
|
export function observeDiagnosticStorage(observer) {
|
|
135
16
|
onStored = observer;
|
|
136
17
|
startDiagnosticRetry();
|
|
@@ -210,7 +91,7 @@ export async function storeDiagnostic(record) {
|
|
|
210
91
|
return null;
|
|
211
92
|
}
|
|
212
93
|
const directory = process.env.OXYGEN_DIAGNOSTICS_SPOOL_DIR;
|
|
213
|
-
let
|
|
94
|
+
let acknowledge;
|
|
214
95
|
try {
|
|
215
96
|
let encoded;
|
|
216
97
|
try {
|
|
@@ -221,26 +102,64 @@ export async function storeDiagnostic(record) {
|
|
|
221
102
|
encoded = encodeDiagnostic(record);
|
|
222
103
|
}
|
|
223
104
|
record = parseDiagnosticRecord(JSON.parse(encoded.bytes.toString("utf8")));
|
|
224
|
-
|
|
225
|
-
path = await admitDiagnostic(directory, record, encoded);
|
|
105
|
+
acknowledge = await admitDiagnosticSpool(directory, record, encoded.compressed);
|
|
226
106
|
startDiagnosticRetry();
|
|
227
|
-
const receipt = await
|
|
228
|
-
await unlink(path).catch(() => undefined);
|
|
107
|
+
const receipt = await uploadEncodedDiagnostic(directory, record, encoded, acknowledge);
|
|
229
108
|
return receipt;
|
|
230
109
|
}
|
|
231
110
|
catch (error) {
|
|
232
111
|
// Neither storage credentials nor upstream exception text belong in logs.
|
|
233
|
-
const fields =
|
|
234
|
-
log("warn",
|
|
235
|
-
|
|
112
|
+
const fields = diagnosticStorageFailureFields(error);
|
|
113
|
+
log("warn", acknowledge ? "diagnostics.capture_deferred" : "diagnostics.capture_lost", { diagnostic_id: record.id,
|
|
114
|
+
reason: acknowledge ? fields.stage === "observer" ? "metadata_publication_failed" : fields.stage === "receipt" ? "receipt_failed" : "upload_failed" : "spool_admission_failed", ...fields });
|
|
115
|
+
recordTelemetryCounter(acknowledge ? "oxygen.diagnostics.deferred" : "oxygen.diagnostics.lost", 1, { source: record.surface, cause: fields.cause, queue_role: diagnosticRole() });
|
|
236
116
|
return null;
|
|
237
117
|
}
|
|
238
118
|
}
|
|
239
|
-
async function uploadEncodedDiagnostic(record, encoded) {
|
|
119
|
+
async function uploadEncodedDiagnostic(directory, record, encoded, acknowledge) {
|
|
120
|
+
const release = await lockDiagnosticUpload(directory, record.id, 5_000);
|
|
121
|
+
try {
|
|
122
|
+
const receipt = await publishEncodedDiagnostic(directory, record, encoded);
|
|
123
|
+
await acknowledge();
|
|
124
|
+
return receipt;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await release();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function diagnosticStoreFingerprint() {
|
|
131
|
+
return createHash("sha256").update(`${process.env.OXYGEN_DIAGNOSTICS_S3_ENDPOINT}\0${process.env.OXYGEN_DIAGNOSTICS_S3_BUCKET}`).digest("hex");
|
|
132
|
+
}
|
|
133
|
+
async function publishEncodedDiagnostic(directory, record, encoded, reservation) {
|
|
134
|
+
// Upload acknowledgement never extends the captured record's retry window.
|
|
135
|
+
if (Date.now() - Date.parse(record.started_at) >= 72 * 60 * 60 * 1000)
|
|
136
|
+
throw new StorageFailure("upload", new Error("Diagnostic upload has expired"));
|
|
137
|
+
const receipt = { key: diagnosticObjectKey(record), sha256: encoded.sha256, bytes: encoded.bytes.length, compressed_bytes: encoded.compressed.length, record };
|
|
138
|
+
// The checksum binds the acknowledged upload to these exact evidence bytes;
|
|
139
|
+
// a receipt from another bucket or endpoint cannot suppress its upload.
|
|
140
|
+
let progress = await storageStage("receipt", async () => {
|
|
141
|
+
const value = await readUploadProgress(directory, record.id, diagnosticStoreFingerprint());
|
|
142
|
+
if (value && value.sha256 !== encoded.sha256)
|
|
143
|
+
throw new Error("Diagnostic upload receipt integrity mismatch");
|
|
144
|
+
return value;
|
|
145
|
+
});
|
|
146
|
+
if (!progress) {
|
|
147
|
+
await storageStage("upload", () => uploadDiagnosticObject(record, encoded));
|
|
148
|
+
progress = { sha256: encoded.sha256, store: diagnosticStoreFingerprint(), published: false };
|
|
149
|
+
await storageStage("receipt", () => saveDiagnosticUploadProgress(directory, record, progress, reservation));
|
|
150
|
+
}
|
|
151
|
+
if (!progress.published) {
|
|
152
|
+
if (onStored)
|
|
153
|
+
await storageStage("observer", () => onStored(receipt));
|
|
154
|
+
// A crash after an ambiguous observer acknowledgement can retry metadata
|
|
155
|
+
// with the same UUID; the destination owns deduplication of that UUID.
|
|
156
|
+
await storageStage("receipt", () => saveDiagnosticUploadProgress(directory, record, { ...progress, published: true }, reservation));
|
|
157
|
+
}
|
|
158
|
+
return receipt;
|
|
159
|
+
}
|
|
160
|
+
async function uploadDiagnosticObject(record, encoded) {
|
|
240
161
|
const captured = Date.parse(record.started_at);
|
|
241
162
|
const now = Date.now();
|
|
242
|
-
if (now - captured >= 72 * 60 * 60 * 1000)
|
|
243
|
-
throw new Error("Diagnostic upload has expired");
|
|
244
163
|
// S3 day-based expiry rounds to the next UTC midnight. Tagged age bands
|
|
245
164
|
// leave that final day inside the 30-day deadline measured from capture.
|
|
246
165
|
const retentionDays = Math.min(29, 29 - (Math.floor(now / 86_400_000) - Math.floor(captured / 86_400_000)));
|
|
@@ -254,10 +173,6 @@ async function uploadEncodedDiagnostic(record, encoded) {
|
|
|
254
173
|
log("info", "diagnostics.capture_stored", { ...diagnosticSummary(record), org_id: record.organization_id, user_id: record.user_id,
|
|
255
174
|
diagnostic_key: key, bytes: encoded.bytes.length, compressed_bytes: encoded.compressed.length, sha256: encoded.sha256, queue_role: diagnosticRole() });
|
|
256
175
|
recordTelemetryCounter("oxygen.diagnostics.stored", 1, { source: record.surface, layer: record.layer });
|
|
257
|
-
const receipt = { key, sha256: encoded.sha256, bytes: encoded.bytes.length, compressed_bytes: encoded.compressed.length, record };
|
|
258
|
-
if (onStored)
|
|
259
|
-
await storageStage("observer", () => onStored(receipt));
|
|
260
|
-
return receipt;
|
|
261
176
|
}
|
|
262
177
|
export function flushDiagnosticSpool() {
|
|
263
178
|
if (flushing)
|
|
@@ -266,82 +181,13 @@ export function flushDiagnosticSpool() {
|
|
|
266
181
|
if (!diagnosticsConfigured())
|
|
267
182
|
return;
|
|
268
183
|
const directory = process.env.OXYGEN_DIAGNOSTICS_SPOOL_DIR;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// lock protects cleanup against a writer replacing an old record path.
|
|
275
|
-
let files;
|
|
276
|
-
const local = admissions.get(directory);
|
|
277
|
-
if (local) {
|
|
278
|
-
// Reuse the active writer's protected inspection. Waiting for local idle
|
|
279
|
-
// can starve retained evidence indefinitely under sustained capture.
|
|
280
|
-
files = local.files?.slice(0, 20) ?? [];
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
const admission = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, ".admission.lock")));
|
|
284
|
-
if (!admission)
|
|
285
|
-
return;
|
|
286
|
-
try {
|
|
287
|
-
files = (await storageStage("inspect", () => inspectDiagnosticSpool(directory))).files.slice(0, 20);
|
|
288
|
-
}
|
|
289
|
-
finally {
|
|
290
|
-
await admission();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
for (const name of files) {
|
|
294
|
-
const path = join(directory, name);
|
|
295
|
-
try {
|
|
296
|
-
const compressed = await storageStage("read", () => readFile(path));
|
|
297
|
-
const record = await storageStage("decode", async () => decodeDiagnostic(compressed));
|
|
298
|
-
await storageStage("upload", () => uploadEncodedDiagnostic(record, encodeDiagnostic(record)));
|
|
299
|
-
await unlink(path).catch(() => undefined);
|
|
300
|
-
}
|
|
301
|
-
catch (error) {
|
|
302
|
-
log("warn", "diagnostics.spool_upload_deferred", { diagnostic_id: name.slice(0, -8), ...storageFailureFields(error) });
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
finally {
|
|
307
|
-
await release();
|
|
308
|
-
}
|
|
309
|
-
})().catch(error => { log("warn", "diagnostics.spool_retry_failed", storageFailureFields(error)); }).finally(() => { flushing = null; });
|
|
184
|
+
await retryDiagnosticSpool(directory, diagnosticStoreFingerprint(), async (compressed, reservation) => {
|
|
185
|
+
const record = await storageStage("decode", async () => decodeDiagnostic(compressed));
|
|
186
|
+
await publishEncodedDiagnostic(directory, record, encodeDiagnostic(record), reservation);
|
|
187
|
+
});
|
|
188
|
+
})().catch(error => { log("warn", "diagnostics.spool_retry_failed", diagnosticStorageFailureFields(error)); }).finally(() => { flushing = null; });
|
|
310
189
|
return flushing;
|
|
311
190
|
}
|
|
312
|
-
/** Caller holds admission lock; count retained bytes and expire actual filenames. */
|
|
313
|
-
async function inspectDiagnosticSpool(directory) {
|
|
314
|
-
let used = 0;
|
|
315
|
-
const retained = [];
|
|
316
|
-
const names = (await readdir(directory)).filter(name => /^[0-9a-f-]+\.json\.gz$/.test(name) || /^[0-9a-f-]+(?:\.[0-9a-f-]+)?\.tmp$/.test(name));
|
|
317
|
-
for (let offset = 0; offset < names.length; offset += 64) {
|
|
318
|
-
const inspected = await Promise.all(names.slice(offset, offset + 64).map(async (name) => {
|
|
319
|
-
const path = join(directory, name);
|
|
320
|
-
const info = await stat(path).catch(error => { if (error?.code === "ENOENT")
|
|
321
|
-
return null; throw error; });
|
|
322
|
-
return { name, path, info };
|
|
323
|
-
}));
|
|
324
|
-
for (const { name, path, info } of inspected) {
|
|
325
|
-
const record = /^[0-9a-f-]+\.json\.gz$/.test(name);
|
|
326
|
-
if (!info)
|
|
327
|
-
continue;
|
|
328
|
-
if (Date.now() - info.mtimeMs >= 72 * 60 * 60 * 1000) {
|
|
329
|
-
const removed = await unlink(path).then(() => true).catch(error => { if (error?.code === "ENOENT")
|
|
330
|
-
return false; throw error; });
|
|
331
|
-
if (removed) {
|
|
332
|
-
log("warn", "diagnostics.capture_lost", { reason: "spool_expired", bytes: info.size, queue_role: diagnosticRole() });
|
|
333
|
-
recordTelemetryCounter("oxygen.diagnostics.lost", 1, { reason: "spool_expired" });
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
used += info.size;
|
|
338
|
-
if (record)
|
|
339
|
-
retained.push({ name, mtimeMs: info.mtimeMs });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return { used, files: retained.sort((a, b) => a.mtimeMs - b.mtimeMs || a.name.localeCompare(b.name)).map(file => file.name) };
|
|
344
|
-
}
|
|
345
191
|
/** A metadata-only fallback for an unsupported value never claims full capture. */
|
|
346
192
|
export function partialDiagnostic(record, reason) {
|
|
347
193
|
return createDiagnosticRecord({ ...record, input: null, output: null, capture_state: "partial", metadata: { capture_reason: reason } });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { credentialBearingOperation } from "./diagnostic-credentials.js";
|
|
2
|
+
import { credentialBearingOperation, SPOKEN_OUTPUT_OPERATIONS } from "./diagnostic-credentials.js";
|
|
3
3
|
import { redactSecretsInString } from "./redaction.js";
|
|
4
4
|
/** Internal diagnostic evidence, distinct from customer run state and usage counts. */
|
|
5
5
|
export const DIAGNOSTIC_VERSION = 1;
|
|
@@ -205,7 +205,8 @@ export function credentialSafeDiagnostic(record) {
|
|
|
205
205
|
const selectedName = ["oxygen_capabilities_read", "oxygen_capabilities_call"].includes(toolName) && args && typeof args === "object" && "tool" in args
|
|
206
206
|
? String(args.tool) : toolName;
|
|
207
207
|
return { ...record, input: credentialBearingOperation(selectedName, "input") ? { withheld: "credential_bearing_operation" } : record.input,
|
|
208
|
-
output: credentialBearingOperation(selectedName, "output") ? { withheld: "credential_bearing_operation" }
|
|
208
|
+
output: credentialBearingOperation(selectedName, "output") ? { withheld: "credential_bearing_operation" }
|
|
209
|
+
: SPOKEN_OUTPUT_OPERATIONS.has(record.operation) ? { withheld: "dictated_speech" } : record.output };
|
|
209
210
|
}
|
|
210
211
|
/** Bound the diagnostic branch of a response tee without delaying its consumer. */
|
|
211
212
|
export async function readDiagnosticProtocolText(body) {
|
|
@@ -46,6 +46,14 @@ export type DeliveryStatusNotification = {
|
|
|
46
46
|
/** Enhanced status code (5.7.1) when present, else the bare SMTP reply (550). */
|
|
47
47
|
smtpCode: string | null;
|
|
48
48
|
};
|
|
49
|
+
/** Bounds HTML hydration for the health rollup and pure fallback alike. */
|
|
50
|
+
export declare const MAX_DELIVERY_STATUS_HTML_CHARS = 20000;
|
|
51
|
+
type DsnInput = {
|
|
52
|
+
fromAddress?: string | null;
|
|
53
|
+
subject?: string | null;
|
|
54
|
+
bodyText?: string | null;
|
|
55
|
+
bodyHtml?: string | null;
|
|
56
|
+
};
|
|
49
57
|
/**
|
|
50
58
|
* Classify one stored inbound message as a delivery-status notification.
|
|
51
59
|
*
|
|
@@ -53,8 +61,11 @@ export type DeliveryStatusNotification = {
|
|
|
53
61
|
* is intentionally wide, and a false positive here would invent a deliverability
|
|
54
62
|
* incident out of a customer reply.
|
|
55
63
|
*/
|
|
56
|
-
export declare function classifyDeliveryStatusNotification(input:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
export declare function classifyDeliveryStatusNotification(input: DsnInput): DeliveryStatusNotification | null;
|
|
65
|
+
/**
|
|
66
|
+
* A bounce generator is not necessarily the server that rejected delivery.
|
|
67
|
+
* Gmail can relay a Mimecast/Sophos refusal. Require receiving-server evidence;
|
|
68
|
+
* an absent or truncated diagnostic remains unattributed, never a Google block.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isGoogleDeliveryStatusRejection(input: DsnInput): boolean;
|
|
71
|
+
export {};
|
|
@@ -24,6 +24,86 @@ export const DELIVERY_STATUS_NOTIFICATION_KINDS = [
|
|
|
24
24
|
"unclassified",
|
|
25
25
|
"other",
|
|
26
26
|
];
|
|
27
|
+
/** Bounds HTML hydration for the health rollup and pure fallback alike. */
|
|
28
|
+
export const MAX_DELIVERY_STATUS_HTML_CHARS = 20_000;
|
|
29
|
+
/** Consume each tag once, including malformed input; a byte cap alone cannot bound regex rescanning. */
|
|
30
|
+
function dsnHtmlText(html) {
|
|
31
|
+
const parts = [];
|
|
32
|
+
let cursor = 0;
|
|
33
|
+
while (cursor < html.length) {
|
|
34
|
+
const start = html.indexOf("<", cursor);
|
|
35
|
+
if (start < 0) {
|
|
36
|
+
parts.push(html.slice(cursor));
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
parts.push(html.slice(cursor, start));
|
|
40
|
+
if (html.startsWith("<!--", start)) {
|
|
41
|
+
const end = html.indexOf("-->", start + 4);
|
|
42
|
+
if (end < 0)
|
|
43
|
+
break;
|
|
44
|
+
cursor = end + 3;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
let end = start + 1;
|
|
48
|
+
let quote = "";
|
|
49
|
+
for (; end < html.length; end++) {
|
|
50
|
+
const char = html[end];
|
|
51
|
+
if (quote) {
|
|
52
|
+
if (char === quote)
|
|
53
|
+
quote = "";
|
|
54
|
+
}
|
|
55
|
+
else if (char === '"' || char === "'")
|
|
56
|
+
quote = char;
|
|
57
|
+
else if (char === ">")
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (end === html.length) {
|
|
61
|
+
parts.push(html.slice(start));
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
const tag = html.slice(start + 1, end).trim();
|
|
65
|
+
const name = /^\/?([a-z][a-z0-9:-]*)/i.exec(tag)?.[1]?.toLowerCase() ?? "";
|
|
66
|
+
const closing = tag.startsWith("/");
|
|
67
|
+
if (!closing && ["head", "style", "script"].includes(name)) {
|
|
68
|
+
const close = new RegExp(`</${name}\\s*>`, "gi");
|
|
69
|
+
close.lastIndex = end + 1;
|
|
70
|
+
if (!close.exec(html))
|
|
71
|
+
break;
|
|
72
|
+
cursor = close.lastIndex;
|
|
73
|
+
parts.push(" ");
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const line = name === "br" || name === "hr"
|
|
77
|
+
|| (closing && ["div", "p", "table", "tr", "li", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6"].includes(name));
|
|
78
|
+
parts.push(line ? "\n" : " ");
|
|
79
|
+
cursor = end + 1;
|
|
80
|
+
}
|
|
81
|
+
return parts.join("");
|
|
82
|
+
}
|
|
83
|
+
function readDsnBody(input) {
|
|
84
|
+
if (typeof input.bodyText === "string" && input.bodyText.trim())
|
|
85
|
+
return input.bodyText;
|
|
86
|
+
if (typeof input.bodyHtml !== "string")
|
|
87
|
+
return "";
|
|
88
|
+
// Unlike a human reply, an NDR may put the actual diagnostic in a blockquote.
|
|
89
|
+
// Keep it and preserve line boundaries for Remote-MTA; never read scripts,
|
|
90
|
+
// styles or markup as diagnostic content. This is text extraction, not HTML rendering.
|
|
91
|
+
const entities = {
|
|
92
|
+
nbsp: " ", amp: "&", lt: "<", gt: ">", quot: '"', apos: "'",
|
|
93
|
+
};
|
|
94
|
+
return dsnHtmlText(input.bodyHtml.slice(0, MAX_DELIVERY_STATUS_HTML_CHARS))
|
|
95
|
+
.replace(/&(#x[\da-f]{1,6}|#\d{1,7}|nbsp|amp|lt|gt|quot|apos);/gi, (_, entity) => {
|
|
96
|
+
const key = entity.toLowerCase();
|
|
97
|
+
if (!key.startsWith("#"))
|
|
98
|
+
return entities[key] ?? " ";
|
|
99
|
+
const code = key.startsWith("#x") ? Number.parseInt(key.slice(2), 16) : Number.parseInt(key.slice(1), 10);
|
|
100
|
+
return code > 0 && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff)
|
|
101
|
+
? String.fromCodePoint(code) : " ";
|
|
102
|
+
})
|
|
103
|
+
.replace(/[^\S\n]+/g, " ")
|
|
104
|
+
.replace(/\n */g, "\n")
|
|
105
|
+
.trim();
|
|
106
|
+
}
|
|
27
107
|
// A message only enters classification when its envelope looks like a DSN. The
|
|
28
108
|
// SQL prefilter that feeds this is deliberately loose (ILIKE), so this gate is
|
|
29
109
|
// what keeps an ordinary customer reply that happens to contain the word
|
|
@@ -106,7 +186,7 @@ function readKind(subject, haystack, hasBody) {
|
|
|
106
186
|
export function classifyDeliveryStatusNotification(input) {
|
|
107
187
|
const from = typeof input.fromAddress === "string" ? input.fromAddress : "";
|
|
108
188
|
const subject = typeof input.subject === "string" ? input.subject : "";
|
|
109
|
-
const body =
|
|
189
|
+
const body = readDsnBody(input);
|
|
110
190
|
if (!DSN_FROM_PATTERN.test(from) && !DSN_SUBJECT_PATTERN.test(subject))
|
|
111
191
|
return null;
|
|
112
192
|
const haystack = `${subject}\n${body}`;
|
|
@@ -118,3 +198,19 @@ export function classifyDeliveryStatusNotification(input) {
|
|
|
118
198
|
smtpCode,
|
|
119
199
|
};
|
|
120
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* A bounce generator is not necessarily the server that rejected delivery.
|
|
203
|
+
* Gmail can relay a Mimecast/Sophos refusal. Require receiving-server evidence;
|
|
204
|
+
* an absent or truncated diagnostic remains unattributed, never a Google block.
|
|
205
|
+
*/
|
|
206
|
+
export function isGoogleDeliveryStatusRejection(input) {
|
|
207
|
+
if (classifyDeliveryStatusNotification(input)?.kind !== "provider_rejected")
|
|
208
|
+
return false;
|
|
209
|
+
const body = readDsnBody(input);
|
|
210
|
+
const remote = /^Remote-MTA:\s*(?:dns|rfc822);\s*([a-z0-9.-]+)/im.exec(body)?.[1];
|
|
211
|
+
if (remote) {
|
|
212
|
+
return /(?:^|\.)(?:google|googlemail)\.com$/i.test(remote.replace(/\.$/, ""));
|
|
213
|
+
}
|
|
214
|
+
return GOOGLE_BLOCK_URL.test(body)
|
|
215
|
+
|| /\b5\.7\.\d{1,3}\b[^\n]*(?:\bgsmtp\b|https?:\/\/support\.google\.com\/mail\/answer\/)/i.test(body);
|
|
216
|
+
}
|
|
@@ -42,6 +42,11 @@ function headerLine(value, fallback) {
|
|
|
42
42
|
}
|
|
43
43
|
function messageBody(value, phrase) {
|
|
44
44
|
const base = value?.trim() || DEFAULT_BODY;
|
|
45
|
+
// Explicit placement copy can put the required matching phrase in a neutral
|
|
46
|
+
// reference line. Keep legacy bodies byte-identical for durable send hashes.
|
|
47
|
+
if (base.includes("{{placement_test_phrase}}")) {
|
|
48
|
+
return base.replaceAll("{{placement_test_phrase}}", phrase);
|
|
49
|
+
}
|
|
45
50
|
if (base.includes(phrase))
|
|
46
51
|
return base;
|
|
47
52
|
return `${base}\n\nEmailGuard test phrase: ${phrase}`;
|
|
@@ -596,11 +596,10 @@ export const ENRICHMENT_INTENT_TAXONOMY = [
|
|
|
596
596
|
rank: 340,
|
|
597
597
|
aliases: ["lookalike", "similar companies", "companies like", "more like these"],
|
|
598
598
|
inputShapes: ["seed_accounts"],
|
|
599
|
-
outputFields: [f("
|
|
600
|
-
surfaces: {},
|
|
601
|
-
status: "
|
|
602
|
-
|
|
603
|
-
decision: "D51",
|
|
599
|
+
outputFields: [f("lookalike_companies", "Similar companies", null, "jsonb"), f("lookalike_company_count", "Number returned", null, "numeric")],
|
|
600
|
+
surfaces: { preset: "company_lookalikes", catalogEntry: "preset:company_lookalikes", registryFunction: "company_search", registryIntent: "find_lookalikes" },
|
|
601
|
+
status: "selectable",
|
|
602
|
+
decision: "D54",
|
|
604
603
|
candidateProviders: ["leadmagic"],
|
|
605
604
|
},
|
|
606
605
|
// ── Signals (streams and watch-lists; referenced) ─────────────────────────
|
|
@@ -67,6 +67,14 @@ export declare function parseRowsSampleText(text: string, format: Exclude<RowsFi
|
|
|
67
67
|
}): RowsSample;
|
|
68
68
|
export declare function inferImportColumnLabels(rows: Record<string, unknown>[]): string[];
|
|
69
69
|
export declare function normalizeRowsForNewTable(rows: Record<string, unknown>[]): NewTableImportRows;
|
|
70
|
+
/**
|
|
71
|
+
* The row without its blank cells, for an import that must leave a record's
|
|
72
|
+
* existing value alone where the file has nothing ("skip blank cells"): an
|
|
73
|
+
* upsert writes only the keys a row carries. Blank is null, undefined or text
|
|
74
|
+
* that is only whitespace. The upsert key always stays, so a row without one is
|
|
75
|
+
* still reported as missing its key instead of quietly dropping out.
|
|
76
|
+
*/
|
|
77
|
+
export declare function dropBlankImportCells(row: Record<string, unknown>, keepKey?: string | null): Record<string, unknown>;
|
|
70
78
|
export declare function normalizeImportColumnKey(value: string): string;
|
|
71
79
|
/**
|
|
72
80
|
* Refuse a JSON or XLSX file over the buffered-parse ceiling before anything is
|
|
@@ -217,6 +217,22 @@ export function normalizeRowsForNewTable(rows) {
|
|
|
217
217
|
// it MATCHES imported file headers back to the column key MINTED by tenant-db's
|
|
218
218
|
// toSnakeIdentifier, and has call sites across web/CLI/API. The two must never
|
|
219
219
|
// diverge \u2014 see packages/shared/src/identifiers.ts + identifiers.test.ts.
|
|
220
|
+
/**
|
|
221
|
+
* The row without its blank cells, for an import that must leave a record's
|
|
222
|
+
* existing value alone where the file has nothing ("skip blank cells"): an
|
|
223
|
+
* upsert writes only the keys a row carries. Blank is null, undefined or text
|
|
224
|
+
* that is only whitespace. The upsert key always stays, so a row without one is
|
|
225
|
+
* still reported as missing its key instead of quietly dropping out.
|
|
226
|
+
*/
|
|
227
|
+
export function dropBlankImportCells(row, keepKey) {
|
|
228
|
+
const kept = {};
|
|
229
|
+
for (const [key, value] of Object.entries(row)) {
|
|
230
|
+
const blank = value === null || value === undefined || (typeof value === "string" && value.trim() === "");
|
|
231
|
+
if (!blank || key === keepKey)
|
|
232
|
+
kept[key] = value;
|
|
233
|
+
}
|
|
234
|
+
return kept;
|
|
235
|
+
}
|
|
220
236
|
export function normalizeImportColumnKey(value) {
|
|
221
237
|
return toSnakeIdentifier(value, "column");
|
|
222
238
|
}
|
|
@@ -332,14 +348,17 @@ async function* iterateCsvRowStreamBatches(chunks, batchSize) {
|
|
|
332
348
|
if (!combined)
|
|
333
349
|
continue;
|
|
334
350
|
const processUntil = Math.max(0, combined.length - 1);
|
|
335
|
-
|
|
351
|
+
let index = 0;
|
|
352
|
+
for (; index < processUntil; index += 1) {
|
|
336
353
|
const ready = applyCsvTextCharacter(state, combined, index, batchSize);
|
|
337
354
|
if (ready.skipNext)
|
|
338
355
|
index += 1;
|
|
339
356
|
if (ready.batch)
|
|
340
357
|
yield ready.batch;
|
|
341
358
|
}
|
|
342
|
-
|
|
359
|
+
// An escaped quote can consume the lookahead character too. Carry only
|
|
360
|
+
// unread text, or that quote is parsed again at the next stream boundary.
|
|
361
|
+
carry = combined.slice(index);
|
|
343
362
|
}
|
|
344
363
|
const tail = carry + decoder.decode();
|
|
345
364
|
for (let index = 0; index < tail.length; index += 1) {
|
|
@@ -77,6 +77,36 @@ export declare const HOSTED_AI_MODEL_REGISTRY: {
|
|
|
77
77
|
* own key and choice. Retired 2026-09-26 (see the ai_column block above).
|
|
78
78
|
*/
|
|
79
79
|
export declare const LEGACY_MANAGED_AI_COLUMN_MODELS: Readonly<Record<string, HostedAiLevel>>;
|
|
80
|
+
/**
|
|
81
|
+
* The exact managed model a code-owned background pass dispatches to, chosen and
|
|
82
|
+
* measured for that pass rather than inherited from a customer tier. A pin moves
|
|
83
|
+
* the model only: the run still bills at its column's tier. It is a function
|
|
84
|
+
* argument (`runAiColumnForOrganization`'s `managedModelPin`), never a stored
|
|
85
|
+
* column field, so no customer column can select it.
|
|
86
|
+
*/
|
|
87
|
+
export type ManagedModelPin = {
|
|
88
|
+
model: string;
|
|
89
|
+
/** OpenRouter fallbacks, tried in order when `model` is unavailable. */
|
|
90
|
+
fallbackModels: readonly string[];
|
|
91
|
+
/** Send `reasoning: {enabled: false}`, as the pass was measured. */
|
|
92
|
+
disableReasoning: boolean;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Signup research synthesis (knowledge bootstrap): the company profile, cited
|
|
96
|
+
* research note and GTM hypotheses written once per new workspace, plus the
|
|
97
|
+
* creator's private research summary.
|
|
98
|
+
*
|
|
99
|
+
* Pinned because inheriting the Balanced AI-column tier broke it silently. The
|
|
100
|
+
* 2026-10-10 tier refresh (#5115) moved Balanced to claude-haiku-5.5 at medium
|
|
101
|
+
* reasoning. In production through 2026-10-12, 30 of 30 traced syntheses then
|
|
102
|
+
* returned the company/offering/icp/market sections as `{}`, 26 left the research
|
|
103
|
+
* note empty, and 30 of 35 signup research runs ended `partial` with no
|
|
104
|
+
* `company-research` page. From 2026-09-12 to 2026-10-10 this configuration
|
|
105
|
+
* (deepseek-v4.1-flash, reasoning off, gpt-6-luna fallback, i.e. Balanced before
|
|
106
|
+
* the refresh) wrote the note in 199 of 211 runs. Change it only after replaying the
|
|
107
|
+
* synthesis on production-shaped evidence and comparing note and section fill rates.
|
|
108
|
+
*/
|
|
109
|
+
export declare const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_MODEL: ManagedModelPin;
|
|
80
110
|
/** The tier a retired managed AI-column model id billed at, or null. */
|
|
81
111
|
export declare function legacyManagedAiColumnLevel(model: string): HostedAiLevel | null;
|
|
82
112
|
/**
|
|
@@ -268,6 +268,26 @@ export const LEGACY_MANAGED_AI_COLUMN_MODELS = {
|
|
|
268
268
|
"deepseek/deepseek-v4.1-flash": "medium",
|
|
269
269
|
"openai/gpt-6-sol": "high",
|
|
270
270
|
};
|
|
271
|
+
/**
|
|
272
|
+
* Signup research synthesis (knowledge bootstrap): the company profile, cited
|
|
273
|
+
* research note and GTM hypotheses written once per new workspace, plus the
|
|
274
|
+
* creator's private research summary.
|
|
275
|
+
*
|
|
276
|
+
* Pinned because inheriting the Balanced AI-column tier broke it silently. The
|
|
277
|
+
* 2026-10-10 tier refresh (#5115) moved Balanced to claude-haiku-5.5 at medium
|
|
278
|
+
* reasoning. In production through 2026-10-12, 30 of 30 traced syntheses then
|
|
279
|
+
* returned the company/offering/icp/market sections as `{}`, 26 left the research
|
|
280
|
+
* note empty, and 30 of 35 signup research runs ended `partial` with no
|
|
281
|
+
* `company-research` page. From 2026-09-12 to 2026-10-10 this configuration
|
|
282
|
+
* (deepseek-v4.1-flash, reasoning off, gpt-6-luna fallback, i.e. Balanced before
|
|
283
|
+
* the refresh) wrote the note in 199 of 211 runs. Change it only after replaying the
|
|
284
|
+
* synthesis on production-shaped evidence and comparing note and section fill rates.
|
|
285
|
+
*/
|
|
286
|
+
export const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_MODEL = {
|
|
287
|
+
model: "deepseek/deepseek-v4.1-flash",
|
|
288
|
+
fallbackModels: ["openai/gpt-6-luna"],
|
|
289
|
+
disableReasoning: true,
|
|
290
|
+
};
|
|
271
291
|
/**
|
|
272
292
|
* Managed model ids OXYGEN ran that never sat in the registry above, mapped to
|
|
273
293
|
* the tier they read as. Labeling only: unlike LEGACY_MANAGED_AI_COLUMN_MODELS,
|