@oxygen-agent/cli 1.1014.18 → 1.1014.69

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 (31) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +1 -1
  3. package/dist/help.js +21 -1
  4. package/dist/index.js +50 -14
  5. package/dist/json-output.d.ts +3 -0
  6. package/dist/json-output.js +33 -0
  7. package/dist/workspace-skill-commands.d.ts +9 -0
  8. package/dist/workspace-skill-commands.js +134 -0
  9. package/node_modules/@oxygen/shared/dist/capability-discovery.js +9 -0
  10. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  11. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +1 -1
  12. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +7 -4
  13. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +14 -0
  14. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +18 -0
  15. package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.d.ts +6 -0
  16. package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.js +62 -0
  17. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +5 -1
  18. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +178 -63
  19. package/node_modules/@oxygen/shared/dist/email-health-status.d.ts +11 -0
  20. package/node_modules/@oxygen/shared/dist/email-health-status.js +16 -0
  21. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +3 -2
  22. package/node_modules/@oxygen/shared/dist/hosted-ai.js +9 -6
  23. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +4 -2
  24. package/node_modules/@oxygen/shared/dist/langfuse.js +123 -66
  25. package/node_modules/@oxygen/shared/dist/table-run-summary.d.ts +2 -0
  26. package/node_modules/@oxygen/shared/dist/table-run-summary.js +26 -0
  27. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  28. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  29. package/node_modules/@oxygen/shared/dist/workspace-skills.d.ts +26 -0
  30. package/node_modules/@oxygen/shared/dist/workspace-skills.js +102 -0
  31. package/package.json +1 -1
@@ -0,0 +1,62 @@
1
+ import { arr, int, obj, str } from "./schema.js";
2
+ const name = str({
3
+ minLength: 1,
4
+ maxLength: 100,
5
+ description: "Exact workspace skill name returned by list or import (workspace-…).",
6
+ });
7
+ const file = str({
8
+ minLength: 1,
9
+ maxLength: 240,
10
+ description: "Relative file path; defaults to SKILL.md.",
11
+ });
12
+ const source_url = str({
13
+ maxLength: 2_000,
14
+ description: "Optional HTTPS origin for provenance. Content must be supplied; this does not fetch the URL.",
15
+ });
16
+ export const WORKSPACE_SKILL_FILE_CONTRACT = obj({ path: file, content: str({ maxLength: 120_000 }) }, { required: ["path", "content"] });
17
+ const operation = (verb, method, contract, summary, manualCli = false) => ({
18
+ id: `workspace_skills.${verb}`,
19
+ route: `/api/cli/skills/workspace/${verb}`,
20
+ method,
21
+ contract,
22
+ cli: { path: ["skills", "workspace", verb], summary },
23
+ mcp: {
24
+ name: `oxygen_workspace_skills_${verb}`,
25
+ title: `Workspace skills: ${verb}`,
26
+ description: summary,
27
+ pack: "workspace",
28
+ ...(verb === "import" ? { annotations: { openWorldHint: true } } : {}),
29
+ },
30
+ mutates: method !== "GET",
31
+ spends: false,
32
+ ...(manualCli ? { generated: { cli: false, mcp: true } } : {}),
33
+ });
34
+ export const WORKSPACE_SKILL_OPERATIONS = [
35
+ operation("list", "GET", obj({
36
+ query: str(),
37
+ cursor: str(),
38
+ limit: int({ minimum: 1, maximum: 100 }),
39
+ }), "Find private workspace-authored or imported skills by name or description. Read-only, 0 credits; next_cursor continues the list."),
40
+ operation("get", "GET", obj({ name, file }, { required: ["name"] }), "Read a workspace skill's SKILL.md or one supporting text file, with exact content, revision and the document index. Skills are task guidance; they grant no tool, spend or approval authority."),
41
+ operation("create", "POST", obj({
42
+ name: str({ minLength: 1, maxLength: 64 }),
43
+ description: str({ minLength: 1, maxLength: 1_024 }),
44
+ instructions: str({ minLength: 1, maxLength: 120_000 }),
45
+ }, { required: ["name", "description", "instructions"] }), "Author a reusable private workspace skill in Knowledge from a lowercase-hyphen name, description and instructions. Creates SKILL.md; no inference, provider call or credit charge.", true),
46
+ operation("import", "POST", obj({
47
+ name: str({ maxLength: 64 }),
48
+ files: arr(WORKSPACE_SKILL_FILE_CONTRACT, { minItems: 1, maxItems: 50 }),
49
+ url: str({
50
+ maxLength: 2_000,
51
+ description: "Public HTTPS SKILL.md URL (raw text or GitHub file). Choose url or files. A URL imports the entrypoint only; supply files for a complete folder.",
52
+ }),
53
+ source_url,
54
+ }), "Import a private workspace skill: supply root SKILL.md plus supporting text files, or a public HTTPS SKILL.md URL. Preserves content and source. Create-only, atomic, at most 50 text files/1 MB. Scripts stay inert. CLI accepts --file, --directory or --url. 0 credits.", true),
55
+ operation("update", "POST", obj({
56
+ name,
57
+ file,
58
+ content: str({ maxLength: 120_000 }),
59
+ expected_revision: int({ minimum: 1 }),
60
+ }, { required: ["name", "content", "expected_revision"] }), "Edit one workspace skill file at its exact expected revision, preserving Knowledge history. Read it first; stale revisions refuse. Defaults to SKILL.md. 0 credits."),
61
+ operation("archive", "POST", obj({ name, expected_revision: int({ minimum: 1 }) }, { required: ["name", "expected_revision"] }), "Archive a workspace skill's SKILL.md at its expected revision, removing the skill from discovery while preserving files and history in Knowledge. 0 credits."),
62
+ ];
@@ -21,7 +21,11 @@ export async function acquireDiagnosticStorageLock(path, waitMs = 0) {
21
21
  database?.close();
22
22
  database = undefined;
23
23
  const busy = error && typeof error === "object" && "errcode" in error && (error.errcode === 5 || error.errcode === 6);
24
- if (!busy || performance.now() >= deadline)
24
+ // Only contention is skippable. Preserve permission, capacity and runtime
25
+ // failures for the caller's safe classification rather than calling them busy.
26
+ if (!busy)
27
+ throw error;
28
+ if (performance.now() >= deadline)
25
29
  return null;
26
30
  // A synchronous SQLite busy timeout would block the same-process owner
27
31
  // from finishing its asynchronous fsync and releasing this lock.
@@ -12,6 +12,125 @@ let warned = false;
12
12
  let flushing = null;
13
13
  let timer;
14
14
  let onStored;
15
+ const admissions = new Map();
16
+ class StorageFailure extends Error {
17
+ stage;
18
+ failure;
19
+ constructor(stage, failure) {
20
+ super("Diagnostic storage failed");
21
+ this.stage = stage;
22
+ this.failure = failure;
23
+ }
24
+ }
25
+ async function storageStage(stage, action) {
26
+ try {
27
+ return await action();
28
+ }
29
+ catch (error) {
30
+ throw error instanceof StorageFailure ? error : new StorageFailure(stage, error);
31
+ }
32
+ }
33
+ function storageFailureFields(error) {
34
+ const stage = error instanceof StorageFailure ? error.stage : "encode";
35
+ const failure = error instanceof StorageFailure ? error.failure : error;
36
+ const value = failure && typeof failure === "object" ? failure : {};
37
+ const code = typeof value.code === "string" && /^(?:E[A-Z0-9_]{1,30}|SQLITE_[A-Z_]{1,30}|spool_full|lock_timeout)$/.test(value.code) ? value.code : null;
38
+ const sqlite = typeof value.errcode === "number" ? value.errcode : null;
39
+ const cause = code === "lock_timeout" ? "lock_timeout" : code === "spool_full" ? "spool_limit"
40
+ : ["EACCES", "EPERM", "EROFS"].includes(code ?? "") || (sqlite !== null && [3, 8, 23].includes(sqlite & 255)) ? "permission"
41
+ : ["ENOSPC", "EDQUOT"].includes(code ?? "") || (sqlite !== null && (sqlite & 255) === 13) ? "filesystem_capacity" : "other";
42
+ const status = value.$metadata?.httpStatusCode;
43
+ return { cause, stage, error_code: code, sqlite_code: sqlite, errno: typeof value.errno === "number" ? value.errno : null,
44
+ http_status: typeof status === "number" && status >= 100 && status <= 599 ? status : null, queue_role: diagnosticRole() };
45
+ }
46
+ function diagnosticRole() {
47
+ const role = process.env.OXYGEN_WORKER_QUEUE_ROLE;
48
+ return role && ["relay", "bulk", "maintenance", "interactive", "provider", "outbound"].includes(role) ? role
49
+ : process.env.OXYGEN_PROCESS_ROLE === "worker" ? "legacy" : "web";
50
+ }
51
+ /** Coalesce local contenders; the kernel mutex still owns cross-process admission. */
52
+ function admitDiagnostic(directory, record, encoded) {
53
+ return new Promise((resolve, reject) => {
54
+ const pending = admissions.get(directory);
55
+ const admission = { record, encoded, deadline: performance.now() + 5_000, resolve, reject };
56
+ if (pending)
57
+ pending.pending.push(admission);
58
+ else {
59
+ const queue = { pending: [admission] };
60
+ admissions.set(directory, queue);
61
+ queueMicrotask(() => { void drainAdmissions(directory, queue); });
62
+ }
63
+ });
64
+ }
65
+ async function drainAdmissions(directory, queue) {
66
+ try {
67
+ while (queue.pending.length) {
68
+ const batch = queue.pending.splice(0, 32);
69
+ let release = null;
70
+ try {
71
+ release = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, ".admission.lock"), Math.max(0, batch[0].deadline - performance.now())));
72
+ if (!release)
73
+ throw new StorageFailure("lock", { code: "lock_timeout" });
74
+ const started = performance.now();
75
+ let { used, files } = await storageStage("inspect", () => inspectDiagnosticSpool(directory));
76
+ queue.files = files;
77
+ const scanMs = performance.now() - started;
78
+ for (const admission of batch) {
79
+ try {
80
+ if (performance.now() >= admission.deadline)
81
+ throw new StorageFailure("lock", { code: "lock_timeout" });
82
+ if (used + admission.encoded.compressed.length > DIAGNOSTIC_MAX_BYTES)
83
+ throw new StorageFailure("capacity", { code: "spool_full" });
84
+ // Reserve conservatively even if cleanup of a failed partial write fails.
85
+ used += admission.encoded.compressed.length;
86
+ admission.resolve(await writeDiagnosticSpool(directory, admission.record.id, admission.encoded.compressed));
87
+ }
88
+ catch (error) {
89
+ admission.reject(error);
90
+ }
91
+ }
92
+ log("info", "diagnostics.spool_admitted", { queue_role: diagnosticRole(), batch_records: batch.length, spool_files: files.length,
93
+ spool_bytes: used, scan_ms: scanMs, admission_wait_ms: performance.now() - (batch[0].deadline - 5_000) });
94
+ }
95
+ catch (error) {
96
+ for (const admission of batch)
97
+ admission.reject(error);
98
+ }
99
+ finally {
100
+ await release?.();
101
+ }
102
+ }
103
+ }
104
+ catch (error) {
105
+ for (const admission of queue.pending.splice(0))
106
+ admission.reject(error);
107
+ }
108
+ finally {
109
+ admissions.delete(directory);
110
+ }
111
+ }
112
+ async function writeDiagnosticSpool(directory, id, compressed) {
113
+ const path = join(directory, `${id}.json.gz`);
114
+ const temporary = join(directory, `${id}.${randomUUID()}.tmp`);
115
+ let handle;
116
+ try {
117
+ handle = await storageStage("open", () => open(temporary, "wx", 0o600));
118
+ await storageStage("write", () => handle.writeFile(compressed));
119
+ await storageStage("sync", () => handle.sync());
120
+ await handle.close();
121
+ handle = undefined;
122
+ await storageStage("rename", () => rename(temporary, path));
123
+ return path;
124
+ }
125
+ finally {
126
+ try {
127
+ await handle?.close();
128
+ }
129
+ finally {
130
+ await unlink(temporary).catch(() => undefined);
131
+ }
132
+ }
133
+ }
15
134
  export function observeDiagnosticStorage(observer) {
16
135
  onStored = observer;
17
136
  startDiagnosticRetry();
@@ -102,40 +221,18 @@ export async function storeDiagnostic(record) {
102
221
  encoded = encodeDiagnostic(record);
103
222
  }
104
223
  record = parseDiagnosticRecord(JSON.parse(encoded.bytes.toString("utf8")));
105
- await mkdir(directory, { recursive: true, mode: 0o700 });
106
- // Admission and expiry share one lock across web/worker processes on a host.
107
- const lockPath = join(directory, ".admission.lock");
108
- const release = await acquireDiagnosticStorageLock(lockPath, 5_000);
109
- if (!release)
110
- throw new Error("Diagnostic spool admission busy");
111
- try {
112
- const { used } = await inspectDiagnosticSpool(directory);
113
- if (used + encoded.compressed.length > DIAGNOSTIC_MAX_BYTES)
114
- throw new Error("Diagnostic spool is full");
115
- path = join(directory, `${record.id}.json.gz`);
116
- const temporary = join(directory, `${record.id}.${randomUUID()}.tmp`);
117
- const handle = await open(temporary, "wx", 0o600);
118
- try {
119
- await handle.writeFile(encoded.compressed);
120
- await handle.sync();
121
- }
122
- finally {
123
- await handle.close();
124
- }
125
- await rename(temporary, path);
126
- }
127
- finally {
128
- await release();
129
- }
224
+ await storageStage("mkdir", () => mkdir(directory, { recursive: true, mode: 0o700 }));
225
+ path = await admitDiagnostic(directory, record, encoded);
130
226
  startDiagnosticRetry();
131
- const receipt = await uploadEncodedDiagnostic(record, encoded);
227
+ const receipt = await storageStage("upload", () => uploadEncodedDiagnostic(record, encoded));
132
228
  await unlink(path).catch(() => undefined);
133
229
  return receipt;
134
230
  }
135
- catch {
231
+ catch (error) {
136
232
  // Neither storage credentials nor upstream exception text belong in logs.
137
- log("warn", path ? "diagnostics.capture_deferred" : "diagnostics.capture_lost", { diagnostic_id: record.id, reason: path ? "upload_failed" : "spool_admission_failed" });
138
- recordTelemetryCounter(path ? "oxygen.diagnostics.deferred" : "oxygen.diagnostics.lost", 1, { source: record.surface });
233
+ const fields = storageFailureFields(error);
234
+ log("warn", path ? "diagnostics.capture_deferred" : "diagnostics.capture_lost", { diagnostic_id: record.id, reason: path ? "upload_failed" : "spool_admission_failed", ...fields });
235
+ recordTelemetryCounter(path ? "oxygen.diagnostics.deferred" : "oxygen.diagnostics.lost", 1, { source: record.surface, cause: fields.cause, queue_role: diagnosticRole() });
139
236
  return null;
140
237
  }
141
238
  }
@@ -155,11 +252,11 @@ async function uploadEncodedDiagnostic(record, encoded) {
155
252
  Metadata: { sha256: encoded.sha256, bytes: String(encoded.bytes.length), expires_at: new Date(Date.parse(record.started_at) + DIAGNOSTIC_RETENTION_MS).toISOString() },
156
253
  }), { abortSignal: AbortSignal.timeout(15_000) });
157
254
  log("info", "diagnostics.capture_stored", { ...diagnosticSummary(record), org_id: record.organization_id, user_id: record.user_id,
158
- diagnostic_key: key, bytes: encoded.bytes.length, compressed_bytes: encoded.compressed.length, sha256: encoded.sha256 });
255
+ diagnostic_key: key, bytes: encoded.bytes.length, compressed_bytes: encoded.compressed.length, sha256: encoded.sha256, queue_role: diagnosticRole() });
159
256
  recordTelemetryCounter("oxygen.diagnostics.stored", 1, { source: record.surface, layer: record.layer });
160
257
  const receipt = { key, sha256: encoded.sha256, bytes: encoded.bytes.length, compressed_bytes: encoded.compressed.length, record };
161
258
  if (onStored)
162
- await onStored(receipt);
259
+ await storageStage("observer", () => onStored(receipt));
163
260
  return receipt;
164
261
  }
165
262
  export function flushDiagnosticSpool() {
@@ -169,63 +266,81 @@ export function flushDiagnosticSpool() {
169
266
  if (!diagnosticsConfigured())
170
267
  return;
171
268
  const directory = process.env.OXYGEN_DIAGNOSTICS_SPOOL_DIR;
172
- const release = await acquireDiagnosticStorageLock(join(directory, ".upload.lock"));
269
+ const release = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, ".upload.lock")));
173
270
  if (!release)
174
271
  return;
175
272
  try {
176
273
  // Idle workers enforce the same expiry as new admissions. The admission
177
274
  // lock protects cleanup against a writer replacing an old record path.
178
- const admission = await acquireDiagnosticStorageLock(join(directory, ".admission.lock"));
179
- if (!admission)
180
- return;
181
275
  let files;
182
- try {
183
- files = (await inspectDiagnosticSpool(directory)).files.slice(0, 20);
276
+ const local = admissions.get(directory);
277
+ if (local) {
278
+ // Reuse the active writer's protected inspection. Waiting for local idle
279
+ // can starve retained evidence indefinitely under sustained capture.
280
+ files = local.files?.slice(0, 20) ?? [];
184
281
  }
185
- finally {
186
- await admission();
282
+ else {
283
+ const admission = await storageStage("lock", () => acquireDiagnosticStorageLock(join(directory, ".admission.lock")));
284
+ if (!admission)
285
+ return;
286
+ try {
287
+ files = (await storageStage("inspect", () => inspectDiagnosticSpool(directory))).files.slice(0, 20);
288
+ }
289
+ finally {
290
+ await admission();
291
+ }
187
292
  }
188
293
  for (const name of files) {
189
294
  const path = join(directory, name);
190
295
  try {
191
- const compressed = await readFile(path);
192
- const record = decodeDiagnostic(compressed);
193
- await uploadEncodedDiagnostic(record, encodeDiagnostic(record));
296
+ const compressed = await storageStage("read", () => readFile(path));
297
+ const record = await storageStage("decode", async () => decodeDiagnostic(compressed));
298
+ await storageStage("upload", () => uploadEncodedDiagnostic(record, encodeDiagnostic(record)));
194
299
  await unlink(path).catch(() => undefined);
195
300
  }
196
- catch { /* retain failed uploads; admission expires them visibly */ }
301
+ catch (error) {
302
+ log("warn", "diagnostics.spool_upload_deferred", { diagnostic_id: name.slice(0, -8), ...storageFailureFields(error) });
303
+ }
197
304
  }
198
305
  }
199
306
  finally {
200
307
  await release();
201
308
  }
202
- })().catch(() => undefined).finally(() => { flushing = null; });
309
+ })().catch(error => { log("warn", "diagnostics.spool_retry_failed", storageFailureFields(error)); }).finally(() => { flushing = null; });
203
310
  return flushing;
204
311
  }
205
312
  /** Caller holds admission lock; count retained bytes and expire actual filenames. */
206
313
  async function inspectDiagnosticSpool(directory) {
207
314
  let used = 0;
208
- const files = [];
209
- for (const name of await readdir(directory)) {
210
- const record = /^[0-9a-f-]+\.json\.gz$/.test(name);
211
- if (!record && !/^[0-9a-f-]+(?:\.[0-9a-f-]+)?\.tmp$/.test(name))
212
- continue;
213
- const path = join(directory, name);
214
- const info = await stat(path).catch(() => null);
215
- if (!info)
216
- continue;
217
- if (Date.now() - info.mtimeMs >= 72 * 60 * 60 * 1000) {
218
- await unlink(path);
219
- log("warn", "diagnostics.capture_lost", { reason: "spool_expired", bytes: info.size });
220
- recordTelemetryCounter("oxygen.diagnostics.lost", 1, { reason: "spool_expired" });
221
- }
222
- else {
223
- used += info.size;
224
- if (record)
225
- files.push(name);
315
+ const retained = [];
316
+ const names = (await readdir(directory)).filter(name => /^[0-9a-f-]+\.json\.gz$/.test(name) || /^[0-9a-f-]+(?:\.[0-9a-f-]+)?\.tmp$/.test(name));
317
+ for (let offset = 0; offset < names.length; offset += 64) {
318
+ const inspected = await Promise.all(names.slice(offset, offset + 64).map(async (name) => {
319
+ const path = join(directory, name);
320
+ const info = await stat(path).catch(error => { if (error?.code === "ENOENT")
321
+ return null; throw error; });
322
+ return { name, path, info };
323
+ }));
324
+ for (const { name, path, info } of inspected) {
325
+ const record = /^[0-9a-f-]+\.json\.gz$/.test(name);
326
+ if (!info)
327
+ continue;
328
+ if (Date.now() - info.mtimeMs >= 72 * 60 * 60 * 1000) {
329
+ const removed = await unlink(path).then(() => true).catch(error => { if (error?.code === "ENOENT")
330
+ return false; throw error; });
331
+ if (removed) {
332
+ log("warn", "diagnostics.capture_lost", { reason: "spool_expired", bytes: info.size, queue_role: diagnosticRole() });
333
+ recordTelemetryCounter("oxygen.diagnostics.lost", 1, { reason: "spool_expired" });
334
+ }
335
+ }
336
+ else {
337
+ used += info.size;
338
+ if (record)
339
+ retained.push({ name, mtimeMs: info.mtimeMs });
340
+ }
226
341
  }
227
342
  }
228
- return { used, files };
343
+ return { used, files: retained.sort((a, b) => a.mtimeMs - b.mtimeMs || a.name.localeCompare(b.name)).map(file => file.name) };
229
344
  }
230
345
  /** A metadata-only fallback for an unsupported value never claims full capture. */
231
346
  export function partialDiagnostic(record, reason) {
@@ -0,0 +1,11 @@
1
+ /** Health polling and measured evidence are independent clocks. Legacy checked_at
2
+ * values also advanced on empty polls and cannot establish measurement freshness. */
3
+ export type EmailHealthSyncStatus = "unknown" | "ok" | "blocked" | "failed";
4
+ export type EmailHealthSyncReason = "byok_requires_paid_plan" | "provider_read_failed";
5
+ export type EmailHealthFreshness = "current" | "stale" | "unmeasured";
6
+ export declare function readEmailHealthStatus(metadata: Record<string, unknown> | null | undefined, now?: number): {
7
+ evidenceAt: Date | null;
8
+ freshness: EmailHealthFreshness;
9
+ syncStatus: EmailHealthSyncStatus;
10
+ syncReason: EmailHealthSyncReason | null;
11
+ };
@@ -0,0 +1,16 @@
1
+ export function readEmailHealthStatus(metadata, now = Date.now()) {
2
+ const evidence = metadata?.health_evidence_at;
3
+ const timestamp = typeof evidence === "string" && /^\d{4}-\d{2}-\d{2}T/.test(evidence)
4
+ ? Date.parse(evidence) : NaN;
5
+ const evidenceAt = Number.isFinite(timestamp) && timestamp <= now ? new Date(timestamp) : null;
6
+ const status = metadata?.health_sync_status;
7
+ const syncStatus = status === "ok" || status === "blocked" || status === "failed" ? status : "unknown";
8
+ const reason = metadata?.health_sync_reason;
9
+ const syncReason = reason === "byok_requires_paid_plan" || reason === "provider_read_failed" ? reason : null;
10
+ return {
11
+ evidenceAt,
12
+ freshness: evidenceAt ? now - timestamp <= 24 * 60 * 60 * 1000 ? "current" : "stale" : "unmeasured",
13
+ syncStatus,
14
+ syncReason,
15
+ };
16
+ }
@@ -184,8 +184,9 @@ export declare function resolveHostedAiModel(input: {
184
184
  export declare function managedModelLabel(model: string, preferredUseCase?: HostedAiUseCase): string | null;
185
185
  /**
186
186
  * What an AI-column run's provenance records as its model: the tier label for
187
- * a managed run, and the model itself only when the customer pinned it on
188
- * their own key. A BYOK run on the tier default is still OXYGEN's pick.
187
+ * a managed generative run, System One for classification, and the model itself
188
+ * only when the customer pinned it on their own key. A BYOK run on the tier
189
+ * default is still OXYGEN's pick.
189
190
  */
190
191
  export declare function customerFacingAiColumnModel(input: {
191
192
  model: string;
@@ -481,11 +481,11 @@ export function resolveHostedAiModel(input) {
481
481
  const BACKUP_MODEL_LABEL = "Oxygen backup model";
482
482
  // TypeSafe's System One classifier (`SYSTEM_ONE_MODEL` in @oxygen/integrations,
483
483
  // pinned as `jev-<version>`) is OXYGEN's choice too. It answers classification
484
- // columns and inbox triage at the managed Fast price, so provenance written with
485
- // its id (before the write-time fix, or by a later version bump) reads as that
486
- // tier. Matched by shape so a version bump stays covered.
484
+ // columns and inbox triage without text generation. Its flat classification
485
+ // price and inference class differ from the generative tiers. Keep a distinct
486
+ // label while hiding the managed vendor id, including after a version bump.
487
487
  const SYSTEM_ONE_MODEL_ID = /^jev-(?:\d+(?:\.\d+)*|latest)$/;
488
- const SYSTEM_ONE_MODEL_LABEL = MANAGED_AI_TIER_LABELS.low;
488
+ const SYSTEM_ONE_MODEL_LABEL = "System One";
489
489
  // Doppler can swap any tier's model without a deploy; the swapped-in id is still
490
490
  // OXYGEN's choice, so it is redacted like the registry default it replaces.
491
491
  const MODEL_OVERRIDE_ENV_PREFIXES = ["OXYGEN_OPENROUTER_MODEL_", "OXYGEN_COPILOT_MODEL_", "OXYGEN_AGENT_MODEL_"];
@@ -566,10 +566,13 @@ function labelForManagedModel(labels, model) {
566
566
  }
567
567
  /**
568
568
  * What an AI-column run's provenance records as its model: the tier label for
569
- * a managed run, and the model itself only when the customer pinned it on
570
- * their own key. A BYOK run on the tier default is still OXYGEN's pick.
569
+ * a managed generative run, System One for classification, and the model itself
570
+ * only when the customer pinned it on their own key. A BYOK run on the tier
571
+ * default is still OXYGEN's pick.
571
572
  */
572
573
  export function customerFacingAiColumnModel(input) {
574
+ if (SYSTEM_ONE_MODEL_ID.test(input.model))
575
+ return SYSTEM_ONE_MODEL_LABEL;
573
576
  if (input.credentialMode === "byok" && managedModelLabel(input.model, "ai_column") === null)
574
577
  return input.model;
575
578
  return MANAGED_AI_TIER_LABELS[input.reasoningLevel];
@@ -105,7 +105,7 @@ export type LlmTracingClient = {
105
105
  score(body: LlmScoreBody): Promise<boolean>;
106
106
  /** Never rejects; bounded at 5s on workers, 15s in serverless after(). */
107
107
  flush(): Promise<void>;
108
- /** Flush + stop timers. Same worker/serverless bound as flush(). */
108
+ /** Flush + stop timers. Never rejects; bounded at 30s to drain active retries. */
109
109
  shutdown(): Promise<void>;
110
110
  };
111
111
  /**
@@ -178,12 +178,14 @@ export type LlmExportSummary = {
178
178
  * spans, so exported:dropped is not a like-for-like ratio.
179
179
  */
180
180
  dropped(count: number): void;
181
+ retried(): void;
182
+ recovered(count: number): void;
181
183
  /** Logs the window when it is at least a minute old; otherwise no-op. */
182
184
  maybeReport(): void;
183
185
  /** Logs whatever is pending regardless of age (process shutdown). */
184
186
  reportNow(): void;
185
187
  };
186
- export declare function createLlmExportSummary(now?: () => number): LlmExportSummary;
188
+ export declare function createLlmExportSummary(now?: () => number, context?: Record<string, unknown>): LlmExportSummary;
187
189
  /**
188
190
  * Resolves a registered code prompt (name + content hash) to the native
189
191
  * Langfuse prompt version the deploy-time sync created for it. Synchronous