@oxygen-agent/cli 1.936.1 → 1.948.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/command-manifest.js +24 -2
- package/dist/help.js +1 -0
- package/dist/index.js +342 -54
- package/dist/ugc-commands.js +353 -12
- package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +11 -6
- package/node_modules/@oxygen/shared/dist/byok-connect.js +14 -6
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +53 -5
- package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.js +120 -0
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +64 -0
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +90 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/index.js +6 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +50 -21
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +47 -21
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +8 -3
- package/node_modules/@oxygen/shared/dist/langfuse.js +177 -130
- package/node_modules/@oxygen/shared/dist/llm-payload.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/llm-payload.js +54 -0
- package/node_modules/@oxygen/shared/dist/llm-usage.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/llm-usage.js +30 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-core.d.ts +98 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-core.js +159 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +46 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +92 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +96 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +21 -1
- 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/workflows/dist/graph/lint.js +22 -0
- package/package.json +1 -1
|
@@ -210,11 +210,11 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
210
210
|
primitive: "tables",
|
|
211
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.",
|
|
212
212
|
notFor: "Canonical CRM truth, message cadence, or an off-platform spreadsheet runtime.",
|
|
213
|
-
execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs.",
|
|
213
|
+
execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs. For standard person or company enrichment, `columns add <table> --preset person_enrich|company_enrich` (MCP oxygen_columns_add with preset) adds the maintained bundle in one call before any hand-built tool column.",
|
|
214
214
|
posture: "mixed",
|
|
215
215
|
gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
|
|
216
216
|
gatewayCommands: ["tables create", "columns add", "enrich-column preview", "tables link", "functions list", "functions draft"],
|
|
217
|
-
skills: ["oxygen-gtm", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration"],
|
|
217
|
+
skills: ["oxygen-gtm", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration", "oxygen-linkedin-marketing"],
|
|
218
218
|
endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
|
|
219
219
|
// "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
|
|
220
220
|
// blind user eval asked for exactly "link two tables" and was routed to
|
|
@@ -335,10 +335,13 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
335
335
|
execution: "Create and schedule on OXYGEN; explicit approval releases the hosted publisher.",
|
|
336
336
|
posture: "external_write",
|
|
337
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"],
|
|
338
|
+
gatewayCommands: ["publishing posts create", "publishing posts list", "publishing posts approve", "ugc programs list", "ugc programs duplicate", "ugc programs archive"],
|
|
339
339
|
skills: ["oxygen-linkedin-marketing", "oxygen-ugc"],
|
|
340
340
|
endpointSections: ["publishing", "ugc"],
|
|
341
|
-
|
|
341
|
+
// "remove program" / "delete program" landed on route: null in a blind eval
|
|
342
|
+
// (2026-09-10); a program is a UGC noun here, and its lifecycle verbs must
|
|
343
|
+
// resolve to the group that owns `ugc programs archive|delete|duplicate`.
|
|
344
|
+
intentTerms: ["publish", "publishing", "schedule post", "content calendar", "posting calendar", "approve post", "social publishing", "ugc", "creator program", "user generated content", "creator voice", "sponsored creator", "ugc program", "archive program", "delete program", "remove program", "duplicate program", "copy program"],
|
|
342
345
|
},
|
|
343
346
|
{
|
|
344
347
|
id: "workflows",
|
|
@@ -555,6 +558,13 @@ export function serializeCapabilityRoute(route) {
|
|
|
555
558
|
function normalizeIntent(query) {
|
|
556
559
|
return query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
557
560
|
}
|
|
561
|
+
function isLinkedInProfileWatcherIntent(query) {
|
|
562
|
+
return /\blinkedin\b/.test(query)
|
|
563
|
+
&& /\bprofiles?\b/.test(query)
|
|
564
|
+
&& /\b(watcher|watch|watching|monitor|monitoring|daily|every day|recurring)\b/.test(query)
|
|
565
|
+
&& (/\b(engagers?|engaging|engagement|reactions?|comments?|posts?)\b/.test(query) || /\bprofile watcher\b/.test(query))
|
|
566
|
+
&& !isNetNewLinkedInInitiation(query);
|
|
567
|
+
}
|
|
558
568
|
function explicitCapabilityIntent(query) {
|
|
559
569
|
if (/\b(infographic|graphic designer|render html|carousel pages|visual design|gtm flow image)\b/.test(query))
|
|
560
570
|
return ROUTE_BY_ID.get("visual-rendering") ?? null;
|
|
@@ -574,6 +584,8 @@ function explicitCapabilityIntent(query) {
|
|
|
574
584
|
if (isMailboxOnboardingIntent(query)) {
|
|
575
585
|
return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
|
|
576
586
|
}
|
|
587
|
+
if (isLinkedInProfileWatcherIntent(query))
|
|
588
|
+
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
577
589
|
if (isHostedWorkflowIntent(query))
|
|
578
590
|
return ROUTE_BY_PRIMITIVE.get("workflows") ?? null;
|
|
579
591
|
if (isOwnedPostCommentIntent(query))
|
|
@@ -612,6 +624,17 @@ function explicitCapabilityIntent(query) {
|
|
|
612
624
|
if (/\b(my|our|own)\b.{0,40}\blinkedin\b.{0,40}\b(post )?engagers?\b/.test(query)) {
|
|
613
625
|
return ROUTE_BY_PRIMITIVE.get("signals") ?? null;
|
|
614
626
|
}
|
|
627
|
+
// Grading addresses a row ALREADY holds — "verify these emails", "which are
|
|
628
|
+
// safe to send", "catch-all or valid" — is Tables work (the Email verification
|
|
629
|
+
// column: `columns add --capability verify_email`, previewed and run through
|
|
630
|
+
// `enrich-column`), with `verify email` as the one-shot twin. Without this
|
|
631
|
+
// rule the bare word "email" scored the Messages card, and a 2026-09-10 blind
|
|
632
|
+
// user asking "verify email deliverability safe to send" was routed to
|
|
633
|
+
// `email send`. Sits after the mailbox rules above so warm-up and sender
|
|
634
|
+
// deliverability keep their owner, and after the scheduling rule so "every
|
|
635
|
+
// Monday verify new signups" still lands on Workflows.
|
|
636
|
+
if (isEmailVerificationIntent(query))
|
|
637
|
+
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
615
638
|
if (/\b(recipe|playbook|proven play|what should i do)\b/.test(query)) {
|
|
616
639
|
return ROUTE_BY_PRIMITIVE.get("recipes") ?? null;
|
|
617
640
|
}
|
|
@@ -752,12 +775,27 @@ function recommendationsFor(card, query) {
|
|
|
752
775
|
};
|
|
753
776
|
}
|
|
754
777
|
if (card.primitive === "tables") {
|
|
778
|
+
if (isLinkedInProfileWatcherIntent(query)) {
|
|
779
|
+
return {
|
|
780
|
+
tools: ["oxygen_tables_watcher"],
|
|
781
|
+
commands: ["tables watcher preview", "tables watcher create", "tables watcher get", "tables watcher update", "tables watcher pause", "tables watcher resume"],
|
|
782
|
+
};
|
|
783
|
+
}
|
|
755
784
|
if (/\b(table )?(action )?runs?\b/.test(query)) {
|
|
756
785
|
return {
|
|
757
786
|
tools: ["oxygen_table_runs_get", "oxygen_table_runs_items", "oxygen_table_runs_wait", "oxygen_table_runs_retry_failed"],
|
|
758
787
|
commands: ["table-runs get", "table-runs items", "table-runs wait", "table-runs retry-failed"],
|
|
759
788
|
};
|
|
760
789
|
}
|
|
790
|
+
if (isEmailVerificationIntent(query)) {
|
|
791
|
+
// Define-without-running first (0 credits), then the free preview that
|
|
792
|
+
// prices it, then the approved run, then the one-shot for a handful of
|
|
793
|
+
// addresses that never needed a table.
|
|
794
|
+
return {
|
|
795
|
+
tools: ["oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_enrich_column_run", "oxygen_verify_email"],
|
|
796
|
+
commands: ["columns add", "enrich-column preview", "enrich-column run", "verify email"],
|
|
797
|
+
};
|
|
798
|
+
}
|
|
761
799
|
if (/\b(waterfall|enrich|enrichment|work email|mobile phone)\b/.test(query)) {
|
|
762
800
|
return {
|
|
763
801
|
tools: ["oxygen_enrich_column_preview", "oxygen_columns_add", "oxygen_enrich_column_run", "oxygen_table_runs_get"],
|
|
@@ -889,7 +927,7 @@ function recommendationsFor(card, query) {
|
|
|
889
927
|
if (card.primitive === "signals" && /\blinkedin\b.{0,40}\bengagers?\b/.test(query)) {
|
|
890
928
|
return {
|
|
891
929
|
tools: ["oxygen_engagement_list_engagers", "oxygen_signals_list", "oxygen_signals_leads_today"],
|
|
892
|
-
commands: ["engagement
|
|
930
|
+
commands: ["engagement engagers", "signals list", "signals leads-today"],
|
|
893
931
|
};
|
|
894
932
|
}
|
|
895
933
|
if (card.id === "connected-whatsapp" && /\b(account|connect|limits?|sync)\b/.test(query)) {
|
|
@@ -900,6 +938,16 @@ function recommendationsFor(card, query) {
|
|
|
900
938
|
}
|
|
901
939
|
return { tools: [...card.gatewayTools], commands: [...card.gatewayCommands] };
|
|
902
940
|
}
|
|
941
|
+
// An email-VERIFICATION ask names a grading verb next to the addresses, or the
|
|
942
|
+
// addresses next to a grade. "check my emails" is deliberately not matched
|
|
943
|
+
// (that is an inbox), and bare "deliverability" is left to the sending rail —
|
|
944
|
+
// only "are these emails deliverable" counts. Bare "bounce" describes an
|
|
945
|
+
// observed delivery failure too; require predictive wording or "bounce risk".
|
|
946
|
+
function isEmailVerificationIntent(query) {
|
|
947
|
+
const gradeThenAddress = /\b(verif(?:y|ied|ication)|validat(?:e|ed|ion)|grade|scrub|clean)\b.{0,40}\b(e ?mails?|email addresses|addresses)\b/;
|
|
948
|
+
const addressThenGrade = /\b(e ?mails?|addresses)\b.{0,40}\b(verif(?:y|ied|ication)|validat(?:e|ed|ion)|valid|invalid|deliverable|(?:will|would|might) bounce|bounce risk|risky|safe to (?:send|email)|catch ?all|accept ?all)\b/;
|
|
949
|
+
return gradeThenAddress.test(query) || addressThenGrade.test(query);
|
|
950
|
+
}
|
|
903
951
|
function isInboxAvatarIntent(query) {
|
|
904
952
|
return /\b(avatar|profile (?:picture|photo)|headshot|hosted (?:picture|image)|mailbox (?:picture|photo))\b/.test(query);
|
|
905
953
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE classifier for delivery-status notifications (bounce NDRs / DSNs) that
|
|
3
|
+
* land back in a sending mailbox's own inbox.
|
|
4
|
+
*
|
|
5
|
+
* WHY this exists: a mailbox-provider reputation block is the loudest
|
|
6
|
+
* deliverability signal a workspace ever receives, and it arrives as an ordinary
|
|
7
|
+
* inbound email — "Message rejected", "support.google.com/mail/answer/69585" —
|
|
8
|
+
* that no OXYGEN surface reads. A warm-up vendor reporting 96% inbox placement
|
|
9
|
+
* cannot contradict it, because the vendor only measures its own seed network.
|
|
10
|
+
* Classifying the DSN turns the workspace's own inbox into first-party
|
|
11
|
+
* deliverability evidence: OXYGEN's send log said we sent, the DSN says the
|
|
12
|
+
* receiving provider refused it, and only the second one is the truth.
|
|
13
|
+
*
|
|
14
|
+
* Pure by design — no clock, no I/O, no provider call — so the same
|
|
15
|
+
* classification runs in the tenant rollup, a worker sweep, and a unit test.
|
|
16
|
+
* Output is stable machine data (kinds and codes), never prose.
|
|
17
|
+
*/
|
|
18
|
+
/** What the notification actually says happened. */
|
|
19
|
+
export type DeliveryStatusNotificationKind =
|
|
20
|
+
/** The receiving provider refused the message on reputation/policy grounds. */
|
|
21
|
+
"provider_rejected"
|
|
22
|
+
/** The address does not exist (list hygiene, not reputation). */
|
|
23
|
+
| "no_such_user"
|
|
24
|
+
/** The recipient mailbox is over quota. */
|
|
25
|
+
| "mailbox_full"
|
|
26
|
+
/** Transient: retried, not dead. */
|
|
27
|
+
| "delayed"
|
|
28
|
+
/**
|
|
29
|
+
* The envelope proves it is a DSN, but nothing classifiable was stored — the
|
|
30
|
+
* body was never synced, so only the subject survives. Kept distinct from
|
|
31
|
+
* "other" because counting these as "other" reads as "we looked and found
|
|
32
|
+
* nothing wrong", when the truth is that we could not look.
|
|
33
|
+
*/
|
|
34
|
+
| "unclassified"
|
|
35
|
+
/** A DSN with real content we can recognize but not attribute. */
|
|
36
|
+
| "other";
|
|
37
|
+
/** Every kind, in one place, so callers can build a complete counts map. */
|
|
38
|
+
export declare const DELIVERY_STATUS_NOTIFICATION_KINDS: readonly ["provider_rejected", "no_such_user", "mailbox_full", "delayed", "unclassified", "other"];
|
|
39
|
+
/** Which mailbox provider generated the notification, when it is knowable. */
|
|
40
|
+
export type DeliveryStatusNotificationProvider = "google" | "microsoft" | "unknown";
|
|
41
|
+
export type DeliveryStatusNotification = {
|
|
42
|
+
kind: DeliveryStatusNotificationKind;
|
|
43
|
+
provider: DeliveryStatusNotificationProvider;
|
|
44
|
+
/** The address the original message was addressed to, when the DSN names it. */
|
|
45
|
+
recipient: string | null;
|
|
46
|
+
/** Enhanced status code (5.7.1) when present, else the bare SMTP reply (550). */
|
|
47
|
+
smtpCode: string | null;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Classify one stored inbound message as a delivery-status notification.
|
|
51
|
+
*
|
|
52
|
+
* Returns null when the message is not a DSN at all — the caller's SQL prefilter
|
|
53
|
+
* is intentionally wide, and a false positive here would invent a deliverability
|
|
54
|
+
* incident out of a customer reply.
|
|
55
|
+
*/
|
|
56
|
+
export declare function classifyDeliveryStatusNotification(input: {
|
|
57
|
+
fromAddress?: string | null;
|
|
58
|
+
subject?: string | null;
|
|
59
|
+
bodyText?: string | null;
|
|
60
|
+
}): DeliveryStatusNotification | null;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE classifier for delivery-status notifications (bounce NDRs / DSNs) that
|
|
3
|
+
* land back in a sending mailbox's own inbox.
|
|
4
|
+
*
|
|
5
|
+
* WHY this exists: a mailbox-provider reputation block is the loudest
|
|
6
|
+
* deliverability signal a workspace ever receives, and it arrives as an ordinary
|
|
7
|
+
* inbound email — "Message rejected", "support.google.com/mail/answer/69585" —
|
|
8
|
+
* that no OXYGEN surface reads. A warm-up vendor reporting 96% inbox placement
|
|
9
|
+
* cannot contradict it, because the vendor only measures its own seed network.
|
|
10
|
+
* Classifying the DSN turns the workspace's own inbox into first-party
|
|
11
|
+
* deliverability evidence: OXYGEN's send log said we sent, the DSN says the
|
|
12
|
+
* receiving provider refused it, and only the second one is the truth.
|
|
13
|
+
*
|
|
14
|
+
* Pure by design — no clock, no I/O, no provider call — so the same
|
|
15
|
+
* classification runs in the tenant rollup, a worker sweep, and a unit test.
|
|
16
|
+
* Output is stable machine data (kinds and codes), never prose.
|
|
17
|
+
*/
|
|
18
|
+
/** Every kind, in one place, so callers can build a complete counts map. */
|
|
19
|
+
export const DELIVERY_STATUS_NOTIFICATION_KINDS = [
|
|
20
|
+
"provider_rejected",
|
|
21
|
+
"no_such_user",
|
|
22
|
+
"mailbox_full",
|
|
23
|
+
"delayed",
|
|
24
|
+
"unclassified",
|
|
25
|
+
"other",
|
|
26
|
+
];
|
|
27
|
+
// A message only enters classification when its envelope looks like a DSN. The
|
|
28
|
+
// SQL prefilter that feeds this is deliberately loose (ILIKE), so this gate is
|
|
29
|
+
// what keeps an ordinary customer reply that happens to contain the word
|
|
30
|
+
// "blocked" out of the deliverability evidence.
|
|
31
|
+
const DSN_FROM_PATTERN = /(mailer-daemon|mail-daemon|postmaster|microsoftexchange)/i;
|
|
32
|
+
const DSN_SUBJECT_PATTERN = /(undeliver|delivery status notification|mail delivery fail|delivery has failed|returned mail)/i;
|
|
33
|
+
const GOOGLE_BLOCK_URL = /support\.google\.com\/mail\/answer\/69585/i;
|
|
34
|
+
const GOOGLE_FROM = /@(?:[a-z0-9-]+\.)*(?:googlemail|google)\.com\b/i;
|
|
35
|
+
const MICROSOFT_FROM = /(?:@(?:[a-z0-9-]+\.)*(?:outlook|office365|hotmail|microsoft)\.com\b|microsoftexchange|\bexchange\b)/i;
|
|
36
|
+
const MICROSOFT_BODY = /(microsoft exchange|exchange server|office\s?365)/i;
|
|
37
|
+
// Enhanced status code (RFC 3463): class.subject.detail, where subject/detail can
|
|
38
|
+
// be multi-digit (Google emits 5.7.708). Kept separate from the bare 3-digit SMTP
|
|
39
|
+
// reply so a caller can tell "550" from "5.7.1".
|
|
40
|
+
const ENHANCED_CODE = /\b([2-5]\.\d{1,3}\.\d{1,3})\b/;
|
|
41
|
+
const SMTP_REPLY_CODE = /\b([2-5]\d{2})\b/;
|
|
42
|
+
const REPUTATION_BLOCK = /\b5\.7\.\d{1,3}\b/;
|
|
43
|
+
const MAILBOX_FULL_CODE = /\b[45]\.2\.2\b/;
|
|
44
|
+
const NO_SUCH_USER_CODE = /\b5\.1\.\d{1,3}\b/;
|
|
45
|
+
const TRANSIENT_CODE = /(\b4\.\d{1,3}\.\d{1,3}\b|\b4\d{2}\b)/;
|
|
46
|
+
const MAILBOX_FULL_TEXT = /(mailbox (?:is )?full|over quota|quota exceeded|insufficient storage)/i;
|
|
47
|
+
const NO_SUCH_USER_TEXT = /(does ?n[o']?t exist|nosuchuser|no such user|user unknown|unknown user|address not found|recipient (?:address )?(?:not found|rejected)|invalid recipient)/i;
|
|
48
|
+
const PROVIDER_REJECTED_TEXT = /(message rejected|has been blocked|\bblocked\b|\bbanned\b|\bspam\b|local policy violation|unsolicited|reputation)/i;
|
|
49
|
+
const TRANSIENT_TEXT = /(temporar|will retry|try again later|rate ?limit|throttl)/i;
|
|
50
|
+
// Greedy address capture: the character class already stops at whitespace and
|
|
51
|
+
// angle brackets, so trailing sentence punctuation is stripped afterwards. A lazy
|
|
52
|
+
// capture would stop at the dot inside "gmail.com".
|
|
53
|
+
const ADDRESS = "([^\\s<>,;()]+@[^\\s<>,;()]+)";
|
|
54
|
+
const RECIPIENT_PATTERNS = [
|
|
55
|
+
new RegExp(`your message to\\s+<?${ADDRESS}>?`, "i"),
|
|
56
|
+
new RegExp(`\\bto\\s+<?${ADDRESS}>?\\s+(?:was|were|could not|couldn't|has been|had been)`, "i"),
|
|
57
|
+
// Machine-readable half of a real RFC 3464 report, when the provider sends one.
|
|
58
|
+
new RegExp(`(?:final|original)-recipient:\\s*rfc822;\\s*<?${ADDRESS}>?`, "i"),
|
|
59
|
+
];
|
|
60
|
+
function readRecipient(haystack) {
|
|
61
|
+
for (const pattern of RECIPIENT_PATTERNS) {
|
|
62
|
+
const match = pattern.exec(haystack);
|
|
63
|
+
const value = match?.[1]?.replace(/[.,;:>]+$/, "").trim().toLowerCase();
|
|
64
|
+
if (value && value.includes("@"))
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function readProvider(from, haystack) {
|
|
70
|
+
// The 69585 URL is decisive: only Google emits it, and it is the exact article
|
|
71
|
+
// a Gmail reputation block cites.
|
|
72
|
+
if (GOOGLE_BLOCK_URL.test(haystack) || GOOGLE_FROM.test(from))
|
|
73
|
+
return "google";
|
|
74
|
+
if (MICROSOFT_FROM.test(from) || MICROSOFT_BODY.test(haystack))
|
|
75
|
+
return "microsoft";
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
function readKind(subject, haystack, hasBody) {
|
|
79
|
+
// Order is load-bearing. A Google "(Delay)" report can quote block-sounding
|
|
80
|
+
// prose while the message is still queued, so the transient marker wins first;
|
|
81
|
+
// an explicit enhanced code then beats keyword matching, because "blocked" and
|
|
82
|
+
// "spam" appear in boilerplate that a 5.1.1 no-such-user report also carries.
|
|
83
|
+
if (/\(delay\)/i.test(subject))
|
|
84
|
+
return "delayed";
|
|
85
|
+
if (REPUTATION_BLOCK.test(haystack))
|
|
86
|
+
return "provider_rejected";
|
|
87
|
+
if (MAILBOX_FULL_CODE.test(haystack) || MAILBOX_FULL_TEXT.test(haystack))
|
|
88
|
+
return "mailbox_full";
|
|
89
|
+
if (NO_SUCH_USER_CODE.test(haystack) || NO_SUCH_USER_TEXT.test(haystack))
|
|
90
|
+
return "no_such_user";
|
|
91
|
+
if (PROVIDER_REJECTED_TEXT.test(haystack))
|
|
92
|
+
return "provider_rejected";
|
|
93
|
+
if (TRANSIENT_CODE.test(haystack) || TRANSIENT_TEXT.test(haystack))
|
|
94
|
+
return "delayed";
|
|
95
|
+
// Nothing matched AND there was no body to match against: the sync stored the
|
|
96
|
+
// envelope only. Reporting that as "other" would be a false clean.
|
|
97
|
+
return hasBody ? "other" : "unclassified";
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Classify one stored inbound message as a delivery-status notification.
|
|
101
|
+
*
|
|
102
|
+
* Returns null when the message is not a DSN at all — the caller's SQL prefilter
|
|
103
|
+
* is intentionally wide, and a false positive here would invent a deliverability
|
|
104
|
+
* incident out of a customer reply.
|
|
105
|
+
*/
|
|
106
|
+
export function classifyDeliveryStatusNotification(input) {
|
|
107
|
+
const from = typeof input.fromAddress === "string" ? input.fromAddress : "";
|
|
108
|
+
const subject = typeof input.subject === "string" ? input.subject : "";
|
|
109
|
+
const body = typeof input.bodyText === "string" ? input.bodyText : "";
|
|
110
|
+
if (!DSN_FROM_PATTERN.test(from) && !DSN_SUBJECT_PATTERN.test(subject))
|
|
111
|
+
return null;
|
|
112
|
+
const haystack = `${subject}\n${body}`;
|
|
113
|
+
const smtpCode = ENHANCED_CODE.exec(haystack)?.[1] ?? SMTP_REPLY_CODE.exec(haystack)?.[1] ?? null;
|
|
114
|
+
return {
|
|
115
|
+
kind: readKind(subject, haystack, body.trim().length > 0),
|
|
116
|
+
provider: readProvider(from, haystack),
|
|
117
|
+
recipient: readRecipient(haystack),
|
|
118
|
+
smtpCode,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE warm-up readiness rule: what daily cold volume a sending mailbox can
|
|
3
|
+
* actually carry, given its warm-up age rather than a provider's opinion of it.
|
|
4
|
+
*
|
|
5
|
+
* WHY it lives in @oxygen/shared and not next to the other email-health rules in
|
|
6
|
+
* @oxygen/integrations: the tenant rollup computes this per mailbox, and
|
|
7
|
+
* @oxygen/tenant-db must never import @oxygen/integrations (integrations depends
|
|
8
|
+
* on tenant-db). Duplicating the thresholds so each package could own a copy is
|
|
9
|
+
* exactly how two surfaces start recommending different numbers, so the rule is
|
|
10
|
+
* defined once here and re-exported from
|
|
11
|
+
* packages/integrations/src/email-health/health-state.ts, which stays the
|
|
12
|
+
* email-health surface every caller reads.
|
|
13
|
+
*
|
|
14
|
+
* DIRECTIONAL, like everything else in the deliverability cluster: it produces a
|
|
15
|
+
* recommendation and a launch-preview warning. It never clamps a cap, never
|
|
16
|
+
* pauses a mailbox, and never changes what a sequence sends.
|
|
17
|
+
*/
|
|
18
|
+
/** Below this warm-up day a mailbox should carry only the starter cold volume. */
|
|
19
|
+
export declare const WARMUP_EARLY_DAY_LIMIT = 14;
|
|
20
|
+
/** Below this warm-up day a mailbox should stay at the reduced cold volume. */
|
|
21
|
+
export declare const WARMUP_ESTABLISHED_DAY_LIMIT = 28;
|
|
22
|
+
/** Recommended cold sends/day before day 14 (and for an un-warmed young mailbox). */
|
|
23
|
+
export declare const WARMUP_EARLY_DAILY_CAP = 5;
|
|
24
|
+
/** Recommended cold sends/day between day 14 and day 28. */
|
|
25
|
+
export declare const WARMUP_ESTABLISHED_DAILY_CAP = 10;
|
|
26
|
+
/** A warm-up health score under this is treated as "not ready for volume". */
|
|
27
|
+
export declare const WARMUP_HEALTH_SCORE_FLOOR = 60;
|
|
28
|
+
/** Hard-bounce rate above which a fleet is burning its domains, not just its list. */
|
|
29
|
+
export declare const HARD_BOUNCE_RATE_CEILING = 0.03;
|
|
30
|
+
/** Below this send volume a bounce rate is noise, not a signal. */
|
|
31
|
+
export declare const HARD_BOUNCE_RATE_MIN_SENDS = 20;
|
|
32
|
+
/**
|
|
33
|
+
* The DIRECTIONAL per-mailbox daily cold-send ceiling warm-up readiness supports,
|
|
34
|
+
* or null when nothing in the evidence argues for holding volume back.
|
|
35
|
+
*
|
|
36
|
+
* Pure — the caller resolves `mailboxAgeDays` from created_at, so this stays
|
|
37
|
+
* clock-free and identical on every surface.
|
|
38
|
+
*
|
|
39
|
+
* - warm-up day < 14 -> 5/day
|
|
40
|
+
* - mailbox younger than 28 days, with either no active
|
|
41
|
+
* warm-up or no warm-up day reported at all -> 5/day
|
|
42
|
+
* - warm-up day < 28 -> 10/day
|
|
43
|
+
* - warm-up health score < 60 -> at most 5/day
|
|
44
|
+
* - otherwise -> null (no advice)
|
|
45
|
+
*
|
|
46
|
+
* The second rule is the one that matters for a freshly provisioned fleet: a
|
|
47
|
+
* mailbox whose warm-up never started (state "unknown") reports no day at all,
|
|
48
|
+
* and a rule keyed only on the day number would have said nothing about the exact
|
|
49
|
+
* mailboxes most likely to get blocked.
|
|
50
|
+
*/
|
|
51
|
+
export declare function recommendedDailyCapForWarmup(input: {
|
|
52
|
+
warmupState?: string | null;
|
|
53
|
+
warmupDay?: number | null;
|
|
54
|
+
warmupHealthScore?: number | null;
|
|
55
|
+
mailboxAgeDays?: number | null;
|
|
56
|
+
}): number | null;
|
|
57
|
+
/**
|
|
58
|
+
* True when a hard-bounce count is high enough, over enough sends, to be a real
|
|
59
|
+
* reputation problem rather than list noise.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hardBounceRateIsHigh(input: {
|
|
62
|
+
hardBounces: number;
|
|
63
|
+
coldSends: number;
|
|
64
|
+
}): boolean;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE warm-up readiness rule: what daily cold volume a sending mailbox can
|
|
3
|
+
* actually carry, given its warm-up age rather than a provider's opinion of it.
|
|
4
|
+
*
|
|
5
|
+
* WHY it lives in @oxygen/shared and not next to the other email-health rules in
|
|
6
|
+
* @oxygen/integrations: the tenant rollup computes this per mailbox, and
|
|
7
|
+
* @oxygen/tenant-db must never import @oxygen/integrations (integrations depends
|
|
8
|
+
* on tenant-db). Duplicating the thresholds so each package could own a copy is
|
|
9
|
+
* exactly how two surfaces start recommending different numbers, so the rule is
|
|
10
|
+
* defined once here and re-exported from
|
|
11
|
+
* packages/integrations/src/email-health/health-state.ts, which stays the
|
|
12
|
+
* email-health surface every caller reads.
|
|
13
|
+
*
|
|
14
|
+
* DIRECTIONAL, like everything else in the deliverability cluster: it produces a
|
|
15
|
+
* recommendation and a launch-preview warning. It never clamps a cap, never
|
|
16
|
+
* pauses a mailbox, and never changes what a sequence sends.
|
|
17
|
+
*/
|
|
18
|
+
/** Below this warm-up day a mailbox should carry only the starter cold volume. */
|
|
19
|
+
export const WARMUP_EARLY_DAY_LIMIT = 14;
|
|
20
|
+
/** Below this warm-up day a mailbox should stay at the reduced cold volume. */
|
|
21
|
+
export const WARMUP_ESTABLISHED_DAY_LIMIT = 28;
|
|
22
|
+
/** Recommended cold sends/day before day 14 (and for an un-warmed young mailbox). */
|
|
23
|
+
export const WARMUP_EARLY_DAILY_CAP = 5;
|
|
24
|
+
/** Recommended cold sends/day between day 14 and day 28. */
|
|
25
|
+
export const WARMUP_ESTABLISHED_DAILY_CAP = 10;
|
|
26
|
+
/** A warm-up health score under this is treated as "not ready for volume". */
|
|
27
|
+
export const WARMUP_HEALTH_SCORE_FLOOR = 60;
|
|
28
|
+
/** Hard-bounce rate above which a fleet is burning its domains, not just its list. */
|
|
29
|
+
export const HARD_BOUNCE_RATE_CEILING = 0.03;
|
|
30
|
+
/** Below this send volume a bounce rate is noise, not a signal. */
|
|
31
|
+
export const HARD_BOUNCE_RATE_MIN_SENDS = 20;
|
|
32
|
+
/** Warm-up states in which a vendor is actively conditioning the mailbox. */
|
|
33
|
+
const ACTIVE_WARMUP_STATES = new Set(["warming", "active"]);
|
|
34
|
+
function finiteOrNull(value) {
|
|
35
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The DIRECTIONAL per-mailbox daily cold-send ceiling warm-up readiness supports,
|
|
39
|
+
* or null when nothing in the evidence argues for holding volume back.
|
|
40
|
+
*
|
|
41
|
+
* Pure — the caller resolves `mailboxAgeDays` from created_at, so this stays
|
|
42
|
+
* clock-free and identical on every surface.
|
|
43
|
+
*
|
|
44
|
+
* - warm-up day < 14 -> 5/day
|
|
45
|
+
* - mailbox younger than 28 days, with either no active
|
|
46
|
+
* warm-up or no warm-up day reported at all -> 5/day
|
|
47
|
+
* - warm-up day < 28 -> 10/day
|
|
48
|
+
* - warm-up health score < 60 -> at most 5/day
|
|
49
|
+
* - otherwise -> null (no advice)
|
|
50
|
+
*
|
|
51
|
+
* The second rule is the one that matters for a freshly provisioned fleet: a
|
|
52
|
+
* mailbox whose warm-up never started (state "unknown") reports no day at all,
|
|
53
|
+
* and a rule keyed only on the day number would have said nothing about the exact
|
|
54
|
+
* mailboxes most likely to get blocked.
|
|
55
|
+
*/
|
|
56
|
+
export function recommendedDailyCapForWarmup(input) {
|
|
57
|
+
const day = finiteOrNull(input.warmupDay);
|
|
58
|
+
const ageDays = finiteOrNull(input.mailboxAgeDays);
|
|
59
|
+
const healthScore = finiteOrNull(input.warmupHealthScore);
|
|
60
|
+
const warmingNow = ACTIVE_WARMUP_STATES.has((input.warmupState ?? "").trim().toLowerCase());
|
|
61
|
+
let cap = null;
|
|
62
|
+
if (day !== null && day < WARMUP_EARLY_DAY_LIMIT) {
|
|
63
|
+
cap = WARMUP_EARLY_DAILY_CAP;
|
|
64
|
+
}
|
|
65
|
+
else if (ageDays !== null &&
|
|
66
|
+
ageDays < WARMUP_ESTABLISHED_DAY_LIMIT &&
|
|
67
|
+
(day === null || !warmingNow)) {
|
|
68
|
+
// A rail that reports "warming" but no day number proves nothing about how
|
|
69
|
+
// far the ramp got, so age still governs. Gating this on !warmingNow alone
|
|
70
|
+
// left exactly that mailbox — enrolled, young, no telemetry — with no advice.
|
|
71
|
+
cap = WARMUP_EARLY_DAILY_CAP;
|
|
72
|
+
}
|
|
73
|
+
else if (day !== null && day < WARMUP_ESTABLISHED_DAY_LIMIT) {
|
|
74
|
+
cap = WARMUP_ESTABLISHED_DAILY_CAP;
|
|
75
|
+
}
|
|
76
|
+
if (healthScore !== null && healthScore < WARMUP_HEALTH_SCORE_FLOOR) {
|
|
77
|
+
cap = cap === null ? WARMUP_EARLY_DAILY_CAP : Math.min(cap, WARMUP_EARLY_DAILY_CAP);
|
|
78
|
+
}
|
|
79
|
+
return cap;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* True when a hard-bounce count is high enough, over enough sends, to be a real
|
|
83
|
+
* reputation problem rather than list noise.
|
|
84
|
+
*/
|
|
85
|
+
export function hardBounceRateIsHigh(input) {
|
|
86
|
+
if (!Number.isFinite(input.coldSends) || input.coldSends < HARD_BOUNCE_RATE_MIN_SENDS) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return input.hardBounces / input.coldSends > HARD_BOUNCE_RATE_CEILING;
|
|
90
|
+
}
|
|
@@ -30,12 +30,17 @@ export * from "./copilot-journeys.js";
|
|
|
30
30
|
export * from "./copilot-plan.js";
|
|
31
31
|
export * from "./credit-guidance.js";
|
|
32
32
|
export * from "./directory.js";
|
|
33
|
+
export * from "./email-dsn.js";
|
|
34
|
+
export * from "./email-warmup-readiness.js";
|
|
33
35
|
export * from "./email-tracking-token.js";
|
|
34
36
|
export * from "./email-unsubscribe-token.js";
|
|
35
37
|
export * from "./error-redaction.js";
|
|
36
38
|
export * from "./future-signup-events.js";
|
|
37
39
|
export * from "./future-signup-lifecycle-projection.js";
|
|
38
40
|
export * from "./feature-gates.js";
|
|
41
|
+
export * from "./product-analytics-core.js";
|
|
42
|
+
export * from "./product-analytics-environment.js";
|
|
43
|
+
export * from "./product-analytics-events.js";
|
|
39
44
|
export * from "./hosted-ai.js";
|
|
40
45
|
export * from "./identifiers.js";
|
|
41
46
|
export * from "./knowledge-constants.js";
|
|
@@ -44,6 +49,7 @@ export * from "./knowledge-links.js";
|
|
|
44
49
|
export * from "./knowledge-markdown.js";
|
|
45
50
|
export * from "./knowledge-seed-content.js";
|
|
46
51
|
export * from "./langfuse.js";
|
|
52
|
+
export * from "./llm-usage.js";
|
|
47
53
|
export * from "./linkedin-mentions.js";
|
|
48
54
|
export * from "./linkedin-post-url.js";
|
|
49
55
|
export * from "./linkedin-quota-denial.js";
|
|
@@ -30,12 +30,17 @@ export * from "./copilot-journeys.js";
|
|
|
30
30
|
export * from "./copilot-plan.js";
|
|
31
31
|
export * from "./credit-guidance.js";
|
|
32
32
|
export * from "./directory.js";
|
|
33
|
+
export * from "./email-dsn.js";
|
|
34
|
+
export * from "./email-warmup-readiness.js";
|
|
33
35
|
export * from "./email-tracking-token.js";
|
|
34
36
|
export * from "./email-unsubscribe-token.js";
|
|
35
37
|
export * from "./error-redaction.js";
|
|
36
38
|
export * from "./future-signup-events.js";
|
|
37
39
|
export * from "./future-signup-lifecycle-projection.js";
|
|
38
40
|
export * from "./feature-gates.js";
|
|
41
|
+
export * from "./product-analytics-core.js";
|
|
42
|
+
export * from "./product-analytics-environment.js";
|
|
43
|
+
export * from "./product-analytics-events.js";
|
|
39
44
|
export * from "./hosted-ai.js";
|
|
40
45
|
export * from "./identifiers.js";
|
|
41
46
|
export * from "./knowledge-constants.js";
|
|
@@ -44,6 +49,7 @@ export * from "./knowledge-links.js";
|
|
|
44
49
|
export * from "./knowledge-markdown.js";
|
|
45
50
|
export * from "./knowledge-seed-content.js";
|
|
46
51
|
export * from "./langfuse.js";
|
|
52
|
+
export * from "./llm-usage.js";
|
|
47
53
|
export * from "./linkedin-mentions.js";
|
|
48
54
|
export * from "./linkedin-post-url.js";
|
|
49
55
|
export * from "./linkedin-quota-denial.js";
|
|
@@ -2,28 +2,35 @@
|
|
|
2
2
|
* Knowledge bootstrap — the once-per-workspace, automatic company research pass.
|
|
3
3
|
*
|
|
4
4
|
* WHAT IT IS. Exactly once in a workspace's life, OXYGEN researches the customer's
|
|
5
|
-
* OWN company from the domain
|
|
6
|
-
* (control-DB `organizations.iconDomain
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* OWN company from the domain the creator typed when the workspace was created
|
|
6
|
+
* (control-DB `organizations.iconDomain`), then fills the typed company profile
|
|
7
|
+
* (`ox_context.company_profile`) and writes one cited wiki page. A founder who
|
|
8
|
+
* signs up should not face an empty Knowledge Graph and have to type their own
|
|
9
|
+
* positioning back at us; every grounded AI action downstream (message drafts, AI
|
|
10
|
+
* columns, agent runs) reads that profile, so an empty one degrades the whole
|
|
11
|
+
* product's first hour.
|
|
12
12
|
*
|
|
13
13
|
* WHY THIS IS NOT A VIOLATION OF THE PAID-ACTIONS RULE. `CLAUDE.md` says never run
|
|
14
14
|
* paid provider actions unless the user explicitly asks. This slice is a deliberate,
|
|
15
|
-
* founder-approved exception:
|
|
16
|
-
* armed table-webhook auto-run
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* founder-approved exception: naming the company at workspace creation IS the
|
|
16
|
+
* standing authorization, the same way an armed table-webhook auto-run
|
|
17
|
+
* configuration is scoped standing permission for the columns it queues — and
|
|
18
|
+
* since 2026-09-11 the pass is OXYGEN-funded, so it never draws down the
|
|
19
|
+
* customer's balance at all. The exception is defensible ONLY because every one of
|
|
20
|
+
* the following properties holds. They are load-bearing — do not drop one for
|
|
19
21
|
* convenience, and if you remove one, the exception no longer stands:
|
|
20
22
|
*
|
|
21
23
|
* 1. CAPPED — `KNOWLEDGE_BOOTSTRAP_MAX_CREDITS` is a hard ceiling on the whole
|
|
22
24
|
* pass, with `KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS` a tighter
|
|
23
|
-
* sub-ceiling on the provider (external-money) half.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* sub-ceiling on the provider (external-money) half. The cap bounds
|
|
26
|
+
* OXYGEN's own money now, and it is the same number.
|
|
27
|
+
* 2. AUTHORIZED — it runs only for a workspace whose creator typed the company
|
|
28
|
+
* website into the REQUIRED field at workspace creation; that
|
|
29
|
+
* entry is recorded as `knowledge_bootstrap_consent` (source
|
|
30
|
+
* `workspace_creation`) and read FAIL-CLOSED, so a workspace that
|
|
31
|
+
* predates the field is never researched and a personal-email
|
|
32
|
+
* signup with no website never reaches the worker. We never
|
|
33
|
+
* research a stranger.
|
|
27
34
|
* 3. JOURNALLED — the marker below records status, timing, run id, credits and the
|
|
28
35
|
* domain in `knowledge_state.watermarks`; the URLs read land as
|
|
29
36
|
* immutable `page_sources` rows plus an `ingest` knowledge_log
|
|
@@ -36,6 +43,14 @@
|
|
|
36
43
|
* ordinary revisioned wiki writes a human can revert.
|
|
37
44
|
* 6. EXACTLY-ONCE — the marker is claimed with a conditional UPDATE, so N worker
|
|
38
45
|
* replicas polling one tenant produce one run, never N runs.
|
|
46
|
+
* 7. OXYGEN-FUNDED — the automatic pass meters managed credits through the ordinary
|
|
47
|
+
* tool and AI-column lanes (so the cap, the marker and the run row
|
|
48
|
+
* all stay honest), and the worker then grants exactly the credits
|
|
49
|
+
* it used back to the workspace as an idempotent `bonus` ledger
|
|
50
|
+
* row keyed by the run (`KNOWLEDGE_BOOTSTRAP_COVERAGE_IDEMPOTENCY_PREFIX`).
|
|
51
|
+
* The customer's balance nets to zero and both rows are visible in
|
|
52
|
+
* their ledger. Only the AUTOMATIC pass is covered: a requested
|
|
53
|
+
* `--live --max-credits` re-run is the customer's own paid action.
|
|
39
54
|
*
|
|
40
55
|
* CAP ARITHMETIC (prices re-read from packages/integrations/src/cogs-rates.ts —
|
|
41
56
|
* confirm them there rather than trusting this comment after a re-pricing):
|
|
@@ -52,8 +67,9 @@
|
|
|
52
67
|
* overall ceiling.
|
|
53
68
|
* Hard cap 300 cr — 85 cr nominal plus room for two retried synthesis
|
|
54
69
|
* passes. 3% of the 10,000-credit signup grant
|
|
55
|
-
* (FREE_SIGNUP_GRANT_CREDITS, ./billing.ts),
|
|
56
|
-
*
|
|
70
|
+
* (FREE_SIGNUP_GRANT_CREDITS, ./billing.ts), which is
|
|
71
|
+
* the headroom the balance gate needs even though the
|
|
72
|
+
* spend is granted back.
|
|
57
73
|
*
|
|
58
74
|
* The obvious implementation — firecrawl.map + 3x firecrawl.scrape — costs
|
|
59
75
|
* 4 x 40 = 160 cr for the SAME job, because Firecrawl bills per page. exa.contents
|
|
@@ -164,9 +180,11 @@ export type KnowledgeBootstrapMarker = {
|
|
|
164
180
|
linkedin_url?: string | null;
|
|
165
181
|
};
|
|
166
182
|
/**
|
|
167
|
-
* Control-DB `organizations.metadata` key carrying the workspace's recorded
|
|
168
|
-
*
|
|
169
|
-
*
|
|
183
|
+
* Control-DB `organizations.metadata` key carrying the workspace's recorded
|
|
184
|
+
* authorization for the automatic research pass. Written once, when the creator
|
|
185
|
+
* names the company website at workspace creation (`source: "workspace_creation"`;
|
|
186
|
+
* rows written by the retired `/setup` checkbox carry `source: "signup"` and stay
|
|
187
|
+
* valid); read by the worker cycle before anything is spent.
|
|
170
188
|
*
|
|
171
189
|
* It lives in shared, not in either caller, so the writer and the reader cannot
|
|
172
190
|
* drift onto two different key names — a silent drift there would either deny every
|
|
@@ -178,7 +196,10 @@ export declare const KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY = "knowledge_boots
|
|
|
178
196
|
export type KnowledgeBootstrapConsent = {
|
|
179
197
|
/** ISO timestamp consent was recorded. Must parse; a marker that does not is not consent. */
|
|
180
198
|
granted_at: string;
|
|
181
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* Where it came from: `workspace_creation` (the required website field on the
|
|
201
|
+
* create-workspace screen) or the retired `signup` checkbox. Informational.
|
|
202
|
+
*/
|
|
182
203
|
source: string | null;
|
|
183
204
|
/** Clerk user who accepted, when the surface knows. Informational. */
|
|
184
205
|
granted_by_clerk_user_id: string | null;
|
|
@@ -227,6 +248,14 @@ export declare const KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY = "company_linked
|
|
|
227
248
|
* dead and the workspace is recoverable through the product.
|
|
228
249
|
*/
|
|
229
250
|
export declare const KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS: number;
|
|
251
|
+
/**
|
|
252
|
+
* Idempotency-key prefix for the ledger row that hands an automatic pass's
|
|
253
|
+
* credits back to the workspace (property 7 above). The key is
|
|
254
|
+
* `${prefix}:${run_id}`, so one pass is covered exactly once no matter how many
|
|
255
|
+
* times the worker revisits the outcome, and a `--force` re-run — a different
|
|
256
|
+
* run — is covered only if it was itself automatic.
|
|
257
|
+
*/
|
|
258
|
+
export declare const KNOWLEDGE_BOOTSTRAP_COVERAGE_IDEMPOTENCY_PREFIX = "knowledge_bootstrap_coverage";
|
|
230
259
|
/**
|
|
231
260
|
* The one wiki page a bootstrap authors. A stable slug is what makes a re-armed pass
|
|
232
261
|
* REVISE the note rather than litter the wiki with `company-research-2`.
|