@oxygen-agent/cli 1.1014.1224 → 1.1014.1341

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 (70) hide show
  1. package/README.md +1 -1
  2. package/dist/browser-login.js +4 -1
  3. package/dist/cli-output.js +14 -0
  4. package/dist/cli-values.js +1 -1
  5. package/dist/command-manifest.js +4 -1
  6. package/dist/commands/billing.js +2 -2
  7. package/dist/commands/crm.js +12 -4
  8. package/dist/commands/db.js +30 -0
  9. package/dist/commands/domains.js +1 -1
  10. package/dist/commands/feeds.js +29 -1
  11. package/dist/commands/knowledge.js +24 -1
  12. package/dist/commands/sequences.js +44 -8
  13. package/dist/deliverability-evidence-commands.js +6 -6
  14. package/dist/functions-commands.js +1 -1
  15. package/dist/help.js +1 -1
  16. package/dist/local-custom-http-column.js +1 -1
  17. package/dist/runtime.js +1 -1
  18. package/dist/session.js +1 -1
  19. package/dist/transcript.js +1 -1
  20. package/dist/util.d.ts +1 -1
  21. package/dist/util.js +1 -1
  22. package/dist/visual-commands.js +1 -1
  23. package/dist/visual-render-wait.js +1 -1
  24. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +34 -173
  25. package/node_modules/@oxygen/shared/dist/agent-sessions.js +134 -18
  26. package/node_modules/@oxygen/shared/dist/capability-discovery.js +1 -1
  27. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -0
  28. package/node_modules/@oxygen/shared/dist/conversation-lineage.d.ts +131 -0
  29. package/node_modules/@oxygen/shared/dist/conversation-lineage.js +72 -0
  30. package/node_modules/@oxygen/shared/dist/copilot-errors.d.ts +3 -3
  31. package/node_modules/@oxygen/shared/dist/copilot-errors.js +6 -3
  32. package/node_modules/@oxygen/shared/dist/crm-record-brief.d.ts +1 -1
  33. package/node_modules/@oxygen/shared/dist/crm-record-brief.js +3 -1
  34. package/node_modules/@oxygen/shared/dist/deliverability-surface.d.ts +4 -2
  35. package/node_modules/@oxygen/shared/dist/deliverability-surface.js +4 -2
  36. package/node_modules/@oxygen/shared/dist/dev-eval-context.d.ts +20 -0
  37. package/node_modules/@oxygen/shared/dist/enrollment-origin.d.ts +65 -0
  38. package/node_modules/@oxygen/shared/dist/enrollment-origin.js +92 -0
  39. package/node_modules/@oxygen/shared/dist/first-month-offer.d.ts +18 -0
  40. package/node_modules/@oxygen/shared/dist/first-month-offer.js +13 -0
  41. package/node_modules/@oxygen/shared/dist/guided-onboarding-recommendations.d.ts +54 -0
  42. package/node_modules/@oxygen/shared/dist/guided-onboarding-recommendations.js +100 -0
  43. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +6 -0
  44. package/node_modules/@oxygen/shared/dist/hosted-ai.js +9 -0
  45. package/node_modules/@oxygen/shared/dist/hubspot-engagements.d.ts +26 -0
  46. package/node_modules/@oxygen/shared/dist/hubspot-engagements.js +107 -0
  47. package/node_modules/@oxygen/shared/dist/integration-secrets.d.ts +19 -0
  48. package/node_modules/@oxygen/shared/dist/integration-secrets.js +77 -0
  49. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +15 -4
  50. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +15 -4
  51. package/node_modules/@oxygen/shared/dist/knowledge-brain-draft.d.ts +2 -0
  52. package/node_modules/@oxygen/shared/dist/knowledge-brain-draft.js +2 -0
  53. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +4 -0
  54. package/node_modules/@oxygen/shared/dist/network-mining-approval.d.ts +90 -0
  55. package/node_modules/@oxygen/shared/dist/network-mining-approval.js +225 -0
  56. package/node_modules/@oxygen/shared/dist/network-mining-signals.d.ts +25 -0
  57. package/node_modules/@oxygen/shared/dist/network-mining-signals.js +34 -0
  58. package/node_modules/@oxygen/shared/dist/reply-oxygent.d.ts +2 -0
  59. package/node_modules/@oxygen/shared/dist/reply-oxygent.js +6 -0
  60. package/node_modules/@oxygen/shared/dist/research-findings.d.ts +11 -2
  61. package/node_modules/@oxygen/shared/dist/research-findings.js +16 -3
  62. package/node_modules/@oxygen/shared/dist/sequences.d.ts +25 -4
  63. package/node_modules/@oxygen/shared/dist/sequences.js +40 -10
  64. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +18 -0
  65. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  66. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  67. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  68. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  69. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +21 -1
  70. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import type { GuidedActivationRoutine } from "./guided-activation.js";
2
+ import { type WorkspaceEventCatalogEntry } from "./workspace-event-catalog.js";
2
3
  import { type AgentSessionSchedule } from "./agent-session-schedule.js";
3
4
  export type AgentSessionStatus = "draft" | "active" | "paused" | "budget_wait" | "completed" | "archived";
4
5
  export type AgentSessionResources = {
@@ -63,8 +64,10 @@ export declare const AGENT_SESSION_ADAPTERS: Readonly<Record<string, {
63
64
  fixed?: string;
64
65
  array?: boolean;
65
66
  }>>;
66
- export declare function isAgentSessionCapability(name: string): boolean;
67
- export declare function agentSessionCapabilityDecision(config: AgentSessionConfig, name: string, args: Record<string, unknown>): "allow" | "ask" | "deny";
67
+ /** The adapters this deployment serves: the reviewed set, plus the Network Mining reads where it is open. */
68
+ export declare function agentSessionAdapters(env?: Record<string, string | undefined>): Readonly<Record<string, (typeof AGENT_SESSION_ADAPTERS)[string]>>;
69
+ export declare function isAgentSessionCapability(name: string, env?: Record<string, string | undefined>): boolean;
70
+ export declare function agentSessionCapabilityDecision(config: AgentSessionConfig, name: string, args: Record<string, unknown>, env?: Record<string, string | undefined>): "allow" | "ask" | "deny";
68
71
  export declare const EMPTY_AGENT_SESSION_RESOURCES: AgentSessionResources;
69
72
  export declare const AGENT_SESSION_EVENT_RESOURCES: {
70
73
  readonly sequence_id: "sequences";
@@ -74,8 +77,25 @@ export declare const AGENT_SESSION_EVENT_RESOURCES: {
74
77
  readonly row_id: "records";
75
78
  readonly record_id: "records";
76
79
  };
77
- export declare const AGENT_SESSION_EVENTS: import("./workspace-event-catalog.js").WorkspaceEventCatalogEntry[];
78
- export declare function agentSessionEventIsScoped(config: AgentSessionConfig, payload: Record<string, unknown>): boolean;
80
+ export declare const AGENT_SESSION_EVENTS: WorkspaceEventCatalogEntry[];
81
+ /**
82
+ * The events a session may wake on here. Where Network Mining is open, a CRM timeline activity can too, through the
83
+ * records it links (S12); elsewhere this is AGENT_SESSION_EVENTS.
84
+ */
85
+ export declare function agentSessionEvents(env?: Record<string, string | undefined>): readonly WorkspaceEventCatalogEntry[];
86
+ export declare function agentSessionEventIsScoped(config: AgentSessionConfig, payload: Record<string, unknown>, env?: Record<string, string | undefined>): boolean;
87
+ /**
88
+ * Focus learning (Network Mining S12, DEV-only): an unattended session over the warmest people in the focus rank. It
89
+ * wakes when one of them shows inbound activity and once a day, reads their brief (which carries the lineage of
90
+ * their enrollments), and may only append cited research findings and notes to them: every other write, and any
91
+ * write that would need a person's card, is refused. Its scope is the focus top-K when it was created, at most
92
+ * FOCUS_LEARNING_MAX_RECORDS people; it is created as a draft and runs only after a person activates it.
93
+ */
94
+ export declare const FOCUS_LEARNING_TEMPLATE_ID = "focus-learning";
95
+ export declare const FOCUS_LEARNING_DEFAULT_RECORDS = 10;
96
+ export declare const FOCUS_LEARNING_MAX_RECORDS = 25;
97
+ export declare const FOCUS_LEARNING_MAX_PER_RUN_CREDITS = 25;
98
+ export declare const FOCUS_LEARNING_MAX_DAILY_CREDITS = 100;
79
99
  export declare const AGENT_SESSION_TEMPLATES: readonly [{
80
100
  readonly id: "campaign-monitor";
81
101
  readonly title: "Campaign monitor";
@@ -133,173 +153,14 @@ export declare const AGENT_SESSION_TEMPLATES: readonly [{
133
153
  readonly actions: readonly ["oxygen_crm_notes_add"];
134
154
  readonly events: readonly [];
135
155
  }];
136
- export declare function agentSessionTemplates(env?: Record<string, string | undefined>): ({
137
- readonly id: "campaign-monitor";
138
- readonly title: "Campaign monitor";
139
- readonly goal: "Watch this native campaign. Compare sends, replies and meetings with earlier reviews; explain changes, propose improvements and schedule the next review.";
140
- readonly resources: readonly ["sequences"];
141
- readonly actions: readonly ["oxygen_sequences_set_status"];
142
- readonly events: readonly ["sequencer.contact_activity.any"];
143
- } | {
144
- readonly id: "lead-replenishment";
145
- readonly title: "Lead replenishment";
146
- readonly goal: "Maintain a qualified lead pool in the selected Table for this native campaign. Research gaps, deduplicate and enroll only qualified leads within the approved scope.";
147
- readonly resources: readonly ["tables", "sequences"];
148
- readonly actions: readonly ["oxygen_rows_upsert", "oxygen_sequences_enroll"];
149
- readonly events: readonly [];
150
- } | {
151
- readonly id: "reply-handling";
152
- readonly title: "Reply handling";
153
- readonly goal: "Review the selected conversations, remember prior responses, qualify intent and draft replies. Send only when explicitly authorized; hand off uncertain or sensitive replies.";
154
- readonly resources: readonly ["conversations"];
155
- readonly actions: readonly ["oxygen_inbox_set_status"];
156
- readonly events: readonly ["crm.email_reply_classified"];
157
- } | {
158
- readonly id: "deliverability";
159
- readonly title: "Deliverability";
160
- readonly goal: "Watch the selected mailboxes for sending health changes. Explain evidence and recommend or apply only approved cap and status adjustments.";
161
- readonly resources: readonly ["mailboxes"];
162
- readonly actions: readonly ["oxygen_mailboxes_set_cap", "oxygen_mailboxes_set_status"];
163
- readonly events: readonly [];
164
- } | {
165
- readonly id: "account-research";
166
- readonly title: "Account research";
167
- readonly goal: "Maintain cited account research for the selected records and knowledge pages. Track changes and recommend next actions; keep facts separate from hypotheses.";
168
- readonly resources: readonly ["records", "knowledge"];
169
- readonly actions: readonly ["oxygen_crm_notes_add", "oxygen_knowledge_page_upsert"];
170
- readonly events: readonly [];
171
- } | {
172
- readonly id: "inbound-qualification";
173
- readonly title: "Inbound qualification";
174
- readonly goal: "Qualify the selected inbound conversations and records against workspace ICP and product knowledge. Record evidence and recommendations; hand off high intent to a person.";
175
- readonly resources: readonly ["conversations", "records"];
176
- readonly actions: readonly ["oxygen_inbox_set_status", "oxygen_crm_notes_add"];
177
- readonly events: readonly ["crm.email_reply_classified"];
178
- } | {
179
- readonly id: "crm-hygiene";
180
- readonly title: "CRM hygiene";
181
- readonly goal: "Review the selected CRM objects for duplicates and missing or inconsistent fields. Propose repairs with evidence, and apply only explicitly authorized changes.";
182
- readonly resources: readonly ["objects"];
183
- readonly actions: readonly ["oxygen_crm_merge"];
184
- readonly events: readonly [];
185
- } | {
186
- readonly id: "pipeline-review";
187
- readonly title: "Pipeline review";
188
- readonly goal: "Review the selected pipeline and lifecycle stages. Remember earlier reviews, identify stalled opportunities and recommend the next action with evidence.";
189
- readonly resources: readonly ["objects", "records"];
190
- readonly actions: readonly ["oxygen_crm_notes_add"];
191
- readonly events: readonly [];
192
- } | {
193
- id: "campaign-monitor";
194
- title: "Campaign monitor";
195
- resources: readonly ["sequences"];
196
- actions: readonly ["oxygen_sequences_set_status"];
197
- events: readonly ["sequencer.contact_activity.any"];
198
- goal: string;
199
- } | {
200
- id: "lead-replenishment";
201
- title: "Lead replenishment";
202
- resources: readonly ["tables", "sequences"];
203
- actions: readonly ["oxygen_rows_upsert", "oxygen_sequences_enroll"];
204
- events: readonly [];
205
- goal: string;
206
- } | {
207
- id: "reply-handling";
208
- title: "Reply handling";
209
- resources: readonly ["conversations"];
210
- actions: readonly ["oxygen_inbox_set_status"];
211
- events: readonly ["crm.email_reply_classified"];
212
- goal: string;
213
- } | {
214
- id: "deliverability";
215
- title: "Deliverability";
216
- resources: readonly ["mailboxes"];
217
- actions: readonly ["oxygen_mailboxes_set_cap", "oxygen_mailboxes_set_status"];
218
- events: readonly [];
219
- goal: string;
220
- } | {
221
- id: "account-research";
222
- title: "Account research";
223
- resources: readonly ["records", "knowledge"];
224
- actions: readonly ["oxygen_crm_notes_add", "oxygen_knowledge_page_upsert"];
225
- events: readonly [];
226
- goal: string;
227
- } | {
228
- id: "inbound-qualification";
229
- title: "Inbound qualification";
230
- resources: readonly ["conversations", "records"];
231
- actions: readonly ["oxygen_inbox_set_status", "oxygen_crm_notes_add"];
232
- events: readonly ["crm.email_reply_classified"];
233
- goal: string;
234
- } | {
235
- id: "crm-hygiene";
236
- title: "CRM hygiene";
237
- resources: readonly ["objects"];
238
- actions: readonly ["oxygen_crm_merge"];
239
- events: readonly [];
240
- goal: string;
241
- } | {
242
- id: "pipeline-review";
243
- title: "Pipeline review";
244
- resources: readonly ["objects", "records"];
245
- actions: readonly ["oxygen_crm_notes_add"];
246
- events: readonly [];
156
+ type AgentSessionTemplate = {
157
+ id: string;
158
+ title: string;
247
159
  goal: string;
248
- })[] | readonly [{
249
- readonly id: "campaign-monitor";
250
- readonly title: "Campaign monitor";
251
- readonly goal: "Watch this native campaign. Compare sends, replies and meetings with earlier reviews; explain changes, propose improvements and schedule the next review.";
252
- readonly resources: readonly ["sequences"];
253
- readonly actions: readonly ["oxygen_sequences_set_status"];
254
- readonly events: readonly ["sequencer.contact_activity.any"];
255
- }, {
256
- readonly id: "lead-replenishment";
257
- readonly title: "Lead replenishment";
258
- readonly goal: "Maintain a qualified lead pool in the selected Table for this native campaign. Research gaps, deduplicate and enroll only qualified leads within the approved scope.";
259
- readonly resources: readonly ["tables", "sequences"];
260
- readonly actions: readonly ["oxygen_rows_upsert", "oxygen_sequences_enroll"];
261
- readonly events: readonly [];
262
- }, {
263
- readonly id: "reply-handling";
264
- readonly title: "Reply handling";
265
- readonly goal: "Review the selected conversations, remember prior responses, qualify intent and draft replies. Send only when explicitly authorized; hand off uncertain or sensitive replies.";
266
- readonly resources: readonly ["conversations"];
267
- readonly actions: readonly ["oxygen_inbox_set_status"];
268
- readonly events: readonly ["crm.email_reply_classified"];
269
- }, {
270
- readonly id: "deliverability";
271
- readonly title: "Deliverability";
272
- readonly goal: "Watch the selected mailboxes for sending health changes. Explain evidence and recommend or apply only approved cap and status adjustments.";
273
- readonly resources: readonly ["mailboxes"];
274
- readonly actions: readonly ["oxygen_mailboxes_set_cap", "oxygen_mailboxes_set_status"];
275
- readonly events: readonly [];
276
- }, {
277
- readonly id: "account-research";
278
- readonly title: "Account research";
279
- readonly goal: "Maintain cited account research for the selected records and knowledge pages. Track changes and recommend next actions; keep facts separate from hypotheses.";
280
- readonly resources: readonly ["records", "knowledge"];
281
- readonly actions: readonly ["oxygen_crm_notes_add", "oxygen_knowledge_page_upsert"];
282
- readonly events: readonly [];
283
- }, {
284
- readonly id: "inbound-qualification";
285
- readonly title: "Inbound qualification";
286
- readonly goal: "Qualify the selected inbound conversations and records against workspace ICP and product knowledge. Record evidence and recommendations; hand off high intent to a person.";
287
- readonly resources: readonly ["conversations", "records"];
288
- readonly actions: readonly ["oxygen_inbox_set_status", "oxygen_crm_notes_add"];
289
- readonly events: readonly ["crm.email_reply_classified"];
290
- }, {
291
- readonly id: "crm-hygiene";
292
- readonly title: "CRM hygiene";
293
- readonly goal: "Review the selected CRM objects for duplicates and missing or inconsistent fields. Propose repairs with evidence, and apply only explicitly authorized changes.";
294
- readonly resources: readonly ["objects"];
295
- readonly actions: readonly ["oxygen_crm_merge"];
296
- readonly events: readonly [];
297
- }, {
298
- readonly id: "pipeline-review";
299
- readonly title: "Pipeline review";
300
- readonly goal: "Review the selected pipeline and lifecycle stages. Remember earlier reviews, identify stalled opportunities and recommend the next action with evidence.";
301
- readonly resources: readonly ["objects", "records"];
302
- readonly actions: readonly ["oxygen_crm_notes_add"];
303
- readonly events: readonly [];
304
- }];
305
- export declare function validateAgentSessionConfig(config: AgentSessionConfig, requireResources?: boolean): void;
160
+ resources: readonly string[];
161
+ actions: readonly string[];
162
+ events: readonly string[];
163
+ };
164
+ export declare function agentSessionTemplates(env?: Record<string, string | undefined>): readonly AgentSessionTemplate[];
165
+ export declare function validateAgentSessionConfig(config: AgentSessionConfig, requireResources?: boolean, env?: Record<string, string | undefined>): void;
166
+ export {};
@@ -5,7 +5,8 @@ import { validateContract } from "./contracts/schema.js";
5
5
  import { GUIDED_ACTIVATION_SCAN_POLICY_CONTRACT } from "./contracts/guided-activation.js";
6
6
  import { isStaffEmail } from "./staff.js";
7
7
  import { OxygenError } from "./cli-result.js";
8
- import { WORKSPACE_EVENT_CATALOG } from "./workspace-event-catalog.js";
8
+ import { RESEARCH_FINDING_ACTIVITY_TYPE } from "./research-findings.js";
9
+ import { servedWorkspaceEventCatalog, WORKSPACE_EVENT_CATALOG } from "./workspace-event-catalog.js";
9
10
  import { agentSessionScheduleProblem } from "./agent-session-schedule.js";
10
11
  /** The routine marker of a session's settings, or null for a general session. */
11
12
  export function agentSessionTeammateRoutine(config) {
@@ -43,6 +44,9 @@ export const AGENT_SESSION_ADAPTERS = {
43
44
  oxygen_sequences_analytics: { resource: "sequences", field: "sequence", read: true },
44
45
  oxygen_sequences_contacts: { resource: "sequences", field: "sequence", read: true },
45
46
  oxygen_sequences_enrollments: { resource: "sequences", field: "sequence", read: true },
47
+ oxygen_sequences_events: { resource: "sequences", field: "sequence", read: true },
48
+ // Email messages of one campaign; one record's messages are the gated `record` scope below.
49
+ oxygen_messages_query: { resource: "sequences", field: "sequence_id", read: true },
46
50
  oxygen_sequences_enroll: { resource: "sequences", field: "sequence", read: false },
47
51
  oxygen_sequences_set_status: { resource: "sequences", field: "sequence", read: false },
48
52
  oxygen_tables_describe: { resource: "tables", field: "table", read: true },
@@ -72,17 +76,41 @@ export const AGENT_SESSION_ADAPTERS = {
72
76
  oxygen_knowledge_page_upsert: { resource: "knowledge", field: "page.slug", read: false },
73
77
  oxygen_find_company: { resource: "domains", field: "domain", read: false },
74
78
  };
79
+ /**
80
+ * Reviewed adapters served only where Network Mining is: the brief exists only there, and an activity log from a
81
+ * session may only append a cited research finding (S12).
82
+ */
83
+ const NETWORK_MINING_SESSION_ADAPTERS = {
84
+ oxygen_crm_record_brief: { resource: "records", field: "row_id", read: true },
85
+ oxygen_crm_activity_log: { resource: "records", field: "row_id", read: false },
86
+ };
87
+ const defaultEnv = () => (typeof process === "undefined" ? {} : process.env);
88
+ /** The adapters this deployment serves: the reviewed set, plus the Network Mining reads where it is open. */
89
+ export function agentSessionAdapters(env = defaultEnv()) {
90
+ return networkMiningEnabled(env) ? { ...AGENT_SESSION_ADAPTERS, ...NETWORK_MINING_SESSION_ADAPTERS } : AGENT_SESSION_ADAPTERS;
91
+ }
75
92
  const CONTEXT_READS = new Set([
76
93
  "oxygen_capabilities_search", "oxygen_capabilities_schema", "oxygen_skills_list", "oxygen_skills_get",
77
94
  "oxygen_knowledge_index", "oxygen_knowledge_search", "oxygen_workspace_get", "oxygen_enrichment_catalog",
78
95
  ]);
79
- export function isAgentSessionCapability(name) { return CONTEXT_READS.has(name) || Boolean(AGENT_SESSION_ADAPTERS[name]); }
80
- export function agentSessionCapabilityDecision(config, name, args) {
96
+ export function isAgentSessionCapability(name, env = defaultEnv()) { return CONTEXT_READS.has(name) || Boolean(agentSessionAdapters(env)[name]); }
97
+ export function agentSessionCapabilityDecision(config, name, args, env = defaultEnv()) {
98
+ const decision = sessionCapabilityDecision(config, name, args, env);
99
+ // Focus learning runs unattended on its own wakes: it never raises a card, so what it may not do it is refused.
100
+ return decision === "ask" && config.template === FOCUS_LEARNING_TEMPLATE_ID ? "deny" : decision;
101
+ }
102
+ function sessionCapabilityDecision(config, name, args, env) {
81
103
  if (CONTEXT_READS.has(name))
82
104
  return "allow";
83
- const adapter = AGENT_SESSION_ADAPTERS[name];
105
+ const adapter = agentSessionAdapters(env)[name];
84
106
  if (!adapter)
85
107
  return "deny";
108
+ // One record's messages (Network Mining): scoped by the record, never the whole inbox.
109
+ if (name === "oxygen_messages_query" && args.record !== undefined) {
110
+ if (!networkMiningEnabled(env) || typeof args.record !== "string" || !args.record || args.record.includes(","))
111
+ return "deny";
112
+ return config.resources.records.includes(args.record) ? "allow" : "ask";
113
+ }
86
114
  // No alternate aggregate scope may widen a resource-specific read.
87
115
  if (["campaigns", "senders", "account", "crm_object", "ignore_sender_bindings"].some(key => args[key] !== undefined && args[key] !== false))
88
116
  return "deny";
@@ -96,6 +124,9 @@ export function agentSessionCapabilityDecision(config, name, args) {
96
124
  return "deny";
97
125
  if (name === "oxygen_knowledge_page_get" && args.id !== undefined)
98
126
  return "deny";
127
+ // A session appends findings, never other timeline facts (calls, meetings, stage changes) about a record.
128
+ if (name === "oxygen_crm_activity_log" && args.activity_type !== RESEARCH_FINDING_ACTIVITY_TYPE)
129
+ return "deny";
99
130
  const ref = adapter.fixed ?? adapter.field.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : undefined, args);
100
131
  if (adapter.array) {
101
132
  if (!Array.isArray(ref) || !ref.length || ref.some(value => typeof value !== "string" || value.includes(",")))
@@ -118,9 +149,59 @@ export const AGENT_SESSION_EVENT_RESOURCES = {
118
149
  mailbox_id: "mailboxes", row_id: "records", record_id: "records",
119
150
  };
120
151
  export const AGENT_SESSION_EVENTS = WORKSPACE_EVENT_CATALOG.filter(event => Object.keys(AGENT_SESSION_EVENT_RESOURCES).some(key => event.payloadFields.includes(key)));
121
- export function agentSessionEventIsScoped(config, payload) {
122
- return Object.entries(AGENT_SESSION_EVENT_RESOURCES).some(([key, resource]) => typeof payload[key] === "string" && config.resources[resource].includes(payload[key]));
152
+ /** A CRM activity names its records in `links[].row_id`, not a top-level field. */
153
+ const LINKED_RECORDS_FIELD = "links";
154
+ /**
155
+ * The events a session may wake on here. Where Network Mining is open, a CRM timeline activity can too, through the
156
+ * records it links (S12); elsewhere this is AGENT_SESSION_EVENTS.
157
+ */
158
+ export function agentSessionEvents(env = defaultEnv()) {
159
+ if (!networkMiningEnabled(env))
160
+ return AGENT_SESSION_EVENTS;
161
+ return [...AGENT_SESSION_EVENTS, ...servedWorkspaceEventCatalog(env).filter(event => event.payloadFields.includes(LINKED_RECORDS_FIELD) && !AGENT_SESSION_EVENTS.includes(event))];
123
162
  }
163
+ function linkedRecordIds(payload) {
164
+ const links = payload[LINKED_RECORDS_FIELD];
165
+ return Array.isArray(links)
166
+ ? links.flatMap(link => link && typeof link === "object" && typeof link.row_id === "string" ? [link.row_id] : [])
167
+ : [];
168
+ }
169
+ export function agentSessionEventIsScoped(config, payload, env = defaultEnv()) {
170
+ return Object.entries(AGENT_SESSION_EVENT_RESOURCES).some(([key, resource]) => typeof payload[key] === "string" && config.resources[resource].includes(payload[key]))
171
+ || (networkMiningEnabled(env) && linkedRecordIds(payload).some(id => config.resources.records.includes(id)));
172
+ }
173
+ /** Whether an event can match a session's selected resources at all. */
174
+ function eventHasSelectedResource(event, resources) {
175
+ return Object.entries(AGENT_SESSION_EVENT_RESOURCES).some(([key, resource]) => event.payloadFields.includes(key) && resources[resource].length > 0)
176
+ || (event.payloadFields.includes(LINKED_RECORDS_FIELD) && resources.records.length > 0);
177
+ }
178
+ /**
179
+ * Focus learning (Network Mining S12, DEV-only): an unattended session over the warmest people in the focus rank. It
180
+ * wakes when one of them shows inbound activity and once a day, reads their brief (which carries the lineage of
181
+ * their enrollments), and may only append cited research findings and notes to them: every other write, and any
182
+ * write that would need a person's card, is refused. Its scope is the focus top-K when it was created, at most
183
+ * FOCUS_LEARNING_MAX_RECORDS people; it is created as a draft and runs only after a person activates it.
184
+ */
185
+ export const FOCUS_LEARNING_TEMPLATE_ID = "focus-learning";
186
+ export const FOCUS_LEARNING_DEFAULT_RECORDS = 10;
187
+ export const FOCUS_LEARNING_MAX_RECORDS = 25;
188
+ export const FOCUS_LEARNING_MAX_PER_RUN_CREDITS = 25;
189
+ export const FOCUS_LEARNING_MAX_DAILY_CREDITS = 100;
190
+ const DAY_MINUTES = 1440;
191
+ const FOCUS_LEARNING_ACTIONS = ["oxygen_crm_activity_log", "oxygen_crm_notes_add"];
192
+ // Inbound activity only: a finding or note the session writes itself never wakes it again.
193
+ const FOCUS_LEARNING_EVENTS = [
194
+ "website_visit", "profile_view", "post_reaction", "new_follower", "connection_accepted",
195
+ "email_reply", "linkedin_message", "meeting_booked", "post_comment", "company_follow",
196
+ ].map(kind => `crm.activity_created.${kind}`);
197
+ const FOCUS_LEARNING_TEMPLATE = {
198
+ id: FOCUS_LEARNING_TEMPLATE_ID,
199
+ title: "Focus learning",
200
+ goal: "Keep learning about the selected people, the warmest in the focus rank. On each wake, read the brief of every person a new event names (oxygen_crm_record_brief: their signals, conversations, enrollments and where they came from), or of each selected person on the daily wake. Append a research finding (oxygen_crm_activity_log with activity_type research_finding and mode live) only for a claim you can cite to this person's own message, activity or a source URL you read, with its confidence; add a note (oxygen_crm_notes_add) for what a seller should do next and why. Never change fields, merge, enroll or send, and write nothing you cannot cite.",
201
+ resources: ["records"],
202
+ actions: [...FOCUS_LEARNING_ACTIONS],
203
+ events: FOCUS_LEARNING_EVENTS,
204
+ };
124
205
  export const AGENT_SESSION_TEMPLATES = [
125
206
  { id: "campaign-monitor", title: "Campaign monitor", goal: "Watch this native campaign. Compare sends, replies and meetings with earlier reviews; explain changes, propose improvements and schedule the next review.", resources: ["sequences"], actions: ["oxygen_sequences_set_status"], events: ["sequencer.contact_activity.any"] },
126
207
  { id: "lead-replenishment", title: "Lead replenishment", goal: "Maintain a qualified lead pool in the selected Table for this native campaign. Research gaps, deduplicate and enroll only qualified leads within the approved scope.", resources: ["tables", "sequences"], actions: ["oxygen_rows_upsert", "oxygen_sequences_enroll"], events: [] },
@@ -137,18 +218,49 @@ export const AGENT_SESSION_TEMPLATES = [
137
218
  */
138
219
  const BRIEF_FIRST_TEMPLATES = new Set(["account-research", "inbound-qualification"]);
139
220
  const BRIEF_FIRST_SENTENCE = "Start each record with its brief (oxygen_crm_record_brief): one free read of its fields, signals, conversations, team connections, notes, tasks and company.";
221
+ const LINEAGE_FIRST_TEMPLATES = new Set(["reply-handling", "inbound-qualification"]);
222
+ const LINEAGE_FIRST_SENTENCE = "Read each conversation with oxygen_inbox_get: its lineage says which sequence, step and variant the reply answered, how fast it came, from which sender and where the lead came from.";
140
223
  export function agentSessionTemplates(env = typeof process === "undefined" ? {} : process.env) {
141
224
  if (!networkMiningEnabled(env))
142
225
  return AGENT_SESSION_TEMPLATES;
143
- return AGENT_SESSION_TEMPLATES.map((template) => BRIEF_FIRST_TEMPLATES.has(template.id)
144
- ? { ...template, goal: `${BRIEF_FIRST_SENTENCE} ${template.goal}` }
145
- : template);
226
+ return [...AGENT_SESSION_TEMPLATES.map((template) => {
227
+ const lead = [
228
+ ...(LINEAGE_FIRST_TEMPLATES.has(template.id) ? [LINEAGE_FIRST_SENTENCE] : []),
229
+ ...(BRIEF_FIRST_TEMPLATES.has(template.id) ? [BRIEF_FIRST_SENTENCE] : []),
230
+ ];
231
+ return lead.length > 0 ? { ...template, goal: `${lead.join(" ")} ${template.goal}` } : template;
232
+ }), {
233
+ ...FOCUS_LEARNING_TEMPLATE,
234
+ events: FOCUS_LEARNING_TEMPLATE.events.filter(id => agentSessionEvents(env).some(event => event.id === id)),
235
+ }];
146
236
  }
147
- export function validateAgentSessionConfig(config, requireResources = true) {
237
+ function focusLearningProblem(config) {
238
+ const { per_run_credits: perRun, daily_credits: daily } = config.budget;
239
+ if (perRun > FOCUS_LEARNING_MAX_PER_RUN_CREDITS || daily > FOCUS_LEARNING_MAX_DAILY_CREDITS) {
240
+ return `Focus learning runs within ${FOCUS_LEARNING_MAX_PER_RUN_CREDITS} credits a run and ${FOCUS_LEARNING_MAX_DAILY_CREDITS} in any 24 hours.`;
241
+ }
242
+ if (config.actions.some(name => !FOCUS_LEARNING_ACTIONS.includes(name)))
243
+ return "Focus learning may only append research findings and notes.";
244
+ if (config.events.some(id => !FOCUS_LEARNING_EVENTS.includes(id)))
245
+ return "Focus learning wakes only on inbound activity of its people.";
246
+ if (Object.entries(config.resources).some(([key, values]) => key !== "records" && values.length > 0))
247
+ return "Focus learning is scoped to people only.";
248
+ if (config.resources.records.length > FOCUS_LEARNING_MAX_RECORDS)
249
+ return `Focus learning covers at most ${FOCUS_LEARNING_MAX_RECORDS} people.`;
250
+ const schedule = config.schedule;
251
+ if (schedule.kind === "cron" || schedule.min_interval_minutes < DAY_MINUTES || schedule.interval_minutes < DAY_MINUTES) {
252
+ return "Focus learning wakes on its people's activity and at most once a day otherwise: use an interval of at least 1,440 minutes.";
253
+ }
254
+ if (config.teammate_routine || config.guided_activation)
255
+ return "Focus learning is its own session, not a routine.";
256
+ return null;
257
+ }
258
+ export function validateAgentSessionConfig(config, requireResources = true, env = defaultEnv()) {
148
259
  const fail = (message) => { throw new OxygenError("invalid_session_config", message); };
149
260
  if (!config.title?.trim() || !config.goal?.trim())
150
261
  fail("A title and goal are required.");
151
- if (config.template !== "custom" && !AGENT_SESSION_TEMPLATES.some(t => t.id === config.template))
262
+ const templates = agentSessionTemplates(env);
263
+ if (config.template !== "custom" && !templates.some(t => t.id === config.template))
152
264
  fail("Unknown session template.");
153
265
  const { per_run_credits: perRun, daily_credits: daily, lifetime_credits: lifetime } = config.budget;
154
266
  if (!Number.isFinite(perRun) || perRun <= 0 || perRun > 25000 || !Number.isFinite(daily) || daily < perRun)
@@ -183,20 +295,24 @@ export function validateAgentSessionConfig(config, requireResources = true) {
183
295
  }
184
296
  if (config.ends_at && !Number.isFinite(Date.parse(config.ends_at)))
185
297
  fail("Use an ISO date for the end time.");
186
- if (config.actions.some(name => !AGENT_SESSION_ADAPTERS[name] || AGENT_SESSION_ADAPTERS[name].read))
298
+ const adapters = agentSessionAdapters(env), events = agentSessionEvents(env);
299
+ if (config.actions.some(name => !adapters[name] || adapters[name].read))
187
300
  fail("Choose a reviewed native action.");
188
- if (config.events.some(id => !AGENT_SESSION_EVENTS.some(event => event.id === id)))
301
+ if (config.events.some(id => !events.some(event => event.id === id)))
189
302
  fail("Choose an event that can match the session's selected resources.");
190
303
  for (const values of Object.values(config.resources)) {
191
304
  if (!Array.isArray(values) || values.some(ref => typeof ref !== "string" || !ref.trim() || ref.includes(",") || ref === "*"))
192
305
  fail("Resources require exact native IDs or slugs.");
193
306
  }
194
- const template = AGENT_SESSION_TEMPLATES.find(t => t.id === config.template);
195
- if (requireResources && config.events.some(id => {
196
- const event = AGENT_SESSION_EVENTS.find(entry => entry.id === id);
197
- return !Object.entries(AGENT_SESSION_EVENT_RESOURCES).some(([key, resource]) => event.payloadFields.includes(key) && config.resources[resource].length);
198
- }))
307
+ if (config.template === FOCUS_LEARNING_TEMPLATE_ID) {
308
+ const problem = focusLearningProblem(config);
309
+ if (problem)
310
+ fail(problem);
311
+ }
312
+ const template = templates.find(t => t.id === config.template);
313
+ if (requireResources && config.events.some(id => !eventHasSelectedResource(events.find(entry => entry.id === id), config.resources))) {
199
314
  fail("Select a matching resource for each event before activating.");
315
+ }
200
316
  if (requireResources && template?.resources.some(key => !config.resources[key]?.length))
201
317
  fail("Select the template's required resources before activating.");
202
318
  }
@@ -1053,7 +1053,7 @@ function recordDossierReadRecommendation(query) {
1053
1053
  const RECORD_DOSSIER_FIRST_COMMANDS = ["crm search", "crm get", "crm activity timeline"];
1054
1054
  const RECORD_BRIEF_TOOL = "oxygen_crm_record_brief";
1055
1055
  const RECORD_BRIEF_COMMAND = "crm brief";
1056
- const RECORD_BRIEF_EXECUTION_PREFIX = `Start with ${RECORD_BRIEF_COMMAND} (${RECORD_BRIEF_TOOL}): one free read of everything stored about the record, with a cursor to each section's rest. `;
1056
+ const RECORD_BRIEF_EXECUTION_PREFIX = `Start with ${RECORD_BRIEF_COMMAND} (${RECORD_BRIEF_TOOL}): one free read of everything stored about the record. It takes the row id, an identity or the exact name, so no search comes first, and each cut section names the read for its rest; use the reads below only for those. `;
1057
1057
  /**
1058
1058
  * Put the Network Mining record brief first on a record-dossier route where the
1059
1059
  * host serves it, and take it off where it does not. The shared router decides
@@ -135,6 +135,7 @@ export const CUSTOMER_OPERATIONS = [
135
135
  operation("tools.check_access", "/api/cli/tools/check-access", "POST", "tools check-access", "oxygen_tools_check_access", "enrichment", "Read tool access for this workspace; verify_credential explicitly opts into a provider credential probe.", { tool_id: text(), connection_id: text(), credential_mode: enumOf(["managed", "user_api_key"]), input: json, verify_credential: bool() }, ["tool_id"], { ...cliOnly, mutates: false }),
136
136
  operation("linkedin.connections.import_status", "/api/cli/linkedin/connections/import", "GET", "linkedin connections import-status", "oxygen_linkedin_connections_import_status", "sequences", "Read the durable LinkedIn connections import status and target Table.", { account: text() }),
137
137
  operation("linkedin.followers.import_status", "/api/cli/linkedin/followers/import", "GET", "linkedin followers import-status", "oxygen_linkedin_followers_import_status", "sequences", "Read the durable LinkedIn followers import status and target Table.", { account: text() }),
138
+ operation("inbox.import", "/api/cli/inbox/import", "POST", "inbox import", "oxygen_inbox_import", "inbox", "Import your own Instantly or Smartlead cold-email history into Messages (record timelines, messages query; not the Unibox). Runs on your connection; each read has the own-key fee. Never stops sequences or changes DNC. Resumable; dry_run reads one page.", { provider: enumOf(["instantly", "smartlead"]), connection_id: text(), campaign_id: text(), since: text("ISO timestamp; older mail is skipped."), import_id: text("Continue this import."), max_pages: int({ minimum: 0, maximum: 10 }), dry_run: bool() }, ["provider"], { spends: true, annotations: { openWorldHint: true } }),
138
139
  operation("linkedin.inbox.backfill_status", "/api/cli/linkedin/inbox/backfill", "GET", "linkedin inbox backfill-status", "oxygen_linkedin_inbox_backfill_status", "inbox", "Read the durable LinkedIn inbox backfill status.", { account: text() }),
139
140
  operation("linkedin.viewers.list", "/api/cli/linkedin/viewers", "GET", "linkedin viewers list", "oxygen_profile_viewers", "publishing", "Read named LinkedIn profile viewers, respecting the connected account's privacy ceiling.", { account: text(), start: int({ minimum: 0 }), include_raw: bool() }, [], cliOnly),
140
141
  operation("signals.search.filters", "/api/cli/signals/search/preview", "GET", "signals search filters", "oxygen_signals_search_preview", "signals", "Read the LinkedIn post-search filter catalog and credential status; runs no search.", { source: enumOf(["linkedin_posts"]) }, [], cliOnly),
@@ -0,0 +1,131 @@
1
+ export declare const CONVERSATION_LINEAGE_SCHEMA_VERSION = 1;
2
+ export declare const CONVERSATION_LINEAGE_MAX_CHARS = 2500;
3
+ export declare const LINEAGE_DIGEST_MAX_CHARS = 600;
4
+ export type LineageCoverage = "complete" | "partial" | "unknown" | "empty";
5
+ /**
6
+ * Where the lead came from, as the enrollment recorded it (migration 0541): the
7
+ * kind of choice, the surface and actor type it came through, the table and the
8
+ * run behind it, and the focus decile at enroll time where Network Mining took
9
+ * one. An enrollment from before 0541 holds none and reads `unknown`.
10
+ */
11
+ export type LineageOrigin = {
12
+ kind: "unknown";
13
+ } | {
14
+ kind: string;
15
+ gateway: string;
16
+ enrolled_by: string;
17
+ table_id: string | null;
18
+ ref: {
19
+ type: string;
20
+ id: string;
21
+ } | null;
22
+ focus?: {
23
+ in_focus: boolean;
24
+ rank: number | null;
25
+ decile: number | null;
26
+ } | null;
27
+ };
28
+ export type LineageEnrollment = {
29
+ enrollment_id: string;
30
+ sequence: {
31
+ id: string;
32
+ name: string | null;
33
+ };
34
+ status: string | null;
35
+ enrolled_at: string | null;
36
+ /** How this conversation was tied to the enrollment. */
37
+ link: "stored" | "chat_id" | "lead_provider_id";
38
+ origin: LineageOrigin;
39
+ touches_before_reply: {
40
+ total: number;
41
+ by_kind: Record<string, number>;
42
+ channels: string[];
43
+ } | null;
44
+ reply: {
45
+ occurred_at: string;
46
+ channel: string;
47
+ answered: {
48
+ action_id: string;
49
+ step_index: number | null;
50
+ variant_id: string | null;
51
+ action_kind: string | null;
52
+ sent_at: string | null;
53
+ } | null;
54
+ latency_seconds: number | null;
55
+ time_to_first_reply_seconds: number | null;
56
+ attribution: "thread" | "latest_send_before_reply" | "unattributed";
57
+ source: string;
58
+ } | null;
59
+ sender: {
60
+ account_id: string | null;
61
+ mailbox_id: string | null;
62
+ label: string | null;
63
+ } | null;
64
+ /** Which reviewed copy produced the send. `null` until sends record it (LN3). */
65
+ copy: null;
66
+ };
67
+ export type ConversationLineage = {
68
+ schema_version: typeof CONVERSATION_LINEAGE_SCHEMA_VERSION;
69
+ conversation: {
70
+ id: string;
71
+ channel: string;
72
+ started_by: "us" | "them" | null;
73
+ first_outbound_at: string | null;
74
+ first_inbound_at: string | null;
75
+ sender: {
76
+ account_id: string | null;
77
+ mailbox_id: string | null;
78
+ label: string | null;
79
+ sender_profile_id: string | null;
80
+ };
81
+ other_senders: {
82
+ account_id: string;
83
+ label: string | null;
84
+ }[];
85
+ };
86
+ record: {
87
+ object: string;
88
+ row_id: string;
89
+ tier: "crm" | "audience";
90
+ match: "identity";
91
+ matched_on: string;
92
+ } | {
93
+ match: "none";
94
+ };
95
+ enrollments: LineageEnrollment[];
96
+ turns: {
97
+ inbound_count: number;
98
+ latest_inbound_at: string | null;
99
+ response_gaps: {
100
+ inbound_message_id: string;
101
+ after_message_id: string;
102
+ seconds: number;
103
+ }[];
104
+ };
105
+ /** Newest first; the first entry is the current status. `message_id` is the newest inbound when it changed. */
106
+ classification_history: {
107
+ status: string;
108
+ source: string;
109
+ at: string | null;
110
+ message_id: string | null;
111
+ confidence: number | null;
112
+ }[];
113
+ other_conversations: {
114
+ id: string;
115
+ channel: string;
116
+ last_at: string | null;
117
+ status: string | null;
118
+ }[];
119
+ coverage: Record<"enrollments" | "record" | "origin" | "copy" | "classification_history" | "other_conversations", {
120
+ state: LineageCoverage;
121
+ reason?: string;
122
+ }>;
123
+ };
124
+ /** "a one-off send via cli, focus decile 2"; `unknown` when the enrollment recorded none. */
125
+ export declare function originText(origin: LineageOrigin): string;
126
+ /**
127
+ * One short, plain-text line set an agent reads beside the thread (≤600 chars):
128
+ * which sequence, step and variant the reply answered, how fast, from which
129
+ * sender, where the lead came from, and what is unknown.
130
+ */
131
+ export declare function renderLineageDigest(lineage: ConversationLineage | null | undefined): string | null;