@oxygen-agent/cli 1.1014.1138 → 1.1014.1221

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.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +2 -0
  3. package/dist/command-support.d.ts +8 -0
  4. package/dist/command-support.js +11 -0
  5. package/dist/commands/api-keys.d.ts +9 -0
  6. package/dist/commands/api-keys.js +4 -1
  7. package/dist/commands/capabilities.js +4 -2
  8. package/dist/commands/crm.js +28 -2
  9. package/dist/commands/dashboards.js +2 -2
  10. package/dist/commands/db.js +34 -0
  11. package/dist/commands/find.js +1 -1
  12. package/dist/commands/knowledge.js +2 -0
  13. package/dist/commands/mailboxes.js +3 -2
  14. package/dist/commands/messages.js +6 -1
  15. package/dist/commands/senders.js +2 -0
  16. package/dist/commands/sequences.js +1 -1
  17. package/dist/commands/signals.js +11 -2
  18. package/dist/deliverability-evidence-commands.d.ts +4 -0
  19. package/dist/deliverability-evidence-commands.js +104 -0
  20. package/dist/diagnostic-context.d.ts +21 -0
  21. package/dist/diagnostic-context.js +25 -0
  22. package/dist/diagnostics.d.ts +1 -3
  23. package/dist/diagnostics.js +12 -30
  24. package/dist/http-client.js +1 -1
  25. package/dist/network-mining-host.d.ts +6 -0
  26. package/dist/network-mining-host.js +10 -0
  27. package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +1 -1
  28. package/node_modules/@oxygen/formula/dist/value-normalizers.js +5 -0
  29. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +169 -0
  30. package/node_modules/@oxygen/shared/dist/agent-sessions.js +17 -0
  31. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  32. package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -5
  33. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
  34. package/node_modules/@oxygen/shared/dist/contracts/operation-request.d.ts +92 -0
  35. package/node_modules/@oxygen/shared/dist/contracts/operation-request.js +36 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +3 -90
  37. package/node_modules/@oxygen/shared/dist/contracts/operations.js +5 -39
  38. package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.d.ts +2 -0
  39. package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.js +123 -0
  40. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +1 -0
  41. package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +4 -0
  42. package/node_modules/@oxygen/shared/dist/crm-activity-events.js +1 -0
  43. package/node_modules/@oxygen/shared/dist/crm-record-brief.d.ts +10 -0
  44. package/node_modules/@oxygen/shared/dist/crm-record-brief.js +22 -0
  45. package/node_modules/@oxygen/shared/dist/deliverability-blacklist.d.ts +74 -0
  46. package/node_modules/@oxygen/shared/dist/deliverability-blacklist.js +118 -0
  47. package/node_modules/@oxygen/shared/dist/deliverability-checks.d.ts +138 -0
  48. package/node_modules/@oxygen/shared/dist/deliverability-checks.js +124 -0
  49. package/node_modules/@oxygen/shared/dist/deliverability-evidence.d.ts +366 -0
  50. package/node_modules/@oxygen/shared/dist/deliverability-evidence.js +169 -0
  51. package/node_modules/@oxygen/shared/dist/deliverability-loop.d.ts +334 -0
  52. package/node_modules/@oxygen/shared/dist/deliverability-loop.js +312 -0
  53. package/node_modules/@oxygen/shared/dist/deliverability-surface.d.ts +42 -0
  54. package/node_modules/@oxygen/shared/dist/deliverability-surface.js +66 -0
  55. package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +34 -4
  56. package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +24 -0
  57. package/node_modules/@oxygen/shared/dist/domain-authentication.js +20 -0
  58. package/node_modules/@oxygen/shared/dist/file-import.js +11 -3
  59. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +16 -0
  60. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +30 -2
  61. package/node_modules/@oxygen/shared/dist/network-mining.d.ts +5 -0
  62. package/node_modules/@oxygen/shared/dist/network-mining.js +9 -0
  63. package/node_modules/@oxygen/shared/dist/received-authentication.d.ts +74 -0
  64. package/node_modules/@oxygen/shared/dist/received-authentication.js +452 -0
  65. package/node_modules/@oxygen/shared/dist/recipient-provider.d.ts +21 -0
  66. package/node_modules/@oxygen/shared/dist/recipient-provider.js +33 -0
  67. package/node_modules/@oxygen/shared/dist/reply-oxygent.d.ts +73 -0
  68. package/node_modules/@oxygen/shared/dist/reply-oxygent.js +31 -0
  69. package/node_modules/@oxygen/shared/dist/repricing.d.ts +10 -1
  70. package/node_modules/@oxygen/shared/dist/repricing.js +16 -4
  71. package/node_modules/@oxygen/shared/dist/research-findings.d.ts +80 -0
  72. package/node_modules/@oxygen/shared/dist/research-findings.js +199 -0
  73. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.d.ts +2 -0
  74. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.js +4 -0
  75. package/node_modules/@oxygen/shared/dist/sequences.d.ts +6 -0
  76. package/node_modules/@oxygen/shared/dist/sequences.js +8 -0
  77. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +20 -3
  78. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  79. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  80. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +8 -2
  81. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +63 -0
  82. package/package.json +1 -1
@@ -521,7 +521,14 @@ export const S42_REVEAL_PRICE_RISES = [
521
521
  // price per hit rises, so the whole change waits for the switch. Lanes named
522
522
  // outside the default chain keep their own price, and a direct tool call keeps
523
523
  // its tool price (P-34). `pricing-resolver.ts` (@oxygen/integrations) applies
524
- // it; a test there pins the flat price to the pricing seed.
524
+ // it; a test there pins the flat price to the pricing seed. The 5 holds while
525
+ // the managed LinkedIn rail runs on Up2Data; on HarvestAPI the resolver charges
526
+ // 5x that lookup's worst case instead (Philipp 2026-10-17), and passes the
527
+ // matching text to `describeRepricingSchedule` through `descriptions`.
528
+ /** The customer-facing sentence for the flat LinkedIn URL price, at `credits`. */
529
+ export function linkedinUrlFlatPriceDescription(credits) {
530
+ return `A LinkedIn URL costs a flat ${credits} credits when one is found that matches the person, and nothing when none is found, instead of each provider's price on every attempt.`;
531
+ }
525
532
  /** Whether the LinkedIn-URL waterfall bills a flat price per URL found. */
526
533
  export const LINKEDIN_URL_FLAT_PRICE_REPRICING = defineRepricedValue({
527
534
  key: "enrichment.linkedin_url_priced_per_found_url",
@@ -529,7 +536,7 @@ export const LINKEDIN_URL_FLAT_PRICE_REPRICING = defineRepricedValue({
529
536
  slice: "S43",
530
537
  decision: "4.2",
531
538
  unit: "boolean",
532
- description: `A LinkedIn URL costs a flat ${SNAPSHOT_LINKEDIN_URL_FOUND_CREDITS} credits when one is found that matches the person, and nothing when none is found, instead of each provider's price on every attempt.`,
539
+ description: linkedinUrlFlatPriceDescription(SNAPSHOT_LINKEDIN_URL_FOUND_CREDITS),
533
540
  before: false,
534
541
  after: true,
535
542
  });
@@ -537,7 +544,12 @@ export const LINKEDIN_URL_FLAT_PRICE_REPRICING = defineRepricedValue({
537
544
  export function linkedinUrlFlatPriceInForce(options = {}) {
538
545
  return repricedValue(LINKEDIN_URL_FLAT_PRICE_REPRICING, options);
539
546
  }
540
- /** The flat price of one LinkedIn URL found, in credits (the pricing seed's `enrichment.linkedin_url_found`). */
547
+ /**
548
+ * The flat price of one LinkedIn URL found while the managed LinkedIn rail runs
549
+ * on Up2Data, in credits (the pricing seed's `enrichment.linkedin_url_found`).
550
+ * Billing reads `linkedinUrlFoundCreditsForBackend()` in pricing-resolver.ts,
551
+ * which follows the supplier switch.
552
+ */
541
553
  export function linkedinUrlFoundCredits() {
542
554
  return SNAPSHOT_LINKEDIN_URL_FOUND_CREDITS;
543
555
  }
@@ -673,7 +685,7 @@ export function describeRepricingSchedule(options = {}) {
673
685
  key: entry.key,
674
686
  kind: entry.kind,
675
687
  unit: entry.unit,
676
- description: entry.description,
688
+ description: options.descriptions?.[entry.key] ?? entry.description,
677
689
  value_in_force: inForce ? entry.after : entry.before,
678
690
  value_before: entry.before,
679
691
  value_after: entry.after,
@@ -0,0 +1,80 @@
1
+ export declare const RESEARCH_FINDING_ACTIVITY_TYPE = "research_finding";
2
+ export type ResearchFindingActivityType = typeof RESEARCH_FINDING_ACTIVITY_TYPE;
3
+ export declare const RESEARCH_FINDING_EVENT_DEFINITION: {
4
+ readonly key: "research_finding";
5
+ readonly label: "Research finding";
6
+ readonly group: "Research";
7
+ };
8
+ /** What a claim is about. Each class has its own shelf life. */
9
+ export declare const RESEARCH_FINDING_CLASSES: readonly ["role", "interest", "stated_need", "company", "other"];
10
+ export type ResearchFindingClass = (typeof RESEARCH_FINDING_CLASSES)[number];
11
+ /** Default shelf life in days when the writer gives no `expires_at`. */
12
+ export declare const RESEARCH_FINDING_TTL_DAYS: Readonly<Record<ResearchFindingClass, number>>;
13
+ export declare const RESEARCH_FINDING_SOURCE_KINDS: readonly ["message", "activity", "url"];
14
+ export type ResearchFindingSourceKind = (typeof RESEARCH_FINDING_SOURCE_KINDS)[number];
15
+ /** Longest claim a finding stores (`summary`). */
16
+ export declare const RESEARCH_FINDING_MAX_CLAIM_CHARS = 500;
17
+ export type ResearchFindingMetadata = {
18
+ source_kind: ResearchFindingSourceKind;
19
+ /** Exactly one of these three, matching `source_kind`. */
20
+ message_id?: string;
21
+ activity_id?: string;
22
+ source_url?: string;
23
+ /** 0..1. */
24
+ confidence: number;
25
+ observed_at: string;
26
+ expires_at: string;
27
+ class: ResearchFindingClass;
28
+ };
29
+ /**
30
+ * Validate a finding's claim and metadata and fill the derived fields.
31
+ * `expires_at` defaults to observed_at plus the class's shelf life. Unknown
32
+ * metadata keys are kept, so a writer can add its own provenance.
33
+ */
34
+ export declare function normalizeResearchFinding(input: {
35
+ claim: unknown;
36
+ metadata: unknown;
37
+ }, now?: Date): {
38
+ claim: string;
39
+ metadata: ResearchFindingMetadata & Record<string, unknown>;
40
+ };
41
+ /** `message:<id>`, `activity:<id>` or `url:<url>`: the one source a finding cites. */
42
+ export declare function researchFindingSourceRef(metadata: Pick<ResearchFindingMetadata, "source_kind" | "message_id" | "activity_id" | "source_url">): string;
43
+ /** The ids a model actually saw. A message or activity finding must cite one of them. */
44
+ export type ResearchFindingCitationScope = {
45
+ messageIds?: Iterable<string>;
46
+ activityIds?: Iterable<string>;
47
+ };
48
+ /**
49
+ * The citation check: keep a finding only when the message or activity it
50
+ * cites was in the model's input. URL findings carry their own source and pass.
51
+ */
52
+ export declare function partitionCitedFindings<T extends {
53
+ metadata: Pick<ResearchFindingMetadata, "source_kind" | "message_id" | "activity_id">;
54
+ }>(findings: readonly T[], scope: ResearchFindingCitationScope): {
55
+ cited: T[];
56
+ uncited: T[];
57
+ };
58
+ /** Whether a finding has passed its shelf life (the brief shows it as stale). */
59
+ export declare function researchFindingIsStale(metadata: {
60
+ expires_at?: unknown;
61
+ }, now?: Date): boolean;
62
+ /** Most facts the reply classifier keeps from one inbound message. */
63
+ export declare const MAX_STATED_FACTS_PER_REPLY = 3;
64
+ /** A fact the prospect stated in one inbound message, cited to that message. */
65
+ export type ConversationStatedFact = {
66
+ claim: string;
67
+ class: ResearchFindingClass;
68
+ confidence: number;
69
+ message_id: string;
70
+ };
71
+ /**
72
+ * Read the classifier's `stated_facts` answer: at most 3 well-formed facts,
73
+ * each citing `messageId` (the one inbound message the model was given). Every
74
+ * other entry is dropped and counted, so a fact citing a message the model
75
+ * never saw can never reach the timeline.
76
+ */
77
+ export declare function normalizeStatedFacts(raw: unknown, messageId: string): {
78
+ facts: ConversationStatedFact[];
79
+ rejected: number;
80
+ };
@@ -0,0 +1,199 @@
1
+ // Research findings (Network Mining S8b): one append-only `research_finding`
2
+ // activity per claim on a Records timeline, cited to the message, activity or
3
+ // web page it came from. Conflicting claims coexist; a finding is never edited,
4
+ // it expires. The kind is DEV-only behind `networkMiningEnabled`, so it stays out
5
+ // of CRM_ACTIVITY_EVENT_DEFINITIONS (the production writer allowlist and event
6
+ // catalog) and out of CRM_SIGNAL_ACTIVITY_TYPES: a finding is evidence, never
7
+ // intent.
8
+ import { OxygenError } from "./cli-result.js";
9
+ export const RESEARCH_FINDING_ACTIVITY_TYPE = "research_finding";
10
+ export const RESEARCH_FINDING_EVENT_DEFINITION = {
11
+ key: RESEARCH_FINDING_ACTIVITY_TYPE,
12
+ label: "Research finding",
13
+ group: "Research",
14
+ };
15
+ /** What a claim is about. Each class has its own shelf life. */
16
+ export const RESEARCH_FINDING_CLASSES = ["role", "interest", "stated_need", "company", "other"];
17
+ /** Default shelf life in days when the writer gives no `expires_at`. */
18
+ export const RESEARCH_FINDING_TTL_DAYS = {
19
+ role: 180,
20
+ interest: 90,
21
+ stated_need: 60,
22
+ company: 180,
23
+ other: 60,
24
+ };
25
+ export const RESEARCH_FINDING_SOURCE_KINDS = ["message", "activity", "url"];
26
+ /** Longest claim a finding stores (`summary`). */
27
+ export const RESEARCH_FINDING_MAX_CLAIM_CHARS = 500;
28
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29
+ const CLOCK_SKEW_MS = 5 * 60_000;
30
+ const DAY_MS = 86_400_000;
31
+ function invalid(problems) {
32
+ return new OxygenError("invalid_research_finding", `Invalid research finding: ${problems.join("; ")}.`, {
33
+ details: { problems, required: ["source_kind", "confidence", "observed_at", "class"] },
34
+ exitCode: 2,
35
+ });
36
+ }
37
+ function isoOrNull(value) {
38
+ if (typeof value !== "string" || !value.trim())
39
+ return null;
40
+ const parsed = new Date(value);
41
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
42
+ }
43
+ /**
44
+ * Validate a finding's claim and metadata and fill the derived fields.
45
+ * `expires_at` defaults to observed_at plus the class's shelf life. Unknown
46
+ * metadata keys are kept, so a writer can add its own provenance.
47
+ */
48
+ export function normalizeResearchFinding(input, now = new Date()) {
49
+ const problems = [];
50
+ const claim = typeof input.claim === "string" ? input.claim.trim() : "";
51
+ if (!claim)
52
+ problems.push("summary (the claim) is required");
53
+ else if (claim.length > RESEARCH_FINDING_MAX_CLAIM_CHARS)
54
+ problems.push(`summary is longer than ${RESEARCH_FINDING_MAX_CLAIM_CHARS} characters`);
55
+ const raw = input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata)
56
+ ? input.metadata
57
+ : {};
58
+ const sourceKind = raw.source_kind;
59
+ const refs = { message: raw.message_id, activity: raw.activity_id, url: raw.source_url };
60
+ if (!RESEARCH_FINDING_SOURCE_KINDS.includes(sourceKind)) {
61
+ problems.push(`source_kind must be one of ${RESEARCH_FINDING_SOURCE_KINDS.join(", ")}`);
62
+ }
63
+ else {
64
+ const kind = sourceKind;
65
+ const ref = refs[kind];
66
+ if (kind === "url") {
67
+ let url = null;
68
+ try {
69
+ url = typeof ref === "string" ? new URL(ref) : null;
70
+ }
71
+ catch {
72
+ url = null;
73
+ }
74
+ if (!url || (url.protocol !== "https:" && url.protocol !== "http:"))
75
+ problems.push("source_url must be an http(s) URL");
76
+ }
77
+ else if (typeof ref !== "string" || !UUID.test(ref)) {
78
+ problems.push(`${kind}_id must be the cited ${kind}'s id`);
79
+ }
80
+ for (const other of RESEARCH_FINDING_SOURCE_KINDS) {
81
+ if (other !== kind && refs[other] !== undefined && refs[other] !== null) {
82
+ problems.push(`a ${kind} finding cites only its ${kind === "url" ? "source_url" : `${kind}_id`}`);
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ const confidence = raw.confidence;
88
+ if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
89
+ problems.push("confidence must be a number from 0 to 1");
90
+ }
91
+ const findingClass = raw.class;
92
+ if (!RESEARCH_FINDING_CLASSES.includes(findingClass)) {
93
+ problems.push(`class must be one of ${RESEARCH_FINDING_CLASSES.join(", ")}`);
94
+ }
95
+ const observedAt = isoOrNull(raw.observed_at);
96
+ if (!observedAt)
97
+ problems.push("observed_at must be an ISO-8601 timestamp");
98
+ else if (observedAt.getTime() > now.getTime() + CLOCK_SKEW_MS)
99
+ problems.push("observed_at is in the future");
100
+ let expiresAt = null;
101
+ if (raw.expires_at !== undefined && raw.expires_at !== null) {
102
+ expiresAt = isoOrNull(raw.expires_at);
103
+ if (!expiresAt)
104
+ problems.push("expires_at must be an ISO-8601 timestamp");
105
+ else if (observedAt && expiresAt.getTime() <= observedAt.getTime())
106
+ problems.push("expires_at must be after observed_at");
107
+ }
108
+ if (problems.length > 0)
109
+ throw invalid(problems);
110
+ const kind = sourceKind;
111
+ const cls = findingClass;
112
+ const observed = observedAt;
113
+ const expires = expiresAt ?? new Date(observed.getTime() + RESEARCH_FINDING_TTL_DAYS[cls] * DAY_MS);
114
+ const { message_id: _m, activity_id: _a, source_url: _u, ...rest } = raw;
115
+ return {
116
+ claim,
117
+ metadata: {
118
+ ...rest,
119
+ source_kind: kind,
120
+ ...(kind === "message" ? { message_id: String(refs.message).toLowerCase() } : {}),
121
+ ...(kind === "activity" ? { activity_id: String(refs.activity).toLowerCase() } : {}),
122
+ ...(kind === "url" ? { source_url: String(refs.url) } : {}),
123
+ confidence: confidence,
124
+ observed_at: observed.toISOString(),
125
+ expires_at: expires.toISOString(),
126
+ class: cls,
127
+ },
128
+ };
129
+ }
130
+ /** `message:<id>`, `activity:<id>` or `url:<url>`: the one source a finding cites. */
131
+ export function researchFindingSourceRef(metadata) {
132
+ if (metadata.source_kind === "message")
133
+ return `message:${metadata.message_id ?? ""}`;
134
+ if (metadata.source_kind === "activity")
135
+ return `activity:${metadata.activity_id ?? ""}`;
136
+ return `url:${metadata.source_url ?? ""}`;
137
+ }
138
+ /**
139
+ * The citation check: keep a finding only when the message or activity it
140
+ * cites was in the model's input. URL findings carry their own source and pass.
141
+ */
142
+ export function partitionCitedFindings(findings, scope) {
143
+ const messages = new Set([...(scope.messageIds ?? [])].map((id) => id.toLowerCase()));
144
+ const activities = new Set([...(scope.activityIds ?? [])].map((id) => id.toLowerCase()));
145
+ const cited = [];
146
+ const uncited = [];
147
+ for (const finding of findings) {
148
+ const { source_kind: kind, message_id: messageId, activity_id: activityId } = finding.metadata;
149
+ const ok = kind === "url"
150
+ || (kind === "message" && typeof messageId === "string" && messages.has(messageId.toLowerCase()))
151
+ || (kind === "activity" && typeof activityId === "string" && activities.has(activityId.toLowerCase()));
152
+ (ok ? cited : uncited).push(finding);
153
+ }
154
+ return { cited, uncited };
155
+ }
156
+ /** Whether a finding has passed its shelf life (the brief shows it as stale). */
157
+ export function researchFindingIsStale(metadata, now = new Date()) {
158
+ const expires = isoOrNull(metadata.expires_at);
159
+ return expires !== null && expires.getTime() <= now.getTime();
160
+ }
161
+ /** Most facts the reply classifier keeps from one inbound message. */
162
+ export const MAX_STATED_FACTS_PER_REPLY = 3;
163
+ const MAX_STATED_FACT_CHARS = 280;
164
+ /**
165
+ * Read the classifier's `stated_facts` answer: at most 3 well-formed facts,
166
+ * each citing `messageId` (the one inbound message the model was given). Every
167
+ * other entry is dropped and counted, so a fact citing a message the model
168
+ * never saw can never reach the timeline.
169
+ */
170
+ export function normalizeStatedFacts(raw, messageId) {
171
+ if (!Array.isArray(raw))
172
+ return { facts: [], rejected: 0 };
173
+ const shaped = [];
174
+ let rejected = 0;
175
+ for (const entry of raw) {
176
+ const value = entry && typeof entry === "object" ? entry : {};
177
+ const claim = typeof value.claim === "string" ? value.claim.trim().replace(/\s+/g, " ") : "";
178
+ const cls = value.class;
179
+ const confidence = typeof value.confidence === "number" && Number.isFinite(value.confidence)
180
+ ? Math.min(1, Math.max(0, value.confidence))
181
+ : null;
182
+ const cited = typeof value.message_id === "string" ? value.message_id.trim() : "";
183
+ if (!claim || claim.length > MAX_STATED_FACT_CHARS || confidence === null
184
+ || !RESEARCH_FINDING_CLASSES.includes(cls)) {
185
+ rejected += 1;
186
+ continue;
187
+ }
188
+ shaped.push({
189
+ claim, class: cls, confidence, message_id: cited.toLowerCase(),
190
+ metadata: { source_kind: "message", message_id: cited },
191
+ });
192
+ }
193
+ const { cited, uncited } = partitionCitedFindings(shaped, { messageIds: [messageId] });
194
+ const kept = cited.slice(0, MAX_STATED_FACTS_PER_REPLY);
195
+ return {
196
+ facts: kept.map(({ metadata: _metadata, ...fact }) => fact),
197
+ rejected: rejected + uncited.length + (cited.length - kept.length),
198
+ };
199
+ }
@@ -185,6 +185,8 @@ export declare function isEspGatewayFamily(family: RecipientEspFamily | null | u
185
185
  * family → esp is decided, so display and matching can't drift.
186
186
  */
187
187
  export declare function matchableEspFromFamily(family: RecipientEspFamily | null | undefined): RecipientEsp | null;
188
+ /** The deterministic consumer-domain table, for readers that classify recipient domains in SQL. */
189
+ export declare function consumerRecipientDomainFamilies(): ReadonlyArray<readonly [string, RecipientEspFamily]>;
188
190
  /**
189
191
  * Map a domain's resolved MX exchange hostnames to a DISPLAY family. Returns null
190
192
  * when no host matches any known vendor — the caller records that as
@@ -417,6 +417,10 @@ const CONSUMER_DOMAIN_FAMILIES = new Map([
417
417
  ["gmx.de", "gmx"],
418
418
  ["web.de", "gmx"],
419
419
  ]);
420
+ /** The deterministic consumer-domain table, for readers that classify recipient domains in SQL. */
421
+ export function consumerRecipientDomainFamilies() {
422
+ return [...CONSUMER_DOMAIN_FAMILIES];
423
+ }
420
424
  /**
421
425
  * Normalize a recipient domain: lowercase, trim, drop a trailing FQDN dot, and if
422
426
  * a full address is passed take the part after the last '@'. Returns "" for junk.
@@ -851,6 +851,12 @@ export declare function sequenceLinkedInOwnerConflicts(steps: readonly SequenceS
851
851
  stepId: string;
852
852
  message: string;
853
853
  }[];
854
+ /**
855
+ * A comment_post step with an ai_prompt drafts its comment with a paid AI run
856
+ * at send time. That run bills the organization, not the sequence's
857
+ * max_credits reservation, so a 0-credit ceiling cannot block it.
858
+ */
859
+ export declare function sequenceStepsDraftAiComments(steps: readonly SequenceStep[]): boolean;
854
860
  export declare function sequenceStepsMissingCopy(steps: readonly SequenceStep[]): SequenceMissingCopy[];
855
861
  /**
856
862
  * Validate + normalize a raw sequence definition. Assigns stable ids to steps
@@ -571,6 +571,14 @@ function sequenceLinkedInOwnerNotice(steps) {
571
571
  message: `Saved as a draft, but it cannot launch yet: LinkedIn connection, message, InMail and withdraw steps must all name the same sender_profile_id, or none (${named.join(", ")}). LinkedIn lets one of your accounts hold a conversation with a lead. To have one person send all of them, name that person on each; to keep every lead's own person, remove it from all.`,
572
572
  };
573
573
  }
574
+ /**
575
+ * A comment_post step with an ai_prompt drafts its comment with a paid AI run
576
+ * at send time. That run bills the organization, not the sequence's
577
+ * max_credits reservation, so a 0-credit ceiling cannot block it.
578
+ */
579
+ export function sequenceStepsDraftAiComments(steps) {
580
+ return steps.some((step) => step.kind === "comment_post" && Boolean(step.ai_prompt?.trim()));
581
+ }
574
582
  export function sequenceStepsMissingCopy(steps) {
575
583
  const missing = [];
576
584
  const check = (step, field, value) => {
@@ -14,6 +14,18 @@ export function capabilityDiscoveryRecovery(query, surface) {
14
14
  // connection routes.
15
15
  export function inferUserCapabilityRoute(query) {
16
16
  const normalized = query.toLowerCase().replace(/[_-]+/g, " ");
17
+ if (/\breply oxygent\b/.test(normalized) || (/\brepl(?:y|ies)\b/.test(normalized) && /\bbatch(?:es)?\b/.test(normalized)
18
+ && /\b(?:approv(?:al|e)|voice|writing style)\b/.test(normalized))) {
19
+ const route = getCapabilityRouteMatch("messages");
20
+ if (route) {
21
+ const recommendedTools = ["oxygen_oxygents_list", "oxygen_inbox_reply_oxygent_get", "oxygen_inbox_reply_oxygent_configure", "oxygen_oxygents_create"];
22
+ const recommendedCommands = ["oxygents list", "inbox reply-oxygent get", "inbox reply-oxygent configure", "oxygents create"];
23
+ return { ...route, recommendedTools, recommendedCommands, card: { ...route.card,
24
+ posture: "mixed", gatewayTools: recommendedTools, gatewayCommands: recommendedCommands,
25
+ execution: "Dev only: select your owned Oxygent with oxygents list, or create one with oxygents create --template reply, then configure. get requires agent_slug or batch_id. Caller-written sender voice and drafts use approved inference budgets; saving makes no model call. Exact batches of 10–30 (or a smaller remainder) need human approval before real sends. Discovery starts no routine or send.",
26
+ } };
27
+ }
28
+ }
17
29
  const journeyAction = savedOnboardingJourneyAction(normalized);
18
30
  if (journeyAction) {
19
31
  const route = getCapabilityRouteMatch("agents");
@@ -216,7 +228,7 @@ function isGuidedOnboardingDiscoveryIntent(query) {
216
228
  if (!/\bonboarding\b/.test(query)
217
229
  || /\b(?:invite|members?|membership|permissions?|roles?|human|colleague|coworker|delete|destroy|wipe|paid)\b/.test(query))
218
230
  return false;
219
- if (/^(?:guided )?onboarding$/.test(query.trim()))
231
+ if (/^(?:guided )?onboarding(?: journey)?$/.test(query.trim()))
220
232
  return true;
221
233
  return /\b(?:first sourcing|daily rhythm)\b/.test(query)
222
234
  || (/\b(?:guide|guided|journey|part [12])\b/.test(query)
@@ -349,15 +361,20 @@ function oxygentsRoute(normalized) {
349
361
  const contentRead = /\b(?:read|see|view|show|transcript)\b/.test(normalized)
350
362
  || (/\bhistory\b/.test(normalized) && !historyContract);
351
363
  const contentSend = /\b(?:messages?|ask|tell|send|talk|continue|resume|handoff|delegat\w*)\b|\b(?:chat with|hand\w* off)\b/.test(normalized);
364
+ const visibilityUpdate = /\bvisibility\b/.test(normalized)
365
+ && /\b(?:confirm visibility|updat\w*|chang\w*)\b/.test(normalized);
352
366
  if (!group && privacy && ((verb === "list" && !rosterRead) || verb === "create"
353
367
  || (verb === "update" && /\bvisibility\b/.test(normalized))
368
+ || ((verb === "access" || verb === "share") && visibilityUpdate && !contentRead && !contentSend && !rosterRead)
354
369
  || (verb === "get" && !contentRead) || (verb === "message" && !contentSend))) {
355
370
  return inferCapabilityRoute(normalized);
356
371
  }
357
372
  // Sharing is read and changed as a pair: who has access, then share.
358
373
  const pair = verb === "share" ? ["access"] : verb === "access" ? ["share"] : [];
359
- // A group's id comes from its list; the roster reads follow every ask.
360
- const verbs = [...new Set([verb, ...pair, ...(group ? ["groups"] : []), "list", "get"])];
374
+ // Access/share are metadata operations. Do not suggest chat previews just
375
+ // to configure their audience. Group and content asks keep their own reads.
376
+ const metadataOnly = !group && (verb === "access" || verb === "share");
377
+ const verbs = [...new Set([verb, ...pair, ...(group ? ["groups"] : []), ...(metadataOnly ? [] : ["list", "get"])])];
361
378
  return {
362
379
  ...route,
363
380
  recommendedTools: verbs.map((name) => `oxygen_oxygents_${name}`),
@@ -1 +1 @@
1
- export declare const OXYGEN_BUILD_VERSION = "1.1014.1138";
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1014.1221";
@@ -1,2 +1,2 @@
1
1
  // GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
2
- export const OXYGEN_BUILD_VERSION = "1.1014.1138";
2
+ export const OXYGEN_BUILD_VERSION = "1.1014.1221";
@@ -1,8 +1,8 @@
1
1
  import { CRM_SIGNAL_WORKFLOW_EVENT_DEFINITIONS } from "./crm-reply-events.js";
2
- export declare const WORKSPACE_EVENT_PRODUCER_READINESS_REQUIREMENTS: readonly ["always", "notetaker_enabled"];
2
+ export declare const WORKSPACE_EVENT_PRODUCER_READINESS_REQUIREMENTS: readonly ["always", "notetaker_enabled", "blacklist_monitoring_active"];
3
3
  export type WorkspaceEventProducerReadinessRequirement = (typeof WORKSPACE_EVENT_PRODUCER_READINESS_REQUIREMENTS)[number];
4
4
  export type CrmSignalWorkflowEvent = (typeof CRM_SIGNAL_WORKFLOW_EVENT_DEFINITIONS)[number]["event"];
5
- export type WorkspaceEventProducerId = "sequencer.contact_activity" | "sequencer.enrollment_terminal" | "crm.activity_created" | `crm.${CrmSignalWorkflowEvent}` | "notetaker.meeting_completed" | "plain_support.message_received";
5
+ export type WorkspaceEventProducerId = "sequencer.contact_activity" | "sequencer.enrollment_terminal" | "crm.activity_created" | `crm.${CrmSignalWorkflowEvent}` | "notetaker.meeting_completed" | "deliverability.blacklist_listing_changed" | "plain_support.message_received";
6
6
  export type WorkspaceEventProducerDefinition = {
7
7
  /** Stable producer identity used by typed internal dispatchers. */
8
8
  id: WorkspaceEventProducerId;
@@ -50,3 +50,9 @@ export declare function findWorkspaceEventProducer(input: {
50
50
  * producer. Integration-specific subscriptions are merged in at request time.
51
51
  */
52
52
  export declare const WORKSPACE_EVENT_CATALOG: readonly WorkspaceEventCatalogEntry[];
53
+ /**
54
+ * The catalog this deployment serves: WORKSPACE_EVENT_CATALOG, plus the DEV-only
55
+ * Network Mining presets where its gate is open. A gate-closed catalog is
56
+ * exactly WORKSPACE_EVENT_CATALOG.
57
+ */
58
+ export declare function servedWorkspaceEventCatalog(env: Record<string, string | undefined>): readonly WorkspaceEventCatalogEntry[];
@@ -3,9 +3,15 @@ import { CRM_SIGNAL_WORKFLOW_EVENT_DEFINITIONS, CRM_WORKFLOW_EVENT_SOURCE, } fro
3
3
  import { SEQUENCE_CONTACT_ACTIVITY_EVENT, SEQUENCE_CRM_EVENT_DEFINITIONS, SEQUENCE_WORKFLOW_EVENT_SOURCE, } from "./sequence-crm-events.js";
4
4
  import { SEQUENCE_ENROLLMENT_TERMINAL_EVENT, SEQUENCE_TERMINAL_EVENT_SOURCE, SEQUENCE_TERMINAL_STATUSES, } from "./sequence-terminal-events.js";
5
5
  import { NOTETAKER_MEETING_COMPLETED_EVENT, NOTETAKER_MEETING_COMPLETED_PAYLOAD_FIELDS, NOTETAKER_WORKFLOW_EVENT_SOURCE, } from "./notetaker-events.js";
6
+ import { BLACKLIST_LISTING_CHANGED_EVENT, BLACKLIST_LISTING_CHANGED_PAYLOAD_FIELDS, DELIVERABILITY_WORKFLOW_EVENT_SOURCE, } from "./deliverability-blacklist.js";
7
+ import { networkMiningEnabled } from "./network-mining.js";
8
+ import { RESEARCH_FINDING_EVENT_DEFINITION } from "./research-findings.js";
6
9
  export const WORKSPACE_EVENT_PRODUCER_READINESS_REQUIREMENTS = [
7
10
  "always",
8
11
  "notetaker_enabled",
12
+ // A blacklist run in the workspace's ledger had a zone answer recently: the resolver is
13
+ // deployed and at least one domain zone is enabled (and keyed when licensed).
14
+ "blacklist_monitoring_active",
9
15
  ];
10
16
  export const WORKSPACE_EVENT_CATALOG_VERSION = 2;
11
17
  const sequenceActivityFields = [
@@ -106,6 +112,15 @@ export const WORKSPACE_EVENT_PRODUCERS = [
106
112
  readiness: "notetaker_enabled",
107
113
  payloadFields: NOTETAKER_MEETING_COMPLETED_PAYLOAD_FIELDS,
108
114
  },
115
+ {
116
+ id: "deliverability.blacklist_listing_changed",
117
+ source: DELIVERABILITY_WORKFLOW_EVENT_SOURCE,
118
+ event: BLACKLIST_LISTING_CHANGED_EVENT,
119
+ delivery: "transactional_outbox",
120
+ idempotency: "The blacklist run and zone identify the transition; the daily check commits it with the run.",
121
+ readiness: "blacklist_monitoring_active",
122
+ payloadFields: BLACKLIST_LISTING_CHANGED_PAYLOAD_FIELDS,
123
+ },
109
124
  ];
110
125
  const workspaceEventProducersById = new Map(WORKSPACE_EVENT_PRODUCERS.map((producer) => [producer.id, producer]));
111
126
  export function getWorkspaceEventProducer(producerId) {
@@ -133,6 +148,9 @@ function crmActivityDescription(key, label) {
133
148
  if (key === "meeting_booked") {
134
149
  return "A meeting-booked activity was added to a CRM record timeline, regardless of which producer logged it. Use the Sequencer preset for an outreach enrollment's own meeting outcome.";
135
150
  }
151
+ if (key === "placement_changed") {
152
+ return "Inbox placement for a sender domain at one receiver provider changed (inbox rate drop or Gmail Primary to Promotions). A workspace Signal with no CRM record; placement never pauses a sender.";
153
+ }
136
154
  return `A ${label.toLowerCase()} activity was added to a CRM timeline.`;
137
155
  }
138
156
  const sequenceActivityEntries = [
@@ -226,6 +244,30 @@ const notetakerEntries = [
226
244
  payloadFields: NOTETAKER_MEETING_COMPLETED_PAYLOAD_FIELDS,
227
245
  },
228
246
  ];
247
+ const deliverabilityEntries = [
248
+ {
249
+ id: "deliverability.blacklist_listing_changed.listed",
250
+ producerId: "deliverability.blacklist_listing_changed",
251
+ source: DELIVERABILITY_WORKFLOW_EVENT_SOURCE,
252
+ event: BLACKLIST_LISTING_CHANGED_EVENT,
253
+ group: "Deliverability",
254
+ label: "Domain blacklisted",
255
+ description: "The daily blacklist check found a sending, tracking or link domain newly listed on a DNS blacklist.",
256
+ filters: [{ path: "transition", op: "eq", value: "listed" }],
257
+ payloadFields: BLACKLIST_LISTING_CHANGED_PAYLOAD_FIELDS,
258
+ },
259
+ {
260
+ id: "deliverability.blacklist_listing_changed.delisted",
261
+ producerId: "deliverability.blacklist_listing_changed",
262
+ source: DELIVERABILITY_WORKFLOW_EVENT_SOURCE,
263
+ event: BLACKLIST_LISTING_CHANGED_EVENT,
264
+ group: "Deliverability",
265
+ label: "Domain delisted",
266
+ description: "The daily blacklist check found a previously listed domain no longer listed on a DNS blacklist.",
267
+ filters: [{ path: "transition", op: "eq", value: "delisted" }],
268
+ payloadFields: BLACKLIST_LISTING_CHANGED_PAYLOAD_FIELDS,
269
+ },
270
+ ];
229
271
  /**
230
272
  * Every app-native workspace event that has a durable or explicitly callable
231
273
  * producer. Integration-specific subscriptions are merged in at request time.
@@ -236,4 +278,25 @@ export const WORKSPACE_EVENT_CATALOG = [
236
278
  ...crmActivityEntries,
237
279
  ...crmSignalEntries,
238
280
  ...notetakerEntries,
281
+ ...deliverabilityEntries,
239
282
  ];
283
+ // Network Mining (DEV-only): a cited research finding landed on a record.
284
+ const researchFindingEntry = {
285
+ id: `crm.activity_created.${RESEARCH_FINDING_EVENT_DEFINITION.key}`,
286
+ producerId: "crm.activity_created",
287
+ source: CRM_WORKFLOW_EVENT_SOURCE,
288
+ event: CRM_ACTIVITY_CREATED_EVENT,
289
+ group: `CRM · ${RESEARCH_FINDING_EVENT_DEFINITION.group}`,
290
+ label: RESEARCH_FINDING_EVENT_DEFINITION.label,
291
+ description: "A cited research finding (a claim with its source, confidence and expiry) was added to a CRM record timeline.",
292
+ filters: [{ path: "activity_type", op: "eq", value: RESEARCH_FINDING_EVENT_DEFINITION.key }],
293
+ payloadFields: crmActivityFields,
294
+ };
295
+ /**
296
+ * The catalog this deployment serves: WORKSPACE_EVENT_CATALOG, plus the DEV-only
297
+ * Network Mining presets where its gate is open. A gate-closed catalog is
298
+ * exactly WORKSPACE_EVENT_CATALOG.
299
+ */
300
+ export function servedWorkspaceEventCatalog(env) {
301
+ return networkMiningEnabled(env) ? [...WORKSPACE_EVENT_CATALOG, researchFindingEntry] : WORKSPACE_EVENT_CATALOG;
302
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.1014.1138",
3
+ "version": "1.1014.1221",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",