@oxygen-agent/cli 1.1014.1087 → 1.1014.1132
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/commands/columns.js +1 -1
- package/dist/commands/viewers.js +1 -2
- package/dist/workspace-placement-commands.js +12 -2
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +6 -1
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +17 -5
- package/node_modules/@oxygen/shared/dist/agents-enabled.d.ts +8 -9
- package/node_modules/@oxygen/shared/dist/agents-enabled.js +9 -8
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +1 -1
- package/node_modules/@oxygen/shared/dist/column-autofill.js +2 -2
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +2 -1
- package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +32 -2
- package/node_modules/@oxygen/shared/dist/data-suppliers.js +44 -4
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +3 -4
- package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -4
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +108 -118
- package/node_modules/@oxygen/shared/dist/knowledge-links.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/knowledge-links.js +84 -1
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -14
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +22 -5
- package/node_modules/@oxygen/shared/dist/managed-scraper-backend.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/managed-scraper-backend.js +17 -0
- package/node_modules/@oxygen/shared/dist/teammates.d.ts +7 -5
- package/node_modules/@oxygen/shared/dist/teammates.js +11 -20
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +12 -2
- 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-placement-view.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/workspace-placement-view.js +5 -4
- package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +16 -3
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/commands/columns.js
CHANGED
|
@@ -830,7 +830,7 @@ export function registerColumnsFamily(program, handleAsyncAction) {
|
|
|
830
830
|
.addCommand(new Command("add")
|
|
831
831
|
.description("Add a nullable column, a catalog template (--prompt-key) or a multi-column enrichment bundle (--preset) to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost. For a page, posting or filing the row already links to, or a company fact Oxygen already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, a LinkedIn or Crunchbase page), pick a template from `columns catalog` (--prompt-key) instead of writing a prompt; or read a URL column with --kind research --research-url. The guide is the oxygen-tables skill (`oxygen skills get oxygen-tables --json`). For ICP fit, use Classify Score: `columns add <table> --label 'ICP fit' --score 'low,medium,high' --prompt 'Assess {{company_name}}: {{description}}, {{employees}} employees' --grounding workspace` (0.075 credits/row). A formula column needs no run at all: it evaluates on read from its current inputs, so `tables query` shows its values immediately. A plain field is --type: text, number, checkbox, date, json, markdown, image, or a choice column — select, multi_select or status, with --options \"Lead,Contacted,Won\". To link another table row to row, use `oxygen tables relate <table> --to <other-table> --live`; to read a value out of one, --lookup-table.")
|
|
832
832
|
.argument("<table>", "Table id or slug.")
|
|
833
|
-
.option("--preset <preset>", "Add a pre-built column bundle instead of one column, which is what --prompt-key adds: a preset writes SEVERAL columns at once and needs no --input on a table whose identity column it can find, while --prompt-key adds one template column and binds each input you name. Presets: `person_enrich` (one LinkedIn profile lookup — Blitz first, Professional Network Data
|
|
833
|
+
.option("--preset <preset>", "Add a pre-built column bundle instead of one column, which is what --prompt-key adds: a preset writes SEVERAL columns at once and needs no --input on a table whose identity column it can find, while --prompt-key adds one template column and binds each input you name. Presets: `person_enrich` (one LinkedIn profile lookup — Blitz first, Professional Network Data only when Blitz has no match — then first/last/full name, current company and its LinkedIn URL, job title, start date, education, school, headline, bio, location and followers as free stored fields that fill when the lookup runs, followers only on rows Professional Network Data answers because Blitz publishes none; the person templates in `columns catalog --category people` all read its payload column), `company_enrich` (one company lookup from a domain or LinkedIn URL — Blitz first, Professional Network Data only when Blitz has no match, about 1 credit a company — keeping the whole profile in its JSON column plus name, domain, LinkedIn URL, headcount, industry, description, founded year and HQ country as free stored fields; no field choices; binds to the person preset's current_company_linkedin_url automatically), `company_tech_stack` (the technologies a company runs, from its website and hiring), `company_funding` (total raised, latest round, number of rounds and investors), `company_revenue` (an annual revenue estimate), `company_hiring` (the roles a company is hiring for, with a count) — each of these four is one paid lookup column that fills its own columns, priced per row in `tools search --for-table` — plus `company_lookalikes` (one managed lookup from --input company_domain=website; keeps similar companies separate from the seed; dry-run for price) — or `domain_check` (0.1 credits/row at list price for a live/redirect/parked/not_found/dead verdict on every domain, cached 30 days, plus a free label column to filter on). For a single cleaned or classified column, use a template from `columns catalog` instead — that is one column, not a bundle. Name the input column with --input: `url` for person_enrich, `domain` and/or `linkedin_url` for the company presets. Any column works whatever it is called; without --input Oxygen guesses from column names. Creating the columns is free; run them afterwards, --dry-run first.")
|
|
834
834
|
.option("--input <slot=column...>", "Bind a preset or template input to an exact column, e.g. --input url=pricing_url with --prompt-key, or --input domain=website --input linkedin_url=company_li with --preset. Repeatable. For a preset it is only needed when the automatic match is wrong or missing; for a template it names the column each declared input reads.", collectRepeatable, [])
|
|
835
835
|
// No preset takes a field list any more (founder, 2026-09-24). Hidden,
|
|
836
836
|
// not removed: an old script passing them gets the server's refusal,
|
package/dist/commands/viewers.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { requestOxygen } from "../http-client.js";
|
|
2
2
|
import { readOption } from "../util.js";
|
|
3
|
-
import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
|
|
4
3
|
import { Command } from "commander";
|
|
5
4
|
export function registerViewersFamily(program, handleAsyncAction) {
|
|
6
5
|
program.addCommand(new Command("viewers")
|
|
@@ -54,7 +53,7 @@ export function registerViewersFamily(program, handleAsyncAction) {
|
|
|
54
53
|
});
|
|
55
54
|
}))
|
|
56
55
|
.addCommand(new Command("enrichment")
|
|
57
|
-
.description("Turn automatic full-profile enrichment of NEW profile viewers on or off for the viewers table (
|
|
56
|
+
.description("Turn automatic full-profile enrichment of NEW profile viewers on or off for the viewers table (managed LinkedIn profile: about, current role and company, experience, education, location). This is STANDING credit spend, charged credits_per_person credits per enriched viewer, applied only to viewers first seen after `enrichment.enabled_at`, each delivery bounded by `enrichment.max_credits_per_delivery` (the plan default). Switching on adds the table's Person profile columns (a paid lookup plus its extractors); they stay, with their data, when switched off. Existing rows are never charged by it — enrich them on demand by running the Person profile column. `viewers status` shows `enrichment` (on, credits_per_person, enabled_at, max_credits_per_delivery, next_capture_at: when new viewers can next arrive); the runs appear on the table's Runs page. Requires exactly one of --on or --off.")
|
|
58
57
|
.option("--on", "Switch standing enrichment on for new viewers.")
|
|
59
58
|
.option("--off", "Switch standing enrichment off.")
|
|
60
59
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -4,7 +4,8 @@ import { readJsonFileValue } from "./command-support.js";
|
|
|
4
4
|
import { requestOxygen } from "./http-client.js";
|
|
5
5
|
export function registerWorkspacePlacementCommands(root, handle) {
|
|
6
6
|
root.addCommand(new Command("workspace-test").description("Compare email copy placement in this workspace's connected Google/Microsoft receiving inboxes: Gmail Primary/Promotions and Microsoft Focused/Other. Native sends, 0 additional credits; preview and exact message approval required.")
|
|
7
|
-
.addCommand(new Command("run").description("Preview
|
|
7
|
+
.addCommand(new Command("run").description("Preview an internal or cross-workspace copy test. Input JSON: sender_mailbox_ids, receiver_mailbox_ids, variants [{key,subject,body_text,body_html?}]. Approve the same input with returned request_id/plan_hash/rendered_at and max_live_sends. Up to five variants and 50 messages; never buys provider tests or resumes campaigns.")
|
|
8
|
+
.option("--receiver-scope <scope>", "workspace or network; network privately selects participating receivers.").option("--receiver-providers <providers>", "Comma-separated google,microsoft for network tests.")
|
|
8
9
|
.requiredOption("--input <file>", "JSON test input file.").option("--approved", "Execute the exact reviewed matrix.")
|
|
9
10
|
.option("--rendered-at <timestamp>", "Rendering timestamp returned by preview; reuse when approving.").option("--tracking-opens", "Preview with open tracking on verified workspace domain.").option("--tracking-clicks", "Preview with click tracking on verified workspace domain.")
|
|
10
11
|
.option("--request-id <id>", "Request UUID from preview.").option("--plan <hash>", "Exact preview plan hash.").option("--max-live-sends <n>", "Maximum approved diagnostic emails (1–50).")
|
|
@@ -18,9 +19,18 @@ export function registerWorkspacePlacementCommands(root, handle) {
|
|
|
18
19
|
return requestOxygen("/api/cli/deliverability/workspace-tests", { method: "POST", body: { ...input, approved: options.approved === true,
|
|
19
20
|
...(options.approved ? { approved: true } : {}), ...(options.requestId ? { request_id: options.requestId } : {}),
|
|
20
21
|
...(options.plan ? { plan_hash: options.plan } : {}), ...(options.renderedAt ? { rendered_at: options.renderedAt } : {}), ...(options.trackingOpens ? { tracking_opens: true } : {}), ...(options.trackingClicks ? { tracking_clicks: true } : {}), ...(options.maxLiveSends ? { max_live_sends: Number(options.maxLiveSends) } : {}),
|
|
21
|
-
...(options.allowPausedProbe ? { allow_paused_probe: true } : {}),
|
|
22
|
+
...(options.allowPausedProbe ? { allow_paused_probe: true } : {}), ...(options.receiverScope ? { receiver_scope: options.receiverScope } : {}), ...(options.receiverProviders ? { receiver_providers: String(options.receiverProviders).split(",") } : {}),
|
|
22
23
|
...(options.previewCursor ? { preview_cursor: options.previewCursor } : {}), ...(options.previewLimit !== undefined ? { preview_limit: Number(options.previewLimit) } : {}),
|
|
23
24
|
...(options.previewProbeId ? { preview_probe_id: options.previewProbeId } : {}), ...(options.previewPart ? { preview_part: options.previewPart } : {}), ...(options.previewOffset !== undefined ? { preview_offset: Number(options.previewOffset) } : {}), ...(options.previewCopyLimit !== undefined ? { preview_copy_limit: Number(options.previewCopyLimit) } : {}) } });
|
|
25
|
+
})))
|
|
26
|
+
.addCommand(new Command("network").description("Read or manage default-on receiving-pool participation. Administrators can opt out or accept the current disclosure. Does not send mail.")
|
|
27
|
+
.option("--enabled <boolean>", "true or false; omit to read.").option("--accept-disclosure", "Accept real-copy delivery to participating mailboxes and metadata processing.").option("--json", "Print JSON.")
|
|
28
|
+
.action(async (options) => handle("deliverability workspace-test network", options, () => {
|
|
29
|
+
if (options.enabled === undefined)
|
|
30
|
+
return requestOxygen("/api/cli/deliverability/workspace-tests?action=network");
|
|
31
|
+
if (options.enabled !== "true" && options.enabled !== "false")
|
|
32
|
+
throw new Error("--enabled must be true or false.");
|
|
33
|
+
return requestOxygen("/api/cli/deliverability/workspace-tests", { method: "POST", body: { operation: "participation", network_enabled: options.enabled === "true", accept_network_disclosure: options.acceptDisclosure === true } });
|
|
24
34
|
})))
|
|
25
35
|
.addCommand(new Command("copy").description("Read a Sequence email step and its copy variants for workspace placement testing. Resolve personalization before preview; no send or charge.").argument("<sequence-id>").option("--step-index <n>", "Zero-based email step index.", "0").option("--json", "Print JSON.")
|
|
26
36
|
.action(async (id, options) => handle("deliverability workspace-test copy", options, () => requestOxygen(`/api/cli/deliverability/workspace-tests/copy?sequence_id=${encodeURIComponent(id)}&step_index=${encodeURIComponent(options.stepIndex)}`))))
|
|
@@ -40,7 +40,12 @@ export type AgentSessionTeammateRoutine = {
|
|
|
40
40
|
};
|
|
41
41
|
/** The routine marker of a session's settings, or null for a general session. */
|
|
42
42
|
export declare function agentSessionTeammateRoutine(config: Pick<AgentSessionConfig, "teammate_routine">): AgentSessionTeammateRoutine | null;
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Who has Ongoing Agent sessions, and so Oxygent routines: "staff" for staff
|
|
45
|
+
* emails, "all" for every member who is not a client login (the customer
|
|
46
|
+
* launch, Tim, 2026-10-17), in a workspace Agents run in (`agentsEnabledIn`).
|
|
47
|
+
*/
|
|
48
|
+
export declare function agentSessionsAudience(env?: Record<string, string | undefined>): "off" | "staff" | "all";
|
|
44
49
|
export declare function agentSessionsEnabled(env?: Record<string, string | undefined>): boolean;
|
|
45
50
|
/** Whether this workspace has Ongoing Agent sessions, and so Oxygent routines. */
|
|
46
51
|
export declare function agentSessionsEnabledIn(organizationId: string | null | undefined, env?: Record<string, string | undefined>): boolean;
|
|
@@ -11,18 +11,30 @@ export function agentSessionTeammateRoutine(config) {
|
|
|
11
11
|
const marker = config.teammate_routine;
|
|
12
12
|
return marker && typeof marker.chat_id === "string" ? marker : null;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Who has Ongoing Agent sessions, and so Oxygent routines: "staff" for staff
|
|
16
|
+
* emails, "all" for every member who is not a client login (the customer
|
|
17
|
+
* launch, Tim, 2026-10-17), in a workspace Agents run in (`agentsEnabledIn`).
|
|
18
|
+
*/
|
|
19
|
+
export function agentSessionsAudience(env = process.env) {
|
|
20
|
+
if (env.OXYGEN_DEPLOY_ENV !== "selfhost-dev" && env.OXYGEN_DEPLOY_ENV !== "selfhost-prod")
|
|
21
|
+
return "off";
|
|
22
|
+
const flag = env.OXYGEN_AGENT_SESSIONS_ENABLED;
|
|
23
|
+
if ((flag !== "staff" && flag !== "all") || !agentsEnabled(env))
|
|
24
|
+
return "off";
|
|
25
|
+
return flag;
|
|
26
|
+
}
|
|
15
27
|
export function agentSessionsEnabled(env = process.env) {
|
|
16
|
-
return (env
|
|
17
|
-
&& env.OXYGEN_AGENT_SESSIONS_ENABLED === "staff" && agentsEnabled(env);
|
|
28
|
+
return agentSessionsAudience(env) !== "off";
|
|
18
29
|
}
|
|
19
30
|
/** Whether this workspace has Ongoing Agent sessions, and so Oxygent routines. */
|
|
20
31
|
export function agentSessionsEnabledIn(organizationId, env = process.env) {
|
|
21
32
|
return agentSessionsEnabled(env) && agentsEnabledIn(organizationId, env);
|
|
22
33
|
}
|
|
23
34
|
export function agentSessionsAvailableTo(env, viewer) {
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
if (!agentSessionsEnabledIn(viewer.organizationId, env) || !viewer.member || viewer.oxygenRole === "client")
|
|
36
|
+
return false;
|
|
37
|
+
return agentSessionsAudience(env) === "all" || guidedActivationEnabled(env) || isStaffEmail(viewer.email, env);
|
|
26
38
|
}
|
|
27
39
|
/** Reviewed native adapters. Unknown tools (including generic dispatch and agent management) fail closed. */
|
|
28
40
|
export const AGENT_SESSION_ADAPTERS = {
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* execution, which carries tool authority and credit spend (feature-gates.ts),
|
|
4
4
|
* and Oxygents and their routines need it too (ADR 0032).
|
|
5
5
|
*
|
|
6
|
-
* - "true" opens every workspace on dev and in tests
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* - "true" opens every workspace: on dev and in tests, and in production since
|
|
7
|
+
* the customer launch (Tim, 2026-10-17: "all customers real launch").
|
|
8
|
+
* - "staff" opens only the workspaces listed in OXYGEN_AGENTS_WORKSPACE_IDS; on
|
|
9
|
+
* production they must also be in PRODUCTION_AGENT_WORKSPACE_IDS below, so the
|
|
10
|
+
* staff stage can be narrowed but never widened by a Doppler edit.
|
|
11
11
|
* - Shadow never opens, a deployment name nobody knows never opens every
|
|
12
12
|
* workspace, and anything else is off, as is an empty list where one is needed.
|
|
13
13
|
*
|
|
@@ -16,10 +16,9 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export type AgentsWorkspaceScope = "off" | "all" | ReadonlySet<string>;
|
|
18
18
|
/**
|
|
19
|
-
* The production workspaces
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* recorded in OXYGEN_PRODUCT.md and a reviewed change here.
|
|
19
|
+
* The production workspaces the "staff" stage may open: staff workspaces only
|
|
20
|
+
* (Tim, 2026-10-16). Production opens every workspace only on "true", the
|
|
21
|
+
* customer launch.
|
|
23
22
|
*/
|
|
24
23
|
export declare const PRODUCTION_AGENT_WORKSPACE_IDS: ReadonlySet<string>;
|
|
25
24
|
export declare function agentsWorkspaceScope(env?: Record<string, string | undefined>): AgentsWorkspaceScope;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { isProductionData, resolveDeployEnv } from "./deploy-env.js";
|
|
2
2
|
/**
|
|
3
|
-
* The production workspaces
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* recorded in OXYGEN_PRODUCT.md and a reviewed change here.
|
|
3
|
+
* The production workspaces the "staff" stage may open: staff workspaces only
|
|
4
|
+
* (Tim, 2026-10-16). Production opens every workspace only on "true", the
|
|
5
|
+
* customer launch.
|
|
7
6
|
*/
|
|
8
7
|
export const PRODUCTION_AGENT_WORKSPACE_IDS = new Set([
|
|
9
8
|
"4257983c-406e-4658-b899-d7ba3cc760a6", // OXYGEN
|
|
@@ -16,12 +15,13 @@ export function agentsWorkspaceScope(env = process.env) {
|
|
|
16
15
|
const tier = deploymentTier(env);
|
|
17
16
|
if (tier === "shadow")
|
|
18
17
|
return "off";
|
|
19
|
-
if (flag === "true" && tier === "other")
|
|
18
|
+
if (flag === "true" && (tier === "other" || tier === "prod"))
|
|
20
19
|
return "all";
|
|
20
|
+
const reviewedOnly = tier === "prod" || tier === "production-data";
|
|
21
21
|
const listed = (env.OXYGEN_AGENTS_WORKSPACE_IDS ?? "")
|
|
22
22
|
.split(",")
|
|
23
23
|
.map((id) => id.trim().toLowerCase())
|
|
24
|
-
.filter((id) => id.length > 0 && (
|
|
24
|
+
.filter((id) => id.length > 0 && (!reviewedOnly || PRODUCTION_AGENT_WORKSPACE_IDS.has(id)));
|
|
25
25
|
return listed.length > 0 ? new Set(listed) : "off";
|
|
26
26
|
}
|
|
27
27
|
/** Whether Agents run in any workspace here: what a worker loop or family checks before it looks at one. */
|
|
@@ -42,8 +42,9 @@ function deploymentTier(env) {
|
|
|
42
42
|
const { tier } = resolveDeployEnv(env);
|
|
43
43
|
if (tier === "prod" || tier === "shadow")
|
|
44
44
|
return tier;
|
|
45
|
-
// Managed dev telemetry can still carry production data via NODE_ENV
|
|
46
|
-
|
|
45
|
+
// Managed dev telemetry can still carry production data via NODE_ENV: it
|
|
46
|
+
// keeps the reviewed staff intersection and never opens every workspace.
|
|
47
|
+
return isProductionData(env) ? "production-data" : "other";
|
|
47
48
|
}
|
|
48
49
|
catch {
|
|
49
50
|
return "unknown";
|
|
@@ -423,7 +423,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
423
423
|
primitive: "agents",
|
|
424
424
|
owns: "Adaptive governed worker definitions, versions, authorization, context, triggers, threads, checkpoints, inference, and first-class runs.",
|
|
425
425
|
notFor: "A known deterministic graph that belongs in Workflows or an opaque uploaded runtime.",
|
|
426
|
-
execution: "Publish and run a governed Agent with explicit tool authority and bounded spend.
|
|
426
|
+
execution: "Publish and run a governed Agent with explicit tool authority and bounded spend. Members can create ongoing sessions: draft, exact resource scope, per-run and rolling 24-hour caps, preview/activate, continuing conversation and bounded self-scheduling. Inspect agent session templates; pause/resume preserves history. Where admitted, Oxygents (AI teammates) default to Workspace: one ongoing chat shared by everyone with access. Choose visibility personal when creating one for creator-only access; an API key acts as its issuing person. Home stays Workspace. Owners change visibility with confirm_visibility: publishing shares all existing history; Personal keeps history but removes others' future access and cannot retract what they saw. Collaborators use the shared chat; managers also change its profile and access. everyone off restricts Workspace access to its owner and named people, with workspace administrators retaining management access; it does not make it Personal. Group chats hold up to six Oxygents and optional teammates; messages go to the Oxygents mentioned or addressed.",
|
|
427
427
|
posture: "mixed",
|
|
428
428
|
gatewayTools: ["oxygen_agent_list", "oxygen_agent_create", "oxygen_agent_run"],
|
|
429
429
|
gatewayCommands: ["agent list", "agent create", "agent run"],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { professionalNetworkDataLabel } from "./data-suppliers.js";
|
|
2
2
|
function readString(definition, key) {
|
|
3
3
|
const value = definition?.[key];
|
|
4
4
|
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
@@ -20,7 +20,7 @@ function enrichmentLabel(column) {
|
|
|
20
20
|
// data-extraction labels say "Professional Network"). The label comes from
|
|
21
21
|
// one owner so it cannot drift from the picker and the Connections card.
|
|
22
22
|
if (toolId?.startsWith("scraper.linkedin"))
|
|
23
|
-
return `Enriched from ${
|
|
23
|
+
return `Enriched from ${professionalNetworkDataLabel()}`;
|
|
24
24
|
if (toolId)
|
|
25
25
|
return "Enriched by a data provider";
|
|
26
26
|
return "Enriched automatically";
|
|
@@ -6,7 +6,8 @@ import { arr, bool, enumOf, int, obj, str } from "./schema.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* AI teammates (ADR 0032), branded Oxygents: Agents with one ongoing chat,
|
|
8
8
|
* shared by default or Personal, the workspace's first one ("New Bot" until named) and
|
|
9
|
-
* any others added, none with a role (Tim, 2026-10-13).
|
|
9
|
+
* any others added, none with a role (Tim, 2026-10-13). Every member but a client
|
|
10
|
+
* login has them where the workspace does (launched 2026-10-17).
|
|
10
11
|
* The people-facing names say Oxygent (`oxygen oxygents`, `oxygen_oxygents_*`,
|
|
11
12
|
* Tim 2026-10-12); the HTTP family and the code keep "teammates".
|
|
12
13
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ManagedScraperBackend } from "./managed-scraper-backend.js";
|
|
1
2
|
/**
|
|
2
3
|
* The companies that supply OXYGEN's managed data, keyed by OXYGEN provider id.
|
|
3
4
|
*
|
|
@@ -12,7 +13,11 @@
|
|
|
12
13
|
* import it.
|
|
13
14
|
*/
|
|
14
15
|
export declare const MANAGED_DATA_SUPPLIERS: {
|
|
15
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* The managed `scraper.*` LinkedIn rail on its default backend. The rail's
|
|
18
|
+
* supplier follows LINKEDIN_SCRAPER_PROVIDER: read it through
|
|
19
|
+
* managedDataSupplier("scraper"), never from this entry.
|
|
20
|
+
*/
|
|
16
21
|
readonly scraper: "Up2Data";
|
|
17
22
|
/** Company-page follower audiences, fulfilled by the vendor. */
|
|
18
23
|
readonly scrapeli: "ScrapeLi";
|
|
@@ -22,6 +27,17 @@ export declare const MANAGED_DATA_SUPPLIERS: {
|
|
|
22
27
|
readonly crustdata: "Crustdata";
|
|
23
28
|
};
|
|
24
29
|
export type ManagedDataSupplierProvider = keyof typeof MANAGED_DATA_SUPPLIERS;
|
|
30
|
+
/**
|
|
31
|
+
* Every company that can supply the managed LinkedIn rail, by backend (Philipp,
|
|
32
|
+
* 2026-10-17). Both are subprocessors: HarvestAPI serves OXYGEN's key once the
|
|
33
|
+
* switch is flipped, and Up2Data keeps serving a workspace's own Up2Data key.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MANAGED_SCRAPER_SUPPLIERS: {
|
|
36
|
+
readonly up2data: "Up2Data";
|
|
37
|
+
readonly harvestapi: "HarvestAPI";
|
|
38
|
+
};
|
|
39
|
+
/** The managed LinkedIn rail's supplier on the backend this server selects. */
|
|
40
|
+
export declare function managedScraperSupplier(backend?: ManagedScraperBackend): string;
|
|
25
41
|
/**
|
|
26
42
|
* Customer-visible name of the managed LinkedIn data rail (`scraper.*`). Founder
|
|
27
43
|
* decision 2026-09-25: data-extraction labels say "Professional Network", while
|
|
@@ -36,7 +52,21 @@ export declare function managedDataSupplier(provider: string | null | undefined)
|
|
|
36
52
|
* · supplied by Bright Data").
|
|
37
53
|
*/
|
|
38
54
|
export declare function suppliedByLabel(name: string, provider: string | null | undefined): string;
|
|
39
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* "Professional Network Data · supplied by <supplier>" — the managed LinkedIn
|
|
57
|
+
* rail's label, naming the supplier the server's backend switch selects. A
|
|
58
|
+
* browser bundle cannot read that switch, so there the label names no supplier
|
|
59
|
+
* rather than a possibly wrong one.
|
|
60
|
+
*/
|
|
61
|
+
export declare function professionalNetworkDataLabel(): string;
|
|
62
|
+
/**
|
|
63
|
+
* "Professional Network Data (<supplier>)" in running prose on a server; the
|
|
64
|
+
* bare name where the backend switch cannot be read (a browser bundle).
|
|
65
|
+
*/
|
|
66
|
+
export declare function professionalNetworkDataSource(): string;
|
|
67
|
+
/** "Supplied by <supplier>." for the managed LinkedIn rail, or "" where the switch cannot be read. */
|
|
68
|
+
export declare function managedScraperSuppliedBySentence(): string;
|
|
69
|
+
/** The label resolved when this module loads (server processes read their switch at boot). */
|
|
40
70
|
export declare const PROFESSIONAL_NETWORK_DATA_LABEL: string;
|
|
41
71
|
/**
|
|
42
72
|
* How a supplier obtains an operation's data. Informational only: it never gates,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readManagedScraperBackend, serverEnv } from "./managed-scraper-backend.js";
|
|
1
2
|
/**
|
|
2
3
|
* The companies that supply OXYGEN's managed data, keyed by OXYGEN provider id.
|
|
3
4
|
*
|
|
@@ -12,7 +13,11 @@
|
|
|
12
13
|
* import it.
|
|
13
14
|
*/
|
|
14
15
|
export const MANAGED_DATA_SUPPLIERS = {
|
|
15
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* The managed `scraper.*` LinkedIn rail on its default backend. The rail's
|
|
18
|
+
* supplier follows LINKEDIN_SCRAPER_PROVIDER: read it through
|
|
19
|
+
* managedDataSupplier("scraper"), never from this entry.
|
|
20
|
+
*/
|
|
16
21
|
scraper: "Up2Data",
|
|
17
22
|
/** Company-page follower audiences, fulfilled by the vendor. */
|
|
18
23
|
scrapeli: "ScrapeLi",
|
|
@@ -21,6 +26,19 @@ export const MANAGED_DATA_SUPPLIERS = {
|
|
|
21
26
|
/** Indexed company and people data on OXYGEN's managed key. */
|
|
22
27
|
crustdata: "Crustdata",
|
|
23
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Every company that can supply the managed LinkedIn rail, by backend (Philipp,
|
|
31
|
+
* 2026-10-17). Both are subprocessors: HarvestAPI serves OXYGEN's key once the
|
|
32
|
+
* switch is flipped, and Up2Data keeps serving a workspace's own Up2Data key.
|
|
33
|
+
*/
|
|
34
|
+
export const MANAGED_SCRAPER_SUPPLIERS = {
|
|
35
|
+
up2data: "Up2Data",
|
|
36
|
+
harvestapi: "HarvestAPI",
|
|
37
|
+
};
|
|
38
|
+
/** The managed LinkedIn rail's supplier on the backend this server selects. */
|
|
39
|
+
export function managedScraperSupplier(backend = readManagedScraperBackend()) {
|
|
40
|
+
return MANAGED_SCRAPER_SUPPLIERS[backend];
|
|
41
|
+
}
|
|
24
42
|
/**
|
|
25
43
|
* Customer-visible name of the managed LinkedIn data rail (`scraper.*`). Founder
|
|
26
44
|
* decision 2026-09-25: data-extraction labels say "Professional Network", while
|
|
@@ -31,6 +49,8 @@ export const PROFESSIONAL_NETWORK_DATA_NAME = "Professional Network Data";
|
|
|
31
49
|
export function managedDataSupplier(provider) {
|
|
32
50
|
if (!provider || !Object.hasOwn(MANAGED_DATA_SUPPLIERS, provider))
|
|
33
51
|
return null;
|
|
52
|
+
if (provider === "scraper")
|
|
53
|
+
return managedScraperSupplier();
|
|
34
54
|
return MANAGED_DATA_SUPPLIERS[provider];
|
|
35
55
|
}
|
|
36
56
|
/**
|
|
@@ -44,11 +64,31 @@ export function suppliedByLabel(name, provider) {
|
|
|
44
64
|
return name;
|
|
45
65
|
return `${name} · supplied by ${supplier}`;
|
|
46
66
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
67
|
+
/**
|
|
68
|
+
* "Professional Network Data · supplied by <supplier>" — the managed LinkedIn
|
|
69
|
+
* rail's label, naming the supplier the server's backend switch selects. A
|
|
70
|
+
* browser bundle cannot read that switch, so there the label names no supplier
|
|
71
|
+
* rather than a possibly wrong one.
|
|
72
|
+
*/
|
|
73
|
+
export function professionalNetworkDataLabel() {
|
|
74
|
+
return serverEnv() ? suppliedByLabel(PROFESSIONAL_NETWORK_DATA_NAME, "scraper") : PROFESSIONAL_NETWORK_DATA_NAME;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* "Professional Network Data (<supplier>)" in running prose on a server; the
|
|
78
|
+
* bare name where the backend switch cannot be read (a browser bundle).
|
|
79
|
+
*/
|
|
80
|
+
export function professionalNetworkDataSource() {
|
|
81
|
+
return serverEnv() ? `${PROFESSIONAL_NETWORK_DATA_NAME} (${managedScraperSupplier()})` : PROFESSIONAL_NETWORK_DATA_NAME;
|
|
82
|
+
}
|
|
83
|
+
/** "Supplied by <supplier>." for the managed LinkedIn rail, or "" where the switch cannot be read. */
|
|
84
|
+
export function managedScraperSuppliedBySentence() {
|
|
85
|
+
return serverEnv() ? `Supplied by ${managedScraperSupplier()}.` : "";
|
|
86
|
+
}
|
|
87
|
+
/** The label resolved when this module loads (server processes read their switch at boot). */
|
|
88
|
+
export const PROFESSIONAL_NETWORK_DATA_LABEL = professionalNetworkDataLabel();
|
|
49
89
|
/** The disclosure an operation carries, in one sentence per method. */
|
|
50
90
|
export function dataSourcingFor(provider, method) {
|
|
51
|
-
const supplier = MANAGED_DATA_SUPPLIERS[provider];
|
|
91
|
+
const supplier = managedDataSupplier(provider) ?? MANAGED_DATA_SUPPLIERS[provider];
|
|
52
92
|
return {
|
|
53
93
|
supplier,
|
|
54
94
|
method,
|
|
@@ -71,10 +71,9 @@ export type FeatureGateResolution = {
|
|
|
71
71
|
* - OXYGEN_AGENTS_ENABLED — (b). Authorizes autonomous Agent run execution —
|
|
72
72
|
* tool authority AND credit spend (`apps/web/src/lib/agent-runtime.ts`,
|
|
73
73
|
* `apps/worker/src/agent-cycle.ts`). Same class as publishing.
|
|
74
|
-
* - OXYGEN_AGENTS_WORKSPACE_IDS — (b). Which workspaces
|
|
75
|
-
* in (`packages/shared/src/agents-enabled.ts`);
|
|
76
|
-
*
|
|
77
|
-
* workspace there needs a code change as well.
|
|
74
|
+
* - OXYGEN_AGENTS_WORKSPACE_IDS — (b). Which workspaces the "staff" stage of
|
|
75
|
+
* that execution may run in (`packages/shared/src/agents-enabled.ts`);
|
|
76
|
+
* production also requires its reviewed PRODUCTION_AGENT_WORKSPACE_IDS.
|
|
78
77
|
* - OXYGEN_REPRICING_2026_09_EFFECTIVE_AT — (b). The instant every held price
|
|
79
78
|
* rise and limit cut of the 2026-09 repricing takes effect
|
|
80
79
|
* (`packages/shared/src/repricing.ts`). A remotely flippable value would let
|
|
@@ -62,10 +62,9 @@
|
|
|
62
62
|
* - OXYGEN_AGENTS_ENABLED — (b). Authorizes autonomous Agent run execution —
|
|
63
63
|
* tool authority AND credit spend (`apps/web/src/lib/agent-runtime.ts`,
|
|
64
64
|
* `apps/worker/src/agent-cycle.ts`). Same class as publishing.
|
|
65
|
-
* - OXYGEN_AGENTS_WORKSPACE_IDS — (b). Which workspaces
|
|
66
|
-
* in (`packages/shared/src/agents-enabled.ts`);
|
|
67
|
-
*
|
|
68
|
-
* workspace there needs a code change as well.
|
|
65
|
+
* - OXYGEN_AGENTS_WORKSPACE_IDS — (b). Which workspaces the "staff" stage of
|
|
66
|
+
* that execution may run in (`packages/shared/src/agents-enabled.ts`);
|
|
67
|
+
* production also requires its reviewed PRODUCTION_AGENT_WORKSPACE_IDS.
|
|
69
68
|
* - OXYGEN_REPRICING_2026_09_EFFECTIVE_AT — (b). The instant every held price
|
|
70
69
|
* rise and limit cut of the 2026-09 repricing takes effect
|
|
71
70
|
* (`packages/shared/src/repricing.ts`). A remotely flippable value would let
|
|
@@ -22,6 +22,101 @@
|
|
|
22
22
|
* can re-source the catalog without dragging the barrel in.
|
|
23
23
|
*/
|
|
24
24
|
import { copilotAudience, isCopilotFlagOn } from "./copilot-access.js";
|
|
25
|
+
/**
|
|
26
|
+
* The Copilot's tiers, which an Oxygent's own chat runs at. Hoisted out of the
|
|
27
|
+
* registry so the Agent use case can share the same spec objects.
|
|
28
|
+
*/
|
|
29
|
+
const COPILOT_MODEL_SPECS = {
|
|
30
|
+
/**
|
|
31
|
+
* "Oxygen Auto" -- the earlier default, retained as an explicit choice.
|
|
32
|
+
* Its conversation thread stays fixed for its whole life. It is NOT a router: the
|
|
33
|
+
* model never changes mid-session, because an OpenRouter prompt cache is
|
|
34
|
+
* model-scoped and a mid-turn switch would turn a ~66% cache HIT rate into a
|
|
35
|
+
* cache WRITE on a fresh model, ten times a turn. Models only change below
|
|
36
|
+
* this thread, where the context is fresh -- see COPILOT_ERRAND_LEVEL.
|
|
37
|
+
*
|
|
38
|
+
* Before the 2026-10-10 Sol refresh, Auto ran Kimi K3. Its
|
|
39
|
+
* `reasoningEffort: "medium"` made that tier worth shipping.
|
|
40
|
+
* Until 2026-09-21 Kimi was sent with NO `reasoning` field at
|
|
41
|
+
* all, on a registry comment asserting K3 was max-reasoning-only. Measured
|
|
42
|
+
* against the live API on the same prompt and tool schema, that claim was
|
|
43
|
+
* false and the omission was the expensive case:
|
|
44
|
+
*
|
|
45
|
+
* omitted -> 412 reasoning tokens, $0.00779, 10.0s
|
|
46
|
+
* low -> 206, $0.00566, 9.2s
|
|
47
|
+
* medium -> 124, $0.00342, 4.8s <- 56% cheaper, 2.1x faster
|
|
48
|
+
* high -> 528, $0.01075, 14.8s
|
|
49
|
+
*
|
|
50
|
+
* All four returned the same answer. Reasoning was 38% of output tokens on a
|
|
51
|
+
* surface where output is ~61% of the bill.
|
|
52
|
+
*/
|
|
53
|
+
auto: {
|
|
54
|
+
model: "openai/gpt-6.1-sol",
|
|
55
|
+
displayName: "Oxygen Auto",
|
|
56
|
+
// Approved 2026-10-10 for NEW sessions. Existing sessions retain their
|
|
57
|
+
// persisted resolved_model; Kimi's priced spec survives in the legacy map.
|
|
58
|
+
fallbackModels: ["anthropic/claude-sonnet-5.5"],
|
|
59
|
+
reasoningEffort: "medium",
|
|
60
|
+
estPromptUsdPerM: 2,
|
|
61
|
+
estCompletionUsdPerM: 10,
|
|
62
|
+
// Reasoning tokens count against this cap, so 8,192 left a long tool call
|
|
63
|
+
// (a full sequence draft, a wide table write) to be cut off mid-argument,
|
|
64
|
+
// which fails the turn on the second time. Credit holds are sized from the
|
|
65
|
+
// expected output (billing.ts), so this moves only the conservative clamp.
|
|
66
|
+
maxOutputTokens: 16_384,
|
|
67
|
+
},
|
|
68
|
+
low: {
|
|
69
|
+
// The errand model: sub-agents, compaction summaries and auto-titles. A
|
|
70
|
+
// child builds its own Agent with its own messages, so it starts cold on
|
|
71
|
+
// any model -- which is why a switch is free HERE and nowhere above it.
|
|
72
|
+
//
|
|
73
|
+
// This was deepseek/deepseek-v4.1-flash for about an hour, chosen for
|
|
74
|
+
// being newer and measured correct on a one-tool arithmetic probe. Probed
|
|
75
|
+
// again against the REAL runtime tool schemas from tools.ts
|
|
76
|
+
// (capability_search + capability_run, nested `args`, optional approval
|
|
77
|
+
// fields) under the same provider.require_parameters=true the transport
|
|
78
|
+
// always sends, it returned `finish_reason: "error"` with zero tool calls
|
|
79
|
+
// on 1 of 5 attempts, every one served by the Relace endpoint. v4-flash
|
|
80
|
+
// went 5/5 clean on StreamLake, is CHEAPER on both axes, and is already
|
|
81
|
+
// what auto-titles run on -- so the newer model was worse on reliability
|
|
82
|
+
// and on price, and the only thing recommending it was its version number.
|
|
83
|
+
//
|
|
84
|
+
// A child that emits no tool call returns nothing for what it spent, and
|
|
85
|
+
// the transport does not retry `finish_reason: "error"` on the primary
|
|
86
|
+
// (classifyModelError sends it straight to the next model), so a 20%
|
|
87
|
+
// error rate here is a 20% double round trip on every delegation.
|
|
88
|
+
model: "deepseek/deepseek-v4-flash",
|
|
89
|
+
displayName: "Oxygen Fast",
|
|
90
|
+
// v4.1-flash earns its place HERE: it works 4 times in 5 and it is a
|
|
91
|
+
// genuinely different endpoint on a different provider, which is the one
|
|
92
|
+
// thing a fallback has to be.
|
|
93
|
+
fallbackModels: ["deepseek/deepseek-v4.1-flash"],
|
|
94
|
+
reasoningEffort: "low",
|
|
95
|
+
estPromptUsdPerM: 0.09,
|
|
96
|
+
estCompletionUsdPerM: 0.18,
|
|
97
|
+
maxOutputTokens: 8192,
|
|
98
|
+
},
|
|
99
|
+
medium: {
|
|
100
|
+
model: "anthropic/claude-haiku-5.5",
|
|
101
|
+
displayName: "Oxygen Balanced",
|
|
102
|
+
fallbackModels: ["deepseek/deepseek-v4.1-flash"],
|
|
103
|
+
reasoningEffort: "medium",
|
|
104
|
+
estPromptUsdPerM: 0.1,
|
|
105
|
+
estCompletionUsdPerM: 0.5,
|
|
106
|
+
// A conversation tier (the default since #5225), so Auto's cap (see Auto).
|
|
107
|
+
maxOutputTokens: 16_384,
|
|
108
|
+
},
|
|
109
|
+
high: {
|
|
110
|
+
model: "openai/gpt-6.1-sol",
|
|
111
|
+
displayName: "Oxygen Max",
|
|
112
|
+
fallbackModels: ["anthropic/claude-sonnet-5.5"],
|
|
113
|
+
reasoningEffort: "medium",
|
|
114
|
+
estPromptUsdPerM: 2,
|
|
115
|
+
estCompletionUsdPerM: 10,
|
|
116
|
+
// A conversation tier, so Auto's cap (see Auto).
|
|
117
|
+
maxOutputTokens: 16_384,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
25
120
|
export const HOSTED_AI_MODEL_REGISTRY = {
|
|
26
121
|
/**
|
|
27
122
|
* Historical calibration of the September AI-column tiers, re-picked
|
|
@@ -112,124 +207,20 @@ export const HOSTED_AI_MODEL_REGISTRY = {
|
|
|
112
207
|
maxOutputTokens: 4096,
|
|
113
208
|
},
|
|
114
209
|
},
|
|
115
|
-
copilot:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* `reasoningEffort: "medium"` made that tier worth shipping.
|
|
126
|
-
* Until 2026-09-21 Kimi was sent with NO `reasoning` field at
|
|
127
|
-
* all, on a registry comment asserting K3 was max-reasoning-only. Measured
|
|
128
|
-
* against the live API on the same prompt and tool schema, that claim was
|
|
129
|
-
* false and the omission was the expensive case:
|
|
130
|
-
*
|
|
131
|
-
* omitted -> 412 reasoning tokens, $0.00779, 10.0s
|
|
132
|
-
* low -> 206, $0.00566, 9.2s
|
|
133
|
-
* medium -> 124, $0.00342, 4.8s <- 56% cheaper, 2.1x faster
|
|
134
|
-
* high -> 528, $0.01075, 14.8s
|
|
135
|
-
*
|
|
136
|
-
* All four returned the same answer. Reasoning was 38% of output tokens on a
|
|
137
|
-
* surface where output is ~61% of the bill.
|
|
138
|
-
*/
|
|
139
|
-
auto: {
|
|
140
|
-
model: "openai/gpt-6.1-sol",
|
|
141
|
-
displayName: "Oxygen Auto",
|
|
142
|
-
// Approved 2026-10-10 for NEW sessions. Existing sessions retain their
|
|
143
|
-
// persisted resolved_model; Kimi's priced spec survives in the legacy map.
|
|
144
|
-
fallbackModels: ["anthropic/claude-sonnet-5.5"],
|
|
145
|
-
reasoningEffort: "medium",
|
|
146
|
-
estPromptUsdPerM: 2,
|
|
147
|
-
estCompletionUsdPerM: 10,
|
|
148
|
-
// Reasoning tokens count against this cap, so 8,192 left a long tool call
|
|
149
|
-
// (a full sequence draft, a wide table write) to be cut off mid-argument,
|
|
150
|
-
// which fails the turn on the second time. Credit holds are sized from the
|
|
151
|
-
// expected output (billing.ts), so this moves only the conservative clamp.
|
|
152
|
-
maxOutputTokens: 16_384,
|
|
153
|
-
},
|
|
154
|
-
low: {
|
|
155
|
-
// The errand model: sub-agents, compaction summaries and auto-titles. A
|
|
156
|
-
// child builds its own Agent with its own messages, so it starts cold on
|
|
157
|
-
// any model -- which is why a switch is free HERE and nowhere above it.
|
|
158
|
-
//
|
|
159
|
-
// This was deepseek/deepseek-v4.1-flash for about an hour, chosen for
|
|
160
|
-
// being newer and measured correct on a one-tool arithmetic probe. Probed
|
|
161
|
-
// again against the REAL runtime tool schemas from tools.ts
|
|
162
|
-
// (capability_search + capability_run, nested `args`, optional approval
|
|
163
|
-
// fields) under the same provider.require_parameters=true the transport
|
|
164
|
-
// always sends, it returned `finish_reason: "error"` with zero tool calls
|
|
165
|
-
// on 1 of 5 attempts, every one served by the Relace endpoint. v4-flash
|
|
166
|
-
// went 5/5 clean on StreamLake, is CHEAPER on both axes, and is already
|
|
167
|
-
// what auto-titles run on -- so the newer model was worse on reliability
|
|
168
|
-
// and on price, and the only thing recommending it was its version number.
|
|
169
|
-
//
|
|
170
|
-
// A child that emits no tool call returns nothing for what it spent, and
|
|
171
|
-
// the transport does not retry `finish_reason: "error"` on the primary
|
|
172
|
-
// (classifyModelError sends it straight to the next model), so a 20%
|
|
173
|
-
// error rate here is a 20% double round trip on every delegation.
|
|
174
|
-
model: "deepseek/deepseek-v4-flash",
|
|
175
|
-
displayName: "Oxygen Fast",
|
|
176
|
-
// v4.1-flash earns its place HERE: it works 4 times in 5 and it is a
|
|
177
|
-
// genuinely different endpoint on a different provider, which is the one
|
|
178
|
-
// thing a fallback has to be.
|
|
179
|
-
fallbackModels: ["deepseek/deepseek-v4.1-flash"],
|
|
180
|
-
reasoningEffort: "low",
|
|
181
|
-
estPromptUsdPerM: 0.09,
|
|
182
|
-
estCompletionUsdPerM: 0.18,
|
|
183
|
-
maxOutputTokens: 8192,
|
|
184
|
-
},
|
|
185
|
-
medium: {
|
|
186
|
-
model: "anthropic/claude-haiku-5.5",
|
|
187
|
-
displayName: "Oxygen Balanced",
|
|
188
|
-
fallbackModels: ["deepseek/deepseek-v4.1-flash"],
|
|
189
|
-
reasoningEffort: "medium",
|
|
190
|
-
estPromptUsdPerM: 0.1,
|
|
191
|
-
estCompletionUsdPerM: 0.5,
|
|
192
|
-
// A conversation tier (the default since #5225), so Auto's cap (see Auto).
|
|
193
|
-
maxOutputTokens: 16_384,
|
|
194
|
-
},
|
|
195
|
-
high: {
|
|
196
|
-
model: "openai/gpt-6.1-sol",
|
|
197
|
-
displayName: "Oxygen Max",
|
|
198
|
-
fallbackModels: ["anthropic/claude-sonnet-5.5"],
|
|
199
|
-
reasoningEffort: "medium",
|
|
200
|
-
estPromptUsdPerM: 2,
|
|
201
|
-
estCompletionUsdPerM: 10,
|
|
202
|
-
// A conversation tier, so Auto's cap (see Auto).
|
|
203
|
-
maxOutputTokens: 16_384,
|
|
204
|
-
},
|
|
205
|
-
},
|
|
210
|
+
copilot: COPILOT_MODEL_SPECS,
|
|
211
|
+
/**
|
|
212
|
+
* An Oxygent's background tasks and routines run on the very models its chat
|
|
213
|
+
* does, so one tier means one model wherever the Oxygent works (Philipp,
|
|
214
|
+
* 2026-10-17). Until then Agents kept their September tiers (DeepSeek V4
|
|
215
|
+
* Flash / V4 Pro / Kimi K2.6), which the 2026-10-10 refresh left out of scope.
|
|
216
|
+
* Shared by reference, so a Copilot tier refresh moves Agents with it;
|
|
217
|
+
* `OXYGEN_AGENT_MODEL_*` still overrides Agents alone. Retired ids keep
|
|
218
|
+
* their tier label through LEGACY_MANAGED_AI_COLUMN_MODELS.
|
|
219
|
+
*/
|
|
206
220
|
agent: {
|
|
207
|
-
low:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
fallbackModels: [],
|
|
211
|
-
reasoningEffort: "medium",
|
|
212
|
-
estPromptUsdPerM: 0.09,
|
|
213
|
-
estCompletionUsdPerM: 0.18,
|
|
214
|
-
maxOutputTokens: 8192,
|
|
215
|
-
},
|
|
216
|
-
medium: {
|
|
217
|
-
model: "deepseek/deepseek-v4-pro",
|
|
218
|
-
displayName: "Oxygen Balanced",
|
|
219
|
-
fallbackModels: ["deepseek/deepseek-v4-flash"],
|
|
220
|
-
reasoningEffort: "high",
|
|
221
|
-
estPromptUsdPerM: 0.435,
|
|
222
|
-
estCompletionUsdPerM: 0.87,
|
|
223
|
-
maxOutputTokens: 8192,
|
|
224
|
-
},
|
|
225
|
-
high: {
|
|
226
|
-
model: "moonshotai/kimi-k2.6",
|
|
227
|
-
displayName: "Oxygen Max",
|
|
228
|
-
fallbackModels: ["deepseek/deepseek-v4-pro"],
|
|
229
|
-
estPromptUsdPerM: 0.66,
|
|
230
|
-
estCompletionUsdPerM: 3.41,
|
|
231
|
-
maxOutputTokens: 8192,
|
|
232
|
-
},
|
|
221
|
+
low: COPILOT_MODEL_SPECS.low,
|
|
222
|
+
medium: COPILOT_MODEL_SPECS.medium,
|
|
223
|
+
high: COPILOT_MODEL_SPECS.high,
|
|
233
224
|
},
|
|
234
225
|
};
|
|
235
226
|
/** Persisted conversations keep their original pricing, effort and fallbacks. */
|
|
@@ -449,7 +440,6 @@ export const IMAGE_INPUT_MODELS = new Set([
|
|
|
449
440
|
"anthropic/claude-sonnet-5.5",
|
|
450
441
|
"anthropic/claude-haiku-5.5",
|
|
451
442
|
"moonshotai/kimi-k3",
|
|
452
|
-
"moonshotai/kimi-k2.6",
|
|
453
443
|
"deepseek/deepseek-v4.1-flash",
|
|
454
444
|
"google/gemini-3.8-flash",
|
|
455
445
|
]);
|
|
@@ -14,3 +14,32 @@ export type KnowledgeWikilink = {
|
|
|
14
14
|
* empty key (e.g. [[---]]) are dropped.
|
|
15
15
|
*/
|
|
16
16
|
export declare function extractKnowledgeLinks(body: string | null | undefined): KnowledgeWikilink[];
|
|
17
|
+
/**
|
|
18
|
+
* The exact vault paths, in priority order, and the basename that
|
|
19
|
+
* resolveKnowledgeVaultLink can match for a [[wikilink]] target written in the
|
|
20
|
+
* file at `currentPath`. A store fetches only these files and resolves against
|
|
21
|
+
* them with the same result as against the whole vault. Null when the target is
|
|
22
|
+
* empty or absolute.
|
|
23
|
+
*/
|
|
24
|
+
export declare function knowledgeVaultLinkCandidates(target: string, currentPath: string): {
|
|
25
|
+
paths: string[];
|
|
26
|
+
basename: string | null;
|
|
27
|
+
} | null;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a [[wikilink]] target in a repository vault the way Obsidian does: a
|
|
30
|
+
* bare name prefers the same folder, then the vault-relative path, a path target
|
|
31
|
+
* is also tried relative to the current folder, and a bare name finally matches
|
|
32
|
+
* a basename that is unique in the vault. A target without an extension names a
|
|
33
|
+
* Markdown note. The web file view and the page-link graph both resolve through
|
|
34
|
+
* here, so a link that opens a note is the edge the graph shows.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveKnowledgeVaultLink<T extends {
|
|
37
|
+
path: string;
|
|
38
|
+
}>(target: string, currentPath: string, files: readonly T[]): T | null;
|
|
39
|
+
/**
|
|
40
|
+
* Every link key a [[wikilink]] that resolves to the vault file at `path` can
|
|
41
|
+
* carry: each trailing path suffix, and for a note also without its .md
|
|
42
|
+
* extension. A new or moved file can satisfy only dangling links with one of
|
|
43
|
+
* these keys.
|
|
44
|
+
*/
|
|
45
|
+
export declare function knowledgeVaultLinkKeys(path: string): string[];
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// here and nowhere else. The regex + alias/heading stripping mirror the vault
|
|
5
5
|
// component's original parser (apps/web/src/components/vault/vault.ts), which
|
|
6
6
|
// now delegates here — [[Target|alias]] and [[Target#heading]] both link to
|
|
7
|
-
// Target.
|
|
7
|
+
// Target. Links inside a connected repository resolve by vault path first
|
|
8
|
+
// (resolveKnowledgeVaultLink), for the mounted file view and the graph alike.
|
|
8
9
|
import { knowledgeLinkKey } from "./knowledge-constants.js";
|
|
9
10
|
const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
10
11
|
/**
|
|
@@ -36,3 +37,85 @@ export function extractKnowledgeLinks(body) {
|
|
|
36
37
|
}
|
|
37
38
|
return [...byKey.values()];
|
|
38
39
|
}
|
|
40
|
+
function normalizedVaultPath(candidate) {
|
|
41
|
+
const normalized = [];
|
|
42
|
+
for (const segment of candidate.split("/")) {
|
|
43
|
+
if (!segment || segment === ".")
|
|
44
|
+
continue;
|
|
45
|
+
if (segment === "..") {
|
|
46
|
+
if (!normalized.pop())
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
normalized.push(segment);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return normalized.join("/") || null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The exact vault paths, in priority order, and the basename that
|
|
57
|
+
* resolveKnowledgeVaultLink can match for a [[wikilink]] target written in the
|
|
58
|
+
* file at `currentPath`. A store fetches only these files and resolves against
|
|
59
|
+
* them with the same result as against the whole vault. Null when the target is
|
|
60
|
+
* empty or absolute.
|
|
61
|
+
*/
|
|
62
|
+
export function knowledgeVaultLinkCandidates(target, currentPath) {
|
|
63
|
+
const pathTarget = target.split("#", 1)[0]?.trim();
|
|
64
|
+
if (!pathTarget || pathTarget.startsWith("/"))
|
|
65
|
+
return null;
|
|
66
|
+
const withExtension = /\.[^/]+$/.test(pathTarget) ? pathTarget : `${pathTarget}.md`;
|
|
67
|
+
const directory = currentPath.includes("/") ? currentPath.slice(0, currentPath.lastIndexOf("/")) : "";
|
|
68
|
+
const paths = [];
|
|
69
|
+
if (!pathTarget.includes("/"))
|
|
70
|
+
paths.push(`${directory ? `${directory}/` : ""}${withExtension}`);
|
|
71
|
+
const vaultPath = normalizedVaultPath(withExtension);
|
|
72
|
+
if (vaultPath)
|
|
73
|
+
paths.push(vaultPath);
|
|
74
|
+
if (pathTarget.includes("/")) {
|
|
75
|
+
const relativePath = normalizedVaultPath(`${directory}/${withExtension}`);
|
|
76
|
+
if (relativePath)
|
|
77
|
+
paths.push(relativePath);
|
|
78
|
+
}
|
|
79
|
+
return { paths, basename: pathTarget.includes("/") ? null : withExtension };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a [[wikilink]] target in a repository vault the way Obsidian does: a
|
|
83
|
+
* bare name prefers the same folder, then the vault-relative path, a path target
|
|
84
|
+
* is also tried relative to the current folder, and a bare name finally matches
|
|
85
|
+
* a basename that is unique in the vault. A target without an extension names a
|
|
86
|
+
* Markdown note. The web file view and the page-link graph both resolve through
|
|
87
|
+
* here, so a link that opens a note is the edge the graph shows.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveKnowledgeVaultLink(target, currentPath, files) {
|
|
90
|
+
const candidates = knowledgeVaultLinkCandidates(target, currentPath);
|
|
91
|
+
if (!candidates)
|
|
92
|
+
return null;
|
|
93
|
+
for (const path of candidates.paths) {
|
|
94
|
+
const file = files.find((candidate) => candidate.path === path);
|
|
95
|
+
if (file)
|
|
96
|
+
return file;
|
|
97
|
+
}
|
|
98
|
+
if (!candidates.basename)
|
|
99
|
+
return null;
|
|
100
|
+
const matches = files.filter((file) => file.path.split("/").at(-1) === candidates.basename);
|
|
101
|
+
return matches.length === 1 ? matches[0] : null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Every link key a [[wikilink]] that resolves to the vault file at `path` can
|
|
105
|
+
* carry: each trailing path suffix, and for a note also without its .md
|
|
106
|
+
* extension. A new or moved file can satisfy only dangling links with one of
|
|
107
|
+
* these keys.
|
|
108
|
+
*/
|
|
109
|
+
export function knowledgeVaultLinkKeys(path) {
|
|
110
|
+
const segments = path.split("/");
|
|
111
|
+
const keys = new Set();
|
|
112
|
+
for (let start = segments.length - 1; start >= 0; start -= 1) {
|
|
113
|
+
const suffix = segments.slice(start).join("/");
|
|
114
|
+
for (const form of /\.md$/.test(suffix) ? [suffix, suffix.slice(0, -3)] : [suffix]) {
|
|
115
|
+
const key = knowledgeLinkKey(form);
|
|
116
|
+
if (key)
|
|
117
|
+
keys.add(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...keys];
|
|
121
|
+
}
|
|
@@ -202,17 +202,6 @@ export type LinkedInRefusalScope =
|
|
|
202
202
|
| "already_engaged"
|
|
203
203
|
/** Unrecognised. Callers MUST preserve their existing behaviour. */
|
|
204
204
|
| "unknown";
|
|
205
|
-
/**
|
|
206
|
-
* Classify a LinkedIn refusal from the provider's own account of it.
|
|
207
|
-
*
|
|
208
|
-
* Deliberately a CLOSED set of anchored, full-string matches, in the same
|
|
209
|
-
* discipline as the sequence-recovery allowlists: matching vendor prose is
|
|
210
|
-
* fragile, so an unrecognised message must degrade to `unknown` and leave the
|
|
211
|
-
* caller's behaviour exactly as it was. Extending the set is an evidence-driven
|
|
212
|
-
* act — callers log what they could not classify.
|
|
213
|
-
*
|
|
214
|
-
* Pure and total; never throws.
|
|
215
|
-
*/
|
|
216
205
|
export declare function classifyLinkedInRefusal(input: {
|
|
217
206
|
providerType: string | null;
|
|
218
207
|
providerMessage: string | null;
|
|
@@ -394,9 +383,11 @@ export declare function linkedInLimitCautionNotices(metadata: Record<string, unk
|
|
|
394
383
|
export declare const LINKEDIN_REINVITE_COOLDOWN_DAYS = 21;
|
|
395
384
|
/**
|
|
396
385
|
* Whether a refused invitation is LinkedIn's re-invite cooldown: Unipile answers
|
|
397
|
-
* HTTP 422 with the `cannot_resend_yet` error type
|
|
398
|
-
*
|
|
399
|
-
* the sentence.
|
|
386
|
+
* HTTP 422 with the `cannot_resend_yet` error type, or with
|
|
387
|
+
* `provider/unprocessable_entity` and LinkedIn's "already handled a request from
|
|
388
|
+
* you in the past 3 weeks" sentence. Anchored on the type, with the message as a
|
|
389
|
+
* fallback for the provider shapes that carry the type only in the sentence.
|
|
390
|
+
* Pure and total.
|
|
400
391
|
*/
|
|
401
392
|
export declare function isLinkedInReinviteCooldownRefusal(input: {
|
|
402
393
|
status: number | null;
|
|
@@ -113,6 +113,14 @@ export const LINKEDIN_INVITE_NOTE_LIMIT_BASIC = 200;
|
|
|
113
113
|
*
|
|
114
114
|
* Pure and total; never throws.
|
|
115
115
|
*/
|
|
116
|
+
/**
|
|
117
|
+
* LinkedIn's sentence when an invitation is refused because the member handled
|
|
118
|
+
* an earlier one from this account (accepted, ignored or withdrawn) within three
|
|
119
|
+
* weeks. Unipile returns it as HTTP 422 `provider/unprocessable_entity`, the v2
|
|
120
|
+
* client prefixes the title. Verified on dev 2026-10-17 (Unipile req-3i5d,
|
|
121
|
+
* req-7edn, req-7nsa): no invitation was created.
|
|
122
|
+
*/
|
|
123
|
+
const LINKEDIN_RECENTLY_HANDLED_INVITE_MESSAGE = /^(?:Unprocessable entity: )?This user has already handled a request from you in the past 3 weeks\./u;
|
|
116
124
|
export function classifyLinkedInRefusal(input) {
|
|
117
125
|
const message = typeof input.providerMessage === "string" ? input.providerMessage.trim() : "";
|
|
118
126
|
if (message.length === 0)
|
|
@@ -146,7 +154,7 @@ export function classifyLinkedInRefusal(input) {
|
|
|
146
154
|
}
|
|
147
155
|
if (input.status === 422
|
|
148
156
|
&& input.providerType === "provider/unprocessable_entity"
|
|
149
|
-
&&
|
|
157
|
+
&& LINKEDIN_RECENTLY_HANDLED_INVITE_MESSAGE.test(message)) {
|
|
150
158
|
return "already_engaged";
|
|
151
159
|
}
|
|
152
160
|
if (input.status === 409
|
|
@@ -440,14 +448,23 @@ export function linkedInLimitCautionNotices(metadata, overrides) {
|
|
|
440
448
|
export const LINKEDIN_REINVITE_COOLDOWN_DAYS = 21;
|
|
441
449
|
/**
|
|
442
450
|
* Whether a refused invitation is LinkedIn's re-invite cooldown: Unipile answers
|
|
443
|
-
* HTTP 422 with the `cannot_resend_yet` error type
|
|
444
|
-
*
|
|
445
|
-
* the sentence.
|
|
451
|
+
* HTTP 422 with the `cannot_resend_yet` error type, or with
|
|
452
|
+
* `provider/unprocessable_entity` and LinkedIn's "already handled a request from
|
|
453
|
+
* you in the past 3 weeks" sentence. Anchored on the type, with the message as a
|
|
454
|
+
* fallback for the provider shapes that carry the type only in the sentence.
|
|
455
|
+
* Pure and total.
|
|
446
456
|
*/
|
|
447
457
|
export function isLinkedInReinviteCooldownRefusal(input) {
|
|
448
458
|
if (input.status !== 422)
|
|
449
459
|
return false;
|
|
450
460
|
if (typeof input.providerType === "string" && /(?:^|\/)cannot_resend_yet$/u.test(input.providerType.trim()))
|
|
451
461
|
return true;
|
|
452
|
-
|
|
462
|
+
if (typeof input.providerMessage !== "string")
|
|
463
|
+
return false;
|
|
464
|
+
if (/\bcannot_resend_yet\b/u.test(input.providerMessage))
|
|
465
|
+
return true;
|
|
466
|
+
// The same refusal in LinkedIn's own words. Without it, the BullMQ runtime
|
|
467
|
+
// read the write as possibly delivered and parked the lead for reconciliation.
|
|
468
|
+
return input.providerType === "provider/unprocessable_entity"
|
|
469
|
+
&& LINKEDIN_RECENTLY_HANDLED_INVITE_MESSAGE.test(input.providerMessage.trim());
|
|
453
470
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which vendor serves OXYGEN's managed `scraper.*` LinkedIn rail
|
|
3
|
+
* (LINKEDIN_SCRAPER_PROVIDER, Philipp 2026-10-17: a pure provider exchange from
|
|
4
|
+
* Up2Data to HarvestAPI behind a Doppler switch). Up2Data is the default, so an
|
|
5
|
+
* unset or unrecognised value never moves paid traffic.
|
|
6
|
+
*
|
|
7
|
+
* Dependency-free and browser-safe: a bundle without the server environment
|
|
8
|
+
* reads the default, so a label rendered in the browser must not rely on it.
|
|
9
|
+
*/
|
|
10
|
+
export type ManagedScraperBackend = "up2data" | "harvestapi";
|
|
11
|
+
export declare const MANAGED_SCRAPER_BACKEND_ENV = "LINKEDIN_SCRAPER_PROVIDER";
|
|
12
|
+
type Env = Readonly<Record<string, string | undefined>>;
|
|
13
|
+
/** The server environment, or null in a bundle that has none. */
|
|
14
|
+
export declare function serverEnv(): Env | null;
|
|
15
|
+
/** The configured backend, or null when the value names neither vendor. */
|
|
16
|
+
export declare function parseManagedScraperBackend(value: string | undefined): ManagedScraperBackend | null;
|
|
17
|
+
export declare function readManagedScraperBackend(env?: Env | null): ManagedScraperBackend;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const MANAGED_SCRAPER_BACKEND_ENV = "LINKEDIN_SCRAPER_PROVIDER";
|
|
2
|
+
/** The server environment, or null in a bundle that has none. */
|
|
3
|
+
export function serverEnv() {
|
|
4
|
+
const env = typeof process !== "undefined" ? process.env : undefined;
|
|
5
|
+
// `window` is absent from this package's types (no DOM lib), so read it off globalThis.
|
|
6
|
+
return env && !("window" in globalThis) ? env : null;
|
|
7
|
+
}
|
|
8
|
+
/** The configured backend, or null when the value names neither vendor. */
|
|
9
|
+
export function parseManagedScraperBackend(value) {
|
|
10
|
+
const normalized = value?.trim().toLowerCase();
|
|
11
|
+
if (!normalized || normalized === "up2data")
|
|
12
|
+
return "up2data";
|
|
13
|
+
return normalized === "harvestapi" ? "harvestapi" : null;
|
|
14
|
+
}
|
|
15
|
+
export function readManagedScraperBackend(env = serverEnv()) {
|
|
16
|
+
return parseManagedScraperBackend(env?.[MANAGED_SCRAPER_BACKEND_ENV]) ?? "up2data";
|
|
17
|
+
}
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* per Workspace Oxygent, on the Copilot runtime. Explicit Personal Oxygents
|
|
4
4
|
* are creator-only; the first one stays Workspace (Philipp, 2026-10-16).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Who has them: `OXYGEN_TEAMMATES_ENABLED` is the switch, "staff" for staff emails
|
|
7
|
+
* and "all" for every member who is not a client login (the customer launch,
|
|
8
|
+
* Tim, 2026-10-17). Unset, it follows the Ongoing Agent sessions switch, because
|
|
9
|
+
* teammates re-scope those sessions into routines. Only workspaces Agents run in
|
|
10
|
+
* qualify (`agentsEnabledIn`); shadow never does.
|
|
11
11
|
*/
|
|
12
|
+
export type TeammatesAudience = "off" | "staff" | "all";
|
|
13
|
+
export declare function teammatesAudience(env?: Record<string, string | undefined>): TeammatesAudience;
|
|
12
14
|
export declare function teammatesEnabled(env?: Record<string, string | undefined>): boolean;
|
|
13
15
|
/** Whether this workspace has Oxygents. */
|
|
14
16
|
export declare function teammatesEnabledIn(organizationId: string | null | undefined, env?: Record<string, string | undefined>): boolean;
|
|
@@ -1,34 +1,25 @@
|
|
|
1
1
|
import { agentsEnabled, agentsEnabledIn } from "./agents-enabled.js";
|
|
2
2
|
import { guidedActivationEnabled } from "./guided-activation.js";
|
|
3
3
|
import { isStaffEmail } from "./staff.js";
|
|
4
|
-
|
|
5
|
-
* AI teammates (ADR 0032): Agents people talk to in one ongoing shared chat
|
|
6
|
-
* per Workspace Oxygent, on the Copilot runtime. Explicit Personal Oxygents
|
|
7
|
-
* are creator-only; the first one stays Workspace (Philipp, 2026-10-16).
|
|
8
|
-
*
|
|
9
|
-
* Staff only: on dev, and in production's staff workspaces, the ones Agents run
|
|
10
|
-
* in (`agentsEnabledIn`; Tim, 2026-10-16). `OXYGEN_TEAMMATES_ENABLED` is the
|
|
11
|
-
* switch; when it is unset the feature follows the Ongoing Agent sessions staff
|
|
12
|
-
* gate, because teammates re-scope those sessions into routines. Shadow never
|
|
13
|
-
* qualifies.
|
|
14
|
-
*/
|
|
15
|
-
export function teammatesEnabled(env = process.env) {
|
|
4
|
+
export function teammatesAudience(env = process.env) {
|
|
16
5
|
if (env.OXYGEN_DEPLOY_ENV !== "selfhost-dev" && env.OXYGEN_DEPLOY_ENV !== "selfhost-prod")
|
|
17
|
-
return
|
|
6
|
+
return "off";
|
|
18
7
|
if (!agentsEnabled(env))
|
|
19
|
-
return
|
|
20
|
-
const flag = env.OXYGEN_TEAMMATES_ENABLED?.trim().toLowerCase();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
8
|
+
return "off";
|
|
9
|
+
const flag = env.OXYGEN_TEAMMATES_ENABLED?.trim().toLowerCase() || env.OXYGEN_AGENT_SESSIONS_ENABLED?.trim();
|
|
10
|
+
return flag === "staff" || flag === "all" ? flag : "off";
|
|
11
|
+
}
|
|
12
|
+
export function teammatesEnabled(env = process.env) {
|
|
13
|
+
return teammatesAudience(env) !== "off";
|
|
24
14
|
}
|
|
25
15
|
/** Whether this workspace has Oxygents. */
|
|
26
16
|
export function teammatesEnabledIn(organizationId, env = process.env) {
|
|
27
17
|
return teammatesEnabled(env) && agentsEnabledIn(organizationId, env);
|
|
28
18
|
}
|
|
29
19
|
export function teammatesAvailableTo(env, viewer) {
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
if (!teammatesEnabledIn(viewer.organizationId, env) || !viewer.member || viewer.oxygenRole === "client")
|
|
21
|
+
return false;
|
|
22
|
+
return teammatesAudience(env) === "all" || guidedActivationEnabled(env) || isStaffEmail(viewer.email, env);
|
|
32
23
|
}
|
|
33
24
|
/**
|
|
34
25
|
* The Workspace Agent's slug. Every workspace's first Oxygent is that Agent: an
|
|
@@ -222,7 +222,14 @@ function isGuidedOnboardingDiscoveryIntent(query) {
|
|
|
222
222
|
|| (/\b(?:guide|guided|journey|part [12])\b/.test(query)
|
|
223
223
|
&& /\b(?:business|company|icp|personas?|prospects?|trial|campaign|sequence|briefing)\b/.test(query))
|
|
224
224
|
|| (/\b(?:trial|prospects?|personas?)\b/.test(query)
|
|
225
|
-
&& /\b(?:context|icp|campaign|sequence)\b/.test(query))
|
|
225
|
+
&& /\b(?:context|icp|campaign|sequence)\b/.test(query))
|
|
226
|
+
// Company-statement review is a guided step even without the words guide
|
|
227
|
+
// or journey. Existing channel conversations keep their message owner.
|
|
228
|
+
|| (/\bcompany profile\b/.test(query)
|
|
229
|
+
&& /\b(?:review|correct|confirm)\b/.test(query)
|
|
230
|
+
&& /\b(?:statement|profile revision)\b/.test(query)
|
|
231
|
+
&& /\b(?:prospects?|evidence|sources?)\b/.test(query)
|
|
232
|
+
&& !/\b(?:inbox|unibox|messages?|threads?|repl(?:y|ies)|email|whatsapp|slack|teams)\b/.test(query));
|
|
226
233
|
}
|
|
227
234
|
function isCompanyOnboardingContextIntent(query) {
|
|
228
235
|
return /\bonboard\w*\b/.test(query)
|
|
@@ -337,7 +344,10 @@ function oxygentsRoute(normalized) {
|
|
|
337
344
|
// Explicit content reads and other operations retain their native verbs.
|
|
338
345
|
const privacy = /\b(?:visibility|personal|workspace|private|shared)\b/.test(normalized);
|
|
339
346
|
const rosterRead = /\b(?:list|inventory|roster)\b|\bwhich\b.*\b(?:have|available)\b/.test(normalized);
|
|
340
|
-
|
|
347
|
+
// Bare history asks read content; creation/configuration asks describe its contract.
|
|
348
|
+
const historyContract = /\b(?:visibility|configur\w*|settings?|privacy|defaults?|create|creating|make|add|new|set up|spawn|hire)\b/.test(normalized);
|
|
349
|
+
const contentRead = /\b(?:read|see|view|show|transcript)\b/.test(normalized)
|
|
350
|
+
|| (/\bhistory\b/.test(normalized) && !historyContract);
|
|
341
351
|
const contentSend = /\b(?:messages?|ask|tell|send|talk|continue|resume|handoff|delegat\w*)\b|\b(?:chat with|hand\w* off)\b/.test(normalized);
|
|
342
352
|
if (!group && privacy && ((verb === "list" && !rosterRead) || verb === "create"
|
|
343
353
|
|| (verb === "update" && /\bvisibility\b/.test(normalized))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OXYGEN_BUILD_VERSION = "1.1014.
|
|
1
|
+
export declare const OXYGEN_BUILD_VERSION = "1.1014.1132";
|
|
@@ -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.1132";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type WorkspacePlacementPlan, type WorkspacePlacementObservation } from "./workspace-placement.js";
|
|
2
2
|
export type WorkspacePlacementView = {
|
|
3
3
|
id: string;
|
|
4
4
|
provider: "internal";
|
|
@@ -123,6 +123,7 @@ export declare function workspacePlacementPreviewPage(plan: WorkspacePlacementPl
|
|
|
123
123
|
billing_mode: "internal";
|
|
124
124
|
credits: 0;
|
|
125
125
|
allow_paused_probe: boolean;
|
|
126
|
+
receiver_scope?: "workspace" | "network";
|
|
126
127
|
rendered_at?: string;
|
|
127
128
|
rendering?: {
|
|
128
129
|
tracking_opens: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliHttpError } from "./cli-http-error.js";
|
|
2
|
+
import { placementReceiverLabel } from "./workspace-placement.js";
|
|
2
3
|
export function workspacePlacementView(input, webUrl) {
|
|
3
4
|
const variants = new Map();
|
|
4
5
|
const probes = input.plan.probes.map(p => {
|
|
@@ -46,12 +47,12 @@ export function workspacePlacementView(input, webUrl) {
|
|
|
46
47
|
else if (out?.state === "read_blocked")
|
|
47
48
|
v.read_blocked++;
|
|
48
49
|
const state = out?.state ?? (input.deliveries?.[p.id]?.status === "sent" ? "awaiting_results" : input.deliveries?.[p.id]?.status ?? "pending");
|
|
49
|
-
return { ...workspacePlacementProbeMeta(p), state, status_label: workspacePlacementStateLabel(state), ...(input.deliveries?.[p.id] ? { delivery: input.deliveries[p.id] } : {}), ...(o ? { observation: o } : {}) };
|
|
50
|
+
return { ...workspacePlacementProbeMeta(p), state, status_label: workspacePlacementStateLabel(state), ...(input.deliveries?.[p.id] ? { delivery: input.deliveries[p.id] } : {}), ...(o ? { observation: p.receiver.network_id ? { ...o, receiver_message_id: "private" } : o } : {}) };
|
|
50
51
|
});
|
|
51
52
|
const settled = input.plan.probes.filter(p => input.outcomes[p.id]).length;
|
|
52
53
|
return { id: input.id, provider: "internal", billing_mode: "internal", credits_used: 0, status: settled < probes.length ? "running" : probes.every(p => p.state === "observed") ? "completed" : "incomplete",
|
|
53
54
|
created_at: new Date(input.created_at).toISOString(), message_count: probes.length, variants: [...variants.values()], probes, plan_hash: input.plan.plan_hash, web_url: webUrl,
|
|
54
|
-
directional_note: "Observed placement in
|
|
55
|
+
directional_note: "Observed placement in participating receiving accounts; not a campaign placement guarantee. Primary and Focused are separate provider classifications." };
|
|
55
56
|
}
|
|
56
57
|
export function workspacePlacementStateLabel(state) {
|
|
57
58
|
return { pending: "Queued", leased: "Preparing send", sent: "Awaiting receiver observation", awaiting_results: "Awaiting receiver observation", observed: "Observed", not_observed: "Not observed within two hours", send_failed: "Send failed", send_unknown: "Send outcome unknown; do not resend", read_blocked: "Receiver could not be checked", uncertain: "Send outcome unknown; do not resend", canceled: "Send canceled", failed: "Send failed" }[state] ?? "Pending";
|
|
@@ -64,7 +65,7 @@ function pageNumber(value, fallback, max, field) {
|
|
|
64
65
|
return value;
|
|
65
66
|
}
|
|
66
67
|
export function workspacePlacementProbeMeta(probe) {
|
|
67
|
-
return { id: probe.id, sender: probe.sender.address, sender_name: probe.sender.display_name ?? null, receiver: probe.receiver
|
|
68
|
+
return { id: probe.id, sender: probe.sender.address, sender_name: probe.sender.display_name ?? null, receiver: placementReceiverLabel(probe.receiver), receiver_provider: probe.receiver.provider, variant: probe.variant.key, subject: probe.variant.subject,
|
|
68
69
|
body_text_characters: Array.from(probe.variant.body_text).length, body_html_characters: Array.from(probe.variant.body_html ?? "").length };
|
|
69
70
|
}
|
|
70
71
|
export function workspacePlacementCopyPage(plan, input) {
|
|
@@ -81,7 +82,7 @@ export function workspacePlacementCopyPage(plan, input) {
|
|
|
81
82
|
if (limit < 1)
|
|
82
83
|
throw new CliHttpError(400, "workspace_test_page_invalid", "copy_limit must be at least 1.");
|
|
83
84
|
const end = Math.min(chars.length, offset + limit);
|
|
84
|
-
return { id: probe.id, variant: probe.variant.key, sender: probe.sender.address, sender_name: probe.sender.display_name ?? null, receiver: probe.receiver
|
|
85
|
+
return { id: probe.id, variant: probe.variant.key, sender: probe.sender.address, sender_name: probe.sender.display_name ?? null, receiver: placementReceiverLabel(probe.receiver), subject: probe.variant.subject, part, offset, total_characters: chars.length, text: chars.slice(offset, end).join(""), has_more: end < chars.length, next_offset: end < chars.length ? end : null };
|
|
85
86
|
}
|
|
86
87
|
export function workspacePlacementProbePage(probes, input, defaultLimit = 10) {
|
|
87
88
|
const limit = pageNumber(input.limit, defaultLimit, 20, "limit");
|
|
@@ -11,6 +11,8 @@ export type WorkspacePlacementMailbox = {
|
|
|
11
11
|
transport: string;
|
|
12
12
|
display_name?: string | null;
|
|
13
13
|
signature_html: string | null;
|
|
14
|
+
organization_id?: string;
|
|
15
|
+
network_id?: string;
|
|
14
16
|
};
|
|
15
17
|
export type WorkspacePlacementProbe = {
|
|
16
18
|
id: string;
|
|
@@ -25,6 +27,7 @@ export type WorkspacePlacementPlan = {
|
|
|
25
27
|
billing_mode: "internal";
|
|
26
28
|
credits: 0;
|
|
27
29
|
allow_paused_probe: boolean;
|
|
30
|
+
receiver_scope?: "workspace" | "network";
|
|
28
31
|
rendered_at?: string;
|
|
29
32
|
rendering?: {
|
|
30
33
|
tracking_opens: boolean;
|
|
@@ -48,6 +51,7 @@ export declare function buildWorkspacePlacementPlan(input: {
|
|
|
48
51
|
allowPausedProbe: boolean;
|
|
49
52
|
renderedAt?: string;
|
|
50
53
|
rendering?: WorkspacePlacementPlan["rendering"];
|
|
54
|
+
network?: boolean;
|
|
51
55
|
renderVariant?: (variant: WorkspacePlacementVariant, sender: WorkspacePlacementMailbox, probeId: string) => WorkspacePlacementVariant;
|
|
52
56
|
}): WorkspacePlacementPlan;
|
|
53
57
|
export type WorkspacePlacementObservation = {
|
|
@@ -75,3 +79,6 @@ export declare function workspacePlacementPayload(probe: WorkspacePlacementProbe
|
|
|
75
79
|
apply_mailbox_signature: boolean;
|
|
76
80
|
diagnostic_probe_id: string;
|
|
77
81
|
};
|
|
82
|
+
/** Foreign identities remain private even in previews, copy pages and manifests. */
|
|
83
|
+
export declare function placementReceiverLabel(receiver: WorkspacePlacementMailbox): string;
|
|
84
|
+
export declare function publicWorkspacePlacementPlan(plan: WorkspacePlacementPlan): WorkspacePlacementPlan;
|
|
@@ -51,17 +51,23 @@ export function buildWorkspacePlacementPlan(input) {
|
|
|
51
51
|
else
|
|
52
52
|
pairs++;
|
|
53
53
|
}
|
|
54
|
-
if (pairs * variants.length < 1 || pairs * variants.length > 50)
|
|
54
|
+
if ((input.network ? senders.length * variants.length * new Set(receivers.map(r => r.provider)).size : pairs * variants.length) < 1 || (input.network ? senders.length * variants.length * new Set(receivers.map(r => r.provider)).size : pairs * variants.length) > 50)
|
|
55
55
|
invalid("A workspace test must send between one and 50 messages after excluding self-sends.");
|
|
56
56
|
const probes = [];
|
|
57
57
|
// Reject oversized matrices before signatures/HTML/tracking allocate per-probe content.
|
|
58
58
|
// Rotate variants per receiver: no variant always gets the first submission.
|
|
59
|
-
for (const sender of senders) {
|
|
59
|
+
for (const [senderIndex, sender] of senders.entries()) {
|
|
60
60
|
for (const [index, receiver] of receivers.entries()) {
|
|
61
61
|
if (self(sender, receiver))
|
|
62
62
|
continue;
|
|
63
63
|
for (let offset = 0; offset < variants.length; offset++) {
|
|
64
64
|
const original = variants[(offset + index) % variants.length];
|
|
65
|
+
if (input.network) {
|
|
66
|
+
const group = receivers.filter(r => r.provider === receiver.provider);
|
|
67
|
+
const sequence = senderIndex * variants.length + variants.indexOf(original);
|
|
68
|
+
if (group[sequence % group.length].id !== receiver.id)
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
65
71
|
const id = probeId(`${organizationId}:${requestId}:${sender.id}:${receiver.id}:${original.key}`);
|
|
66
72
|
const variant = input.renderVariant ? input.renderVariant(original, sender, id) : original;
|
|
67
73
|
probes.push({ id, sender, receiver, variant });
|
|
@@ -71,7 +77,7 @@ export function buildWorkspacePlacementPlan(input) {
|
|
|
71
77
|
if (!probes.length || probes.length > 50)
|
|
72
78
|
invalid("A workspace test must send between one and 50 messages after excluding self-sends.");
|
|
73
79
|
const plan = { request_id: requestId, organization_id: organizationId, provider: "internal",
|
|
74
|
-
billing_mode: "internal", credits: 0, allow_paused_probe: input.allowPausedProbe,
|
|
80
|
+
...(input.network ? { receiver_scope: "network" } : {}), billing_mode: "internal", credits: 0, allow_paused_probe: input.allowPausedProbe,
|
|
75
81
|
...(input.renderedAt ? { rendered_at: input.renderedAt } : {}), ...(input.rendering ? { rendering: input.rendering } : {}),
|
|
76
82
|
probes, excluded_self_sends: excluded, message_count: probes.length };
|
|
77
83
|
return { ...plan, plan_hash: createHash("sha256").update(JSON.stringify(plan)).digest("hex") };
|
|
@@ -92,3 +98,10 @@ export function workspacePlacementPayload(probe) {
|
|
|
92
98
|
return { kind: "email_send", to: probe.receiver.address, subject: probe.variant.subject, body: probe.variant.body_text,
|
|
93
99
|
...(probe.variant.body_html ? { body_html: probe.variant.body_html } : {}), apply_mailbox_signature: false, diagnostic_probe_id: probe.id };
|
|
94
100
|
}
|
|
101
|
+
/** Foreign identities remain private even in previews, copy pages and manifests. */
|
|
102
|
+
export function placementReceiverLabel(receiver) {
|
|
103
|
+
return receiver.network_id ? `${receiver.provider === "google" ? "Google" : "Microsoft"} receiver ${receiver.network_id.slice(0, 8)}` : receiver.address;
|
|
104
|
+
}
|
|
105
|
+
export function publicWorkspacePlacementPlan(plan) {
|
|
106
|
+
return { ...plan, probes: plan.probes.map(p => ({ ...p, receiver: p.receiver.network_id ? { id: p.receiver.network_id, address: placementReceiverLabel(p.receiver), provider: p.receiver.provider, transport: "network", signature_html: null } : p.receiver })) };
|
|
107
|
+
}
|