@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.
Files changed (169) hide show
  1. package/README.md +28 -0
  2. package/dist/activity/notify.d.ts.map +1 -1
  3. package/dist/activity/notify.js +18 -8
  4. package/dist/activity/notify.js.map +1 -1
  5. package/dist/activity/push-sender.d.ts +7 -3
  6. package/dist/activity/push-sender.d.ts.map +1 -1
  7. package/dist/activity/push-sender.js +45 -11
  8. package/dist/activity/push-sender.js.map +1 -1
  9. package/dist/activity/query.d.ts.map +1 -1
  10. package/dist/activity/query.js +8 -0
  11. package/dist/activity/query.js.map +1 -1
  12. package/dist/activity/recorder.d.ts +11 -1
  13. package/dist/activity/recorder.d.ts.map +1 -1
  14. package/dist/activity/recorder.js +52 -2
  15. package/dist/activity/recorder.js.map +1 -1
  16. package/dist/activity/runtime.d.ts.map +1 -1
  17. package/dist/activity/runtime.js +2 -5
  18. package/dist/activity/runtime.js.map +1 -1
  19. package/dist/activity/sql.d.ts +34 -0
  20. package/dist/activity/sql.d.ts.map +1 -0
  21. package/dist/activity/sql.js +114 -0
  22. package/dist/activity/sql.js.map +1 -0
  23. package/dist/activity/store.d.ts +7 -0
  24. package/dist/activity/store.d.ts.map +1 -1
  25. package/dist/activity/store.js +48 -113
  26. package/dist/activity/store.js.map +1 -1
  27. package/dist/activity/stream.d.ts +3 -1
  28. package/dist/activity/stream.d.ts.map +1 -1
  29. package/dist/activity/stream.js +32 -17
  30. package/dist/activity/stream.js.map +1 -1
  31. package/dist/app-env.d.ts +8 -0
  32. package/dist/app-env.d.ts.map +1 -0
  33. package/dist/app-env.js +2 -0
  34. package/dist/app-env.js.map +1 -0
  35. package/dist/app.d.ts.map +1 -1
  36. package/dist/app.js +28 -3
  37. package/dist/app.js.map +1 -1
  38. package/dist/brain/client.d.ts +4 -2
  39. package/dist/brain/client.d.ts.map +1 -1
  40. package/dist/brain/client.js +60 -20
  41. package/dist/brain/client.js.map +1 -1
  42. package/dist/brain/types.d.ts +9 -0
  43. package/dist/brain/types.d.ts.map +1 -1
  44. package/dist/cron/run-job.d.ts.map +1 -1
  45. package/dist/cron/run-job.js +60 -10
  46. package/dist/cron/run-job.js.map +1 -1
  47. package/dist/db/principals.d.ts +72 -0
  48. package/dist/db/principals.d.ts.map +1 -0
  49. package/dist/db/principals.js +294 -0
  50. package/dist/db/principals.js.map +1 -0
  51. package/dist/files/walker.d.ts.map +1 -1
  52. package/dist/files/walker.js +6 -1
  53. package/dist/files/walker.js.map +1 -1
  54. package/dist/index.d.ts +4 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/middleware/auth.d.ts +37 -9
  59. package/dist/middleware/auth.d.ts.map +1 -1
  60. package/dist/middleware/auth.js +148 -74
  61. package/dist/middleware/auth.js.map +1 -1
  62. package/dist/middleware/passkeys.d.ts +9 -7
  63. package/dist/middleware/passkeys.d.ts.map +1 -1
  64. package/dist/middleware/passkeys.js +25 -7
  65. package/dist/middleware/passkeys.js.map +1 -1
  66. package/dist/middleware/principals.d.ts +18 -0
  67. package/dist/middleware/principals.d.ts.map +1 -0
  68. package/dist/middleware/principals.js +138 -0
  69. package/dist/middleware/principals.js.map +1 -0
  70. package/dist/middleware/require-owner.d.ts +5 -0
  71. package/dist/middleware/require-owner.d.ts.map +1 -0
  72. package/dist/middleware/require-owner.js +10 -0
  73. package/dist/middleware/require-owner.js.map +1 -0
  74. package/dist/observability/loggers.js +1 -1
  75. package/dist/observability/loggers.js.map +1 -1
  76. package/dist/routes/activity.d.ts.map +1 -1
  77. package/dist/routes/activity.js +3 -0
  78. package/dist/routes/activity.js.map +1 -1
  79. package/dist/routes/brain.d.ts.map +1 -1
  80. package/dist/routes/brain.js +102 -63
  81. package/dist/routes/brain.js.map +1 -1
  82. package/dist/routes/files.d.ts.map +1 -1
  83. package/dist/routes/files.js +7 -2
  84. package/dist/routes/files.js.map +1 -1
  85. package/dist/routes/push.d.ts +61 -2
  86. package/dist/routes/push.d.ts.map +1 -1
  87. package/dist/routes/push.js +8 -3
  88. package/dist/routes/push.js.map +1 -1
  89. package/dist/routes/sessions.d.ts.map +1 -1
  90. package/dist/routes/sessions.js +30 -4
  91. package/dist/routes/sessions.js.map +1 -1
  92. package/dist/share/staging.d.ts.map +1 -1
  93. package/dist/share/staging.js +11 -4
  94. package/dist/share/staging.js.map +1 -1
  95. package/dist/ws/bridge.d.ts.map +1 -1
  96. package/dist/ws/bridge.js +6 -2
  97. package/dist/ws/bridge.js.map +1 -1
  98. package/dist/ws/clients.d.ts +9 -2
  99. package/dist/ws/clients.d.ts.map +1 -1
  100. package/dist/ws/clients.js +44 -6
  101. package/dist/ws/clients.js.map +1 -1
  102. package/dist/ws/connection.d.ts +7 -4
  103. package/dist/ws/connection.d.ts.map +1 -1
  104. package/dist/ws/connection.js +83 -12
  105. package/dist/ws/connection.js.map +1 -1
  106. package/dist/ws/dispatch.d.ts +12 -1
  107. package/dist/ws/dispatch.d.ts.map +1 -1
  108. package/dist/ws/dispatch.js +46 -6
  109. package/dist/ws/dispatch.js.map +1 -1
  110. package/dist/ws/history.d.ts.map +1 -1
  111. package/dist/ws/history.js +7 -5
  112. package/dist/ws/history.js.map +1 -1
  113. package/dist/ws/host.d.ts +22 -0
  114. package/dist/ws/host.d.ts.map +1 -1
  115. package/dist/ws/host.js +39 -1
  116. package/dist/ws/host.js.map +1 -1
  117. package/dist/ws/run-session.d.ts +7 -7
  118. package/dist/ws/run-session.d.ts.map +1 -1
  119. package/dist/ws/run-session.js +208 -52
  120. package/dist/ws/run-session.js.map +1 -1
  121. package/dist/ws/shrink.d.ts +3 -1
  122. package/dist/ws/shrink.d.ts.map +1 -1
  123. package/dist/ws/shrink.js +46 -16
  124. package/dist/ws/shrink.js.map +1 -1
  125. package/dist/ws/turns.d.ts +58 -3
  126. package/dist/ws/turns.d.ts.map +1 -1
  127. package/dist/ws/turns.js +101 -2
  128. package/dist/ws/turns.js.map +1 -1
  129. package/migrations/011_principals.sql +13 -0
  130. package/migrations/012_activity_principals.sql +12 -0
  131. package/migrations/013_push_subscription_principals.sql +7 -0
  132. package/migrations/014_invalidate_legacy_sessions.sql +14 -0
  133. package/package.json +3 -3
  134. package/src/activity/notify.ts +16 -7
  135. package/src/activity/push-sender.ts +79 -16
  136. package/src/activity/query.ts +8 -0
  137. package/src/activity/recorder.ts +68 -3
  138. package/src/activity/runtime.ts +2 -5
  139. package/src/activity/sql.ts +122 -0
  140. package/src/activity/store.ts +73 -155
  141. package/src/activity/stream.ts +37 -17
  142. package/src/app-env.ts +8 -0
  143. package/src/app.ts +33 -4
  144. package/src/brain/client.ts +61 -20
  145. package/src/brain/types.ts +10 -0
  146. package/src/cron/run-job.ts +41 -17
  147. package/src/db/principals.ts +452 -0
  148. package/src/files/walker.ts +5 -1
  149. package/src/index.ts +6 -1
  150. package/src/middleware/auth.ts +241 -81
  151. package/src/middleware/passkeys.ts +35 -14
  152. package/src/middleware/principals.ts +189 -0
  153. package/src/middleware/require-owner.ts +13 -0
  154. package/src/observability/loggers.ts +1 -1
  155. package/src/routes/activity.ts +3 -0
  156. package/src/routes/brain.ts +107 -70
  157. package/src/routes/files.ts +10 -5
  158. package/src/routes/push.ts +11 -5
  159. package/src/routes/sessions.ts +45 -20
  160. package/src/share/staging.ts +11 -4
  161. package/src/ws/bridge.ts +14 -2
  162. package/src/ws/clients.ts +52 -7
  163. package/src/ws/connection.ts +98 -20
  164. package/src/ws/dispatch.ts +66 -6
  165. package/src/ws/history.ts +7 -5
  166. package/src/ws/host.ts +59 -1
  167. package/src/ws/run-session.ts +247 -74
  168. package/src/ws/shrink.ts +48 -14
  169. package/src/ws/turns.ts +136 -5
@@ -46,8 +46,21 @@ export interface TurnRecorder {
46
46
  readonly runId: string;
47
47
  observeFrame(msg: ServerMessage): void;
48
48
  observeActivity(event: BackendActivityEvent): void;
49
+ /** Record that this turn's principal was revoked while work kept running. */
50
+ recordPrincipalRevocation(principalId: string): void;
51
+ /** Record a live follow-up that joined this already-running turn. */
52
+ recordFollowUp(principalId: string): void;
53
+ /** Record who cancelled this turn or an interactive prompt within it. */
54
+ recordCancellation(principalId: string, kind?: "turn" | "ask_user"): void;
55
+ /** Record who answered an ask-user interaction. */
56
+ recordAskUserResponse(principalId: string): void;
49
57
  /** The user's approval decision arrived for a gated tool call. */
50
- onApprovalDecision(toolUseId: string, allowed: boolean): void;
58
+ onApprovalDecision(
59
+ toolUseId: string,
60
+ decision: "allow" | "always_allow" | "deny",
61
+ requestKind: "tool" | "command",
62
+ principalId?: string
63
+ ): void;
51
64
  /**
52
65
  * Close the turn: merge the buffered result enrichment into the root's
53
66
  * single terminal write, cascade-cancel anything left open, roll up.
@@ -64,6 +77,8 @@ export function createTurnRecorder(
64
77
  profileId?: string;
65
78
  /** Billing classification of that profile, resolved at run start (U3). */
66
79
  billingMode?: BillingMode;
80
+ /** Principal that initiated this turn; absent means unattributed. */
81
+ principalId?: string;
67
82
  }
68
83
  ): TurnRecorder {
69
84
  const { store, onWrite, log } = deps;
@@ -76,6 +91,7 @@ export function createTurnRecorder(
76
91
  let resultOutcome: SpanOutcome | null = null;
77
92
  let resultUsage: SpanUsage | undefined;
78
93
  let resultAttrs: Record<string, unknown> | undefined;
94
+ let principalRevocationRecorded = false;
79
95
 
80
96
  const guard = (fn: () => void) => {
81
97
  try {
@@ -99,6 +115,7 @@ export function createTurnRecorder(
99
115
  kind: "turn",
100
116
  origin: "session",
101
117
  sessionId,
118
+ principalId: turn.principalId,
102
119
  // Profile + billing ride the ROOT span so the rollup can price the run
103
120
  // without any registry or env lookup of its own (a root missing the
104
121
  // billing attr falls back to env classification at rollup time).
@@ -253,9 +270,57 @@ export function createTurnRecorder(
253
270
  });
254
271
  },
255
272
 
256
- onApprovalDecision(toolUseId, allowed) {
273
+ recordPrincipalRevocation(principalId) {
257
274
  guard(() => {
258
- if (allowed) {
275
+ if (finished || principalRevocationRecorded) return;
276
+ ensureRoot();
277
+ store.appendEvent(rootSpanId, "principal_revoked", { principalId });
278
+ principalRevocationRecorded = true;
279
+ onWrite?.();
280
+ });
281
+ },
282
+
283
+ recordFollowUp(principalId) {
284
+ guard(() => {
285
+ if (finished) return;
286
+ ensureRoot();
287
+ store.appendEvent(rootSpanId, "follow_up", { principalId });
288
+ onWrite?.();
289
+ });
290
+ },
291
+
292
+ recordCancellation(principalId, kind = "turn") {
293
+ guard(() => {
294
+ if (finished) return;
295
+ ensureRoot();
296
+ store.appendEvent(rootSpanId, `${kind}_cancelled`, { principalId });
297
+ onWrite?.();
298
+ });
299
+ },
300
+
301
+ recordAskUserResponse(principalId) {
302
+ guard(() => {
303
+ if (finished) return;
304
+ ensureRoot();
305
+ store.appendEvent(rootSpanId, "ask_user_response", { principalId });
306
+ onWrite?.();
307
+ });
308
+ },
309
+
310
+ onApprovalDecision(toolUseId, decision, requestKind, principalId) {
311
+ guard(() => {
312
+ if (principalId) {
313
+ // One tool can raise several approval requests. The span-level actor
314
+ // remains the latest responder for compact views, while this
315
+ // append-only event preserves every decision and actor in order.
316
+ store.appendEvent(toolUseId, "approval_decision", {
317
+ principalId,
318
+ decision,
319
+ requestKind,
320
+ });
321
+ store.patchSpan(toolUseId, { principalId });
322
+ }
323
+ if (decision !== "deny") {
259
324
  // Everything before this moment was approval wait, not execution.
260
325
  store.patchSpan(toolUseId, { waitUntil: Date.now() });
261
326
  } else {
@@ -58,11 +58,8 @@ export function createActivityRuntime(
58
58
  const store = createActivityStore(db, deps.pricing ? { pricing: deps.pricing } : {});
59
59
  const stream = createActivityStream(store, log);
60
60
 
61
- // The notifier is created BEFORE the boot sweep: its change cursor starts
62
- // at the current head, so the terminal changes the sweep writes are above
63
- // it and the first tick turns restart-interrupted runs into failure
64
- // intents. Created after, the sweep's changes would sit below the cursor
65
- // and a turn killed by a restart would never be notified.
61
+ // The notifier resumes its persisted cursor, including terminal changes
62
+ // committed while this server was down and interruptions from the boot sweep.
66
63
  const notifier = createActivityNotifier({
67
64
  db,
68
65
  store,
@@ -0,0 +1,122 @@
1
+ /** SQL owned by the activity store, kept together so the store reads as operations. */
2
+ export const ACTIVITY_SQL = {
3
+ nextSeq: "SELECT COALESCE(MAX(seq), 0) AS hi FROM activity_changes WHERE run_id = ?",
4
+ insertChange:
5
+ "INSERT INTO activity_changes (run_id, seq, span_id, event_index) VALUES (?, ?, ?, ?)",
6
+ spanById: "SELECT * FROM activity_spans WHERE span_id = ?",
7
+ spanStateById: "SELECT run_id, outcome, attrs FROM activity_spans WHERE span_id = ?",
8
+ endSpan: `UPDATE activity_spans SET outcome = ?, outcome_reason = ?, ended_at = ?, attrs = ?,
9
+ input_tokens = COALESCE(?, input_tokens),
10
+ output_tokens = COALESCE(?, output_tokens),
11
+ cache_read_tokens = COALESCE(?, cache_read_tokens),
12
+ cache_creation_tokens = COALESCE(?, cache_creation_tokens),
13
+ cost_usd = COALESCE(?, cost_usd),
14
+ model = COALESCE(?, model)
15
+ WHERE span_id = ?`,
16
+ openSpansByRun:
17
+ "SELECT span_id FROM activity_spans WHERE run_id = ? AND outcome IS NULL ORDER BY started_at DESC",
18
+ spansByRun: "SELECT * FROM activity_spans WHERE run_id = ? ORDER BY started_at",
19
+ upsertRollup: `INSERT INTO activity_run_rollups
20
+ (run_id, origin, name, session_id, job_name, principal_id, principal_label,
21
+ principal_kind, started_at, ended_at, outcome,
22
+ duration_ms, span_count, input_tokens, output_tokens, cache_read_tokens,
23
+ cache_creation_tokens, cost_usd, effective_cost_usd, billing_mode,
24
+ pricing_estimate, failure_reason, detail_pruned)
25
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
26
+ ON CONFLICT(run_id) DO UPDATE SET
27
+ ended_at = excluded.ended_at, outcome = excluded.outcome,
28
+ duration_ms = excluded.duration_ms, span_count = excluded.span_count,
29
+ input_tokens = excluded.input_tokens, output_tokens = excluded.output_tokens,
30
+ cache_read_tokens = excluded.cache_read_tokens,
31
+ cache_creation_tokens = excluded.cache_creation_tokens,
32
+ cost_usd = COALESCE(activity_run_rollups.cost_usd, excluded.cost_usd),
33
+ effective_cost_usd = CASE
34
+ WHEN activity_run_rollups.effective_cost_usd IS NOT NULL
35
+ THEN activity_run_rollups.effective_cost_usd
36
+ WHEN activity_run_rollups.billing_mode IS NULL
37
+ OR activity_run_rollups.billing_mode = excluded.billing_mode
38
+ THEN excluded.effective_cost_usd
39
+ ELSE NULL
40
+ END,
41
+ billing_mode = COALESCE(activity_run_rollups.billing_mode, excluded.billing_mode),
42
+ pricing_estimate = CASE
43
+ WHEN activity_run_rollups.pricing_estimate IS NOT NULL
44
+ THEN activity_run_rollups.pricing_estimate
45
+ WHEN activity_run_rollups.billing_mode IS NULL
46
+ OR activity_run_rollups.billing_mode = excluded.billing_mode
47
+ THEN excluded.pricing_estimate
48
+ ELSE NULL
49
+ END,
50
+ failure_reason = excluded.failure_reason`,
51
+ insertSpan: `INSERT INTO activity_spans
52
+ (span_id, run_id, parent_span_id, name, kind, origin, session_id, job_name, principal_id,
53
+ attrs, started_at, writer, last_heartbeat_at)
54
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
55
+ patchSpan: `UPDATE activity_spans SET principal_id = COALESCE(?, principal_id),
56
+ attrs = ?, wait_until = COALESCE(?, wait_until),
57
+ input_tokens = COALESCE(?, input_tokens),
58
+ output_tokens = COALESCE(?, output_tokens),
59
+ cache_read_tokens = COALESCE(?, cache_read_tokens),
60
+ cache_creation_tokens = COALESCE(?, cache_creation_tokens),
61
+ cost_usd = COALESCE(?, cost_usd),
62
+ model = COALESCE(?, model)
63
+ WHERE span_id = ?`,
64
+ spanRunById: "SELECT run_id FROM activity_spans WHERE span_id = ?",
65
+ nextEventIndex:
66
+ "SELECT COALESCE(MAX(event_index), -1) + 1 AS idx FROM activity_events WHERE span_id = ?",
67
+ insertEvent:
68
+ "INSERT INTO activity_events (span_id, event_index, ts, event_type, payload) VALUES (?, ?, ?, ?, ?)",
69
+ heartbeat:
70
+ "UPDATE activity_spans SET last_heartbeat_at = ? WHERE span_id = ? AND outcome IS NULL",
71
+ openSessionRuns:
72
+ "SELECT DISTINCT run_id FROM activity_spans WHERE outcome IS NULL AND origin = 'session'",
73
+ staleRoots: `SELECT span_id, run_id FROM activity_spans
74
+ WHERE outcome IS NULL AND parent_span_id IS NULL
75
+ AND writer != ? AND COALESCE(last_heartbeat_at, started_at) < ?`,
76
+ stuckRoots:
77
+ "SELECT * FROM activity_spans WHERE outcome IS NULL AND parent_span_id IS NULL AND started_at < ?",
78
+ spanChanges: `SELECT c.change_id, c.run_id, c.seq, s.*
79
+ FROM activity_changes c
80
+ JOIN activity_spans s ON s.span_id = c.span_id
81
+ WHERE c.change_id > ? AND c.event_index IS NULL
82
+ ORDER BY c.change_id LIMIT ?`,
83
+ eventChanges: `SELECT c.change_id, c.run_id, c.seq, e.*
84
+ FROM activity_changes c
85
+ JOIN activity_events e
86
+ ON e.span_id = c.span_id AND e.event_index = c.event_index
87
+ WHERE c.change_id > ? AND c.event_index IS NOT NULL
88
+ ORDER BY c.change_id LIMIT ?`,
89
+ terminalRootChanges: `SELECT MAX(c.change_id) AS change_id, s.*
90
+ FROM activity_changes c
91
+ JOIN activity_spans s ON s.span_id = c.span_id
92
+ WHERE c.change_id > ? AND c.event_index IS NULL
93
+ AND s.parent_span_id IS NULL AND s.outcome IS NOT NULL
94
+ GROUP BY s.span_id
95
+ ORDER BY change_id LIMIT ?`,
96
+ latestChangeCursor: "SELECT COALESCE(MAX(change_id), 0) AS hi FROM activity_changes",
97
+ eventsByRun: `SELECT e.* FROM activity_events e
98
+ JOIN activity_spans s ON s.span_id = e.span_id
99
+ WHERE s.run_id = ? ORDER BY e.ts, e.event_index`,
100
+ openRootSpans:
101
+ "SELECT * FROM activity_spans WHERE outcome IS NULL AND parent_span_id IS NULL ORDER BY started_at DESC",
102
+ pruneCandidates: `SELECT run_id, ended_at AS ended FROM activity_run_rollups r
103
+ WHERE detail_pruned = 0 AND ended_at IS NOT NULL
104
+ AND ((ended_at < ? AND ended_at < ?) OR ended_at < ?)
105
+ AND NOT EXISTS (
106
+ SELECT 1 FROM activity_spans s
107
+ WHERE s.run_id = r.run_id AND s.outcome IS NULL
108
+ )
109
+ LIMIT ?`,
110
+ markDetailPruned: "UPDATE activity_run_rollups SET detail_pruned = 1 WHERE run_id = ?",
111
+ markDigestCoverageGap:
112
+ "UPDATE activity_run_rollups SET failure_reason = COALESCE(failure_reason, 'digest coverage gap') WHERE run_id = ?",
113
+ deleteEventsByRun: `DELETE FROM activity_events WHERE span_id IN
114
+ (SELECT span_id FROM activity_spans WHERE run_id = ?)`,
115
+ deleteSpansByRun: "DELETE FROM activity_spans WHERE run_id = ?",
116
+ deleteChangesByRun: "DELETE FROM activity_changes WHERE run_id = ?",
117
+ compactChanges: `DELETE FROM activity_changes WHERE change_id < (
118
+ SELECT COALESCE(MAX(change_id), 0) FROM activity_changes
119
+ ) - 100000 AND run_id NOT IN (
120
+ SELECT DISTINCT run_id FROM activity_spans WHERE outcome IS NULL
121
+ )`,
122
+ } as const;