@schlessera/brain-ui-server 0.34.0 → 0.35.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 +28 -0
- package/dist/activity/notify.d.ts.map +1 -1
- package/dist/activity/notify.js +18 -8
- package/dist/activity/notify.js.map +1 -1
- package/dist/activity/push-sender.d.ts +7 -3
- package/dist/activity/push-sender.d.ts.map +1 -1
- package/dist/activity/push-sender.js +45 -11
- package/dist/activity/push-sender.js.map +1 -1
- package/dist/activity/query.d.ts.map +1 -1
- package/dist/activity/query.js +8 -0
- package/dist/activity/query.js.map +1 -1
- package/dist/activity/recorder.d.ts +11 -1
- package/dist/activity/recorder.d.ts.map +1 -1
- package/dist/activity/recorder.js +52 -2
- package/dist/activity/recorder.js.map +1 -1
- package/dist/activity/runtime.d.ts.map +1 -1
- package/dist/activity/runtime.js +2 -5
- package/dist/activity/runtime.js.map +1 -1
- package/dist/activity/sql.d.ts +34 -0
- package/dist/activity/sql.d.ts.map +1 -0
- package/dist/activity/sql.js +114 -0
- package/dist/activity/sql.js.map +1 -0
- package/dist/activity/store.d.ts +7 -0
- package/dist/activity/store.d.ts.map +1 -1
- package/dist/activity/store.js +48 -113
- package/dist/activity/store.js.map +1 -1
- package/dist/activity/stream.d.ts +3 -1
- package/dist/activity/stream.d.ts.map +1 -1
- package/dist/activity/stream.js +32 -17
- package/dist/activity/stream.js.map +1 -1
- package/dist/app-env.d.ts +8 -0
- package/dist/app-env.d.ts.map +1 -0
- package/dist/app-env.js +2 -0
- package/dist/app-env.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +28 -3
- package/dist/app.js.map +1 -1
- package/dist/brain/client.d.ts +4 -2
- package/dist/brain/client.d.ts.map +1 -1
- package/dist/brain/client.js +60 -20
- package/dist/brain/client.js.map +1 -1
- package/dist/brain/types.d.ts +9 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/cron/run-job.d.ts.map +1 -1
- package/dist/cron/run-job.js +60 -10
- package/dist/cron/run-job.js.map +1 -1
- package/dist/db/principals.d.ts +72 -0
- package/dist/db/principals.d.ts.map +1 -0
- package/dist/db/principals.js +294 -0
- package/dist/db/principals.js.map +1 -0
- package/dist/files/walker.d.ts.map +1 -1
- package/dist/files/walker.js +6 -1
- package/dist/files/walker.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +37 -9
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +148 -74
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/passkeys.d.ts +9 -7
- package/dist/middleware/passkeys.d.ts.map +1 -1
- package/dist/middleware/passkeys.js +25 -7
- package/dist/middleware/passkeys.js.map +1 -1
- package/dist/middleware/principals.d.ts +18 -0
- package/dist/middleware/principals.d.ts.map +1 -0
- package/dist/middleware/principals.js +138 -0
- package/dist/middleware/principals.js.map +1 -0
- package/dist/middleware/require-owner.d.ts +5 -0
- package/dist/middleware/require-owner.d.ts.map +1 -0
- package/dist/middleware/require-owner.js +10 -0
- package/dist/middleware/require-owner.js.map +1 -0
- package/dist/observability/loggers.js +1 -1
- package/dist/observability/loggers.js.map +1 -1
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/activity.js +3 -0
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/brain.d.ts.map +1 -1
- package/dist/routes/brain.js +102 -63
- package/dist/routes/brain.js.map +1 -1
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +7 -2
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/push.d.ts +61 -2
- package/dist/routes/push.d.ts.map +1 -1
- package/dist/routes/push.js +8 -3
- package/dist/routes/push.js.map +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +30 -4
- package/dist/routes/sessions.js.map +1 -1
- package/dist/share/staging.d.ts.map +1 -1
- package/dist/share/staging.js +11 -4
- package/dist/share/staging.js.map +1 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +6 -2
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/clients.d.ts +9 -2
- package/dist/ws/clients.d.ts.map +1 -1
- package/dist/ws/clients.js +44 -6
- package/dist/ws/clients.js.map +1 -1
- package/dist/ws/connection.d.ts +7 -4
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +83 -12
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts +12 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +46 -6
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/history.d.ts.map +1 -1
- package/dist/ws/history.js +7 -5
- package/dist/ws/history.js.map +1 -1
- package/dist/ws/host.d.ts +22 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +39 -1
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/run-session.d.ts +7 -7
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +208 -52
- package/dist/ws/run-session.js.map +1 -1
- package/dist/ws/shrink.d.ts +3 -1
- package/dist/ws/shrink.d.ts.map +1 -1
- package/dist/ws/shrink.js +46 -16
- package/dist/ws/shrink.js.map +1 -1
- package/dist/ws/turns.d.ts +58 -3
- package/dist/ws/turns.d.ts.map +1 -1
- package/dist/ws/turns.js +101 -2
- package/dist/ws/turns.js.map +1 -1
- package/migrations/011_principals.sql +13 -0
- package/migrations/012_activity_principals.sql +12 -0
- package/migrations/013_push_subscription_principals.sql +7 -0
- package/migrations/014_invalidate_legacy_sessions.sql +14 -0
- package/package.json +3 -3
- package/src/activity/notify.ts +16 -7
- package/src/activity/push-sender.ts +79 -16
- package/src/activity/query.ts +8 -0
- package/src/activity/recorder.ts +68 -3
- package/src/activity/runtime.ts +2 -5
- package/src/activity/sql.ts +122 -0
- package/src/activity/store.ts +73 -155
- package/src/activity/stream.ts +37 -17
- package/src/app-env.ts +8 -0
- package/src/app.ts +33 -4
- package/src/brain/client.ts +61 -20
- package/src/brain/types.ts +10 -0
- package/src/cron/run-job.ts +41 -17
- package/src/db/principals.ts +452 -0
- package/src/files/walker.ts +5 -1
- package/src/index.ts +6 -1
- package/src/middleware/auth.ts +241 -81
- package/src/middleware/passkeys.ts +35 -14
- package/src/middleware/principals.ts +189 -0
- package/src/middleware/require-owner.ts +13 -0
- package/src/observability/loggers.ts +1 -1
- package/src/routes/activity.ts +3 -0
- package/src/routes/brain.ts +107 -70
- package/src/routes/files.ts +10 -5
- package/src/routes/push.ts +11 -5
- package/src/routes/sessions.ts +45 -20
- package/src/share/staging.ts +11 -4
- package/src/ws/bridge.ts +14 -2
- package/src/ws/clients.ts +52 -7
- package/src/ws/connection.ts +98 -20
- package/src/ws/dispatch.ts +66 -6
- package/src/ws/history.ts +7 -5
- package/src/ws/host.ts +59 -1
- package/src/ws/run-session.ts +247 -74
- package/src/ws/shrink.ts +48 -14
- package/src/ws/turns.ts +136 -5
package/src/activity/store.ts
CHANGED
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
resolveStandalonePricingConfig,
|
|
39
39
|
} from "../config/env.js";
|
|
40
40
|
import { createModelPricing, type PricingRates } from "../pricing/model-pricing.js";
|
|
41
|
+
import type { PrincipalKind } from "../db/principals.js";
|
|
42
|
+
import { ACTIVITY_SQL } from "./sql.js";
|
|
41
43
|
|
|
42
44
|
export const SPAN_OUTCOMES = [
|
|
43
45
|
"success",
|
|
@@ -52,6 +54,12 @@ export type SpanOutcome = (typeof SPAN_OUTCOMES)[number];
|
|
|
52
54
|
export type SpanKind = "turn" | "tool" | "subagent" | "cron";
|
|
53
55
|
export type SpanOrigin = "session" | "cron";
|
|
54
56
|
|
|
57
|
+
function isPrincipalKind(value: unknown): value is PrincipalKind {
|
|
58
|
+
return (
|
|
59
|
+
value === "owner" || value === "agent" || value === "ambient" || value === "system"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
/** Token usage as spans carry it — OTel GenAI attribute semantics. */
|
|
56
64
|
export interface SpanUsage {
|
|
57
65
|
inputTokens?: number;
|
|
@@ -71,6 +79,7 @@ export interface SpanRow {
|
|
|
71
79
|
origin: SpanOrigin;
|
|
72
80
|
sessionId: string | null;
|
|
73
81
|
jobName: string | null;
|
|
82
|
+
principalId: string | null;
|
|
74
83
|
attrs: Record<string, unknown>;
|
|
75
84
|
startedAt: number;
|
|
76
85
|
waitUntil: number | null;
|
|
@@ -98,6 +107,9 @@ export interface RunRollupRow {
|
|
|
98
107
|
name: string;
|
|
99
108
|
sessionId: string | null;
|
|
100
109
|
jobName: string | null;
|
|
110
|
+
principalId: string | null;
|
|
111
|
+
principalLabel: string | null;
|
|
112
|
+
principalKind: PrincipalKind | null;
|
|
101
113
|
startedAt: number;
|
|
102
114
|
endedAt: number | null;
|
|
103
115
|
outcome: SpanOutcome | null;
|
|
@@ -130,6 +142,9 @@ export function rowToRunRollup(r: any): RunRollupRow {
|
|
|
130
142
|
name: r.name,
|
|
131
143
|
sessionId: r.session_id,
|
|
132
144
|
jobName: r.job_name,
|
|
145
|
+
principalId: r.principal_id ?? null,
|
|
146
|
+
principalLabel: r.principal_label ?? null,
|
|
147
|
+
principalKind: isPrincipalKind(r.principal_kind) ? r.principal_kind : null,
|
|
133
148
|
startedAt: r.started_at,
|
|
134
149
|
endedAt: r.ended_at,
|
|
135
150
|
outcome: r.outcome,
|
|
@@ -182,6 +197,7 @@ export interface StartSpanInput {
|
|
|
182
197
|
origin: SpanOrigin;
|
|
183
198
|
sessionId?: string;
|
|
184
199
|
jobName?: string;
|
|
200
|
+
principalId?: string;
|
|
185
201
|
attrs?: Record<string, unknown>;
|
|
186
202
|
startedAt?: number;
|
|
187
203
|
}
|
|
@@ -236,7 +252,12 @@ export interface ActivityStore {
|
|
|
236
252
|
/** Non-terminal field updates (usage enrichment, wait boundary, attrs). */
|
|
237
253
|
patchSpan(
|
|
238
254
|
spanId: string,
|
|
239
|
-
patch: {
|
|
255
|
+
patch: {
|
|
256
|
+
principalId?: string;
|
|
257
|
+
attrs?: Record<string, unknown>;
|
|
258
|
+
usage?: SpanUsage;
|
|
259
|
+
waitUntil?: number;
|
|
260
|
+
}
|
|
240
261
|
): boolean;
|
|
241
262
|
/**
|
|
242
263
|
* Append one event; null (nothing written) for an unknown span. `cap`
|
|
@@ -327,15 +348,13 @@ export function createActivityStore(
|
|
|
327
348
|
|
|
328
349
|
function nextSeq(runId: string): number {
|
|
329
350
|
const row = db
|
|
330
|
-
.query(
|
|
351
|
+
.query(ACTIVITY_SQL.nextSeq)
|
|
331
352
|
.get(runId) as { hi: number };
|
|
332
353
|
return row.hi + 1;
|
|
333
354
|
}
|
|
334
355
|
|
|
335
356
|
function logChange(runId: string, seq: number, spanId: string, eventIndex?: number) {
|
|
336
|
-
db.query(
|
|
337
|
-
"INSERT INTO activity_changes (run_id, seq, span_id, event_index) VALUES (?, ?, ?, ?)"
|
|
338
|
-
).run(runId, seq, spanId, eventIndex ?? null);
|
|
357
|
+
db.query(ACTIVITY_SQL.insertChange).run(runId, seq, spanId, eventIndex ?? null);
|
|
339
358
|
}
|
|
340
359
|
|
|
341
360
|
function rowToSpan(r: any): SpanRow {
|
|
@@ -348,6 +367,7 @@ export function createActivityStore(
|
|
|
348
367
|
origin: r.origin,
|
|
349
368
|
sessionId: r.session_id,
|
|
350
369
|
jobName: r.job_name,
|
|
370
|
+
principalId: r.principal_id ?? null,
|
|
351
371
|
attrs: safeParse(r.attrs) ?? {},
|
|
352
372
|
startedAt: r.started_at,
|
|
353
373
|
waitUntil: r.wait_until,
|
|
@@ -380,20 +400,20 @@ export function createActivityStore(
|
|
|
380
400
|
}
|
|
381
401
|
|
|
382
402
|
function getSpanRaw(spanId: string): SpanRow | null {
|
|
383
|
-
const r = db.query(
|
|
403
|
+
const r = db.query(ACTIVITY_SQL.spanById).get(spanId);
|
|
384
404
|
return r ? rowToSpan(r) : null;
|
|
385
405
|
}
|
|
386
406
|
|
|
387
407
|
function runHighWaterSeqRaw(runId: string): number {
|
|
388
408
|
const row = db
|
|
389
|
-
.query(
|
|
409
|
+
.query(ACTIVITY_SQL.nextSeq)
|
|
390
410
|
.get(runId) as { hi: number };
|
|
391
411
|
return row.hi;
|
|
392
412
|
}
|
|
393
413
|
|
|
394
414
|
function endSpanInTx(spanId: string, input: EndSpanInput): boolean {
|
|
395
415
|
const existing = db
|
|
396
|
-
.query(
|
|
416
|
+
.query(ACTIVITY_SQL.spanStateById)
|
|
397
417
|
.get(spanId) as { run_id: string; outcome: string | null; attrs: string | null } | null;
|
|
398
418
|
if (!existing || existing.outcome !== null) return false;
|
|
399
419
|
|
|
@@ -402,16 +422,7 @@ export function createActivityStore(
|
|
|
402
422
|
? JSON.stringify({ ...(safeParse(existing.attrs) ?? {}), ...input.attrs })
|
|
403
423
|
: existing.attrs;
|
|
404
424
|
const u = input.usage ?? {};
|
|
405
|
-
db.query(
|
|
406
|
-
`UPDATE activity_spans SET outcome = ?, outcome_reason = ?, ended_at = ?, attrs = ?,
|
|
407
|
-
input_tokens = COALESCE(?, input_tokens),
|
|
408
|
-
output_tokens = COALESCE(?, output_tokens),
|
|
409
|
-
cache_read_tokens = COALESCE(?, cache_read_tokens),
|
|
410
|
-
cache_creation_tokens = COALESCE(?, cache_creation_tokens),
|
|
411
|
-
cost_usd = COALESCE(?, cost_usd),
|
|
412
|
-
model = COALESCE(?, model)
|
|
413
|
-
WHERE span_id = ?`
|
|
414
|
-
).run(
|
|
425
|
+
db.query(ACTIVITY_SQL.endSpan).run(
|
|
415
426
|
input.outcome,
|
|
416
427
|
input.reason ?? null,
|
|
417
428
|
endedAt,
|
|
@@ -431,9 +442,7 @@ export function createActivityStore(
|
|
|
431
442
|
/** Close every open span of the run (children first) and roll it up. */
|
|
432
443
|
function closeRunInTx(runId: string, outcome: SpanOutcome, reason: string): number {
|
|
433
444
|
const open = db
|
|
434
|
-
.query(
|
|
435
|
-
"SELECT span_id FROM activity_spans WHERE run_id = ? AND outcome IS NULL ORDER BY started_at DESC"
|
|
436
|
-
)
|
|
445
|
+
.query(ACTIVITY_SQL.openSpansByRun)
|
|
437
446
|
.all(runId) as Array<{ span_id: string }>;
|
|
438
447
|
let closed = 0;
|
|
439
448
|
for (const { span_id } of open) {
|
|
@@ -445,7 +454,7 @@ export function createActivityStore(
|
|
|
445
454
|
|
|
446
455
|
function rollupRunInTx(runId: string) {
|
|
447
456
|
const spans = db
|
|
448
|
-
.query(
|
|
457
|
+
.query(ACTIVITY_SQL.spansByRun)
|
|
449
458
|
.all(runId)
|
|
450
459
|
.map(rowToSpan);
|
|
451
460
|
if (spans.length === 0) return;
|
|
@@ -504,39 +513,14 @@ export function createActivityStore(
|
|
|
504
513
|
? (priced.estimate ? 1 : 0)
|
|
505
514
|
: null;
|
|
506
515
|
const costUsd = root.usage.costUsd ?? priced?.costUsd ?? null;
|
|
516
|
+
const principal = root.principalId
|
|
517
|
+
? (db
|
|
518
|
+
.query("SELECT label, kind FROM principals WHERE id = ?")
|
|
519
|
+
.get(root.principalId) as { label: string; kind: PrincipalKind } | null)
|
|
520
|
+
: null;
|
|
507
521
|
|
|
508
522
|
db.query(
|
|
509
|
-
|
|
510
|
-
(run_id, origin, name, session_id, job_name, started_at, ended_at, outcome,
|
|
511
|
-
duration_ms, span_count, input_tokens, output_tokens, cache_read_tokens,
|
|
512
|
-
cache_creation_tokens, cost_usd, effective_cost_usd, billing_mode,
|
|
513
|
-
pricing_estimate, failure_reason, detail_pruned)
|
|
514
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
515
|
-
ON CONFLICT(run_id) DO UPDATE SET
|
|
516
|
-
ended_at = excluded.ended_at, outcome = excluded.outcome,
|
|
517
|
-
duration_ms = excluded.duration_ms, span_count = excluded.span_count,
|
|
518
|
-
input_tokens = excluded.input_tokens, output_tokens = excluded.output_tokens,
|
|
519
|
-
cache_read_tokens = excluded.cache_read_tokens,
|
|
520
|
-
cache_creation_tokens = excluded.cache_creation_tokens,
|
|
521
|
-
cost_usd = COALESCE(activity_run_rollups.cost_usd, excluded.cost_usd),
|
|
522
|
-
effective_cost_usd = CASE
|
|
523
|
-
WHEN activity_run_rollups.effective_cost_usd IS NOT NULL
|
|
524
|
-
THEN activity_run_rollups.effective_cost_usd
|
|
525
|
-
WHEN activity_run_rollups.billing_mode IS NULL
|
|
526
|
-
OR activity_run_rollups.billing_mode = excluded.billing_mode
|
|
527
|
-
THEN excluded.effective_cost_usd
|
|
528
|
-
ELSE NULL
|
|
529
|
-
END,
|
|
530
|
-
billing_mode = COALESCE(activity_run_rollups.billing_mode, excluded.billing_mode),
|
|
531
|
-
pricing_estimate = CASE
|
|
532
|
-
WHEN activity_run_rollups.pricing_estimate IS NOT NULL
|
|
533
|
-
THEN activity_run_rollups.pricing_estimate
|
|
534
|
-
WHEN activity_run_rollups.billing_mode IS NULL
|
|
535
|
-
OR activity_run_rollups.billing_mode = excluded.billing_mode
|
|
536
|
-
THEN excluded.pricing_estimate
|
|
537
|
-
ELSE NULL
|
|
538
|
-
END,
|
|
539
|
-
failure_reason = excluded.failure_reason`
|
|
523
|
+
ACTIVITY_SQL.upsertRollup
|
|
540
524
|
// Every cost column is FROZEN at first non-NULL write (AE5): a re-rollup
|
|
541
525
|
// after a pricing refresh must not silently reprice history — only a
|
|
542
526
|
// still-NULL slot may be filled by a later computation. cost_usd gets
|
|
@@ -553,6 +537,9 @@ export function createActivityStore(
|
|
|
553
537
|
root.name,
|
|
554
538
|
root.sessionId,
|
|
555
539
|
root.jobName,
|
|
540
|
+
root.principalId,
|
|
541
|
+
principal?.label ?? null,
|
|
542
|
+
principal?.kind ?? null,
|
|
556
543
|
root.startedAt,
|
|
557
544
|
root.endedAt,
|
|
558
545
|
root.outcome,
|
|
@@ -576,12 +563,7 @@ export function createActivityStore(
|
|
|
576
563
|
startSpan(input) {
|
|
577
564
|
return inWrite(() => {
|
|
578
565
|
const startedAt = input.startedAt ?? Date.now();
|
|
579
|
-
db.query(
|
|
580
|
-
`INSERT INTO activity_spans
|
|
581
|
-
(span_id, run_id, parent_span_id, name, kind, origin, session_id, job_name,
|
|
582
|
-
attrs, started_at, writer, last_heartbeat_at)
|
|
583
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
584
|
-
).run(
|
|
566
|
+
db.query(ACTIVITY_SQL.insertSpan).run(
|
|
585
567
|
input.spanId,
|
|
586
568
|
input.runId,
|
|
587
569
|
input.parentSpanId ?? null,
|
|
@@ -590,6 +572,7 @@ export function createActivityStore(
|
|
|
590
572
|
input.origin,
|
|
591
573
|
input.sessionId ?? null,
|
|
592
574
|
input.jobName ?? null,
|
|
575
|
+
input.principalId ?? null,
|
|
593
576
|
input.attrs ? JSON.stringify(input.attrs) : null,
|
|
594
577
|
startedAt,
|
|
595
578
|
writer,
|
|
@@ -607,23 +590,15 @@ export function createActivityStore(
|
|
|
607
590
|
patchSpan(spanId, patch) {
|
|
608
591
|
return inWrite(() => {
|
|
609
592
|
const existing = db
|
|
610
|
-
.query(
|
|
593
|
+
.query(ACTIVITY_SQL.spanStateById)
|
|
611
594
|
.get(spanId) as { run_id: string; outcome: string | null; attrs: string | null } | null;
|
|
612
595
|
if (!existing || existing.outcome !== null) return false;
|
|
613
596
|
const attrs = patch.attrs
|
|
614
597
|
? JSON.stringify({ ...(safeParse(existing.attrs) ?? {}), ...patch.attrs })
|
|
615
598
|
: existing.attrs;
|
|
616
599
|
const u = patch.usage ?? {};
|
|
617
|
-
db.query(
|
|
618
|
-
|
|
619
|
-
input_tokens = COALESCE(?, input_tokens),
|
|
620
|
-
output_tokens = COALESCE(?, output_tokens),
|
|
621
|
-
cache_read_tokens = COALESCE(?, cache_read_tokens),
|
|
622
|
-
cache_creation_tokens = COALESCE(?, cache_creation_tokens),
|
|
623
|
-
cost_usd = COALESCE(?, cost_usd),
|
|
624
|
-
model = COALESCE(?, model)
|
|
625
|
-
WHERE span_id = ?`
|
|
626
|
-
).run(
|
|
600
|
+
db.query(ACTIVITY_SQL.patchSpan).run(
|
|
601
|
+
patch.principalId ?? null,
|
|
627
602
|
attrs,
|
|
628
603
|
patch.waitUntil ?? null,
|
|
629
604
|
u.inputTokens ?? null,
|
|
@@ -642,19 +617,21 @@ export function createActivityStore(
|
|
|
642
617
|
appendEvent(spanId, eventType, payload, ts, cap) {
|
|
643
618
|
return inWrite(() => {
|
|
644
619
|
const span = db
|
|
645
|
-
.query(
|
|
620
|
+
.query(ACTIVITY_SQL.spanRunById)
|
|
646
621
|
.get(spanId) as { run_id: string } | null;
|
|
647
622
|
if (!span) return null;
|
|
648
623
|
const next = db
|
|
649
|
-
.query(
|
|
650
|
-
"SELECT COALESCE(MAX(event_index), -1) + 1 AS idx FROM activity_events WHERE span_id = ?"
|
|
651
|
-
)
|
|
624
|
+
.query(ACTIVITY_SQL.nextEventIndex)
|
|
652
625
|
.get(spanId) as { idx: number };
|
|
653
626
|
const stored = capPayload(payload, cap);
|
|
654
627
|
const at = ts ?? Date.now();
|
|
655
|
-
db.query(
|
|
656
|
-
|
|
657
|
-
|
|
628
|
+
db.query(ACTIVITY_SQL.insertEvent).run(
|
|
629
|
+
spanId,
|
|
630
|
+
next.idx,
|
|
631
|
+
at,
|
|
632
|
+
eventType,
|
|
633
|
+
JSON.stringify(stored)
|
|
634
|
+
);
|
|
658
635
|
logChange(span.run_id, nextSeq(span.run_id), spanId, next.idx);
|
|
659
636
|
// Built from the values just written — no read-back needed.
|
|
660
637
|
return {
|
|
@@ -672,9 +649,7 @@ export function createActivityStore(
|
|
|
672
649
|
inWrite(() => {
|
|
673
650
|
// Deliberately NOT change-logged: a heartbeat is liveness metadata,
|
|
674
651
|
// not activity the client needs a delta for.
|
|
675
|
-
db.query(
|
|
676
|
-
"UPDATE activity_spans SET last_heartbeat_at = ? WHERE span_id = ? AND outcome IS NULL"
|
|
677
|
-
).run(at ?? Date.now(), spanId);
|
|
652
|
+
db.query(ACTIVITY_SQL.heartbeat).run(at ?? Date.now(), spanId);
|
|
678
653
|
});
|
|
679
654
|
},
|
|
680
655
|
|
|
@@ -697,9 +672,7 @@ export function createActivityStore(
|
|
|
697
672
|
// loop re-checks open-ness inside the transaction (endSpanInTx is
|
|
698
673
|
// write-once), so a race just skips.
|
|
699
674
|
const open = db
|
|
700
|
-
.query(
|
|
701
|
-
"SELECT DISTINCT run_id FROM activity_spans WHERE outcome IS NULL AND origin = 'session'"
|
|
702
|
-
)
|
|
675
|
+
.query(ACTIVITY_SQL.openSessionRuns)
|
|
703
676
|
.all() as Array<{ run_id: string }>;
|
|
704
677
|
if (open.length === 0) return 0;
|
|
705
678
|
getPricing(); // construct outside the write lock; rollup reuses it
|
|
@@ -718,11 +691,7 @@ export function createActivityStore(
|
|
|
718
691
|
// legitimately long, quiet run with a live writer is never killed.
|
|
719
692
|
// Candidate read outside the write transaction, same as sweepOwnOrphans.
|
|
720
693
|
const staleRoots = db
|
|
721
|
-
.query(
|
|
722
|
-
`SELECT span_id, run_id FROM activity_spans
|
|
723
|
-
WHERE outcome IS NULL AND parent_span_id IS NULL
|
|
724
|
-
AND writer != ? AND COALESCE(last_heartbeat_at, started_at) < ?`
|
|
725
|
-
)
|
|
694
|
+
.query(ACTIVITY_SQL.staleRoots)
|
|
726
695
|
.all(writer, cutoff) as Array<{ span_id: string; run_id: string }>;
|
|
727
696
|
if (staleRoots.length === 0) return 0;
|
|
728
697
|
getPricing(); // construct outside the write lock; rollup reuses it
|
|
@@ -739,9 +708,7 @@ export function createActivityStore(
|
|
|
739
708
|
const cutoff = (now ?? Date.now()) - thresholdMs;
|
|
740
709
|
return (
|
|
741
710
|
db
|
|
742
|
-
.query(
|
|
743
|
-
"SELECT * FROM activity_spans WHERE outcome IS NULL AND parent_span_id IS NULL AND started_at < ?"
|
|
744
|
-
)
|
|
711
|
+
.query(ACTIVITY_SQL.stuckRoots)
|
|
745
712
|
.all(cutoff) as any[]
|
|
746
713
|
).map(rowToSpan);
|
|
747
714
|
},
|
|
@@ -751,23 +718,10 @@ export function createActivityStore(
|
|
|
751
718
|
// change whose span/event row was pruned drops out via the join, same
|
|
752
719
|
// as the old per-row miss.
|
|
753
720
|
const spanRows = db
|
|
754
|
-
.query(
|
|
755
|
-
`SELECT c.change_id, c.run_id, c.seq, s.*
|
|
756
|
-
FROM activity_changes c
|
|
757
|
-
JOIN activity_spans s ON s.span_id = c.span_id
|
|
758
|
-
WHERE c.change_id > ? AND c.event_index IS NULL
|
|
759
|
-
ORDER BY c.change_id LIMIT ?`
|
|
760
|
-
)
|
|
721
|
+
.query(ACTIVITY_SQL.spanChanges)
|
|
761
722
|
.all(changeCursor, limit) as any[];
|
|
762
723
|
const eventRows = db
|
|
763
|
-
.query(
|
|
764
|
-
`SELECT c.change_id, c.run_id, c.seq, e.*
|
|
765
|
-
FROM activity_changes c
|
|
766
|
-
JOIN activity_events e
|
|
767
|
-
ON e.span_id = c.span_id AND e.event_index = c.event_index
|
|
768
|
-
WHERE c.change_id > ? AND c.event_index IS NOT NULL
|
|
769
|
-
ORDER BY c.change_id LIMIT ?`
|
|
770
|
-
)
|
|
724
|
+
.query(ACTIVITY_SQL.eventChanges)
|
|
771
725
|
.all(changeCursor, limit) as any[];
|
|
772
726
|
const changes: ActivityChange[] = [
|
|
773
727
|
...spanRows.map(
|
|
@@ -795,22 +749,14 @@ export function createActivityStore(
|
|
|
795
749
|
|
|
796
750
|
terminalRootChangesSince(changeCursor, limit = 500) {
|
|
797
751
|
const rows = db
|
|
798
|
-
.query(
|
|
799
|
-
`SELECT MAX(c.change_id) AS change_id, s.*
|
|
800
|
-
FROM activity_changes c
|
|
801
|
-
JOIN activity_spans s ON s.span_id = c.span_id
|
|
802
|
-
WHERE c.change_id > ? AND c.event_index IS NULL
|
|
803
|
-
AND s.parent_span_id IS NULL AND s.outcome IS NOT NULL
|
|
804
|
-
GROUP BY s.span_id
|
|
805
|
-
ORDER BY change_id LIMIT ?`
|
|
806
|
-
)
|
|
752
|
+
.query(ACTIVITY_SQL.terminalRootChanges)
|
|
807
753
|
.all(changeCursor, limit) as any[];
|
|
808
754
|
return rows.map((r) => ({ changeId: r.change_id, span: rowToSpan(r) }));
|
|
809
755
|
},
|
|
810
756
|
|
|
811
757
|
latestChangeCursor() {
|
|
812
758
|
const row = db
|
|
813
|
-
.query(
|
|
759
|
+
.query(ACTIVITY_SQL.latestChangeCursor)
|
|
814
760
|
.get() as { hi: number };
|
|
815
761
|
return row.hi;
|
|
816
762
|
},
|
|
@@ -822,21 +768,17 @@ export function createActivityStore(
|
|
|
822
768
|
return db.transaction(() => {
|
|
823
769
|
const spans = (
|
|
824
770
|
db
|
|
825
|
-
.query(
|
|
771
|
+
.query(ACTIVITY_SQL.spansByRun)
|
|
826
772
|
.all(runId) as any[]
|
|
827
773
|
).map(rowToSpan);
|
|
828
774
|
if (spans.length === 0) return null;
|
|
829
775
|
const events = (
|
|
830
776
|
db
|
|
831
|
-
.query(
|
|
832
|
-
`SELECT e.* FROM activity_events e
|
|
833
|
-
JOIN activity_spans s ON s.span_id = e.span_id
|
|
834
|
-
WHERE s.run_id = ? ORDER BY e.ts, e.event_index`
|
|
835
|
-
)
|
|
777
|
+
.query(ACTIVITY_SQL.eventsByRun)
|
|
836
778
|
.all(runId) as any[]
|
|
837
779
|
).map(rowToEvent);
|
|
838
780
|
const cursor = db
|
|
839
|
-
.query(
|
|
781
|
+
.query(ACTIVITY_SQL.latestChangeCursor)
|
|
840
782
|
.get() as { hi: number };
|
|
841
783
|
return {
|
|
842
784
|
runId,
|
|
@@ -855,9 +797,7 @@ export function createActivityStore(
|
|
|
855
797
|
openRootSpans() {
|
|
856
798
|
return (
|
|
857
799
|
db
|
|
858
|
-
.query(
|
|
859
|
-
"SELECT * FROM activity_spans WHERE outcome IS NULL AND parent_span_id IS NULL ORDER BY started_at DESC"
|
|
860
|
-
)
|
|
800
|
+
.query(ACTIVITY_SQL.openRootSpans)
|
|
861
801
|
.all() as any[]
|
|
862
802
|
).map(rowToSpan);
|
|
863
803
|
},
|
|
@@ -887,46 +827,24 @@ export function createActivityStore(
|
|
|
887
827
|
// run — that terminal write is what made the run prunable), guarded
|
|
888
828
|
// against any run that still has an open span.
|
|
889
829
|
const candidates = db
|
|
890
|
-
.query(
|
|
891
|
-
`SELECT run_id, ended_at AS ended FROM activity_run_rollups r
|
|
892
|
-
WHERE detail_pruned = 0 AND ended_at IS NOT NULL
|
|
893
|
-
AND ((ended_at < ? AND ended_at < ?) OR ended_at < ?)
|
|
894
|
-
AND NOT EXISTS (
|
|
895
|
-
SELECT 1 FROM activity_spans s
|
|
896
|
-
WHERE s.run_id = r.run_id AND s.outcome IS NULL
|
|
897
|
-
)
|
|
898
|
-
LIMIT ?`
|
|
899
|
-
)
|
|
830
|
+
.query(ACTIVITY_SQL.pruneCandidates)
|
|
900
831
|
.all(floor, retentionCutoff, ceiling, batch) as Array<{ run_id: string; ended: number }>;
|
|
901
832
|
let spansDeleted = 0;
|
|
902
833
|
for (const { run_id, ended } of candidates) {
|
|
903
|
-
db.query(
|
|
904
|
-
"UPDATE activity_run_rollups SET detail_pruned = 1 WHERE run_id = ?"
|
|
905
|
-
).run(run_id);
|
|
834
|
+
db.query(ACTIVITY_SQL.markDetailPruned).run(run_id);
|
|
906
835
|
if (ended >= floor) {
|
|
907
836
|
// Pruned by the hard ceiling alone — the digest never covered
|
|
908
837
|
// this run; make the coverage gap visible on the rollup.
|
|
909
|
-
db.query(
|
|
910
|
-
"UPDATE activity_run_rollups SET failure_reason = COALESCE(failure_reason, 'digest coverage gap') WHERE run_id = ?"
|
|
911
|
-
).run(run_id);
|
|
838
|
+
db.query(ACTIVITY_SQL.markDigestCoverageGap).run(run_id);
|
|
912
839
|
}
|
|
913
|
-
db.query(
|
|
914
|
-
|
|
915
|
-
(SELECT span_id FROM activity_spans WHERE run_id = ?)`
|
|
916
|
-
).run(run_id);
|
|
917
|
-
const res = db.query("DELETE FROM activity_spans WHERE run_id = ?").run(run_id);
|
|
840
|
+
db.query(ACTIVITY_SQL.deleteEventsByRun).run(run_id);
|
|
841
|
+
const res = db.query(ACTIVITY_SQL.deleteSpansByRun).run(run_id);
|
|
918
842
|
spansDeleted += res.changes;
|
|
919
|
-
db.query(
|
|
843
|
+
db.query(ACTIVITY_SQL.deleteChangesByRun).run(run_id);
|
|
920
844
|
}
|
|
921
845
|
// The change log only serves live polling; rows this deep in the past
|
|
922
846
|
// are unreachable by any live cursor. Keep runs with open spans.
|
|
923
|
-
db.query(
|
|
924
|
-
`DELETE FROM activity_changes WHERE change_id < (
|
|
925
|
-
SELECT COALESCE(MAX(change_id), 0) FROM activity_changes
|
|
926
|
-
) - 100000 AND run_id NOT IN (
|
|
927
|
-
SELECT DISTINCT run_id FROM activity_spans WHERE outcome IS NULL
|
|
928
|
-
)`
|
|
929
|
-
).run();
|
|
847
|
+
db.query(ACTIVITY_SQL.compactChanges).run();
|
|
930
848
|
return { runsPruned: candidates.length, spansDeleted };
|
|
931
849
|
});
|
|
932
850
|
},
|
package/src/activity/stream.ts
CHANGED
|
@@ -77,6 +77,7 @@ export function toWireSpan(span: SpanRow): ActivitySpan {
|
|
|
77
77
|
origin: span.origin,
|
|
78
78
|
sessionId: span.sessionId ?? undefined,
|
|
79
79
|
jobName: span.jobName ?? undefined,
|
|
80
|
+
principalId: span.principalId ?? undefined,
|
|
80
81
|
startedAt: span.startedAt,
|
|
81
82
|
waitUntil: span.waitUntil ?? undefined,
|
|
82
83
|
endedAt: span.endedAt ?? undefined,
|
|
@@ -122,6 +123,9 @@ export function toWireRollup(r: RunRollupRow): ActivityRunRollup {
|
|
|
122
123
|
name: r.name,
|
|
123
124
|
sessionId: r.sessionId,
|
|
124
125
|
jobName: r.jobName,
|
|
126
|
+
principalId: r.principalId,
|
|
127
|
+
principalLabel: r.principalLabel,
|
|
128
|
+
principalKind: r.principalKind,
|
|
125
129
|
startedAt: r.startedAt,
|
|
126
130
|
endedAt: r.endedAt,
|
|
127
131
|
outcome: r.outcome,
|
|
@@ -136,10 +140,12 @@ export function toWireRollup(r: RunRollupRow): ActivityRunRollup {
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
export interface ActivityStream {
|
|
139
|
-
handleSubscribe(ws: WSContext, msg: ClientActivitySubscribe): void;
|
|
143
|
+
handleSubscribe(ws: WSContext, msg: ClientActivitySubscribe, principalId: string): void;
|
|
140
144
|
handleUnsubscribe(ws: WSContext, msg: ClientActivityUnsubscribe): void;
|
|
141
145
|
/** Forget a closed socket. */
|
|
142
146
|
dropConnection(ws: WSContext): void;
|
|
147
|
+
/** Forget every subscription attached by one revoked principal. */
|
|
148
|
+
dropFor(principalId: string): void;
|
|
143
149
|
/** Drain new committed changes to subscribers. Reentrancy-safe. */
|
|
144
150
|
pump(): void;
|
|
145
151
|
/** Whether any connection currently watches the given session's activity. */
|
|
@@ -150,7 +156,10 @@ export interface ActivityStream {
|
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
export function createActivityStream(store: ActivityStore, log?: Logger): ActivityStream {
|
|
153
|
-
const subscriptions = new Map<
|
|
159
|
+
const subscriptions = new Map<
|
|
160
|
+
unknown,
|
|
161
|
+
{ ws: WSContext; principalId: string; subscriptions: Subscription[] }
|
|
162
|
+
>();
|
|
154
163
|
let cursor = store.latestChangeCursor();
|
|
155
164
|
let pumping = false;
|
|
156
165
|
let pollHandle: ReturnType<typeof setInterval> | null = null;
|
|
@@ -222,9 +231,9 @@ export function createActivityStream(store: ActivityStore, log?: Logger): Activi
|
|
|
222
231
|
const span = change.kind === "span" ? change.span : undefined;
|
|
223
232
|
if (span) noteSpan(span);
|
|
224
233
|
const frame = deltaFrame(change);
|
|
225
|
-
for (const
|
|
226
|
-
if (
|
|
227
|
-
sendTo(ws, frame);
|
|
234
|
+
for (const entry of subscriptions.values()) {
|
|
235
|
+
if (entry.subscriptions.some((s) => matches(s, change.runId, span))) {
|
|
236
|
+
sendTo(entry.ws, frame);
|
|
228
237
|
}
|
|
229
238
|
}
|
|
230
239
|
}
|
|
@@ -310,7 +319,7 @@ export function createActivityStream(store: ActivityStore, log?: Logger): Activi
|
|
|
310
319
|
}
|
|
311
320
|
|
|
312
321
|
return {
|
|
313
|
-
handleSubscribe(ws, msg) {
|
|
322
|
+
handleSubscribe(ws, msg, principalId) {
|
|
314
323
|
// A scoped view without its scope id can never match anything and
|
|
315
324
|
// would only leak an all-runs snapshot shape — ignore it up front.
|
|
316
325
|
if ((msg.view === "session" && !msg.sessionId) || (msg.view === "run" && !msg.runId)) {
|
|
@@ -321,9 +330,11 @@ export function createActivityStream(store: ActivityStore, log?: Logger): Activi
|
|
|
321
330
|
...(msg.sessionId ? { sessionId: msg.sessionId } : {}),
|
|
322
331
|
...(msg.runId ? { runId: msg.runId } : {}),
|
|
323
332
|
};
|
|
324
|
-
const
|
|
333
|
+
const identity = ws.raw ?? ws;
|
|
334
|
+
const existing = subscriptions.get(identity);
|
|
335
|
+
const list = existing?.subscriptions ?? [];
|
|
325
336
|
if (!list.some((s) => subKey(s) === subKey(sub))) list.push(sub);
|
|
326
|
-
subscriptions.set(ws, list);
|
|
337
|
+
subscriptions.set(identity, { ws, principalId, subscriptions: list });
|
|
327
338
|
ensurePolling();
|
|
328
339
|
try {
|
|
329
340
|
sendSnapshot(ws, sub);
|
|
@@ -337,29 +348,38 @@ export function createActivityStream(store: ActivityStore, log?: Logger): Activi
|
|
|
337
348
|
},
|
|
338
349
|
|
|
339
350
|
handleUnsubscribe(ws, msg) {
|
|
340
|
-
const
|
|
341
|
-
|
|
351
|
+
const identity = ws.raw ?? ws;
|
|
352
|
+
const entry = subscriptions.get(identity);
|
|
353
|
+
if (!entry) return;
|
|
342
354
|
const key = subKey({
|
|
343
355
|
view: msg.view,
|
|
344
356
|
...(msg.sessionId ? { sessionId: msg.sessionId } : {}),
|
|
345
357
|
...(msg.runId ? { runId: msg.runId } : {}),
|
|
346
358
|
});
|
|
347
|
-
const remaining =
|
|
348
|
-
if (remaining.length > 0)
|
|
349
|
-
|
|
359
|
+
const remaining = entry.subscriptions.filter((s) => subKey(s) !== key);
|
|
360
|
+
if (remaining.length > 0) {
|
|
361
|
+
subscriptions.set(identity, { ...entry, ws, subscriptions: remaining });
|
|
362
|
+
} else subscriptions.delete(identity);
|
|
350
363
|
stopPollingIfIdle();
|
|
351
364
|
},
|
|
352
365
|
|
|
353
366
|
dropConnection(ws) {
|
|
354
|
-
subscriptions.delete(ws);
|
|
367
|
+
subscriptions.delete(ws.raw ?? ws);
|
|
368
|
+
stopPollingIfIdle();
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
dropFor(principalId) {
|
|
372
|
+
for (const [identity, entry] of subscriptions) {
|
|
373
|
+
if (entry.principalId === principalId) subscriptions.delete(identity);
|
|
374
|
+
}
|
|
355
375
|
stopPollingIfIdle();
|
|
356
376
|
},
|
|
357
377
|
|
|
358
378
|
pump,
|
|
359
379
|
|
|
360
380
|
isWatched(scope) {
|
|
361
|
-
for (const
|
|
362
|
-
for (const s of
|
|
381
|
+
for (const entry of subscriptions.values()) {
|
|
382
|
+
for (const s of entry.subscriptions) {
|
|
363
383
|
if (s.view === "index") return true;
|
|
364
384
|
if (scope.runId && s.view === "run" && s.runId === scope.runId) return true;
|
|
365
385
|
if (scope.sessionId && s.view === "session" && s.sessionId === scope.sessionId)
|
|
@@ -371,7 +391,7 @@ export function createActivityStream(store: ActivityStore, log?: Logger): Activi
|
|
|
371
391
|
|
|
372
392
|
subscriptionCount() {
|
|
373
393
|
let n = 0;
|
|
374
|
-
for (const
|
|
394
|
+
for (const entry of subscriptions.values()) n += entry.subscriptions.length;
|
|
375
395
|
return n;
|
|
376
396
|
},
|
|
377
397
|
|