@schlessera/brain-ui-server 0.19.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.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/routes/models.js
CHANGED
|
@@ -5,13 +5,22 @@
|
|
|
5
5
|
// with its visibility, plus discovery freshness so the UI can say when the list
|
|
6
6
|
// was last refreshed and why it might be stale.
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import {
|
|
8
|
+
import { isBillingMode, } from "@schlessera/brain-ui-sdk";
|
|
9
|
+
import { getBillingOverrides, getHiddenModelIds, setBillingOverrides, setHiddenModelIds, } from "../db/settings.js";
|
|
9
10
|
export function createModelRoutes(deps) {
|
|
10
|
-
const { registry, db } = deps;
|
|
11
|
+
const { registry, db, pricing } = deps;
|
|
11
12
|
async function buildCatalog() {
|
|
12
13
|
const source = await registry.getModelSource();
|
|
13
14
|
const hidden = new Set(getHiddenModelIds(db));
|
|
14
|
-
const
|
|
15
|
+
const overrides = getBillingOverrides(db);
|
|
16
|
+
// `billingMode` already rides each provider entry (the registry applies the
|
|
17
|
+
// override last); the catalog additionally tags WHICH rows carry an explicit
|
|
18
|
+
// override, so the settings screen can render auto vs forced.
|
|
19
|
+
const models = (await registry.listAllProviders({ includeHidden: true })).map((profile) => ({
|
|
20
|
+
...profile,
|
|
21
|
+
hidden: hidden.has(profile.id),
|
|
22
|
+
...(overrides[profile.id] ? { billingOverride: overrides[profile.id] } : {}),
|
|
23
|
+
}));
|
|
15
24
|
const state = source?.state();
|
|
16
25
|
return {
|
|
17
26
|
models,
|
|
@@ -30,6 +39,17 @@ export function createModelRoutes(deps) {
|
|
|
30
39
|
const source = await registry.getModelSource();
|
|
31
40
|
await source?.ensureFresh();
|
|
32
41
|
return c.json(await buildCatalog());
|
|
42
|
+
})
|
|
43
|
+
.get("/models/pricing", async (c) => {
|
|
44
|
+
// Absent instance (a host that opted out) → 404; the client hides the
|
|
45
|
+
// indicator on any rejection, so old and opted-out servers degrade alike.
|
|
46
|
+
if (!pricing)
|
|
47
|
+
return c.json({ error: "pricing not available" }, 404);
|
|
48
|
+
// Kick a refresh behind the response when stale — reading the state is
|
|
49
|
+
// also the natural moment to heal it. Never blocks: ensureFresh awaits
|
|
50
|
+
// only on a true cold start.
|
|
51
|
+
void pricing.ensureFresh().catch(() => { });
|
|
52
|
+
return c.json(pricing.state());
|
|
33
53
|
})
|
|
34
54
|
.put("/models/hidden", async (c) => {
|
|
35
55
|
const body = (await c.req.json().catch(() => null));
|
|
@@ -42,6 +62,21 @@ export function createModelRoutes(deps) {
|
|
|
42
62
|
// The picker reads through a memo — drop it so the change is immediate.
|
|
43
63
|
registry.invalidateProfiles();
|
|
44
64
|
return c.json(await buildCatalog());
|
|
65
|
+
})
|
|
66
|
+
.put("/models/billing", async (c) => {
|
|
67
|
+
const body = (await c.req.json().catch(() => null));
|
|
68
|
+
const billing = body?.billing;
|
|
69
|
+
if (typeof billing !== "object" ||
|
|
70
|
+
billing === null ||
|
|
71
|
+
Array.isArray(billing) ||
|
|
72
|
+
Object.values(billing).some((mode) => !isBillingMode(mode))) {
|
|
73
|
+
return c.json({ error: 'billing must map profile ids to "subscription" or "api"' }, 400);
|
|
74
|
+
}
|
|
75
|
+
setBillingOverrides(db, billing);
|
|
76
|
+
// Same memo discipline as the hidden set: the next roster read (and the
|
|
77
|
+
// next run's classification) must see the override immediately.
|
|
78
|
+
registry.invalidateProfiles();
|
|
79
|
+
return c.json(await buildCatalog());
|
|
45
80
|
})
|
|
46
81
|
.post("/models/refresh", async (c) => {
|
|
47
82
|
const source = await registry.getModelSource();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/routes/models.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,gFAAgF;AAChF,gDAAgD;AAGhD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/routes/models.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-session.d.ts","sourceRoot":"","sources":["../../src/ws/run-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"run-session.d.ts","sourceRoot":"","sources":["../../src/ws/run-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,mCAAmC,CAAC;AAE3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAO9C,OAAO,EAKL,KAAK,MAAM,EACZ,MAAM,WAAW,CAAC;AAEnB;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,IAAI,CAAC,CAyKf;AAqCD,iFAAiF;AACjF,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,SAAS,EACb,GAAG,EAAE;IACH,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,IAAI,CAAC,CAkGf"}
|
package/dist/ws/run-session.js
CHANGED
|
@@ -93,13 +93,20 @@ export async function runSession(host, initial) {
|
|
|
93
93
|
}, host.turnTimeoutMs);
|
|
94
94
|
turn.timeoutHandle = timeoutHandle;
|
|
95
95
|
// One recorder per turn identity: a queued follow-up re-mints turnId
|
|
96
|
-
// and gets its own run in the activity record.
|
|
96
|
+
// and gets its own run in the activity record. The recorder is handed
|
|
97
|
+
// the RESOLVED profile — `profileId` here is post pin-drop fallback and
|
|
98
|
+
// carries any session_info re-pin forward between queued turns — plus
|
|
99
|
+
// its billing mode, so the run's root span classifies what actually ran,
|
|
100
|
+
// never what was requested.
|
|
101
|
+
const billing = host.activity
|
|
102
|
+
? await resolveRunBilling(host.registry, backend.id, profileId)
|
|
103
|
+
: undefined;
|
|
97
104
|
const recorder = host.activity
|
|
98
105
|
? createTurnRecorder({
|
|
99
106
|
store: host.activity.store,
|
|
100
107
|
onWrite: () => host.activity.stream.pump(),
|
|
101
108
|
log: host.log,
|
|
102
|
-
}, { turnId: turn.turnId, sessionId: turn.sessionId })
|
|
109
|
+
}, { turnId: turn.turnId, sessionId: turn.sessionId, ...billing })
|
|
103
110
|
: undefined;
|
|
104
111
|
const bridge = makeBridge(host, turn, text, backend.id, recorder);
|
|
105
112
|
const startedAt = Date.now();
|
|
@@ -166,6 +173,33 @@ export async function runSession(host, initial) {
|
|
|
166
173
|
coordinator.drainPendingForTurn(turn, "Session ended");
|
|
167
174
|
}
|
|
168
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Profile identity + billing mode for a turn's root span. When the slot has no
|
|
178
|
+
* resolved profile id (a resumed session whose pin was dropped), the backend
|
|
179
|
+
* runs its default profile — the backend's first roster entry — so THAT
|
|
180
|
+
* profile's identity and billing are recorded, not the dead pin's. Hidden
|
|
181
|
+
* profiles are included: a session pinned to a profile the user later hid
|
|
182
|
+
* still runs on it and must classify as it. Never throws: an unreadable
|
|
183
|
+
* roster records no billing attrs, and the rollup falls back to env
|
|
184
|
+
* classification.
|
|
185
|
+
*/
|
|
186
|
+
async function resolveRunBilling(registry, backendId, profileId) {
|
|
187
|
+
try {
|
|
188
|
+
const providers = await registry.listAllProviders({ includeHidden: true });
|
|
189
|
+
const resolved = profileId
|
|
190
|
+
? providers.find((provider) => provider.id === profileId)
|
|
191
|
+
: providers.find((provider) => provider.backendId === backendId);
|
|
192
|
+
if (!resolved)
|
|
193
|
+
return undefined;
|
|
194
|
+
return {
|
|
195
|
+
profileId: resolved.id,
|
|
196
|
+
...(resolved.billingMode ? { billingMode: resolved.billingMode } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
169
203
|
/** Queue sizes are only ever reported to a human, so one decimal of MB is plenty. */
|
|
170
204
|
function formatMb(bytes) {
|
|
171
205
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-session.js","sourceRoot":"","sources":["../../src/ws/run-session.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"run-session.js","sourceRoot":"","sources":["../../src/ws/run-session.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAqB,MAAM,yBAAyB,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAElB,MAAM,WAAW,CAAC;AAEnB;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,OAQC;IAED,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,gBAAgB,IAAI,CAAC,CAAC;IAClC,IAAI,MAAqD,CAAC;IAC1D,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IACvG,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,qEAAqE;QACrE,iEAAiE;QACjE,IAAI,CAAC,gBAAgB,CAAC,eAAe,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,OAAO,CAAC,CAAC;QAClF,IAAI,CAAC,aAAa,CAAC;YACjB,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,eAAe;YACrB,OAAO;YACP,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC/D,CAAC,CAAC;QACH,OAAO;IACT,CAAC;YAAS,CAAC;QACT,WAAW,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAAC;IACxD,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACtB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YACZ,YAAY,EAAE,MAAM;YACpB,IAAI,EAAE,4DAA4D;YAClE,UAAU,EAAE,EAAE,YAAY,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE;SAC5E,CAAC,CAAC;QACH,IAAI,CAAC,aAAa,CAAC;YACjB,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,UAAU;YAClB,MAAM,EAAE,iBAAiB,MAAM,CAAC,UAAU,4CAA4C;YACtF,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC/D,CAAC,CAAC;IACL,CAAC;IACD,MAAM,IAAI,GAAgB;QACxB,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI;QACpC,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE;QAC3B,6DAA6D;QAC7D,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC;QAC7D,UAAU,EAAE,gBAAgB,IAAI,IAAI;QACpC,OAAO;QACP,eAAe,EAAE,IAAI,eAAe,EAAE;QACtC,aAAa,EAAE,UAAU,CAAC,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC,CAAC;QACtC,KAAK,EAAE,EAAE;QACT,SAAS,EAAE,KAAK;QAChB,UAAU,EAAE,IAAI;KACjB,CAAC;IACF,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACjC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,IAAI,CAAC,SAAS;QAAE,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAEpE,IAAI,SAAS,GAAG,gBAAgB,CAAC;IACjC,IAAI,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IACjC,IAAI,IAAI,GAA0B;QAChC,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAAC;IAEF,IAAI,CAAC;QACH,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAC/B,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;YAC3C,IAAI,GAAG,IAAI,CAAC;YAEZ,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;YAC9C,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;YACvC,MAAM,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;gBACpC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;oBACZ,YAAY,EAAE,MAAM;oBACpB,IAAI,EAAE,gBAAgB;oBACtB,mEAAmE;oBACnE,8BAA8B;oBAC9B,UAAU,EAAE,EAAE,GAAG,iBAAiB,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,aAAa,EAAE;iBAC7E,CAAC,CAAC;gBACH,eAAe,CAAC,KAAK,EAAE,CAAC;gBACxB,qEAAqE;gBACrE,qEAAqE;gBACrE,oEAAoE;gBACpE,gEAAgE;gBAChE,WAAW,CAAC,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;YAC1D,CAAC,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;YACvB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;YAEnC,qEAAqE;YACrE,sEAAsE;YACtE,wEAAwE;YACxE,sEAAsE;YACtE,yEAAyE;YACzE,4BAA4B;YAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ;gBAC3B,CAAC,CAAC,MAAM,iBAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,CAAC;gBAC/D,CAAC,CAAC,SAAS,CAAC;YACd,MAAM,QAAQ,GAA6B,IAAI,CAAC,QAAQ;gBACtD,CAAC,CAAC,kBAAkB,CAChB;oBACE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK;oBAC1B,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,QAAS,CAAC,MAAM,CAAC,IAAI,EAAE;oBAC3C,GAAG,EAAE,IAAI,CAAC,GAAG;iBACd,EACD,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,OAAO,EAAE,CAC/D;gBACH,CAAC,CAAC,SAAS,CAAC;YACd,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;YAClE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAC7B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;YAC7B,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,SAAS,CAAC;oBACtB,MAAM,EAAE,IAAI;oBACZ,WAAW;oBACX,SAAS,EAAE,QAAQ;oBACnB,SAAS;oBACT,MAAM,EAAE,eAAe,CAAC,MAAM;oBAC9B,kEAAkE;oBAClE,kEAAkE;oBAClE,cAAc;oBACd,YAAY,EAAE,IAAI,CAAC,aAAa;oBAChC,MAAM;oBACN,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAC9B,CAAC,CAAC;gBACH,sEAAsE;gBACtE,uEAAuE;gBACvE,mCAAmC;gBACnC,IAAI,IAAI,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;oBAChC,IAAI,CAAC,gBAAgB,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAC;gBACtD,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC;gBACzD,CAAC;gBACD,sEAAsE;gBACtE,iEAAiE;gBACjE,QAAQ,EAAE,MAAM,CACd,IAAI,CAAC,SAAS;oBACZ,CAAC,CAAC,WAAW;oBACb,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,OAAO;wBAC9B,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,IAAI,CAAC,UAAU,KAAK,OAAO;4BAC3B,CAAC,CAAC,OAAO;4BACT,CAAC,CAAC,SAAS,CAClB,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;gBAC/B,QAAQ,EAAE,MAAM,CACd,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CACpF,CAAC;YACJ,CAAC;oBAAS,CAAC;gBACT,YAAY,CAAC,aAAa,CAAC,CAAC;YAC9B,CAAC;YAED,sEAAsE;YACtE,yEAAyE;YACzE,qEAAqE;YACrE,2EAA2E;YAC3E,QAAQ,GAAG,IAAI,CAAC,SAAS,IAAI,QAAQ,CAAC;YACtC,SAAS,GAAG,IAAI,CAAC,UAAU,IAAI,SAAS,CAAC;YAEzC,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7C,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAG,CAAC;gBAC3B,qEAAqE;gBACrE,gCAAgC;gBAChC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;gBAClC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACzB,CAAC;QACH,CAAC;IACH,CAAC;YAAS,CAAC;QACT,IAAI,IAAI,CAAC,SAAS;YAAE,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACjE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACjC,WAAW,CAAC,mBAAmB,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;IACzD,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,iBAAiB,CAC9B,QAAyB,EACzB,SAAiB,EACjB,SAA6B;IAE7B,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,gBAAgB,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3E,MAAM,QAAQ,GAAG,SAAS;YACxB,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,KAAK,SAAS,CAAC;YACzD,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;QACnE,IAAI,CAAC,QAAQ;YAAE,OAAO,SAAS,CAAC;QAChC,OAAO;YACL,SAAS,EAAE,QAAQ,CAAC,EAAE;YACtB,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvE,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,SAAS,QAAQ,CAAC,KAAa;IAC7B,OAAO,GAAG,CAAC,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AAClD,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAY,EACZ,EAAa,EACb,GAOC;IAED,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC;IAC/F,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IAC7B,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAEjF,IAAI,WAAW,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC;QACpC,4DAA4D;QAC5D,IAAI,OAAO,CAAC,YAAY,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACtD,oEAAoE;YACpE,qEAAqE;YACrE,oEAAoE;YACpE,OAAO,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACvE,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjE,IAAI,CAAC,gBAAgB,CAAC,iBAAiB,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;gBAC/D,IAAI,CAAC,aAAa,CAChB,aAAa,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,CAChF,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,MAAM,KAAK,GAAmB,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YACnF,MAAM,MAAM,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;YACxC,MAAM,QAAQ,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;YAC5C,yEAAyE;YACzE,yEAAyE;YACzE,qDAAqD;YACrD,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,eAAe,CAAC;YACvD,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,IAAI,iBAAiB,CAAC;YAEhE,IAAI,UAAU,IAAI,SAAS,EAAE,CAAC;gBAC5B,IAAI,CAAC,WAAW,CACd,EAAE;gBACF,gEAAgE;gBAChE,2DAA2D;gBAC3D,aAAa,CACX;oBACE,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,oBAAoB;oBAC1B,OAAO,EAAE,UAAU;wBACjB,CAAC,CAAC,iCAAiC,QAAQ,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,eAAe,CAAC,wBAAwB,QAAQ,CAAC,QAAQ,CAAC,6BAA6B;wBAC1J,CAAC,CAAC,iCAAiC,iBAAiB,sCAAsC;iBAC7F,EACD,SAAS,CACV,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YAED,kEAAkE;YAClE,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC9B,MAAM,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;YAChC,yEAAyE;YACzE,4EAA4E;YAC5E,MAAM,MAAM,GACV,KAAK,IAAI,gBAAgB;gBACvB,CAAC,CAAC,oBAAoB,QAAQ,CAAC,KAAK,CAAC,WAAW,WAAW,CAAC,KAAK,CAAC,MAAM,oBAAoB,QAAQ,CAAC,eAAe,CAAC,IAAI;gBACzH,CAAC,CAAC,SAAS,CAAC;YAChB,IAAI,MAAM,EAAE,CAAC;gBACX,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;oBACZ,YAAY,EAAE,MAAM;oBACpB,IAAI,EAAE,kCAAkC;oBACxC,UAAU,EAAE;wBACV,YAAY,EAAE,SAAS;wBACvB,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC;wBACrB,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC,MAAM;qBACjC;iBACF,CAAC,CAAC;YACL,CAAC;YACD,IAAI,CAAC,aAAa,CAChB,aAAa,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,SAAS,CAAC,CAC9F,CAAC;QACJ,CAAC;QACD,OAAO;IACT,CAAC;IAED,yEAAyE;IACzE,MAAM,GAAG,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;IACzC,IAAI,WAAW,CAAC,OAAO,CAAC,IAAI,GAAG,WAAW,CAAC,gBAAgB,IAAI,GAAG,EAAE,CAAC;QACnE,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE;YACnB,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,qCAAqC,GAAG,4BAA4B;YAC7E,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpC,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC;IACvF,CAAC;IACD,KAAK,UAAU,CAAC,IAAI,EAAE;QACpB,IAAI;QACJ,SAAS;QACT,WAAW;QACX,UAAU,EAAE,mBAAmB;QAC/B,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Review follow-ups on the activity layer.
|
|
2
|
+
|
|
3
|
+
-- Retry budget for push delivery: a send_failed intent is retried (with
|
|
4
|
+
-- backoff, see notify.ts) until this counter reaches the cap — one transient
|
|
5
|
+
-- push-service failure no longer forfeits push for the intent.
|
|
6
|
+
ALTER TABLE notification_intents ADD COLUMN send_attempts INTEGER NOT NULL DEFAULT 0;
|
|
7
|
+
|
|
8
|
+
-- Pre-migration send_failed rows were TERMINAL under the old semantics; a
|
|
9
|
+
-- zero counter would re-arm them and batch-resend arbitrarily old
|
|
10
|
+
-- notifications on the first tick after upgrade. Backfill them as having
|
|
11
|
+
-- spent the budget (3 = MAX_SEND_ATTEMPTS in notify.ts).
|
|
12
|
+
UPDATE notification_intents SET send_attempts = 3 WHERE status = 'send_failed';
|
|
13
|
+
|
|
14
|
+
-- The hourly prune's candidate query filters on detail_pruned = 0 and
|
|
15
|
+
-- ended_at; without an index it scans every already-pruned row, growing with
|
|
16
|
+
-- exactly the table it is trying to shrink. Partial: pruned rows leave it.
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_activity_rollups_prunable
|
|
18
|
+
ON activity_run_rollups(ended_at) WHERE detail_pruned = 0;
|
|
19
|
+
|
|
20
|
+
-- No query reads spans by (session_id, started_at): session-scoped reads go
|
|
21
|
+
-- through run_id (runs are looked up first). Drop the write-amplifying index.
|
|
22
|
+
DROP INDEX IF EXISTS idx_activity_spans_session;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Frozen effective-cost accounting on run rollups (computed in rollupRunInTx).
|
|
2
|
+
--
|
|
3
|
+
-- effective_cost_usd: what the run actually cost — 0 for subscription-billed
|
|
4
|
+
-- work regardless of tokens, priced per-model usage for api-billed. NULL =
|
|
5
|
+
-- unknown, 0 = genuinely free (never conflated). billing_mode is the
|
|
6
|
+
-- 'subscription' | 'api' classification behind that number; pricing_estimate
|
|
7
|
+
-- is 1 when the price came from estimated rates (snapshot data, or an
|
|
8
|
+
-- OpenRouter routing variant priced at its base rate).
|
|
9
|
+
--
|
|
10
|
+
-- Deliberately NO backfill: pre-feature rows carry no billing attrs and their
|
|
11
|
+
-- span detail may already be pruned, so any classification would be a guess.
|
|
12
|
+
-- They stay NULL (= unknown) by decision — every surface renders unknown,
|
|
13
|
+
-- never $0.
|
|
14
|
+
-- The CHECKs admit NULL (unknown) by SQL semantics; they reject only a wrong
|
|
15
|
+
-- non-NULL value at the door. rowToRunRollup still validates on read, so a
|
|
16
|
+
-- row written around them degrades to NULL rather than surfacing garbage.
|
|
17
|
+
ALTER TABLE activity_run_rollups ADD COLUMN effective_cost_usd REAL;
|
|
18
|
+
ALTER TABLE activity_run_rollups ADD COLUMN billing_mode TEXT
|
|
19
|
+
CHECK (billing_mode IN ('subscription', 'api'));
|
|
20
|
+
ALTER TABLE activity_run_rollups ADD COLUMN pricing_estimate INTEGER
|
|
21
|
+
CHECK (pricing_estimate IN (0, 1));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schlessera/brain-ui-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "brain-kit chat-UI server: Hono app factory, WebSocket turn coordinator, auth (password/passkeys/tailscale/proxy), session catalog, and brain/files/voice routes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"src",
|
|
40
40
|
"dist",
|
|
41
41
|
"migrations",
|
|
42
|
+
"data",
|
|
42
43
|
"README.md"
|
|
43
44
|
],
|
|
44
45
|
"scripts": {
|
|
@@ -48,8 +49,8 @@
|
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@opentelemetry/api": "^1.9.1",
|
|
50
51
|
"@opentelemetry/api-logs": "0.221.0",
|
|
51
|
-
"@schlessera/brain-render-template": "0.
|
|
52
|
-
"@schlessera/brain-ui-sdk": "0.
|
|
52
|
+
"@schlessera/brain-render-template": "0.20.0",
|
|
53
|
+
"@schlessera/brain-ui-sdk": "0.20.0",
|
|
53
54
|
"@simplewebauthn/server": "^13.3.2",
|
|
54
55
|
"hono": "^4",
|
|
55
56
|
"ignore": "^7.0.5",
|
package/src/activity/digest.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { Database } from "bun:sqlite";
|
|
|
15
15
|
import { isFailureOutcome, type ActivityDigest } from "@schlessera/brain-ui-sdk/protocol";
|
|
16
16
|
|
|
17
17
|
import { getSetting, setSetting } from "../db/settings.js";
|
|
18
|
-
import { rowToRunRollup } from "./store.js";
|
|
18
|
+
import { rowToRunRollup, sumEffectiveCost } from "./store.js";
|
|
19
19
|
|
|
20
20
|
export type { ActivityDigest } from "@schlessera/brain-ui-sdk/protocol";
|
|
21
21
|
|
|
@@ -29,7 +29,27 @@ const FIRST_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
|
29
29
|
|
|
30
30
|
/** Generate and persist the digest for [coveredUntil, now]. */
|
|
31
31
|
export function generateActivityDigest(db: Database, now = Date.now()): ActivityDigest {
|
|
32
|
+
// The read-compute-write of coveredUntil runs as ONE immediate transaction:
|
|
33
|
+
// the cron digest job and a manual generation are separate processes on the
|
|
34
|
+
// same DB, and unserialized they both window from the same coveredUntil —
|
|
35
|
+
// double-counting the overlap and letting the retention floor regress when
|
|
36
|
+
// the earlier `now` lands last. BEGIN IMMEDIATE + busy_timeout serializes
|
|
37
|
+
// them; the loser re-reads the winner's coveredUntil and windows after it.
|
|
38
|
+
return db.transaction(() => generateInTx(db, now)).immediate();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function generateInTx(db: Database, now: number): ActivityDigest {
|
|
32
42
|
const covered = getSetting<number | null>(db, DIGEST_COVERED_KEY, null);
|
|
43
|
+
if (covered !== null && now <= covered) {
|
|
44
|
+
// A newer generation already committed — we serialized behind the race's
|
|
45
|
+
// winner, or our clock trails coveredUntil. The window would be inverted
|
|
46
|
+
// and empty; return the winner's digest instead of overwriting it.
|
|
47
|
+
const latest = latestActivityDigest(db);
|
|
48
|
+
if (latest) return latest;
|
|
49
|
+
// Covered set but no stored digest (partial state): degrade to an empty
|
|
50
|
+
// window at the floor rather than inverting it.
|
|
51
|
+
now = covered;
|
|
52
|
+
}
|
|
33
53
|
const windowStart = covered ?? now - FIRST_WINDOW_MS;
|
|
34
54
|
|
|
35
55
|
// Windowed on ended_at, not started_at: every FINISHED run gets
|
|
@@ -47,6 +67,9 @@ export function generateActivityDigest(db: Database, now = Date.now()): Activity
|
|
|
47
67
|
|
|
48
68
|
const relevant = rows.filter((r) => r.jobName !== DIGEST_JOB_NAME);
|
|
49
69
|
const failures = relevant.filter((r) => isFailureOutcome(r.outcome));
|
|
70
|
+
// Both cost sums are sum-of-knowns; sumEffectiveCost counts the excluded
|
|
71
|
+
// unknown-cost runs so the digest never passes an unknown off as $0 (AE3).
|
|
72
|
+
const effective = sumEffectiveCost(relevant);
|
|
50
73
|
|
|
51
74
|
const digest: ActivityDigest = {
|
|
52
75
|
generatedAt: now,
|
|
@@ -55,6 +78,8 @@ export function generateActivityDigest(db: Database, now = Date.now()): Activity
|
|
|
55
78
|
runs: relevant.length,
|
|
56
79
|
failures: failures.length,
|
|
57
80
|
costUsd: relevant.reduce((a, r) => a + (r.costUsd ?? 0), 0),
|
|
81
|
+
effectiveCostUsd: effective.effectiveCostUsd,
|
|
82
|
+
unpricedRuns: effective.unpricedRuns,
|
|
58
83
|
inputTokens: relevant.reduce((a, r) => a + (r.inputTokens ?? 0), 0),
|
|
59
84
|
outputTokens: relevant.reduce((a, r) => a + (r.outputTokens ?? 0), 0),
|
|
60
85
|
notable: failures.slice(0, 10).map((r) => ({
|
|
@@ -69,7 +94,10 @@ export function generateActivityDigest(db: Database, now = Date.now()): Activity
|
|
|
69
94
|
};
|
|
70
95
|
|
|
71
96
|
setSetting(db, DIGEST_LATEST_KEY, digest);
|
|
72
|
-
|
|
97
|
+
// Monotonic: a generator whose clock trails the previous coveredUntil
|
|
98
|
+
// (skew, or a back-to-back manual run) must not walk the retention floor
|
|
99
|
+
// backwards and re-expose already-covered spans to double coverage.
|
|
100
|
+
setSetting(db, DIGEST_COVERED_KEY, Math.max(now, covered ?? 0));
|
|
73
101
|
return digest;
|
|
74
102
|
}
|
|
75
103
|
|
package/src/activity/notify.ts
CHANGED
|
@@ -65,6 +65,12 @@ export interface ActivityNotifier {
|
|
|
65
65
|
|
|
66
66
|
/** Global cap on intents CREATED per hour — failure-storm circuit. */
|
|
67
67
|
const MAX_INTENTS_PER_HOUR = 20;
|
|
68
|
+
/** A send_failed intent is retried this many times before push is forfeited
|
|
69
|
+
* (the inbox — the guaranteed tier — has it regardless). */
|
|
70
|
+
const MAX_SEND_ATTEMPTS = 3;
|
|
71
|
+
/** Backoff between retry attempts, so a transient push-service outage longer
|
|
72
|
+
* than one tick still gets fresh attempts spread over ~15 minutes. */
|
|
73
|
+
const SEND_RETRY_BACKOFF_MS = 5 * 60 * 1000;
|
|
68
74
|
/** Watchdog default: a live root run older than this is flagged as stuck. */
|
|
69
75
|
const DEFAULT_STUCK_THRESHOLD_MS = 45 * 60 * 1000;
|
|
70
76
|
|
|
@@ -194,7 +200,15 @@ export function createActivityNotifier(deps: ActivityNotifierDeps): ActivityNoti
|
|
|
194
200
|
{},
|
|
195
201
|
log
|
|
196
202
|
);
|
|
197
|
-
|
|
203
|
+
// Scan with the SMALLEST effective threshold: findStuck pre-filters
|
|
204
|
+
// by age, so a per-job override below the default would otherwise
|
|
205
|
+
// never see its candidates. The per-span check below still applies
|
|
206
|
+
// each job's own threshold.
|
|
207
|
+
const overrideValues = Object.values(overrides).filter(
|
|
208
|
+
(v): v is number => typeof v === "number" && v > 0
|
|
209
|
+
);
|
|
210
|
+
const scanThreshold = Math.min(defaultThreshold, ...overrideValues);
|
|
211
|
+
for (const span of store.findStuck(scanThreshold, now)) {
|
|
198
212
|
if (stuckFlagged.has(span.runId)) continue;
|
|
199
213
|
const threshold = span.jobName ? (overrides[span.jobName] ?? defaultThreshold) : defaultThreshold;
|
|
200
214
|
if ((now ?? Date.now()) - span.startedAt < threshold) continue;
|
|
@@ -229,21 +243,29 @@ export function createActivityNotifier(deps: ActivityNotifierDeps): ActivityNoti
|
|
|
229
243
|
|
|
230
244
|
pending(limit = 20) {
|
|
231
245
|
// Acknowledged means dismissed in the inbox — never push it later.
|
|
246
|
+
// send_failed is retried with backoff until the attempt budget is
|
|
247
|
+
// spent: one transient push-service failure must not forfeit push.
|
|
232
248
|
return (
|
|
233
249
|
db
|
|
234
250
|
.query(
|
|
235
|
-
|
|
251
|
+
`SELECT * FROM notification_intents
|
|
252
|
+
WHERE acknowledged = 0
|
|
253
|
+
AND (status = 'pending'
|
|
254
|
+
OR (status = 'send_failed' AND send_attempts < ? AND updated_at < ?))
|
|
255
|
+
ORDER BY created_at LIMIT ?`
|
|
236
256
|
)
|
|
237
|
-
.all(limit) as any[]
|
|
257
|
+
.all(MAX_SEND_ATTEMPTS, Date.now() - SEND_RETRY_BACKOFF_MS, limit) as any[]
|
|
238
258
|
).map(rowToIntent);
|
|
239
259
|
},
|
|
240
260
|
|
|
241
261
|
markDelivered(id, status) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
262
|
+
// A failed pass spends one attempt from the retry budget.
|
|
263
|
+
db.query(
|
|
264
|
+
`UPDATE notification_intents
|
|
265
|
+
SET status = ?, updated_at = ?,
|
|
266
|
+
send_attempts = send_attempts + (CASE WHEN ? = 'send_failed' THEN 1 ELSE 0 END)
|
|
267
|
+
WHERE id = ?`
|
|
268
|
+
).run(status, Date.now(), status, id);
|
|
247
269
|
},
|
|
248
270
|
|
|
249
271
|
acknowledge(id) {
|
package/src/activity/query.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { isFailureOutcome } from "@schlessera/brain-ui-sdk/protocol";
|
|
|
14
14
|
|
|
15
15
|
import { latestActivityDigest } from "./digest.js";
|
|
16
16
|
import type { ActivityNotifier } from "./notify.js";
|
|
17
|
-
import { rowToRunRollup, type ActivityStore, type SpanRow } from "./store.js";
|
|
17
|
+
import { rowToRunRollup, sumEffectiveCost, type ActivityStore, type SpanRow } from "./store.js";
|
|
18
18
|
|
|
19
19
|
export function runActivityQuery(
|
|
20
20
|
db: Database,
|
|
@@ -54,6 +54,11 @@ export function runActivityQuery(
|
|
|
54
54
|
outcome: r.outcome,
|
|
55
55
|
durationMs: r.durationMs,
|
|
56
56
|
costUsd: r.costUsd,
|
|
57
|
+
// Explicit nulls, never omitted: the model must be able to tell
|
|
58
|
+
// "unknown" (null) from "genuinely free" ($0) — AE3.
|
|
59
|
+
effectiveCostUsd: r.effectiveCostUsd,
|
|
60
|
+
billingMode: r.billingMode,
|
|
61
|
+
pricingEstimate: r.pricingEstimate,
|
|
57
62
|
failureReason: r.failureReason,
|
|
58
63
|
})),
|
|
59
64
|
};
|
|
@@ -63,8 +68,22 @@ export function runActivityQuery(
|
|
|
63
68
|
if (!query.runId) return { error: "scope 'run' needs runId" };
|
|
64
69
|
const snapshot = store.snapshotRun(query.runId);
|
|
65
70
|
if (snapshot) {
|
|
71
|
+
// The cost facts live on the rollup, not the spans — attach them here
|
|
72
|
+
// the same way the REST detail route does, so the agent sees the
|
|
73
|
+
// frozen triple for a finished run without waiting for detail
|
|
74
|
+
// pruning. Explicit nulls (AE3): a still-running run has no rollup
|
|
75
|
+
// yet, and unknown must never read as $0.
|
|
76
|
+
const rollupRow = db
|
|
77
|
+
.query("SELECT * FROM activity_run_rollups WHERE run_id = ?")
|
|
78
|
+
.get(query.runId) as any;
|
|
79
|
+
const liveRollup = rollupRow ? rowToRunRollup(rollupRow) : null;
|
|
66
80
|
return {
|
|
67
81
|
runId: query.runId,
|
|
82
|
+
costUsd: liveRollup?.costUsd ?? null,
|
|
83
|
+
effectiveCostUsd: liveRollup?.effectiveCostUsd ?? null,
|
|
84
|
+
billingMode: liveRollup?.billingMode ?? null,
|
|
85
|
+
pricingEstimate: liveRollup?.pricingEstimate ?? null,
|
|
86
|
+
failureReason: liveRollup?.failureReason ?? null,
|
|
68
87
|
spans: snapshot.spans.map((s) => ({
|
|
69
88
|
spanId: s.spanId,
|
|
70
89
|
parent: s.parentSpanId,
|
|
@@ -97,6 +116,9 @@ export function runActivityQuery(
|
|
|
97
116
|
startedAt: iso(rollup.startedAt),
|
|
98
117
|
durationMs: rollup.durationMs,
|
|
99
118
|
costUsd: rollup.costUsd,
|
|
119
|
+
effectiveCostUsd: rollup.effectiveCostUsd,
|
|
120
|
+
billingMode: rollup.billingMode,
|
|
121
|
+
pricingEstimate: rollup.pricingEstimate,
|
|
100
122
|
failureReason: rollup.failureReason,
|
|
101
123
|
},
|
|
102
124
|
};
|
|
@@ -109,11 +131,16 @@ export function runActivityQuery(
|
|
|
109
131
|
.all(since) as any[]
|
|
110
132
|
).map(rowToRunRollup);
|
|
111
133
|
const digest = latestActivityDigest(db);
|
|
134
|
+
const effective = sumEffectiveCost(rows);
|
|
112
135
|
return {
|
|
113
136
|
windowHours: hoursBack,
|
|
114
137
|
runs: rows.length,
|
|
115
138
|
failures: rows.filter((r) => isFailureOutcome(r.outcome)).length,
|
|
139
|
+
// Both sums are sum-of-KNOWNS; the runs excluded from the effective
|
|
140
|
+
// sum ride along as unpricedRuns so unknown never reads as $0 (AE3).
|
|
116
141
|
costUsd: round(rows.reduce((a, r) => a + (r.costUsd ?? 0), 0)),
|
|
142
|
+
effectiveCostUsd: round(effective.effectiveCostUsd),
|
|
143
|
+
unpricedRuns: effective.unpricedRuns,
|
|
117
144
|
inputTokens: rows.reduce((a, r) => a + (r.inputTokens ?? 0), 0),
|
|
118
145
|
outputTokens: rows.reduce((a, r) => a + (r.outputTokens ?? 0), 0),
|
|
119
146
|
...(digest ? { lastDigestAt: iso(digest.generatedAt) } : {}),
|
package/src/activity/recorder.ts
CHANGED
|
@@ -24,6 +24,12 @@ import type {
|
|
|
24
24
|
ServerMessage,
|
|
25
25
|
TurnUsage,
|
|
26
26
|
} from "@schlessera/brain-ui-sdk/server";
|
|
27
|
+
import {
|
|
28
|
+
SPAN_OP_EXECUTE_TOOL,
|
|
29
|
+
SPAN_OP_INVOKE_AGENT,
|
|
30
|
+
SPAN_TOOL_NAME_PREFIX,
|
|
31
|
+
type BillingMode,
|
|
32
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
27
33
|
import type { Logger } from "@opentelemetry/api-logs";
|
|
28
34
|
|
|
29
35
|
import type { ActivityStore, SpanOutcome, SpanUsage } from "./store.js";
|
|
@@ -51,7 +57,14 @@ export interface TurnRecorder {
|
|
|
51
57
|
|
|
52
58
|
export function createTurnRecorder(
|
|
53
59
|
deps: TurnRecorderDeps,
|
|
54
|
-
turn: {
|
|
60
|
+
turn: {
|
|
61
|
+
turnId: string;
|
|
62
|
+
sessionId: string | null;
|
|
63
|
+
/** The RESOLVED inference profile this turn runs on (post pin-drop fallback). */
|
|
64
|
+
profileId?: string;
|
|
65
|
+
/** Billing classification of that profile, resolved at run start (U3). */
|
|
66
|
+
billingMode?: BillingMode;
|
|
67
|
+
}
|
|
55
68
|
): TurnRecorder {
|
|
56
69
|
const { store, onWrite, log } = deps;
|
|
57
70
|
const runId = turn.turnId;
|
|
@@ -82,11 +95,18 @@ export function createTurnRecorder(
|
|
|
82
95
|
store.startSpan({
|
|
83
96
|
spanId: rootSpanId,
|
|
84
97
|
runId,
|
|
85
|
-
name:
|
|
98
|
+
name: SPAN_OP_INVOKE_AGENT,
|
|
86
99
|
kind: "turn",
|
|
87
100
|
origin: "session",
|
|
88
101
|
sessionId,
|
|
89
|
-
|
|
102
|
+
// Profile + billing ride the ROOT span so the rollup can price the run
|
|
103
|
+
// without any registry or env lookup of its own (a root missing the
|
|
104
|
+
// billing attr falls back to env classification at rollup time).
|
|
105
|
+
attrs: {
|
|
106
|
+
"gen_ai.operation.name": SPAN_OP_INVOKE_AGENT,
|
|
107
|
+
...(turn.profileId ? { "brain.profile_id": turn.profileId } : {}),
|
|
108
|
+
...(turn.billingMode ? { "brain.billing_mode": turn.billingMode } : {}),
|
|
109
|
+
},
|
|
90
110
|
});
|
|
91
111
|
onWrite?.();
|
|
92
112
|
}
|
|
@@ -124,12 +144,12 @@ export function createTurnRecorder(
|
|
|
124
144
|
spanId: msg.toolUseId,
|
|
125
145
|
runId,
|
|
126
146
|
parentSpanId: msg.parentToolUseId ?? rootSpanId,
|
|
127
|
-
name:
|
|
147
|
+
name: `${SPAN_TOOL_NAME_PREFIX}${msg.toolName}`,
|
|
128
148
|
kind: isSubagent ? "subagent" : "tool",
|
|
129
149
|
origin: "session",
|
|
130
150
|
sessionId,
|
|
131
151
|
attrs: {
|
|
132
|
-
"gen_ai.operation.name": isSubagent ?
|
|
152
|
+
"gen_ai.operation.name": isSubagent ? SPAN_OP_INVOKE_AGENT : SPAN_OP_EXECUTE_TOOL,
|
|
133
153
|
"gen_ai.tool.name": msg.toolName,
|
|
134
154
|
},
|
|
135
155
|
});
|
|
@@ -142,6 +162,20 @@ export function createTurnRecorder(
|
|
|
142
162
|
// the wait/execution boundary.
|
|
143
163
|
break;
|
|
144
164
|
}
|
|
165
|
+
case "tool_use_complete": {
|
|
166
|
+
// The complete input object in one frame — tool_input_delta
|
|
167
|
+
// frames stay ignored, no accumulation needed. Recorded as a
|
|
168
|
+
// span event so the Activity drill-in can expand the call (R14).
|
|
169
|
+
store.appendEvent(
|
|
170
|
+
msg.toolUseId,
|
|
171
|
+
"tool_input",
|
|
172
|
+
safeStringify(msg.input),
|
|
173
|
+
undefined,
|
|
174
|
+
PAYLOAD_EVENT_CAP
|
|
175
|
+
);
|
|
176
|
+
onWrite?.();
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
145
179
|
case "tool_result": {
|
|
146
180
|
// endSpan is write-once: a span already closed (a denial landed
|
|
147
181
|
// the outcome first) rejects this as a no-op, and an unknown
|
|
@@ -151,6 +185,9 @@ export function createTurnRecorder(
|
|
|
151
185
|
outcome,
|
|
152
186
|
reason: outcome === "error" ? clip(msg.output, 500) : undefined,
|
|
153
187
|
});
|
|
188
|
+
// Error output rides along too — a failed call's output is what
|
|
189
|
+
// the drill-in needs most. appendEvent no-ops on unknown spans.
|
|
190
|
+
store.appendEvent(msg.toolUseId, "tool_output", msg.output, undefined, PAYLOAD_EVENT_CAP);
|
|
154
191
|
onWrite?.();
|
|
155
192
|
break;
|
|
156
193
|
}
|
|
@@ -259,3 +296,18 @@ export function createTurnRecorder(
|
|
|
259
296
|
function clip(text: string, max: number): string {
|
|
260
297
|
return text.length > max ? text.slice(0, max) : text;
|
|
261
298
|
}
|
|
299
|
+
|
|
300
|
+
/** Cap for recorded tool input/output payload events — roomier than the
|
|
301
|
+
* 500-char outcome-reason clip, still far under the store's 16 KB event cap.
|
|
302
|
+
* Passed to appendEvent, whose clipping sets the wire `truncated` flag. */
|
|
303
|
+
const PAYLOAD_EVENT_CAP = 4096;
|
|
304
|
+
|
|
305
|
+
/** Tool inputs are arbitrary values: serialization must never take the turn
|
|
306
|
+
* down with it (BigInt members throw; a toJSON can return undefined). */
|
|
307
|
+
function safeStringify(value: unknown): string {
|
|
308
|
+
try {
|
|
309
|
+
return JSON.stringify(value) ?? String(value);
|
|
310
|
+
} catch {
|
|
311
|
+
return String(value);
|
|
312
|
+
}
|
|
313
|
+
}
|