@schlessera/brain-ui-server 0.18.0 → 0.20.0
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 +5 -1
- package/data/model-prices.json +63 -0
- package/dist/activity/digest.d.ts +29 -0
- package/dist/activity/digest.d.ts.map +1 -0
- package/dist/activity/digest.js +89 -0
- package/dist/activity/digest.js.map +1 -0
- package/dist/activity/notify.d.ts +62 -0
- package/dist/activity/notify.d.ts.map +1 -0
- package/dist/activity/notify.js +193 -0
- package/dist/activity/notify.js.map +1 -0
- package/dist/activity/push-sender.d.ts +63 -0
- package/dist/activity/push-sender.d.ts.map +1 -0
- package/dist/activity/push-sender.js +131 -0
- package/dist/activity/push-sender.js.map +1 -0
- package/dist/activity/query.d.ts +16 -0
- package/dist/activity/query.d.ts.map +1 -0
- package/dist/activity/query.js +155 -0
- package/dist/activity/query.js.map +1 -0
- package/dist/activity/recorder.d.ts +53 -0
- package/dist/activity/recorder.d.ts.map +1 -0
- package/dist/activity/recorder.js +238 -0
- package/dist/activity/recorder.js.map +1 -0
- package/dist/activity/runtime.d.ts +37 -0
- package/dist/activity/runtime.d.ts.map +1 -0
- package/dist/activity/runtime.js +127 -0
- package/dist/activity/runtime.js.map +1 -0
- package/dist/activity/span-sink.d.ts +22 -0
- package/dist/activity/span-sink.d.ts.map +1 -0
- package/dist/activity/span-sink.js +138 -0
- package/dist/activity/span-sink.js.map +1 -0
- package/dist/activity/store.d.ts +256 -0
- package/dist/activity/store.d.ts.map +1 -0
- package/dist/activity/store.js +657 -0
- package/dist/activity/store.js.map +1 -0
- package/dist/activity/stream.d.ts +49 -0
- package/dist/activity/stream.d.ts.map +1 -0
- package/dist/activity/stream.js +353 -0
- package/dist/activity/stream.js.map +1 -0
- package/dist/agent/backend.d.ts +13 -4
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +68 -5
- package/dist/agent/backend.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +33 -2
- package/dist/app.js.map +1 -1
- package/dist/config/env.d.ts +36 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +66 -2
- package/dist/config/env.js.map +1 -1
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +20 -5
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +32 -6
- package/dist/db/client.js.map +1 -1
- package/dist/db/settings.d.ts +15 -0
- package/dist/db/settings.d.ts.map +1 -1
- package/dist/db/settings.js +43 -0
- package/dist/db/settings.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/pricing/model-pricing.d.ts +61 -0
- package/dist/pricing/model-pricing.d.ts.map +1 -0
- package/dist/pricing/model-pricing.js +379 -0
- package/dist/pricing/model-pricing.js.map +1 -0
- package/dist/routes/activity.d.ts +20 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +308 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/models.d.ts +6 -0
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +38 -3
- package/dist/routes/models.js.map +1 -1
- package/dist/routes/push.d.ts +6 -0
- package/dist/routes/push.d.ts.map +1 -0
- package/dist/routes/push.js +73 -0
- package/dist/routes/push.js.map +1 -0
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +20 -4
- package/dist/routes/sessions.js.map +1 -1
- package/dist/ws/bridge.d.ts +2 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +27 -2
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +9 -1
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +11 -0
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/host.d.ts +16 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +2 -0
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +55 -1
- package/dist/ws/run-session.js.map +1 -1
- package/migrations/007_activity.sql +114 -0
- package/migrations/008_push_subscriptions.sql +24 -0
- package/migrations/009_activity_followups.sql +22 -0
- package/migrations/010_effective_cost.sql +21 -0
- package/package.json +6 -3
- package/src/activity/digest.ts +120 -0
- package/src/activity/notify.ts +307 -0
- package/src/activity/push-sender.ts +215 -0
- package/src/activity/query.ts +187 -0
- package/src/activity/recorder.ts +313 -0
- package/src/activity/runtime.ts +169 -0
- package/src/activity/span-sink.ts +152 -0
- package/src/activity/store.ts +1069 -0
- package/src/activity/stream.ts +396 -0
- package/src/agent/backend.ts +91 -10
- package/src/app.ts +35 -2
- package/src/config/env.ts +90 -2
- package/src/cron/scheduler.ts +28 -8
- package/src/db/client.ts +31 -6
- package/src/db/settings.ts +56 -0
- package/src/index.ts +23 -0
- package/src/pricing/model-pricing.ts +497 -0
- package/src/routes/activity.ts +371 -0
- package/src/routes/models.ts +57 -6
- package/src/routes/push.ts +90 -0
- package/src/routes/sessions.ts +24 -4
- package/src/ws/bridge.ts +29 -2
- package/src/ws/connection.ts +9 -1
- package/src/ws/dispatch.ts +13 -0
- package/src/ws/host.ts +18 -0
- package/src/ws/run-session.ts +71 -2
package/src/config/env.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
import { join } from "path";
|
|
17
17
|
|
|
18
|
+
import type { BillingMode } from "@schlessera/brain-ui-sdk/protocol";
|
|
19
|
+
|
|
18
20
|
import { SEVERITIES, type Severity } from "../observability/types.js";
|
|
19
21
|
import { envFlag } from "./env-core.js";
|
|
20
22
|
|
|
@@ -257,6 +259,25 @@ export const ENV_VARS: readonly EnvVarDescriptor[] = [
|
|
|
257
259
|
default: "(none)",
|
|
258
260
|
required: false,
|
|
259
261
|
},
|
|
262
|
+
{
|
|
263
|
+
name: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
264
|
+
description:
|
|
265
|
+
"Consulted for PRESENCE only, to classify billing: with it set and no " +
|
|
266
|
+
"ANTHROPIC_API_KEY, ambient-credential Claude profiles (the built-in " +
|
|
267
|
+
"default and discovered models) count as subscription-billed. The token " +
|
|
268
|
+
"itself is consumed by the Claude backend / Agent SDK, not this package.",
|
|
269
|
+
default: null,
|
|
270
|
+
required: false,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "ANTHROPIC_API_KEY",
|
|
274
|
+
description:
|
|
275
|
+
"Consulted for PRESENCE only, to classify billing: when set it wins " +
|
|
276
|
+
"over CLAUDE_CODE_OAUTH_TOKEN (mirroring the Agent SDK's credential " +
|
|
277
|
+
"precedence), so ambient-credential profiles count as api-billed.",
|
|
278
|
+
default: null,
|
|
279
|
+
required: false,
|
|
280
|
+
},
|
|
260
281
|
{
|
|
261
282
|
name: "BRAIN_UI_MODEL_DISCOVERY",
|
|
262
283
|
description:
|
|
@@ -270,10 +291,23 @@ export const ENV_VARS: readonly EnvVarDescriptor[] = [
|
|
|
270
291
|
default: "24",
|
|
271
292
|
required: false,
|
|
272
293
|
},
|
|
294
|
+
{
|
|
295
|
+
name: "BRAIN_UI_PRICING_DISCOVERY",
|
|
296
|
+
description:
|
|
297
|
+
'Remote model-pricing refresh (LiteLLM + OpenRouter catalogs); "0"/"off"/"false" disables, and runs then roll up with unknown effective cost. Defaults ON, except under a test runner (NODE_ENV=test) where it defaults OFF.',
|
|
298
|
+
default: "on (off under NODE_ENV=test)",
|
|
299
|
+
required: false,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "BRAIN_UI_PRICING_TTL_HOURS",
|
|
303
|
+
description: "How long a fetched model-pricing table stays fresh, in hours.",
|
|
304
|
+
default: "24",
|
|
305
|
+
required: false,
|
|
306
|
+
},
|
|
273
307
|
{
|
|
274
308
|
name: "NODE_ENV",
|
|
275
309
|
description:
|
|
276
|
-
"Only consulted for test-runner detection: flips the model-discovery
|
|
310
|
+
"Only consulted for test-runner detection: flips the model-discovery and pricing-discovery defaults to off under bun test. Never gates any security behavior.",
|
|
277
311
|
default: "(unset)",
|
|
278
312
|
required: false,
|
|
279
313
|
},
|
|
@@ -347,6 +381,15 @@ export interface AgentConfig {
|
|
|
347
381
|
profilesJson: string | null;
|
|
348
382
|
modelDiscovery: boolean;
|
|
349
383
|
modelTtlMs: number;
|
|
384
|
+
/**
|
|
385
|
+
* Billing classification for profiles running on AMBIENT credentials (the
|
|
386
|
+
* built-in default and discovered models): "subscription" iff the
|
|
387
|
+
* environment holds a CLAUDE_CODE_OAUTH_TOKEN and no ANTHROPIC_API_KEY —
|
|
388
|
+
* the same precedence the Agent SDK applies — else "api". Declared profiles
|
|
389
|
+
* carrying their own credential env vars are classified "api" by the
|
|
390
|
+
* registry regardless of this value.
|
|
391
|
+
*/
|
|
392
|
+
ambientBilling: BillingMode;
|
|
350
393
|
}
|
|
351
394
|
|
|
352
395
|
export interface VoiceConfig {
|
|
@@ -380,6 +423,8 @@ export interface ServerConfig {
|
|
|
380
423
|
webauthn: WebAuthnConfig;
|
|
381
424
|
agent: AgentConfig;
|
|
382
425
|
voice: VoiceConfig;
|
|
426
|
+
/** Model-pricing service (BRAIN_UI_PRICING_*); inline like wsRate. */
|
|
427
|
+
pricing: { enabled: boolean; ttlMs: number };
|
|
383
428
|
}
|
|
384
429
|
|
|
385
430
|
// --- resolver ----------------------------------------------------------------
|
|
@@ -438,12 +483,50 @@ function parseSeverity(raw: string | undefined): Severity {
|
|
|
438
483
|
|
|
439
484
|
const AUTH_MODES: readonly AuthModeName[] = ["password", "tailscale", "proxy", "none"];
|
|
440
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Ambient billing classification from an environment (presence-only reads).
|
|
488
|
+
* The API key wins over the OAuth token — the Agent SDK's own precedence —
|
|
489
|
+
* and no usable credential at all classifies "api" (nothing
|
|
490
|
+
* subscription-billed can run without the token). Exported for the activity
|
|
491
|
+
* store's standalone default, so the cron wrapper classifies from the same
|
|
492
|
+
* predicate the server config does.
|
|
493
|
+
*/
|
|
494
|
+
export function resolveAmbientBillingMode(env: EnvRecord = process.env): BillingMode {
|
|
495
|
+
return !env.ANTHROPIC_API_KEY?.trim() && env.CLAUDE_CODE_OAUTH_TOKEN?.trim()
|
|
496
|
+
? "subscription"
|
|
497
|
+
: "api";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* THE derivation of the pricing config (kill switch, TTL, brain path) —
|
|
502
|
+
* consumed by resolveServerConfig and used directly by the cron wrapper's
|
|
503
|
+
* bare `createActivityStore(db)` path, where no ServerConfig exists. Kept
|
|
504
|
+
* here so process.env reads stay in the one chokepoint the env-access gate
|
|
505
|
+
* allows.
|
|
506
|
+
*/
|
|
507
|
+
export function resolveStandalonePricingConfig(env: EnvRecord = process.env): {
|
|
508
|
+
brainPath: string;
|
|
509
|
+
enabled: boolean;
|
|
510
|
+
ttlMs: number;
|
|
511
|
+
} {
|
|
512
|
+
const ttlHours = Number(env.BRAIN_UI_PRICING_TTL_HOURS);
|
|
513
|
+
return {
|
|
514
|
+
brainPath: env.BRAIN_PATH || join(env.HOME || "/root", "brain"),
|
|
515
|
+
enabled: envFlag(env.BRAIN_UI_PRICING_DISCOVERY, env.NODE_ENV !== "test"),
|
|
516
|
+
ttlMs:
|
|
517
|
+
Number.isFinite(ttlHours) && ttlHours > 0
|
|
518
|
+
? ttlHours * 60 * 60 * 1000
|
|
519
|
+
: 24 * 60 * 60 * 1000,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
441
523
|
/**
|
|
442
524
|
* Resolve an environment into a {@link ServerConfig}. Defaults to the real
|
|
443
525
|
* process environment; tests pass their own record instead of mutating it.
|
|
444
526
|
*/
|
|
445
527
|
export function resolveServerConfig(env: EnvRecord = process.env): ServerConfig {
|
|
446
|
-
const brainPath
|
|
528
|
+
const { brainPath, enabled: pricingEnabled, ttlMs: pricingTtlMs } =
|
|
529
|
+
resolveStandalonePricingConfig(env);
|
|
447
530
|
|
|
448
531
|
const rawAuthMode = env.AUTH_MODE?.trim().toLowerCase() || null;
|
|
449
532
|
const validMode = AUTH_MODES.find((mode) => mode === rawAuthMode) ?? null;
|
|
@@ -494,6 +577,7 @@ export function resolveServerConfig(env: EnvRecord = process.env): ServerConfig
|
|
|
494
577
|
profilesJson: env.BRAIN_UI_CLAUDE_PROFILES?.trim() || null,
|
|
495
578
|
modelDiscovery,
|
|
496
579
|
modelTtlMs: ttlHours * 60 * 60 * 1000,
|
|
580
|
+
ambientBilling: resolveAmbientBillingMode(env),
|
|
497
581
|
},
|
|
498
582
|
logLevel: parseSeverity(env.BRAIN_UI_LOG_LEVEL),
|
|
499
583
|
wsRate: {
|
|
@@ -506,6 +590,10 @@ export function resolveServerConfig(env: EnvRecord = process.env): ServerConfig
|
|
|
506
590
|
keytermLimit: Number(env.VOICE_KEYTERM_LIMIT || 500),
|
|
507
591
|
cacheDir: env.VOICE_CACHE_DIR || join(brainPath, ".brain-ui"),
|
|
508
592
|
},
|
|
593
|
+
pricing: {
|
|
594
|
+
enabled: pricingEnabled,
|
|
595
|
+
ttlMs: pricingTtlMs,
|
|
596
|
+
},
|
|
509
597
|
};
|
|
510
598
|
}
|
|
511
599
|
|
package/src/cron/scheduler.ts
CHANGED
|
@@ -115,15 +115,35 @@ export function createCronScheduler(deps: {
|
|
|
115
115
|
|
|
116
116
|
return {
|
|
117
117
|
getCronStatus() {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
// Every job name that ever recorded a run, not just the two in-process
|
|
119
|
+
// triggers: module jobs and the maintain/digest crontab entries land in
|
|
120
|
+
// cron_runs through the same recorder and were previously invisible
|
|
121
|
+
// here (the "cron-status name gap"). The in-process names are unioned
|
|
122
|
+
// in so a fresh deployment still lists them before their first run.
|
|
123
|
+
// One window query yields each job's latest run.
|
|
124
|
+
const lastRuns = db
|
|
125
|
+
.query(
|
|
126
|
+
`SELECT job_name, started_at, status, duration_ms, error_message FROM (
|
|
127
|
+
SELECT *, ROW_NUMBER() OVER (
|
|
128
|
+
PARTITION BY job_name ORDER BY started_at DESC
|
|
129
|
+
) AS rn FROM cron_runs
|
|
130
|
+
) WHERE rn = 1 ORDER BY job_name`
|
|
131
|
+
)
|
|
132
|
+
.all() as Array<{
|
|
133
|
+
job_name: string;
|
|
134
|
+
started_at: number;
|
|
135
|
+
status: string;
|
|
136
|
+
duration_ms: number | null;
|
|
137
|
+
error_message: string | null;
|
|
138
|
+
}>;
|
|
139
|
+
const byName = new Map(lastRuns.map((r) => [r.job_name, r]));
|
|
140
|
+
const names = [
|
|
141
|
+
...new Set([...jobs.map((j) => j.name), ...lastRuns.map((r) => r.job_name)]),
|
|
142
|
+
];
|
|
143
|
+
return names.map((name) => {
|
|
144
|
+
const lastRun = byName.get(name);
|
|
125
145
|
return {
|
|
126
|
-
name
|
|
146
|
+
name,
|
|
127
147
|
lastRunAt: lastRun?.started_at ?? null,
|
|
128
148
|
lastStatus: lastRun?.status ?? null,
|
|
129
149
|
lastDurationMs: lastRun?.duration_ms ?? null,
|
package/src/db/client.ts
CHANGED
|
@@ -26,6 +26,12 @@ export function createUiDb(dbPath: string, options: CreateUiDbOptions = {}): Dat
|
|
|
26
26
|
const db = new Database(dbPath, { create: true });
|
|
27
27
|
db.exec("PRAGMA journal_mode = WAL");
|
|
28
28
|
db.exec("PRAGMA foreign_keys = ON");
|
|
29
|
+
// Two processes write this database (the server and the cron wrapper). The
|
|
30
|
+
// timeout makes a writer wait for a held lock instead of failing on it.
|
|
31
|
+
// It does NOT rescue a deferred transaction that loses the snapshot-upgrade
|
|
32
|
+
// race (that still throws SQLITE_BUSY) — which is why activity writes use
|
|
33
|
+
// immediate transactions; see src/activity/store.ts.
|
|
34
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
29
35
|
runMigrations(db, options.log);
|
|
30
36
|
return db;
|
|
31
37
|
}
|
|
@@ -67,11 +73,30 @@ function runMigrations(database: Database, log?: Logger) {
|
|
|
67
73
|
log?.emit({ severityText: "INFO", body: "applying migration", attributes: { file } });
|
|
68
74
|
const sql = readFileSync(join(migrationsDir, file), "utf-8");
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
database.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
try {
|
|
77
|
+
database.transaction(() => {
|
|
78
|
+
database.exec(sql);
|
|
79
|
+
database
|
|
80
|
+
.prepare("INSERT INTO _migrations (filename, applied_at) VALUES (?, ?)")
|
|
81
|
+
.run(file, Date.now());
|
|
82
|
+
})();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// Two processes racing a first boot: the loser's transaction fails on
|
|
85
|
+
// the UNIQUE _migrations.filename insert (or on DDL the winner already
|
|
86
|
+
// ran). If the file is now recorded as applied, treat it as such and
|
|
87
|
+
// move on; anything else is a real migration failure.
|
|
88
|
+
const nowApplied = database
|
|
89
|
+
.query("SELECT 1 FROM _migrations WHERE filename = ?")
|
|
90
|
+
.get(file);
|
|
91
|
+
const uniqueViolation =
|
|
92
|
+
err instanceof Error &&
|
|
93
|
+
err.message.includes("UNIQUE constraint failed: _migrations.filename");
|
|
94
|
+
if (!nowApplied && !uniqueViolation) throw err;
|
|
95
|
+
log?.emit({
|
|
96
|
+
severityText: "INFO",
|
|
97
|
+
body: "migration applied concurrently by another process; skipping",
|
|
98
|
+
attributes: { file },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
76
101
|
}
|
|
77
102
|
}
|
package/src/db/settings.ts
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Logger } from "@opentelemetry/api-logs";
|
|
10
10
|
import type { Database } from "bun:sqlite";
|
|
11
|
+
import { isBillingMode, type BillingMode } from "@schlessera/brain-ui-sdk/protocol";
|
|
11
12
|
|
|
12
13
|
const HIDDEN_MODELS_KEY = "models.hidden";
|
|
14
|
+
const BILLING_OVERRIDES_KEY = "models.billing";
|
|
15
|
+
const DETAIL_RETENTION_KEY = "activity.retention.detailDays";
|
|
16
|
+
const DETAIL_RETENTION_DEFAULT_DAYS = 7;
|
|
13
17
|
|
|
14
18
|
export function getSetting<T>(db: Database, key: string, fallback: T, log?: Logger): T {
|
|
15
19
|
const row = db
|
|
@@ -47,3 +51,55 @@ export function setHiddenModelIds(db: Database, ids: string[]): void {
|
|
|
47
51
|
const unique = [...new Set(ids.filter((id) => typeof id === "string" && id))];
|
|
48
52
|
setSetting(db, HIDDEN_MODELS_KEY, unique);
|
|
49
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Per-profile billing-mode overrides: profile id → forced classification. A
|
|
57
|
+
* profile absent from the record is "auto" (the registry's derived mode
|
|
58
|
+
* applies). Anything that is not exactly "subscription" or "api" is dropped
|
|
59
|
+
* on read, so a corrupt row degrades to auto rather than misbilling.
|
|
60
|
+
*/
|
|
61
|
+
export function getBillingOverrides(
|
|
62
|
+
db: Database,
|
|
63
|
+
log?: Logger
|
|
64
|
+
): Record<string, BillingMode> {
|
|
65
|
+
const value = getSetting<unknown>(db, BILLING_OVERRIDES_KEY, {}, log);
|
|
66
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
67
|
+
return Object.create(null);
|
|
68
|
+
}
|
|
69
|
+
// Null-prototype: profile ids are user-supplied strings — an id like
|
|
70
|
+
// "__proto__" must be an ordinary key, and a "constructor" lookup by a
|
|
71
|
+
// consumer must not resolve a prototype member.
|
|
72
|
+
const overrides: Record<string, BillingMode> = Object.create(null);
|
|
73
|
+
for (const [profileId, mode] of Object.entries(value)) {
|
|
74
|
+
if (isBillingMode(mode)) overrides[profileId] = mode;
|
|
75
|
+
}
|
|
76
|
+
return overrides;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Replace the override record (the client always sends the full record, not a delta). */
|
|
80
|
+
export function setBillingOverrides(
|
|
81
|
+
db: Database,
|
|
82
|
+
overrides: Record<string, BillingMode>
|
|
83
|
+
): void {
|
|
84
|
+
const clean = Object.fromEntries(
|
|
85
|
+
Object.entries(overrides).filter(
|
|
86
|
+
([profileId, mode]) => profileId && isBillingMode(mode)
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
setSetting(db, BILLING_OVERRIDES_KEY, clean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Minimum days a finished run keeps its detail (spans/events) before the
|
|
93
|
+
* digest-covered prune may take it. 0 is valid (prune as soon as covered);
|
|
94
|
+
* anything non-numeric or negative degrades to the default. */
|
|
95
|
+
export function getDetailRetentionDays(db: Database, log?: Logger): number {
|
|
96
|
+
const value = getSetting<unknown>(db, DETAIL_RETENTION_KEY, DETAIL_RETENTION_DEFAULT_DAYS, log);
|
|
97
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
98
|
+
return DETAIL_RETENTION_DEFAULT_DAYS;
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function setDetailRetentionDays(db: Database, days: number): void {
|
|
104
|
+
setSetting(db, DETAIL_RETENTION_KEY, days);
|
|
105
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -80,6 +80,29 @@ export { createBrainClient, type BrainClient } from "./brain/client.js";
|
|
|
80
80
|
// `cronJobs` reflects what actually ran.
|
|
81
81
|
export { recordCronRun, type CronRunRecord } from "./cron/scheduler.js";
|
|
82
82
|
|
|
83
|
+
// The activity record. The cron wrapper is a second PROCESS writing spans
|
|
84
|
+
// into the same store (root span + heartbeat + span-sink ingest); everything
|
|
85
|
+
// else consumes it through the app.
|
|
86
|
+
export {
|
|
87
|
+
createActivityStore,
|
|
88
|
+
SPAN_OUTCOMES,
|
|
89
|
+
type ActivityStore,
|
|
90
|
+
type SpanRow,
|
|
91
|
+
type SpanEventRow,
|
|
92
|
+
type SpanOutcome,
|
|
93
|
+
type SpanUsage,
|
|
94
|
+
type ActivityChange,
|
|
95
|
+
type RollupPricing,
|
|
96
|
+
} from "./activity/store.js";
|
|
97
|
+
export { createActivityStream, type ActivityStream } from "./activity/stream.js";
|
|
98
|
+
export { ingestSpanSink } from "./activity/span-sink.js";
|
|
99
|
+
export {
|
|
100
|
+
generateActivityDigest,
|
|
101
|
+
latestActivityDigest,
|
|
102
|
+
DIGEST_JOB_NAME,
|
|
103
|
+
type ActivityDigest,
|
|
104
|
+
} from "./activity/digest.js";
|
|
105
|
+
|
|
83
106
|
// Share staging: a deployment can sweep expired staging dirs at boot; the
|
|
84
107
|
// intake route also sweeps opportunistically on every share.
|
|
85
108
|
export { pruneShareStaging, shareStagingRoot } from "./share/staging.js";
|