@schlessera/brain-ui-server 0.19.0 → 0.21.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.map +1 -1
- package/dist/activity/digest.js +30 -2
- package/dist/activity/digest.js.map +1 -1
- package/dist/activity/notify.d.ts.map +1 -1
- package/dist/activity/notify.js +26 -4
- package/dist/activity/notify.js.map +1 -1
- package/dist/activity/query.d.ts.map +1 -1
- package/dist/activity/query.js +28 -1
- package/dist/activity/query.js.map +1 -1
- package/dist/activity/recorder.d.ts +5 -0
- package/dist/activity/recorder.d.ts.map +1 -1
- package/dist/activity/recorder.js +37 -4
- package/dist/activity/recorder.js.map +1 -1
- package/dist/activity/runtime.d.ts +7 -1
- package/dist/activity/runtime.d.ts.map +1 -1
- package/dist/activity/runtime.js +28 -10
- package/dist/activity/runtime.js.map +1 -1
- package/dist/activity/store.d.ts +51 -2
- package/dist/activity/store.d.ts.map +1 -1
- package/dist/activity/store.js +236 -19
- package/dist/activity/store.js.map +1 -1
- package/dist/activity/stream.d.ts +6 -2
- package/dist/activity/stream.d.ts.map +1 -1
- package/dist/activity/stream.js +42 -1
- package/dist/activity/stream.js.map +1 -1
- 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 +18 -3
- 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/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 +1 -1
- package/dist/index.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/routes/activity.js +40 -32
- package/dist/routes/activity.js.map +1 -1
- 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/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +36 -2
- package/dist/ws/run-session.js.map +1 -1
- package/migrations/009_activity_followups.sql +22 -0
- package/migrations/010_effective_cost.sql +21 -0
- package/package.json +4 -3
- package/src/activity/digest.ts +30 -2
- package/src/activity/notify.ts +30 -8
- package/src/activity/query.ts +28 -1
- package/src/activity/recorder.ts +57 -5
- package/src/activity/runtime.ts +41 -13
- package/src/activity/store.ts +316 -21
- package/src/activity/stream.ts +39 -9
- package/src/agent/backend.ts +91 -10
- package/src/app.ts +19 -3
- package/src/config/env.ts +90 -2
- package/src/db/settings.ts +56 -0
- package/src/index.ts +1 -0
- package/src/pricing/model-pricing.ts +497 -0
- package/src/routes/activity.ts +41 -33
- package/src/routes/models.ts +57 -6
- package/src/ws/run-session.ts +45 -3
package/dist/activity/runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createActivityNotifier } from "./notify.js";
|
|
|
4
4
|
import { createPushSender } from "./push-sender.js";
|
|
5
5
|
import { digestRetentionFloor } from "./digest.js";
|
|
6
6
|
import { runActivityQuery } from "./query.js";
|
|
7
|
+
import { getDetailRetentionDays } from "../db/settings.js";
|
|
7
8
|
// Activity lifecycle cadence. The stale threshold must comfortably exceed
|
|
8
9
|
// the cron wrapper's heartbeat interval (~30s) so a live writer is never
|
|
9
10
|
// swept; the tick doubles as the always-on low-frequency sweep the
|
|
@@ -16,8 +17,20 @@ const ACTIVITY_HARD_CEILING_MS = 90 * 24 * 60 * 60 * 1000;
|
|
|
16
17
|
const INTENT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
17
18
|
export function createActivityRuntime(db, deps) {
|
|
18
19
|
const { log } = deps;
|
|
19
|
-
const store = createActivityStore(db);
|
|
20
|
+
const store = createActivityStore(db, deps.pricing ? { pricing: deps.pricing } : {});
|
|
20
21
|
const stream = createActivityStream(store, log);
|
|
22
|
+
// The notifier is created BEFORE the boot sweep: its change cursor starts
|
|
23
|
+
// at the current head, so the terminal changes the sweep writes are above
|
|
24
|
+
// it and the first tick turns restart-interrupted runs into failure
|
|
25
|
+
// intents. Created after, the sweep's changes would sit below the cursor
|
|
26
|
+
// and a turn killed by a restart would never be notified.
|
|
27
|
+
const notifier = createActivityNotifier({
|
|
28
|
+
db,
|
|
29
|
+
store,
|
|
30
|
+
isWatched: (scope) => stream.isWatched(scope),
|
|
31
|
+
log,
|
|
32
|
+
});
|
|
33
|
+
const pushSender = createPushSender(db, { log });
|
|
21
34
|
// Boot sweep: close this server's orphans from a previous life (interrupted).
|
|
22
35
|
try {
|
|
23
36
|
const orphans = store.sweepOwnOrphans();
|
|
@@ -36,13 +49,6 @@ export function createActivityRuntime(db, deps) {
|
|
|
36
49
|
attributes: { error: err instanceof Error ? err.message : String(err) },
|
|
37
50
|
});
|
|
38
51
|
}
|
|
39
|
-
const notifier = createActivityNotifier({
|
|
40
|
-
db,
|
|
41
|
-
store,
|
|
42
|
-
isWatched: (scope) => stream.isWatched(scope),
|
|
43
|
-
log,
|
|
44
|
-
});
|
|
45
|
-
const pushSender = createPushSender(db, { log });
|
|
46
52
|
let delivering = false;
|
|
47
53
|
let lastPrune = 0;
|
|
48
54
|
const tick = setInterval(() => {
|
|
@@ -74,10 +80,22 @@ export function createActivityRuntime(db, deps) {
|
|
|
74
80
|
}
|
|
75
81
|
if (Date.now() - lastPrune > ACTIVITY_PRUNE_INTERVAL_MS) {
|
|
76
82
|
lastPrune = Date.now();
|
|
83
|
+
// Self-heal the pricing table on server traffic: without this, a
|
|
84
|
+
// long-lived server rolls up from whatever the boot-time refresh
|
|
85
|
+
// fetched. Single-flight + TTL inside ensureFresh make the hourly
|
|
86
|
+
// call free when fresh. The rollup path itself stays synchronous —
|
|
87
|
+
// never refresh from inside the store.
|
|
88
|
+
const pricing = deps.pricing;
|
|
89
|
+
if (typeof pricing?.ensureFresh === "function") {
|
|
90
|
+
void pricing.ensureFresh().catch(() => { });
|
|
91
|
+
}
|
|
77
92
|
store.prune({
|
|
78
|
-
// Full detail survives until the digest has covered it
|
|
79
|
-
//
|
|
93
|
+
// Full detail survives until the digest has covered it AND the
|
|
94
|
+
// retention window has passed; the hard ceiling bounds growth even
|
|
95
|
+
// if the digest job silently dies. The setting is read every pass
|
|
96
|
+
// so a change applies without a restart.
|
|
80
97
|
digestFloorAt: digestRetentionFloor(db),
|
|
98
|
+
detailRetentionMs: getDetailRetentionDays(db, log) * 24 * 60 * 60 * 1000,
|
|
81
99
|
hardCeilingMs: ACTIVITY_HARD_CEILING_MS,
|
|
82
100
|
});
|
|
83
101
|
notifier.pruneAcknowledged(INTENT_RETENTION_MS);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/activity/runtime.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,mBAAmB,
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/activity/runtime.ts"],"names":[],"mappings":"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"}
|
package/dist/activity/store.d.ts
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
* loud one.
|
|
32
32
|
*/
|
|
33
33
|
import type { Database } from "bun:sqlite";
|
|
34
|
+
import { type BillingMode } from "@schlessera/brain-ui-sdk/protocol";
|
|
35
|
+
import { type PricingRates } from "../pricing/model-pricing.js";
|
|
34
36
|
export declare const SPAN_OUTCOMES: readonly ["success", "error", "timeout", "cancelled", "denied", "interrupted"];
|
|
35
37
|
export type SpanOutcome = (typeof SPAN_OUTCOMES)[number];
|
|
36
38
|
export type SpanKind = "turn" | "tool" | "subagent" | "cron";
|
|
@@ -88,11 +90,32 @@ export interface RunRollupRow {
|
|
|
88
90
|
cacheReadTokens: number | null;
|
|
89
91
|
cacheCreationTokens: number | null;
|
|
90
92
|
costUsd: number | null;
|
|
93
|
+
/**
|
|
94
|
+
* What the run actually cost, frozen at first computation (migration 010):
|
|
95
|
+
* 0 for subscription-billed work regardless of tokens, priced usage for
|
|
96
|
+
* api-billed. NULL = unknown, 0 = genuinely free.
|
|
97
|
+
*/
|
|
98
|
+
effectiveCostUsd: number | null;
|
|
99
|
+
/** Billing classification behind `effectiveCostUsd`; NULL = unknown. */
|
|
100
|
+
billingMode: BillingMode | null;
|
|
101
|
+
/** True when the effective cost was computed from estimated rates. */
|
|
102
|
+
pricingEstimate: boolean | null;
|
|
91
103
|
failureReason: string | null;
|
|
92
104
|
detailPruned: boolean;
|
|
93
105
|
}
|
|
94
106
|
/** The one snake→camel mapper for rollup rows — every reader shares it. */
|
|
95
107
|
export declare function rowToRunRollup(r: any): RunRollupRow;
|
|
108
|
+
/**
|
|
109
|
+
* Sum-of-KNOWNS over effective costs: unknown (NULL) rows contribute nothing
|
|
110
|
+
* to the sum and are counted instead, so no aggregate can pass an unknown off
|
|
111
|
+
* as $0 (AE3). One definition for every reader that folds rollup rows.
|
|
112
|
+
*/
|
|
113
|
+
export declare function sumEffectiveCost(rows: Array<{
|
|
114
|
+
effectiveCostUsd: number | null;
|
|
115
|
+
}>): {
|
|
116
|
+
effectiveCostUsd: number;
|
|
117
|
+
unpricedRuns: number;
|
|
118
|
+
};
|
|
96
119
|
/** One committed write, as the delta stream sees it. */
|
|
97
120
|
export type ActivityChange = {
|
|
98
121
|
changeId: number;
|
|
@@ -140,6 +163,10 @@ export interface PruneOptions {
|
|
|
140
163
|
/** Spans of runs that ended before this are prunable (digest floor).
|
|
141
164
|
* Absolute epoch ms, not an offset. */
|
|
142
165
|
digestFloorAt: number;
|
|
166
|
+
/** Minimum age (ms since ended) a run must reach before the digest floor
|
|
167
|
+
* may prune its detail. 0 reproduces floor-only pruning. The hard
|
|
168
|
+
* ceiling ignores it. */
|
|
169
|
+
detailRetentionMs: number;
|
|
143
170
|
/** Runs older than this are pruned REGARDLESS of the digest floor. */
|
|
144
171
|
hardCeilingMs: number;
|
|
145
172
|
now?: number;
|
|
@@ -148,7 +175,6 @@ export interface PruneOptions {
|
|
|
148
175
|
}
|
|
149
176
|
/** A single event payload is capped so no delta can approach the WS frame cap. */
|
|
150
177
|
export declare const MAX_EVENT_PAYLOAD_BYTES = 16384;
|
|
151
|
-
export declare const TRUNCATION_MARKER = "\u2026[truncated]";
|
|
152
178
|
export interface ActivityStore {
|
|
153
179
|
readonly writer: string;
|
|
154
180
|
startSpan(input: StartSpanInput): SpanRow;
|
|
@@ -165,7 +191,13 @@ export interface ActivityStore {
|
|
|
165
191
|
usage?: SpanUsage;
|
|
166
192
|
waitUntil?: number;
|
|
167
193
|
}): boolean;
|
|
168
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Append one event; null (nothing written) for an unknown span. `cap`
|
|
196
|
+
* lowers the payload size cap for this call — the store's 16 KB invariant
|
|
197
|
+
* is the ceiling regardless. A capped payload is clipped and flagged
|
|
198
|
+
* `truncated`.
|
|
199
|
+
*/
|
|
200
|
+
appendEvent(spanId: string, eventType: string, payload: unknown, ts?: number, cap?: number): SpanEventRow | null;
|
|
169
201
|
/** External writers touch their root span so staleness is heartbeat-age based. */
|
|
170
202
|
heartbeat(spanId: string, at?: number): void;
|
|
171
203
|
/** Close every open span of a run as `outcome` (children first), reason on all. */
|
|
@@ -199,9 +231,26 @@ export interface ActivityStore {
|
|
|
199
231
|
spansDeleted: number;
|
|
200
232
|
};
|
|
201
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* The pricing dependency rollups price through. `resolve` MUST be synchronous
|
|
236
|
+
* — it is called inside the rollup write transaction, which never awaits.
|
|
237
|
+
*/
|
|
238
|
+
export interface RollupPricing {
|
|
239
|
+
resolve(modelId: string): PricingRates | null;
|
|
240
|
+
}
|
|
202
241
|
export interface CreateActivityStoreOptions {
|
|
203
242
|
/** Process identity stamped on every span this store writes. */
|
|
204
243
|
writer?: string;
|
|
244
|
+
/**
|
|
245
|
+
* Pricing seam for rollup-time effective cost. Defaults to an env-derived
|
|
246
|
+
* `createModelPricing` instance (the `$BRAIN_PATH` cache and the bundled
|
|
247
|
+
* snapshot are both synchronous reads), constructed on first rollup — so
|
|
248
|
+
* the cron wrapper's bare `createActivityStore(db)` prices identically to
|
|
249
|
+
* the server with no wiring of its own. Tests inject a fake to stay
|
|
250
|
+
* network- and disk-free (the default is also disabled under NODE_ENV=test,
|
|
251
|
+
* mirroring the discovery flag's default).
|
|
252
|
+
*/
|
|
253
|
+
pricing?: RollupPricing;
|
|
205
254
|
}
|
|
206
255
|
export declare function createActivityStore(db: Database, options?: CreateActivityStoreOptions): ActivityStore;
|
|
207
256
|
//# sourceMappingURL=store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/activity/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/activity/store.ts"],"names":[],"mappings":"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"}
|
package/dist/activity/store.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { isBillingMode } from "@schlessera/brain-ui-sdk/protocol";
|
|
2
|
+
import { resolveAmbientBillingMode, resolveStandalonePricingConfig, } from "../config/env.js";
|
|
3
|
+
import { createModelPricing } from "../pricing/model-pricing.js";
|
|
1
4
|
export const SPAN_OUTCOMES = [
|
|
2
5
|
"success",
|
|
3
6
|
"error",
|
|
@@ -24,15 +27,43 @@ export function rowToRunRollup(r) {
|
|
|
24
27
|
cacheReadTokens: r.cache_read_tokens,
|
|
25
28
|
cacheCreationTokens: r.cache_creation_tokens,
|
|
26
29
|
costUsd: r.cost_usd,
|
|
30
|
+
effectiveCostUsd: r.effective_cost_usd ?? null,
|
|
31
|
+
// Migration 010 CHECKs this column, but a row written around them (older
|
|
32
|
+
// binary, manual edit) must degrade to unknown, never to a wrong mode.
|
|
33
|
+
billingMode: isBillingMode(r.billing_mode) ? r.billing_mode : null,
|
|
34
|
+
pricingEstimate: r.pricing_estimate == null ? null : r.pricing_estimate === 1,
|
|
27
35
|
failureReason: r.failure_reason,
|
|
28
36
|
detailPruned: r.detail_pruned === 1,
|
|
29
37
|
};
|
|
30
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Sum-of-KNOWNS over effective costs: unknown (NULL) rows contribute nothing
|
|
41
|
+
* to the sum and are counted instead, so no aggregate can pass an unknown off
|
|
42
|
+
* as $0 (AE3). One definition for every reader that folds rollup rows.
|
|
43
|
+
*/
|
|
44
|
+
export function sumEffectiveCost(rows) {
|
|
45
|
+
let effectiveCostUsd = 0;
|
|
46
|
+
let unpricedRuns = 0;
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
if (row.effectiveCostUsd === null)
|
|
49
|
+
unpricedRuns += 1;
|
|
50
|
+
else
|
|
51
|
+
effectiveCostUsd += row.effectiveCostUsd;
|
|
52
|
+
}
|
|
53
|
+
return { effectiveCostUsd, unpricedRuns };
|
|
54
|
+
}
|
|
31
55
|
/** A single event payload is capped so no delta can approach the WS frame cap. */
|
|
32
56
|
export const MAX_EVENT_PAYLOAD_BYTES = 16_384;
|
|
33
|
-
export const TRUNCATION_MARKER = "…[truncated]";
|
|
34
57
|
export function createActivityStore(db, options = {}) {
|
|
35
58
|
const writer = options.writer ?? `pid:${process.pid}:${Date.now()}`;
|
|
59
|
+
// Lazy so a store that never rolls up (span-only paths, most tests)
|
|
60
|
+
// touches neither the env nor the disk. The env reads live in
|
|
61
|
+
// config/env.ts — the one chokepoint the env-access gate allows. Every
|
|
62
|
+
// wrapper whose transaction can reach rollupRunInTx calls this BEFORE
|
|
63
|
+
// entering `inWrite`, so the SQLite write lock never covers the two
|
|
64
|
+
// synchronous JSON reads construction costs.
|
|
65
|
+
let pricing = options.pricing;
|
|
66
|
+
const getPricing = () => (pricing ??= createModelPricing(resolveStandalonePricingConfig()));
|
|
36
67
|
// bun:sqlite transactions: `.immediate` takes the write lock up front, so a
|
|
37
68
|
// concurrent writer waits (busy_timeout) instead of failing mid-upgrade.
|
|
38
69
|
const inWrite = (fn) => db.transaction(fn).immediate();
|
|
@@ -143,18 +174,88 @@ export function createActivityStore(db, options = {}) {
|
|
|
143
174
|
// tree would double-count every fan-out. Verified empirically (plan U3
|
|
144
175
|
// smoke test).
|
|
145
176
|
const failure = spans.find((s) => (s.outcome === "error" || s.outcome === "timeout") && s.outcomeReason != null)?.outcomeReason ?? (root.outcome === "interrupted" ? "interrupted" : null);
|
|
177
|
+
// Billing rides the root span when the recorder knew it at run start
|
|
178
|
+
// (session runs, U3). A root without the attr classifies from THIS
|
|
179
|
+
// process's env ONLY for non-session origins: for cron rollups the
|
|
180
|
+
// executing process is the wrapper itself — exactly the credential set
|
|
181
|
+
// the job's agent authenticated under — so classification and reality
|
|
182
|
+
// move together. A SESSION root without the attr (a custom backend the
|
|
183
|
+
// registry could not classify, or a failed profile resolution) stays
|
|
184
|
+
// unknown instead: this process's ambient credentials say nothing about
|
|
185
|
+
// whichever backend ran the turn, and a wrong subscription-$0 would
|
|
186
|
+
// freeze forever where unknown stays honestly unpriced.
|
|
187
|
+
const attrBilling = root.attrs["brain.billing_mode"];
|
|
188
|
+
const billingMode = isBillingMode(attrBilling)
|
|
189
|
+
? attrBilling
|
|
190
|
+
: root.origin === "session"
|
|
191
|
+
? null
|
|
192
|
+
: resolveAmbientBillingMode();
|
|
193
|
+
// List-price math runs regardless of billing mode: it gap-fills a
|
|
194
|
+
// missing backend cost_usd (pi without snapshots, cron) AND provides the
|
|
195
|
+
// api-billed effective number. resolve() is synchronous by contract —
|
|
196
|
+
// nothing here may await inside the write transaction.
|
|
197
|
+
const usage = usageForPricing(root, spans);
|
|
198
|
+
const priced = usage.kind === "usage" ? priceUsage(usage.byModel, getPricing()) : null;
|
|
199
|
+
// Effective-cost semantics (NULL = unknown, 0 = genuinely free):
|
|
200
|
+
// subscription → 0 regardless of tokens (AE1); api → the priced sum —
|
|
201
|
+
// 0 when the run verifiably consumed nothing, NULL when usage was never
|
|
202
|
+
// recorded (denied before inference) or any contributing model/rate is
|
|
203
|
+
// missing. The estimate flag qualifies the effective number, so it is
|
|
204
|
+
// NULL exactly when that is, and 0 for a subscription $0 (exact, not
|
|
205
|
+
// estimated).
|
|
206
|
+
// An unclassified run (billingMode null — session root without the
|
|
207
|
+
// attr) prices as unknown: no subscription-zero, no api pricing.
|
|
208
|
+
const effectiveCostUsd = billingMode === null ? null : billingMode === "subscription" ? 0 : (priced?.costUsd ?? null);
|
|
209
|
+
const pricingEstimate = billingMode === null
|
|
210
|
+
? null
|
|
211
|
+
: billingMode === "subscription"
|
|
212
|
+
? 0
|
|
213
|
+
: priced
|
|
214
|
+
? (priced.estimate ? 1 : 0)
|
|
215
|
+
: null;
|
|
216
|
+
const costUsd = root.usage.costUsd ?? priced?.costUsd ?? null;
|
|
146
217
|
db.query(`INSERT INTO activity_run_rollups
|
|
147
218
|
(run_id, origin, name, session_id, job_name, started_at, ended_at, outcome,
|
|
148
219
|
duration_ms, span_count, input_tokens, output_tokens, cache_read_tokens,
|
|
149
|
-
cache_creation_tokens, cost_usd,
|
|
150
|
-
|
|
220
|
+
cache_creation_tokens, cost_usd, effective_cost_usd, billing_mode,
|
|
221
|
+
pricing_estimate, failure_reason, detail_pruned)
|
|
222
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
151
223
|
ON CONFLICT(run_id) DO UPDATE SET
|
|
152
224
|
ended_at = excluded.ended_at, outcome = excluded.outcome,
|
|
153
225
|
duration_ms = excluded.duration_ms, span_count = excluded.span_count,
|
|
154
226
|
input_tokens = excluded.input_tokens, output_tokens = excluded.output_tokens,
|
|
155
227
|
cache_read_tokens = excluded.cache_read_tokens,
|
|
156
228
|
cache_creation_tokens = excluded.cache_creation_tokens,
|
|
157
|
-
cost_usd =
|
|
229
|
+
cost_usd = COALESCE(activity_run_rollups.cost_usd, excluded.cost_usd),
|
|
230
|
+
effective_cost_usd = CASE
|
|
231
|
+
WHEN activity_run_rollups.effective_cost_usd IS NOT NULL
|
|
232
|
+
THEN activity_run_rollups.effective_cost_usd
|
|
233
|
+
WHEN activity_run_rollups.billing_mode IS NULL
|
|
234
|
+
OR activity_run_rollups.billing_mode = excluded.billing_mode
|
|
235
|
+
THEN excluded.effective_cost_usd
|
|
236
|
+
ELSE NULL
|
|
237
|
+
END,
|
|
238
|
+
billing_mode = COALESCE(activity_run_rollups.billing_mode, excluded.billing_mode),
|
|
239
|
+
pricing_estimate = CASE
|
|
240
|
+
WHEN activity_run_rollups.pricing_estimate IS NOT NULL
|
|
241
|
+
THEN activity_run_rollups.pricing_estimate
|
|
242
|
+
WHEN activity_run_rollups.billing_mode IS NULL
|
|
243
|
+
OR activity_run_rollups.billing_mode = excluded.billing_mode
|
|
244
|
+
THEN excluded.pricing_estimate
|
|
245
|
+
ELSE NULL
|
|
246
|
+
END,
|
|
247
|
+
failure_reason = excluded.failure_reason`
|
|
248
|
+
// Every cost column is FROZEN at first non-NULL write (AE5): a re-rollup
|
|
249
|
+
// after a pricing refresh must not silently reprice history — only a
|
|
250
|
+
// still-NULL slot may be filled by a later computation. cost_usd gets
|
|
251
|
+
// the plain first-write-wins COALESCE (the terminal endSpan already
|
|
252
|
+
// merged the backend's authoritative number before the first rollup).
|
|
253
|
+
// The effective/estimate pair additionally requires the LATER fill to
|
|
254
|
+
// agree with the frozen classification: billing_mode is first-write-wins,
|
|
255
|
+
// and a slot left NULL under one classification must never be filled by
|
|
256
|
+
// a number computed under a different one (a subscription $0 landing on
|
|
257
|
+
// an api-classified row would fabricate a cross-classified price).
|
|
258
|
+
).run(runId, root.origin, root.name, root.sessionId, root.jobName, root.startedAt, root.endedAt, root.outcome, root.endedAt !== null ? root.endedAt - root.startedAt : null, spans.length, root.usage.inputTokens ?? null, root.usage.outputTokens ?? null, root.usage.cacheReadTokens ?? null, root.usage.cacheCreationTokens ?? null, costUsd, effectiveCostUsd, billingMode, pricingEstimate, failure);
|
|
158
259
|
}
|
|
159
260
|
return {
|
|
160
261
|
writer,
|
|
@@ -195,7 +296,7 @@ export function createActivityStore(db, options = {}) {
|
|
|
195
296
|
return true;
|
|
196
297
|
});
|
|
197
298
|
},
|
|
198
|
-
appendEvent(spanId, eventType, payload, ts) {
|
|
299
|
+
appendEvent(spanId, eventType, payload, ts, cap) {
|
|
199
300
|
return inWrite(() => {
|
|
200
301
|
const span = db
|
|
201
302
|
.query("SELECT run_id FROM activity_spans WHERE span_id = ?")
|
|
@@ -205,13 +306,19 @@ export function createActivityStore(db, options = {}) {
|
|
|
205
306
|
const next = db
|
|
206
307
|
.query("SELECT COALESCE(MAX(event_index), -1) + 1 AS idx FROM activity_events WHERE span_id = ?")
|
|
207
308
|
.get(spanId);
|
|
208
|
-
const stored = capPayload(payload);
|
|
209
|
-
|
|
309
|
+
const stored = capPayload(payload, cap);
|
|
310
|
+
const at = ts ?? Date.now();
|
|
311
|
+
db.query("INSERT INTO activity_events (span_id, event_index, ts, event_type, payload) VALUES (?, ?, ?, ?, ?)").run(spanId, next.idx, at, eventType, JSON.stringify(stored));
|
|
210
312
|
logChange(span.run_id, nextSeq(span.run_id), spanId, next.idx);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
313
|
+
// Built from the values just written — no read-back needed.
|
|
314
|
+
return {
|
|
315
|
+
spanId,
|
|
316
|
+
eventIndex: next.idx,
|
|
317
|
+
ts: at,
|
|
318
|
+
eventType,
|
|
319
|
+
payload: stored.v,
|
|
320
|
+
truncated: stored.truncated === true,
|
|
321
|
+
};
|
|
215
322
|
});
|
|
216
323
|
},
|
|
217
324
|
heartbeat(spanId, at) {
|
|
@@ -222,6 +329,7 @@ export function createActivityStore(db, options = {}) {
|
|
|
222
329
|
});
|
|
223
330
|
},
|
|
224
331
|
cascadeClose(runId, outcome, reason) {
|
|
332
|
+
getPricing(); // construct outside the write lock; rollup reuses it
|
|
225
333
|
return inWrite(() => closeRunInTx(runId, outcome, reason));
|
|
226
334
|
},
|
|
227
335
|
sweepOwnOrphans() {
|
|
@@ -242,6 +350,7 @@ export function createActivityStore(db, options = {}) {
|
|
|
242
350
|
.all();
|
|
243
351
|
if (open.length === 0)
|
|
244
352
|
return 0;
|
|
353
|
+
getPricing(); // construct outside the write lock; rollup reuses it
|
|
245
354
|
return inWrite(() => {
|
|
246
355
|
let closed = 0;
|
|
247
356
|
for (const { run_id } of open) {
|
|
@@ -262,6 +371,7 @@ export function createActivityStore(db, options = {}) {
|
|
|
262
371
|
.all(writer, cutoff);
|
|
263
372
|
if (staleRoots.length === 0)
|
|
264
373
|
return 0;
|
|
374
|
+
getPricing(); // construct outside the write lock; rollup reuses it
|
|
265
375
|
return inWrite(() => {
|
|
266
376
|
let closed = 0;
|
|
267
377
|
for (const { run_id } of staleRoots) {
|
|
@@ -367,6 +477,7 @@ export function createActivityStore(db, options = {}) {
|
|
|
367
477
|
.all().map(rowToSpan);
|
|
368
478
|
},
|
|
369
479
|
rollupRun(runId) {
|
|
480
|
+
getPricing(); // construct outside the write lock; rollup reuses it
|
|
370
481
|
inWrite(() => rollupRunInTx(runId));
|
|
371
482
|
},
|
|
372
483
|
prune(options) {
|
|
@@ -374,8 +485,15 @@ export function createActivityStore(db, options = {}) {
|
|
|
374
485
|
const batch = options.batch ?? 50;
|
|
375
486
|
// The digest floor gates pruning, but a dead digest job must not freeze
|
|
376
487
|
// it forever: the hard ceiling prunes regardless (marking the rollup so
|
|
377
|
-
// the coverage gap is visible).
|
|
488
|
+
// the coverage gap is visible). The floor alone is NOT enough: it meant
|
|
489
|
+
// "safe to prune once summarized", but drill-in debugging is a second
|
|
490
|
+
// reader of detail with a different clock — nightly cron runs end
|
|
491
|
+
// before the morning digest, day sessions after, so floor-only pruning
|
|
492
|
+
// took cron span trees within the hour while sessions kept theirs (it
|
|
493
|
+
// looked cron-specific; it was clock skew). The retention cutoff ANDs
|
|
494
|
+
// with the floor so covered runs still keep detail for a minimum window.
|
|
378
495
|
const floor = Math.max(options.digestFloorAt, 0);
|
|
496
|
+
const retentionCutoff = now - options.detailRetentionMs;
|
|
379
497
|
const ceiling = now - options.hardCeilingMs;
|
|
380
498
|
return inWrite(() => {
|
|
381
499
|
// Candidates come from the rollups (they exist for every finished
|
|
@@ -384,13 +502,13 @@ export function createActivityStore(db, options = {}) {
|
|
|
384
502
|
const candidates = db
|
|
385
503
|
.query(`SELECT run_id, ended_at AS ended FROM activity_run_rollups r
|
|
386
504
|
WHERE detail_pruned = 0 AND ended_at IS NOT NULL
|
|
387
|
-
AND (ended_at < ? OR ended_at < ?)
|
|
505
|
+
AND ((ended_at < ? AND ended_at < ?) OR ended_at < ?)
|
|
388
506
|
AND NOT EXISTS (
|
|
389
507
|
SELECT 1 FROM activity_spans s
|
|
390
508
|
WHERE s.run_id = r.run_id AND s.outcome IS NULL
|
|
391
509
|
)
|
|
392
510
|
LIMIT ?`)
|
|
393
|
-
.all(floor, ceiling, batch);
|
|
511
|
+
.all(floor, retentionCutoff, ceiling, batch);
|
|
394
512
|
let spansDeleted = 0;
|
|
395
513
|
for (const { run_id, ended } of candidates) {
|
|
396
514
|
db.query("UPDATE activity_run_rollups SET detail_pruned = 1 WHERE run_id = ?").run(run_id);
|
|
@@ -417,6 +535,103 @@ export function createActivityStore(db, options = {}) {
|
|
|
417
535
|
},
|
|
418
536
|
};
|
|
419
537
|
}
|
|
538
|
+
function tokenCount(value) {
|
|
539
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* The usage a run is priced from. Session runs carry the SDK's per-model
|
|
543
|
+
* breakdown as a root attr (result-level accounting, subagents included —
|
|
544
|
+
* the same root-only scope the token rollup uses). Cron runs have no root
|
|
545
|
+
* aggregate: the wrapper's children carry the usage, summed per model here
|
|
546
|
+
* — the sanctioned origin-scoped exception to root-only aggregation, safe
|
|
547
|
+
* because sink children are not double-counted in their root.
|
|
548
|
+
*/
|
|
549
|
+
function usageForPricing(root, spans) {
|
|
550
|
+
if (root.origin === "cron") {
|
|
551
|
+
// Null-prototype: model ids are foreign strings — an id like "__proto__"
|
|
552
|
+
// must be an ordinary key, never a prototype write.
|
|
553
|
+
const byModel = Object.create(null);
|
|
554
|
+
let sawUsage = false;
|
|
555
|
+
for (const span of spans) {
|
|
556
|
+
if (span.spanId === root.spanId)
|
|
557
|
+
continue;
|
|
558
|
+
const u = span.usage;
|
|
559
|
+
if (u.inputTokens == null &&
|
|
560
|
+
u.outputTokens == null &&
|
|
561
|
+
u.cacheReadTokens == null &&
|
|
562
|
+
u.cacheCreationTokens == null) {
|
|
563
|
+
continue; // no inference on this span (tool spans etc.) — not "unknown"
|
|
564
|
+
}
|
|
565
|
+
sawUsage = true;
|
|
566
|
+
// Tokens without a model cannot be priced at any rate — guessing one
|
|
567
|
+
// would silently misprice, so the whole run goes unknown.
|
|
568
|
+
if (!u.model)
|
|
569
|
+
return { kind: "unpriceable" };
|
|
570
|
+
const agg = (byModel[u.model] ??= {});
|
|
571
|
+
agg.inputTokens = (agg.inputTokens ?? 0) + (u.inputTokens ?? 0);
|
|
572
|
+
agg.outputTokens = (agg.outputTokens ?? 0) + (u.outputTokens ?? 0);
|
|
573
|
+
agg.cacheReadTokens = (agg.cacheReadTokens ?? 0) + (u.cacheReadTokens ?? 0);
|
|
574
|
+
agg.cacheCreationTokens = (agg.cacheCreationTokens ?? 0) + (u.cacheCreationTokens ?? 0);
|
|
575
|
+
}
|
|
576
|
+
return sawUsage ? { kind: "usage", byModel } : { kind: "none" };
|
|
577
|
+
}
|
|
578
|
+
const perModel = root.attrs["gen_ai.usage.per_model"];
|
|
579
|
+
if (typeof perModel !== "object" || perModel === null || Array.isArray(perModel)) {
|
|
580
|
+
return { kind: "none" };
|
|
581
|
+
}
|
|
582
|
+
const byModel = Object.create(null);
|
|
583
|
+
for (const [model, value] of Object.entries(perModel)) {
|
|
584
|
+
if (typeof value !== "object" || value === null)
|
|
585
|
+
continue;
|
|
586
|
+
const v = value;
|
|
587
|
+
byModel[model] = {
|
|
588
|
+
inputTokens: tokenCount(v.inputTokens) ?? 0,
|
|
589
|
+
outputTokens: tokenCount(v.outputTokens) ?? 0,
|
|
590
|
+
cacheReadTokens: tokenCount(v.cacheReadTokens) ?? 0,
|
|
591
|
+
cacheCreationTokens: tokenCount(v.cacheCreationTokens) ?? 0,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
return Object.keys(byModel).length > 0 ? { kind: "usage", byModel } : { kind: "none" };
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Price one run's per-model usage at list rates. Null = unknown: an
|
|
598
|
+
* unresolvable model with consumed tokens, or a consumed token class with no
|
|
599
|
+
* rate, poisons the WHOLE run — cache reads dominate Claude usage, so
|
|
600
|
+
* partial pricing would systematically understate (the binding
|
|
601
|
+
* missing-cache-rate decision). A model with zero consumption contributes
|
|
602
|
+
* nothing and needs no rate. Variant/snapshot fallbacks are the pricing
|
|
603
|
+
* service's job (`resolve()`); the store only propagates the estimate flag.
|
|
604
|
+
*/
|
|
605
|
+
function priceUsage(byModel, pricing) {
|
|
606
|
+
let costUsd = 0;
|
|
607
|
+
let estimate = false;
|
|
608
|
+
for (const [model, tokens] of Object.entries(byModel)) {
|
|
609
|
+
const classes = [
|
|
610
|
+
[tokens.inputTokens ?? 0, (r) => r.input],
|
|
611
|
+
[tokens.outputTokens ?? 0, (r) => r.output],
|
|
612
|
+
[tokens.cacheReadTokens ?? 0, (r) => r.cacheRead],
|
|
613
|
+
[tokens.cacheCreationTokens ?? 0, (r) => r.cacheWrite],
|
|
614
|
+
];
|
|
615
|
+
if (!classes.some(([count]) => count > 0))
|
|
616
|
+
continue;
|
|
617
|
+
const rates = pricing.resolve(model);
|
|
618
|
+
if (!rates)
|
|
619
|
+
return null;
|
|
620
|
+
for (const [count, rateOf] of classes) {
|
|
621
|
+
if (count <= 0)
|
|
622
|
+
continue;
|
|
623
|
+
const perToken = rateOf(rates);
|
|
624
|
+
if (perToken === null)
|
|
625
|
+
return null;
|
|
626
|
+
costUsd += count * perToken;
|
|
627
|
+
}
|
|
628
|
+
if (rates.estimate)
|
|
629
|
+
estimate = true;
|
|
630
|
+
}
|
|
631
|
+
// A non-finite sum (overflowed or poisoned rates) must surface as unknown —
|
|
632
|
+
// once frozen into the rollup it would render as an exact number forever.
|
|
633
|
+
return Number.isFinite(costUsd) ? { costUsd, estimate } : null;
|
|
634
|
+
}
|
|
420
635
|
function safeParse(text) {
|
|
421
636
|
if (!text)
|
|
422
637
|
return undefined;
|
|
@@ -427,14 +642,16 @@ function safeParse(text) {
|
|
|
427
642
|
return undefined;
|
|
428
643
|
}
|
|
429
644
|
}
|
|
430
|
-
/** Cap a payload's serialized size
|
|
431
|
-
|
|
645
|
+
/** Cap a payload's serialized size; a clipped payload carries the `truncated`
|
|
646
|
+
* flag (which rides the wire), so no in-text marker is stored. */
|
|
647
|
+
function capPayload(payload, cap) {
|
|
648
|
+
const limit = Math.min(cap ?? MAX_EVENT_PAYLOAD_BYTES, MAX_EVENT_PAYLOAD_BYTES);
|
|
432
649
|
const json = JSON.stringify(payload ?? null);
|
|
433
|
-
if (json.length <=
|
|
650
|
+
if (json.length <= limit)
|
|
434
651
|
return { v: payload ?? null };
|
|
435
652
|
if (typeof payload === "string") {
|
|
436
|
-
return { v: payload.slice(0,
|
|
653
|
+
return { v: payload.slice(0, limit), truncated: true };
|
|
437
654
|
}
|
|
438
|
-
return { v: json.slice(0,
|
|
655
|
+
return { v: json.slice(0, limit), truncated: true };
|
|
439
656
|
}
|
|
440
657
|
//# sourceMappingURL=store.js.map
|