@oxygen-agent/cli 1.1014.1136 → 1.1014.1219
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 +2 -0
- package/dist/command-support.d.ts +8 -0
- package/dist/command-support.js +11 -0
- package/dist/commands/api-keys.d.ts +9 -0
- package/dist/commands/api-keys.js +4 -1
- package/dist/commands/capabilities.js +4 -2
- package/dist/commands/crm.js +28 -2
- package/dist/commands/dashboards.js +2 -2
- package/dist/commands/db.js +34 -0
- package/dist/commands/find.js +1 -1
- package/dist/commands/knowledge.js +2 -0
- package/dist/commands/mailboxes.js +3 -2
- package/dist/commands/messages.js +6 -1
- package/dist/commands/senders.js +2 -0
- package/dist/commands/sequences.js +1 -1
- package/dist/commands/signals.js +11 -2
- package/dist/deliverability-evidence-commands.d.ts +4 -0
- package/dist/deliverability-evidence-commands.js +104 -0
- package/dist/diagnostic-context.d.ts +21 -0
- package/dist/diagnostic-context.js +25 -0
- package/dist/diagnostics.d.ts +1 -3
- package/dist/diagnostics.js +12 -30
- package/dist/http-client.js +1 -1
- package/dist/network-mining-host.d.ts +6 -0
- package/dist/network-mining-host.js +10 -0
- package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +1 -1
- package/node_modules/@oxygen/formula/dist/value-normalizers.js +5 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +169 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +17 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -5
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/operation-request.d.ts +92 -0
- package/node_modules/@oxygen/shared/dist/contracts/operation-request.js +36 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +3 -90
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +5 -39
- package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.js +123 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +1 -0
- package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/crm-activity-events.js +1 -0
- package/node_modules/@oxygen/shared/dist/crm-record-brief.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/crm-record-brief.js +22 -0
- package/node_modules/@oxygen/shared/dist/deliverability-blacklist.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/deliverability-blacklist.js +118 -0
- package/node_modules/@oxygen/shared/dist/deliverability-checks.d.ts +138 -0
- package/node_modules/@oxygen/shared/dist/deliverability-checks.js +124 -0
- package/node_modules/@oxygen/shared/dist/deliverability-evidence.d.ts +366 -0
- package/node_modules/@oxygen/shared/dist/deliverability-evidence.js +169 -0
- package/node_modules/@oxygen/shared/dist/deliverability-loop.d.ts +334 -0
- package/node_modules/@oxygen/shared/dist/deliverability-loop.js +312 -0
- package/node_modules/@oxygen/shared/dist/deliverability-surface.d.ts +42 -0
- package/node_modules/@oxygen/shared/dist/deliverability-surface.js +66 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +34 -4
- package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/domain-authentication.js +20 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +11 -3
- package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +30 -2
- package/node_modules/@oxygen/shared/dist/network-mining.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/network-mining.js +9 -0
- package/node_modules/@oxygen/shared/dist/received-authentication.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/received-authentication.js +452 -0
- package/node_modules/@oxygen/shared/dist/recipient-provider.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/recipient-provider.js +33 -0
- package/node_modules/@oxygen/shared/dist/reply-oxygent.d.ts +73 -0
- package/node_modules/@oxygen/shared/dist/reply-oxygent.js +31 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +10 -1
- package/node_modules/@oxygen/shared/dist/repricing.js +16 -4
- package/node_modules/@oxygen/shared/dist/research-findings.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/research-findings.js +199 -0
- package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.js +4 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +8 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +20 -3
- 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/workspace-event-catalog.d.ts +8 -2
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +63 -0
- 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:
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
//
|
|
360
|
-
|
|
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.
|
|
1
|
+
export declare const OXYGEN_BUILD_VERSION = "1.1014.1219";
|
|
@@ -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.
|
|
2
|
+
export const OXYGEN_BUILD_VERSION = "1.1014.1219";
|
|
@@ -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
|
+
}
|