@oxygen-agent/cli 1.1014.19 → 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.
- package/README.md +1 -1
- package/dist/command-manifest.js +1 -1
- package/dist/help.js +21 -1
- package/dist/index.js +50 -14
- package/dist/json-output.d.ts +3 -0
- package/dist/json-output.js +33 -0
- package/dist/workspace-skill-commands.d.ts +9 -0
- package/dist/workspace-skill-commands.js +134 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +9 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +7 -4
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +18 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.js +62 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +5 -1
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +178 -63
- package/node_modules/@oxygen/shared/dist/email-health-status.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/email-health-status.js +16 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +3 -2
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +9 -6
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +4 -2
- package/node_modules/@oxygen/shared/dist/langfuse.js +123 -66
- package/node_modules/@oxygen/shared/dist/table-run-summary.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/table-run-summary.js +26 -0
- 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-skills.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/workspace-skills.js +102 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
await
|
|
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 {
|
|
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(
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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,
|
|
188
|
-
* their own key. A BYOK run on the tier
|
|
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
|
|
485
|
-
//
|
|
486
|
-
//
|
|
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 =
|
|
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,
|
|
570
|
-
* their own key. A BYOK run on the tier
|
|
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.
|
|
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
|