@schlessera/brain-ui-server 0.16.0 → 0.17.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 (89) hide show
  1. package/README.md +1 -0
  2. package/dist/agent/backend.d.ts +31 -10
  3. package/dist/agent/backend.d.ts.map +1 -1
  4. package/dist/agent/backend.js +86 -26
  5. package/dist/agent/backend.js.map +1 -1
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js +47 -16
  8. package/dist/app.js.map +1 -1
  9. package/dist/config/env-core.d.ts +73 -0
  10. package/dist/config/env-core.d.ts.map +1 -0
  11. package/dist/config/env-core.js +62 -0
  12. package/dist/config/env-core.js.map +1 -0
  13. package/dist/config/env.d.ts +19 -5
  14. package/dist/config/env.d.ts.map +1 -1
  15. package/dist/config/env.js +43 -11
  16. package/dist/config/env.js.map +1 -1
  17. package/dist/cron/scheduler.d.ts +17 -0
  18. package/dist/cron/scheduler.d.ts.map +1 -1
  19. package/dist/cron/scheduler.js +33 -6
  20. package/dist/cron/scheduler.js.map +1 -1
  21. package/dist/db/settings.d.ts +1 -1
  22. package/dist/db/settings.d.ts.map +1 -1
  23. package/dist/db/settings.js +2 -2
  24. package/dist/db/settings.js.map +1 -1
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/middleware/auth.d.ts +5 -0
  30. package/dist/middleware/auth.d.ts.map +1 -1
  31. package/dist/middleware/auth.js +46 -1
  32. package/dist/middleware/auth.js.map +1 -1
  33. package/dist/routes/health.d.ts +4 -14
  34. package/dist/routes/health.d.ts.map +1 -1
  35. package/dist/routes/health.js +22 -6
  36. package/dist/routes/health.js.map +1 -1
  37. package/dist/routes/share.d.ts.map +1 -1
  38. package/dist/routes/share.js +2 -2
  39. package/dist/routes/share.js.map +1 -1
  40. package/dist/voice/keyterm-builder.js +2 -2
  41. package/dist/voice/keyterm-builder.js.map +1 -1
  42. package/dist/ws/bridge.d.ts.map +1 -1
  43. package/dist/ws/bridge.js +17 -2
  44. package/dist/ws/bridge.js.map +1 -1
  45. package/dist/ws/clients.d.ts +10 -3
  46. package/dist/ws/clients.d.ts.map +1 -1
  47. package/dist/ws/clients.js +16 -4
  48. package/dist/ws/clients.js.map +1 -1
  49. package/dist/ws/connection.d.ts +1 -1
  50. package/dist/ws/connection.d.ts.map +1 -1
  51. package/dist/ws/connection.js +11 -2
  52. package/dist/ws/connection.js.map +1 -1
  53. package/dist/ws/dispatch.d.ts.map +1 -1
  54. package/dist/ws/dispatch.js +3 -1
  55. package/dist/ws/dispatch.js.map +1 -1
  56. package/dist/ws/host.d.ts +36 -0
  57. package/dist/ws/host.d.ts.map +1 -1
  58. package/dist/ws/host.js +93 -4
  59. package/dist/ws/host.js.map +1 -1
  60. package/dist/ws/run-session.d.ts.map +1 -1
  61. package/dist/ws/run-session.js +27 -5
  62. package/dist/ws/run-session.js.map +1 -1
  63. package/dist/ws/session-catalog.d.ts +2 -1
  64. package/dist/ws/session-catalog.d.ts.map +1 -1
  65. package/dist/ws/session-catalog.js +20 -3
  66. package/dist/ws/session-catalog.js.map +1 -1
  67. package/dist/ws/turns.d.ts +8 -0
  68. package/dist/ws/turns.d.ts.map +1 -1
  69. package/dist/ws/turns.js.map +1 -1
  70. package/package.json +7 -6
  71. package/src/agent/backend.ts +123 -29
  72. package/src/app.ts +55 -16
  73. package/src/config/env-core.ts +93 -0
  74. package/src/config/env.ts +61 -17
  75. package/src/cron/scheduler.ts +48 -14
  76. package/src/db/settings.ts +2 -2
  77. package/src/index.ts +6 -0
  78. package/src/middleware/auth.ts +50 -1
  79. package/src/routes/health.ts +22 -6
  80. package/src/routes/share.ts +11 -7
  81. package/src/voice/keyterm-builder.ts +2 -2
  82. package/src/ws/bridge.ts +17 -5
  83. package/src/ws/clients.ts +24 -6
  84. package/src/ws/connection.ts +11 -2
  85. package/src/ws/dispatch.ts +3 -1
  86. package/src/ws/host.ts +115 -6
  87. package/src/ws/run-session.ts +32 -8
  88. package/src/ws/session-catalog.ts +21 -3
  89. package/src/ws/turns.ts +8 -0
@@ -5,7 +5,13 @@ import { makeBridge, emitTurnError } from "./bridge.js";
5
5
  import { resolveTurnTarget } from "./routing.js";
6
6
  import type { QueuedFollowUp, RunningTurn } from "./turns.js";
7
7
  import { queuedBytes, queuedFollowUpBytes } from "./turns.js";
8
- import { MAX_SESSION_QUEUE, QUEUE_MAX_BYTES, QUEUE_WARN_BYTES, type WsHost } from "./host.js";
8
+ import {
9
+ MAX_SESSION_QUEUE,
10
+ QUEUE_MAX_BYTES,
11
+ QUEUE_WARN_BYTES,
12
+ turnLogAttributes,
13
+ type WsHost,
14
+ } from "./host.js";
9
15
 
10
16
  /**
11
17
  * Run one session slot: the initial turn, then any queued follow-up turns in
@@ -30,10 +36,14 @@ export async function runSession(
30
36
  try {
31
37
  target = await resolveTurnTarget(host.registry, host.catalog, initial.sessionId, initial.providerId);
32
38
  } catch (err) {
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ // No turn exists yet — routing failed before one was minted — so the
41
+ // report carries only the session identity the client asked for.
42
+ host.reportTurnFailed("BACKEND_ERROR", { sessionId: initial.sessionId }, message);
33
43
  host.sendToClients({
34
44
  type: "error",
35
45
  code: "BACKEND_ERROR",
36
- message: err instanceof Error ? err.message : String(err),
46
+ message,
37
47
  ...(initial.sessionId ? { sessionId: initial.sessionId } : {}),
38
48
  });
39
49
  return;
@@ -66,6 +76,7 @@ export async function runSession(
66
76
  timeoutHandle: setTimeout(() => {}, 0),
67
77
  queue: [],
68
78
  cancelled: false,
79
+ lastResult: null,
69
80
  };
70
81
  clearTimeout(turn.timeoutHandle);
71
82
  coordinator.running.add(turn);
@@ -90,7 +101,9 @@ export async function runSession(
90
101
  host.log.emit({
91
102
  severityText: "WARN",
92
103
  body: "turn timed out",
93
- attributes: { "timeout.ms": host.turnTimeoutMs },
104
+ // Read from the live turn: session_info may have named the session
105
+ // after this timer was armed.
106
+ attributes: { ...turnLogAttributes(turn), "timeout.ms": host.turnTimeoutMs },
94
107
  });
95
108
  abortController.abort();
96
109
  // Reject any pending interactive request for this turn too. A bridge
@@ -102,6 +115,8 @@ export async function runSession(
102
115
  turn.timeoutHandle = timeoutHandle;
103
116
 
104
117
  const bridge = makeBridge(host, turn, text, backend.id);
118
+ const startedAt = Date.now();
119
+ host.reportTurnStarted(turn);
105
120
  try {
106
121
  await backend.startTurn({
107
122
  prompt: text,
@@ -112,6 +127,14 @@ export async function runSession(
112
127
  bridge,
113
128
  ...(client ? { client } : {}),
114
129
  });
130
+ // A resolved startTurn is not a successful turn: backends resolve for
131
+ // runtime failures and report them on the terminal result frame, which
132
+ // the bridge recorded on the turn.
133
+ if (turn.lastResult === "error") {
134
+ host.reportTurnFailed("BACKEND_RESULT_ERROR", turn);
135
+ } else {
136
+ host.reportTurnCompleted(turn, Date.now() - startedAt);
137
+ }
115
138
  } catch (err) {
116
139
  emitTurnError(host, turn, err);
117
140
  } finally {
@@ -127,8 +150,10 @@ export async function runSession(
127
150
 
128
151
  if (!turn.cancelled && turn.queue.length > 0) {
129
152
  next = turn.queue.shift()!;
130
- // A queued follow-up is its own turn — give it a fresh identity.
153
+ // A queued follow-up is its own turn — give it a fresh identity, and
154
+ // its own terminal disposition.
131
155
  turn.turnId = crypto.randomUUID();
156
+ turn.lastResult = null;
132
157
  }
133
158
  }
134
159
  } finally {
@@ -168,11 +193,10 @@ export async function handleChatMessage(
168
193
  // device snapshot is deliberately not forwarded: a follow-up joins a
169
194
  // turn whose system prompt was already built and cannot be revised.
170
195
  backend.followUp({ sessionId, prompt: text, attachments }).catch((err) => {
196
+ const message = err instanceof Error ? err.message : String(err);
197
+ host.reportTurnFailed("FOLLOWUP_FAILED", runningTurn, message);
171
198
  host.sendToClients(
172
- withTurnScope(
173
- { type: "error", code: "FOLLOWUP_FAILED", message: err instanceof Error ? err.message : String(err) },
174
- runningTurn
175
- )
199
+ withTurnScope({ type: "error", code: "FOLLOWUP_FAILED", message }, runningTurn)
176
200
  );
177
201
  });
178
202
  } else {
@@ -1,4 +1,5 @@
1
1
  import type { Database } from "bun:sqlite";
2
+ import type { Logger } from "@opentelemetry/api-logs";
2
3
  import type { ServerResultMessage } from "@schlessera/brain-ui-sdk/protocol";
3
4
 
4
5
  /**
@@ -42,7 +43,20 @@ const UPSERT_SESSION_SQL = `INSERT INTO sessions (id, title, created_at, last_ac
42
43
  backend_id = COALESCE(excluded.backend_id, backend_id)`;
43
44
 
44
45
  /** SQLite-backed catalog over the app's own database (injected by createApp). */
45
- export function createSessionCatalog(db: () => Database): SessionCatalog {
46
+ export function createSessionCatalog(db: () => Database, log?: Logger): SessionCatalog {
47
+ // Persistence stays non-throwing — losing one accounting row must not kill
48
+ // the turn — but never silent: a full disk or locked database would
49
+ // otherwise drop every session record with nothing anywhere.
50
+ const reportWriteFailure = (sessionId: string, err: unknown): void => {
51
+ log?.emit({
52
+ severityText: "WARN",
53
+ body: "session persistence failed",
54
+ attributes: {
55
+ "session.id": sessionId,
56
+ error: err instanceof Error ? err.message : String(err),
57
+ },
58
+ });
59
+ };
46
60
  return {
47
61
  getStoredProviderId(sessionId) {
48
62
  const row = db()
@@ -72,7 +86,9 @@ export function createSessionCatalog(db: () => Database): SessionCatalog {
72
86
  providerId,
73
87
  backendId
74
88
  );
75
- } catch {}
89
+ } catch (err) {
90
+ reportWriteFailure(sessionId, err);
91
+ }
76
92
  },
77
93
 
78
94
  persistSession(msg, promptText, providerId, backendId) {
@@ -90,7 +106,9 @@ export function createSessionCatalog(db: () => Database): SessionCatalog {
90
106
  providerId,
91
107
  backendId
92
108
  );
93
- } catch {}
109
+ } catch (err) {
110
+ reportWriteFailure(msg.sessionId, err);
111
+ }
94
112
  },
95
113
  };
96
114
  }
package/src/ws/turns.ts CHANGED
@@ -55,6 +55,14 @@ export interface RunningTurn {
55
55
  timeoutHandle: ReturnType<typeof setTimeout>;
56
56
  queue: QueuedFollowUp[];
57
57
  cancelled: boolean;
58
+ /**
59
+ * Terminal disposition of the CURRENT turn's `result` frame, when one has
60
+ * streamed. Backends RESOLVE startTurn for runtime failures (the failure
61
+ * rides the result frame as outcome "error"), so completion accounting must
62
+ * read this — a resolved startTurn alone does not mean the turn succeeded.
63
+ * Reset when a queued follow-up becomes the next turn.
64
+ */
65
+ lastResult: "success" | "error" | "cancelled" | null;
58
66
  /**
59
67
  * Correlation id the client minted for this NEW conversation, echoed back on
60
68
  * `session_info` so the client can tell its own turn's identity from a