@schlessera/brain-ui-server 0.4.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 (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/agent/backend.d.ts +30 -0
  4. package/dist/agent/backend.d.ts.map +1 -0
  5. package/dist/agent/backend.js +226 -0
  6. package/dist/agent/backend.js.map +1 -0
  7. package/dist/app.d.ts +27 -0
  8. package/dist/app.d.ts.map +1 -0
  9. package/dist/app.js +116 -0
  10. package/dist/app.js.map +1 -0
  11. package/dist/brain/client.d.ts +43 -0
  12. package/dist/brain/client.d.ts.map +1 -0
  13. package/dist/brain/client.js +154 -0
  14. package/dist/brain/client.js.map +1 -0
  15. package/dist/brain/types.d.ts +41 -0
  16. package/dist/brain/types.d.ts.map +1 -0
  17. package/dist/brain/types.js +2 -0
  18. package/dist/brain/types.js.map +1 -0
  19. package/dist/cron/scheduler.d.ts +9 -0
  20. package/dist/cron/scheduler.d.ts.map +1 -0
  21. package/dist/cron/scheduler.js +58 -0
  22. package/dist/cron/scheduler.js.map +1 -0
  23. package/dist/db/client.d.ts +10 -0
  24. package/dist/db/client.d.ts.map +1 -0
  25. package/dist/db/client.js +73 -0
  26. package/dist/db/client.js.map +1 -0
  27. package/dist/files/walker.d.ts +63 -0
  28. package/dist/files/walker.d.ts.map +1 -0
  29. package/dist/files/walker.js +416 -0
  30. package/dist/files/walker.js.map +1 -0
  31. package/dist/index.d.ts +18 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +25 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/middleware/auth.d.ts +59 -0
  36. package/dist/middleware/auth.d.ts.map +1 -0
  37. package/dist/middleware/auth.js +246 -0
  38. package/dist/middleware/auth.js.map +1 -0
  39. package/dist/middleware/passkeys.d.ts +36 -0
  40. package/dist/middleware/passkeys.d.ts.map +1 -0
  41. package/dist/middleware/passkeys.js +427 -0
  42. package/dist/middleware/passkeys.js.map +1 -0
  43. package/dist/middleware/tailscale.d.ts +31 -0
  44. package/dist/middleware/tailscale.d.ts.map +1 -0
  45. package/dist/middleware/tailscale.js +64 -0
  46. package/dist/middleware/tailscale.js.map +1 -0
  47. package/dist/render/template.d.ts +7 -0
  48. package/dist/render/template.d.ts.map +1 -0
  49. package/dist/render/template.js +112 -0
  50. package/dist/render/template.js.map +1 -0
  51. package/dist/routes/brain.d.ts +155 -0
  52. package/dist/routes/brain.d.ts.map +1 -0
  53. package/dist/routes/brain.js +250 -0
  54. package/dist/routes/brain.js.map +1 -0
  55. package/dist/routes/files.d.ts +89 -0
  56. package/dist/routes/files.d.ts.map +1 -0
  57. package/dist/routes/files.js +91 -0
  58. package/dist/routes/files.js.map +1 -0
  59. package/dist/routes/health.d.ts +37 -0
  60. package/dist/routes/health.d.ts.map +1 -0
  61. package/dist/routes/health.js +27 -0
  62. package/dist/routes/health.js.map +1 -0
  63. package/dist/routes/providers.d.ts +33 -0
  64. package/dist/routes/providers.d.ts.map +1 -0
  65. package/dist/routes/providers.js +9 -0
  66. package/dist/routes/providers.js.map +1 -0
  67. package/dist/routes/render.d.ts +25 -0
  68. package/dist/routes/render.d.ts.map +1 -0
  69. package/dist/routes/render.js +47 -0
  70. package/dist/routes/render.js.map +1 -0
  71. package/dist/routes/sessions.d.ts +79 -0
  72. package/dist/routes/sessions.d.ts.map +1 -0
  73. package/dist/routes/sessions.js +37 -0
  74. package/dist/routes/sessions.js.map +1 -0
  75. package/dist/routes/voice.d.ts +92 -0
  76. package/dist/routes/voice.d.ts.map +1 -0
  77. package/dist/routes/voice.js +64 -0
  78. package/dist/routes/voice.js.map +1 -0
  79. package/dist/voice/deepgram-token.d.ts +5 -0
  80. package/dist/voice/deepgram-token.d.ts.map +1 -0
  81. package/dist/voice/deepgram-token.js +63 -0
  82. package/dist/voice/deepgram-token.js.map +1 -0
  83. package/dist/voice/keyterm-builder.d.ts +18 -0
  84. package/dist/voice/keyterm-builder.d.ts.map +1 -0
  85. package/dist/voice/keyterm-builder.js +391 -0
  86. package/dist/voice/keyterm-builder.js.map +1 -0
  87. package/dist/voice/speech-providers.d.ts +25 -0
  88. package/dist/voice/speech-providers.d.ts.map +1 -0
  89. package/dist/voice/speech-providers.js +104 -0
  90. package/dist/voice/speech-providers.js.map +1 -0
  91. package/dist/ws/attachments.d.ts +10 -0
  92. package/dist/ws/attachments.d.ts.map +1 -0
  93. package/dist/ws/attachments.js +63 -0
  94. package/dist/ws/attachments.js.map +1 -0
  95. package/dist/ws/bridge.d.ts +7 -0
  96. package/dist/ws/bridge.d.ts.map +1 -0
  97. package/dist/ws/bridge.js +86 -0
  98. package/dist/ws/bridge.js.map +1 -0
  99. package/dist/ws/clients.d.ts +20 -0
  100. package/dist/ws/clients.d.ts.map +1 -0
  101. package/dist/ws/clients.js +48 -0
  102. package/dist/ws/clients.js.map +1 -0
  103. package/dist/ws/connection.d.ts +8 -0
  104. package/dist/ws/connection.d.ts.map +1 -0
  105. package/dist/ws/connection.js +116 -0
  106. package/dist/ws/connection.js.map +1 -0
  107. package/dist/ws/dispatch.d.ts +5 -0
  108. package/dist/ws/dispatch.d.ts.map +1 -0
  109. package/dist/ws/dispatch.js +139 -0
  110. package/dist/ws/dispatch.js.map +1 -0
  111. package/dist/ws/frames.d.ts +17 -0
  112. package/dist/ws/frames.d.ts.map +1 -0
  113. package/dist/ws/frames.js +37 -0
  114. package/dist/ws/frames.js.map +1 -0
  115. package/dist/ws/handler.d.ts +34 -0
  116. package/dist/ws/handler.d.ts.map +1 -0
  117. package/dist/ws/handler.js +33 -0
  118. package/dist/ws/handler.js.map +1 -0
  119. package/dist/ws/history.d.ts +17 -0
  120. package/dist/ws/history.d.ts.map +1 -0
  121. package/dist/ws/history.js +45 -0
  122. package/dist/ws/history.js.map +1 -0
  123. package/dist/ws/host.d.ts +40 -0
  124. package/dist/ws/host.d.ts.map +1 -0
  125. package/dist/ws/host.js +49 -0
  126. package/dist/ws/host.js.map +1 -0
  127. package/dist/ws/routing.d.ts +12 -0
  128. package/dist/ws/routing.d.ts.map +1 -0
  129. package/dist/ws/routing.js +31 -0
  130. package/dist/ws/routing.js.map +1 -0
  131. package/dist/ws/run-session.d.ts +17 -0
  132. package/dist/ws/run-session.d.ts.map +1 -0
  133. package/dist/ws/run-session.js +160 -0
  134. package/dist/ws/run-session.js.map +1 -0
  135. package/dist/ws/session-catalog.d.ts +25 -0
  136. package/dist/ws/session-catalog.d.ts.map +1 -0
  137. package/dist/ws/session-catalog.js +45 -0
  138. package/dist/ws/session-catalog.js.map +1 -0
  139. package/dist/ws/shrink.d.ts +34 -0
  140. package/dist/ws/shrink.d.ts.map +1 -0
  141. package/dist/ws/shrink.js +91 -0
  142. package/dist/ws/shrink.js.map +1 -0
  143. package/dist/ws/turns.d.ts +77 -0
  144. package/dist/ws/turns.d.ts.map +1 -0
  145. package/dist/ws/turns.js +89 -0
  146. package/dist/ws/turns.js.map +1 -0
  147. package/migrations/001_initial.sql +29 -0
  148. package/migrations/002_session_provider.sql +1 -0
  149. package/migrations/003_session_backend.sql +5 -0
  150. package/migrations/004_drop_gemini_backend.sql +20 -0
  151. package/migrations/005_passkey_credentials.sql +18 -0
  152. package/package.json +72 -0
  153. package/src/agent/backend.ts +308 -0
  154. package/src/app.ts +159 -0
  155. package/src/brain/client.ts +186 -0
  156. package/src/brain/types.ts +44 -0
  157. package/src/cron/scheduler.ts +80 -0
  158. package/src/db/client.ts +85 -0
  159. package/src/files/walker.ts +418 -0
  160. package/src/index.ts +39 -0
  161. package/src/middleware/auth.ts +321 -0
  162. package/src/middleware/passkeys.ts +510 -0
  163. package/src/middleware/tailscale.ts +81 -0
  164. package/src/render/template.ts +124 -0
  165. package/src/routes/brain.ts +305 -0
  166. package/src/routes/files.ts +99 -0
  167. package/src/routes/health.ts +29 -0
  168. package/src/routes/providers.ts +9 -0
  169. package/src/routes/render.ts +68 -0
  170. package/src/routes/sessions.ts +49 -0
  171. package/src/routes/voice.ts +80 -0
  172. package/src/voice/deepgram-token.ts +80 -0
  173. package/src/voice/keyterm-builder.ts +426 -0
  174. package/src/voice/speech-providers.ts +111 -0
  175. package/src/ws/attachments.ts +86 -0
  176. package/src/ws/bridge.ts +139 -0
  177. package/src/ws/clients.ts +57 -0
  178. package/src/ws/connection.ts +132 -0
  179. package/src/ws/dispatch.ts +162 -0
  180. package/src/ws/frames.ts +42 -0
  181. package/src/ws/handler.ts +50 -0
  182. package/src/ws/history.ts +51 -0
  183. package/src/ws/host.ts +69 -0
  184. package/src/ws/routing.ts +43 -0
  185. package/src/ws/run-session.ts +192 -0
  186. package/src/ws/session-catalog.ts +97 -0
  187. package/src/ws/shrink.ts +95 -0
  188. package/src/ws/turns.ts +151 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Composition root for the WebSocket side: one process-wide host instance
3
+ * wired to the default SQLite session catalog and the backend registry. The
4
+ * actual behavior lives in the focused modules next to this file —
5
+ * turns/bridge/run-session/dispatch/connection — which all take the host
6
+ * explicitly. createApp() configures this host from its options before
7
+ * building the upgrade handler.
8
+ */
9
+ import type { ClientMessage } from "@schlessera/brain-ui-sdk/protocol";
10
+ import type { WSContext } from "./clients.js";
11
+ import { WsHost, type WsHostOptions } from "./host.js";
12
+ import { websocket } from "./connection.js";
13
+ export { websocket };
14
+ export { WsHost, type WsHostOptions } from "./host.js";
15
+ export { resolveTurnTarget } from "./routing.js";
16
+ export { validateAttachments, estimateDecodedBase64Bytes } from "./attachments.js";
17
+ export { createSessionCatalog, type SessionCatalog } from "./session-catalog.js";
18
+ /** The process-default host. */
19
+ export declare const defaultWsHost: WsHost;
20
+ /** Apply embedder options to the default host (called by createApp). */
21
+ export declare function configureWsHost(options: WsHostOptions): void;
22
+ /** Hono upgrade handler bound to the default host. */
23
+ export declare const wsUpgrade: import("hono").MiddlewareHandler<any, string, {
24
+ outputFormat: "ws";
25
+ }>;
26
+ /** True while any session has a running turn. */
27
+ export declare function isTurnActive(): boolean;
28
+ /** Cancel every running turn (used on shutdown). */
29
+ export declare function cancelActiveTurn(): boolean;
30
+ /** Drive a client message against the default host (also the test seam). */
31
+ export declare function handleClientMessage(ws: WSContext, msg: ClientMessage): Promise<void>;
32
+ /** Test seam: clear all turn/pending state on the default host. */
33
+ export declare function resetForTests(): void;
34
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/ws/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAmB,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG7D,OAAO,EAAE,SAAS,EAAE,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEjF,gCAAgC;AAChC,eAAO,MAAM,aAAa,QAAe,CAAC;AAE1C,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAE5D;AAED,sDAAsD;AACtD,eAAO,MAAM,SAAS;;EAAiC,CAAC;AAExD,iDAAiD;AACjD,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED,oDAAoD;AACpD,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED,4EAA4E;AAC5E,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F;AAED,mEAAmE;AACnE,wBAAgB,aAAa,IAAI,IAAI,CAEpC"}
@@ -0,0 +1,33 @@
1
+ import { WsHost } from "./host.js";
2
+ import { createWsUpgrade, websocket } from "./connection.js";
3
+ import { handleClientMessage as dispatchClientMessage } from "./dispatch.js";
4
+ export { websocket };
5
+ export { WsHost } from "./host.js";
6
+ export { resolveTurnTarget } from "./routing.js";
7
+ export { validateAttachments, estimateDecodedBase64Bytes } from "./attachments.js";
8
+ export { createSessionCatalog } from "./session-catalog.js";
9
+ /** The process-default host. */
10
+ export const defaultWsHost = new WsHost();
11
+ /** Apply embedder options to the default host (called by createApp). */
12
+ export function configureWsHost(options) {
13
+ defaultWsHost.configure(options);
14
+ }
15
+ /** Hono upgrade handler bound to the default host. */
16
+ export const wsUpgrade = createWsUpgrade(defaultWsHost);
17
+ /** True while any session has a running turn. */
18
+ export function isTurnActive() {
19
+ return defaultWsHost.coordinator.isTurnActive();
20
+ }
21
+ /** Cancel every running turn (used on shutdown). */
22
+ export function cancelActiveTurn() {
23
+ return defaultWsHost.coordinator.cancelAll("Server shutting down");
24
+ }
25
+ /** Drive a client message against the default host (also the test seam). */
26
+ export async function handleClientMessage(ws, msg) {
27
+ return dispatchClientMessage(defaultWsHost, ws, msg);
28
+ }
29
+ /** Test seam: clear all turn/pending state on the default host. */
30
+ export function resetForTests() {
31
+ defaultWsHost.coordinator.reset();
32
+ }
33
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/ws/handler.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,MAAM,EAAsB,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,IAAI,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE7E,OAAO,EAAE,SAAS,EAAE,CAAC;AACrB,OAAO,EAAE,MAAM,EAAsB,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAuB,MAAM,sBAAsB,CAAC;AAEjF,gCAAgC;AAChC,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,MAAM,EAAE,CAAC;AAE1C,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,OAAsB;IACpD,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC;AAED,sDAAsD;AACtD,MAAM,CAAC,MAAM,SAAS,GAAG,eAAe,CAAC,aAAa,CAAC,CAAC;AAExD,iDAAiD;AACjD,MAAM,UAAU,YAAY;IAC1B,OAAO,aAAa,CAAC,WAAW,CAAC,YAAY,EAAE,CAAC;AAClD,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,gBAAgB;IAC9B,OAAO,aAAa,CAAC,WAAW,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;AACrE,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAa,EAAE,GAAkB;IACzE,OAAO,qBAAqB,CAAC,aAAa,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;AACvD,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,aAAa;IAC3B,aAAa,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;AACpC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { SessionHistoryMessage } from "@schlessera/brain-ui-sdk/protocol";
2
+ import { type WSContext } from "./clients.js";
3
+ /**
4
+ * Send a structured history to one socket as one or more `session_history`
5
+ * frames. Each message is size-bounded, then batched into byte-limited chunks
6
+ * so no single frame exceeds the socket's per-message limit (which would drop
7
+ * the connection and wedge the reconnect loop). The first frame replaces the
8
+ * client transcript; continuation frames carry `append: true`. An empty
9
+ * history still emits one (empty, replacing) frame so the client settles.
10
+ *
11
+ * Every frame carries `sessionId` so a client with multiple sessions open can
12
+ * demux it: a history load for a background session must never replace the
13
+ * transcript in view (a reconnect or an out-of-order getHistory otherwise
14
+ * silently overwrites the wrong pane).
15
+ */
16
+ export declare function sendSessionHistory(ws: WSContext, sessionId: string, messages: SessionHistoryMessage[]): void;
17
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/ws/history.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC/E,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,cAAc,CAAC;AAGtD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,SAAS,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,qBAAqB,EAAE,GAChC,IAAI,CA6BN"}
@@ -0,0 +1,45 @@
1
+ import { sendTo } from "./clients.js";
2
+ import { HISTORY_CHUNK_BYTES, shrinkForReplication } from "./shrink.js";
3
+ /**
4
+ * Send a structured history to one socket as one or more `session_history`
5
+ * frames. Each message is size-bounded, then batched into byte-limited chunks
6
+ * so no single frame exceeds the socket's per-message limit (which would drop
7
+ * the connection and wedge the reconnect loop). The first frame replaces the
8
+ * client transcript; continuation frames carry `append: true`. An empty
9
+ * history still emits one (empty, replacing) frame so the client settles.
10
+ *
11
+ * Every frame carries `sessionId` so a client with multiple sessions open can
12
+ * demux it: a history load for a background session must never replace the
13
+ * transcript in view (a reconnect or an out-of-order getHistory otherwise
14
+ * silently overwrites the wrong pane).
15
+ */
16
+ export function sendSessionHistory(ws, sessionId, messages) {
17
+ const bounded = messages.map((m) => shrinkForReplication(m));
18
+ if (bounded.length === 0) {
19
+ sendTo(ws, { type: "session_history", sessionId, messages: [] });
20
+ return;
21
+ }
22
+ let batch = [];
23
+ let batchBytes = 0;
24
+ let isFirst = true;
25
+ const flush = () => {
26
+ sendTo(ws, {
27
+ type: "session_history",
28
+ sessionId,
29
+ messages: batch,
30
+ ...(isFirst ? {} : { append: true }),
31
+ });
32
+ isFirst = false;
33
+ batch = [];
34
+ batchBytes = 0;
35
+ };
36
+ for (const message of bounded) {
37
+ const size = JSON.stringify(message).length;
38
+ if (batch.length > 0 && batchBytes + size > HISTORY_CHUNK_BYTES)
39
+ flush();
40
+ batch.push(message);
41
+ batchBytes += size;
42
+ }
43
+ flush();
44
+ }
45
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/ws/history.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,MAAM,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExE;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAChC,EAAa,EACb,SAAiB,EACjB,QAAiC;IAEjC,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;QACjE,OAAO;IACT,CAAC;IAED,IAAI,KAAK,GAA4B,EAAE,CAAC;IACxC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,OAAO,GAAG,IAAI,CAAC;IACnB,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,MAAM,CAAC,EAAE,EAAE;YACT,IAAI,EAAE,iBAAiB;YACvB,SAAS;YACT,QAAQ,EAAE,KAAK;YACf,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;SACrC,CAAC,CAAC;QACH,OAAO,GAAG,KAAK,CAAC;QAChB,KAAK,GAAG,EAAE,CAAC;QACX,UAAU,GAAG,CAAC,CAAC;IACjB,CAAC,CAAC;IAEF,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAC5C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,GAAG,IAAI,GAAG,mBAAmB;YAAE,KAAK,EAAE,CAAC;QACzE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,UAAU,IAAI,IAAI,CAAC;IACrB,CAAC;IACD,KAAK,EAAE,CAAC;AACV,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { ServerMessage } from "@schlessera/brain-ui-sdk/protocol";
2
+ import { type WSContext } from "./clients.js";
3
+ import { TurnCoordinator } from "./turns.js";
4
+ import { type SessionCatalog } from "./session-catalog.js";
5
+ /** Cap on host-side follow-up queue depth per session (backends without followUp). */
6
+ export declare const MAX_SESSION_QUEUE = 5;
7
+ export interface WsHostOptions {
8
+ /** Session persistence seam; defaults to the package's SQLite catalog. */
9
+ catalog?: SessionCatalog;
10
+ /** Display name used in connection/status copy. */
11
+ appName?: string;
12
+ /** Per-turn timeout in ms (default 10 minutes). */
13
+ turnTimeoutMs?: number;
14
+ /**
15
+ * Deployment-time cap on concurrent RUNNING sessions. Each running turn is
16
+ * roughly one CLI subprocess, so this bounds memory/CPU. Read per request so
17
+ * the deploy-time env is honored without a restart.
18
+ */
19
+ maxConcurrentSessions?: () => number;
20
+ }
21
+ /**
22
+ * Everything one WebSocket coordinator instance owns: turn state, the session
23
+ * catalog, branding copy, and the frame senders. Handlers receive this host
24
+ * explicitly instead of reaching for module globals.
25
+ */
26
+ export declare class WsHost {
27
+ readonly coordinator: TurnCoordinator;
28
+ catalog: SessionCatalog;
29
+ appName: string;
30
+ turnTimeoutMs: number;
31
+ maxConcurrentSessions: () => number;
32
+ constructor(options?: WsHostOptions);
33
+ /** Re-apply embedder options (createApp configures the default host). */
34
+ configure(options: WsHostOptions): void;
35
+ /** Fan a frame out to every attached client (size-bounded per frame). */
36
+ sendToClients(msg: ServerMessage): void;
37
+ /** Send a frame to one specific socket (size-bounded). */
38
+ sendMessage(ws: WSContext, msg: ServerMessage): void;
39
+ }
40
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/ws/host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAKjF,sFAAsF;AACtF,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAMD;;;;GAIG;AACH,qBAAa,MAAM;IACjB,QAAQ,CAAC,WAAW,kBAAyB;IAC7C,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB,EAAE,MAAM,MAAM,CAAC;gBAExB,OAAO,GAAE,aAAkB;IAOvC,yEAAyE;IACzE,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IASvC,yEAAyE;IACzE,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI;IAIvC,0DAA0D;IAC1D,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI;CAGrD"}
@@ -0,0 +1,49 @@
1
+ import { broadcast, sendTo } from "./clients.js";
2
+ import { TurnCoordinator } from "./turns.js";
3
+ import { createSessionCatalog } from "./session-catalog.js";
4
+ /** Host-side turn timeout. The backend no longer times out — the host owns it. */
5
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
6
+ /** Cap on host-side follow-up queue depth per session (backends without followUp). */
7
+ export const MAX_SESSION_QUEUE = 5;
8
+ function envMaxConcurrentSessions() {
9
+ return Math.max(1, Number(process.env.MAX_CONCURRENT_SESSIONS) || 3);
10
+ }
11
+ /**
12
+ * Everything one WebSocket coordinator instance owns: turn state, the session
13
+ * catalog, branding copy, and the frame senders. Handlers receive this host
14
+ * explicitly instead of reaching for module globals.
15
+ */
16
+ export class WsHost {
17
+ coordinator = new TurnCoordinator();
18
+ catalog;
19
+ appName;
20
+ turnTimeoutMs;
21
+ maxConcurrentSessions;
22
+ constructor(options = {}) {
23
+ this.catalog = options.catalog ?? createSessionCatalog();
24
+ this.appName = options.appName ?? "Brain UI";
25
+ this.turnTimeoutMs = options.turnTimeoutMs ?? DEFAULT_TIMEOUT_MS;
26
+ this.maxConcurrentSessions = options.maxConcurrentSessions ?? envMaxConcurrentSessions;
27
+ }
28
+ /** Re-apply embedder options (createApp configures the default host). */
29
+ configure(options) {
30
+ if (options.catalog)
31
+ this.catalog = options.catalog;
32
+ if (options.appName)
33
+ this.appName = options.appName;
34
+ if (options.turnTimeoutMs)
35
+ this.turnTimeoutMs = options.turnTimeoutMs;
36
+ if (options.maxConcurrentSessions) {
37
+ this.maxConcurrentSessions = options.maxConcurrentSessions;
38
+ }
39
+ }
40
+ /** Fan a frame out to every attached client (size-bounded per frame). */
41
+ sendToClients(msg) {
42
+ broadcast(msg);
43
+ }
44
+ /** Send a frame to one specific socket (size-bounded). */
45
+ sendMessage(ws, msg) {
46
+ sendTo(ws, msg);
47
+ }
48
+ }
49
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../../src/ws/host.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAkB,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAuB,MAAM,sBAAsB,CAAC;AAEjF,kFAAkF;AAClF,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,aAAa;AAExD,sFAAsF;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAiBnC,SAAS,wBAAwB;IAC/B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,MAAM;IACR,WAAW,GAAG,IAAI,eAAe,EAAE,CAAC;IAC7C,OAAO,CAAiB;IACxB,OAAO,CAAS;IAChB,aAAa,CAAS;IACtB,qBAAqB,CAAe;IAEpC,YAAY,UAAyB,EAAE;QACrC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,oBAAoB,EAAE,CAAC;QACzD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,UAAU,CAAC;QAC7C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,kBAAkB,CAAC;QACjE,IAAI,CAAC,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,IAAI,wBAAwB,CAAC;IACzF,CAAC;IAED,yEAAyE;IACzE,SAAS,CAAC,OAAsB;QAC9B,IAAI,OAAO,CAAC,OAAO;YAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QACpD,IAAI,OAAO,CAAC,OAAO;YAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QACpD,IAAI,OAAO,CAAC,aAAa;YAAE,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;QACtE,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC;YAClC,IAAI,CAAC,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,aAAa,CAAC,GAAkB;QAC9B,SAAS,CAAC,GAAG,CAAC,CAAC;IACjB,CAAC;IAED,0DAA0D;IAC1D,WAAW,CAAC,EAAa,EAAE,GAAkB;QAC3C,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,12 @@
1
+ import type { AgentBackend } from "@schlessera/brain-ui-sdk/server";
2
+ import type { SessionCatalog } from "./session-catalog.js";
3
+ /**
4
+ * Resolve one turn slot's pinned backend and profile. Resumed sessions route by
5
+ * their stored backend id; new sessions route by the requested profile owner.
6
+ */
7
+ export declare function resolveTurnTarget(catalog: SessionCatalog, sessionId: string | undefined, requested: string | undefined): Promise<{
8
+ profileId?: string;
9
+ backend: AgentBackend;
10
+ droppedPin?: string;
11
+ }>;
12
+ //# sourceMappingURL=routing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/ws/routing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAOpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAAC;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB7E"}
@@ -0,0 +1,31 @@
1
+ import { getBackendForProfile, getBackendForSession, getDefaultBackend, listAllProviders, } from "../agent/backend.js";
2
+ /**
3
+ * Resolve one turn slot's pinned backend and profile. Resumed sessions route by
4
+ * their stored backend id; new sessions route by the requested profile owner.
5
+ */
6
+ export async function resolveTurnTarget(catalog, sessionId, requested) {
7
+ if (sessionId) {
8
+ const backend = await getBackendForSession(catalog.getStoredBackendId(sessionId));
9
+ const stored = catalog.getStoredProviderId(sessionId);
10
+ const available = await backend.listProfiles();
11
+ if (stored && available.some((profile) => profile.id === stored)) {
12
+ return { backend, profileId: stored };
13
+ }
14
+ // A stored pin that no longer resolves: fall back to the default profile
15
+ // but report it (droppedPin) so the caller can surface the switch instead
16
+ // of silently changing model/endpoint/billing with nothing in the logs.
17
+ return { backend, ...(stored ? { droppedPin: stored } : {}) };
18
+ }
19
+ if (requested) {
20
+ const available = await listAllProviders();
21
+ if (available.some((profile) => profile.id === requested)) {
22
+ const backend = await getBackendForProfile(requested);
23
+ if (backend)
24
+ return { backend, profileId: requested };
25
+ }
26
+ }
27
+ const backend = await getDefaultBackend();
28
+ const profileId = (await backend.listProfiles())[0]?.id;
29
+ return { backend, ...(profileId ? { profileId } : {}) };
30
+ }
31
+ //# sourceMappingURL=routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing.js","sourceRoot":"","sources":["../../src/ws/routing.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAG7B;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAuB,EACvB,SAA6B,EAC7B,SAA6B;IAE7B,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,OAAO,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,CAAC;QAClF,MAAM,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;QACtD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;QAC/C,IAAI,MAAM,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC;YACjE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QACxC,CAAC;QACD,yEAAyE;QACzE,0EAA0E;QAC1E,wEAAwE;QACxE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAChE,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,SAAS,GAAG,MAAM,gBAAgB,EAAE,CAAC;QAC3C,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,SAAS,CAAC,EAAE,CAAC;YAC1D,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,SAAS,CAAC,CAAC;YACtD,IAAI,OAAO;gBAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;QACxD,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAC1C,MAAM,SAAS,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACxD,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { ChatImageAttachment } from "@schlessera/brain-ui-sdk/protocol";
2
+ import type { WSContext } from "./clients.js";
3
+ import { type WsHost } from "./host.js";
4
+ /**
5
+ * Run one session slot: the initial turn, then any queued follow-up turns in
6
+ * order. The host owns per-turn cancellation (AbortController + timeout). Never
7
+ * throws — runtime failures are emitted as error frames.
8
+ */
9
+ export declare function runSession(host: WsHost, initial: {
10
+ text: string;
11
+ sessionId?: string;
12
+ attachments: ChatImageAttachment[];
13
+ providerId?: string;
14
+ }): Promise<void>;
15
+ /** Dispatch a chat_message: follow-up to a running session, or a new session. */
16
+ export declare function handleChatMessage(host: WsHost, ws: WSContext, text: string, sessionId: string | undefined, attachments: ChatImageAttachment[], requestedProviderId: string | undefined): Promise<void>;
17
+ //# sourceMappingURL=run-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-session.d.ts","sourceRoot":"","sources":["../../src/ws/run-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAK9C,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAE3D;;;;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;CACrB,GACA,OAAO,CAAC,IAAI,CAAC,CAwGf;AAED,iFAAiF;AACjF,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,EAAE,mBAAmB,EAAE,EAClC,mBAAmB,EAAE,MAAM,GAAG,SAAS,GACtC,OAAO,CAAC,IAAI,CAAC,CAwDf"}
@@ -0,0 +1,160 @@
1
+ import { withSessionId, withTurnScope } from "./frames.js";
2
+ import { makeBridge, emitTurnError } from "./bridge.js";
3
+ import { resolveTurnTarget } from "./routing.js";
4
+ import { MAX_SESSION_QUEUE } from "./host.js";
5
+ /**
6
+ * Run one session slot: the initial turn, then any queued follow-up turns in
7
+ * order. The host owns per-turn cancellation (AbortController + timeout). Never
8
+ * throws — runtime failures are emitted as error frames.
9
+ */
10
+ export async function runSession(host, initial) {
11
+ const { coordinator } = host;
12
+ coordinator.startingSessions += 1;
13
+ let target;
14
+ try {
15
+ target = await resolveTurnTarget(host.catalog, initial.sessionId, initial.providerId);
16
+ }
17
+ catch (err) {
18
+ host.sendToClients({
19
+ type: "error",
20
+ code: "BACKEND_ERROR",
21
+ message: err instanceof Error ? err.message : String(err),
22
+ ...(initial.sessionId ? { sessionId: initial.sessionId } : {}),
23
+ });
24
+ return;
25
+ }
26
+ finally {
27
+ coordinator.startingSessions = Math.max(0, coordinator.startingSessions - 1);
28
+ }
29
+ const { backend, profileId: initialProfileId } = target;
30
+ if (target.droppedPin) {
31
+ console.warn(`[agent] session ${initial.sessionId} pinned profile "${target.droppedPin}" ` +
32
+ `is unavailable; running on the default profile.`);
33
+ host.sendToClients({
34
+ type: "status",
35
+ status: "thinking",
36
+ detail: `Pinned model "${target.droppedPin}" is unavailable — running on the default.`,
37
+ ...(initial.sessionId ? { sessionId: initial.sessionId } : {}),
38
+ });
39
+ }
40
+ const turn = {
41
+ sessionId: initial.sessionId ?? null,
42
+ turnId: crypto.randomUUID(),
43
+ providerId: initialProfileId ?? null,
44
+ backend,
45
+ abortController: new AbortController(),
46
+ timeoutHandle: setTimeout(() => { }, 0),
47
+ queue: [],
48
+ cancelled: false,
49
+ };
50
+ clearTimeout(turn.timeoutHandle);
51
+ coordinator.running.add(turn);
52
+ if (turn.sessionId)
53
+ coordinator.bySession.set(turn.sessionId, turn);
54
+ let profileId = initialProfileId;
55
+ let resumeId = initial.sessionId;
56
+ let next = {
57
+ text: initial.text,
58
+ attachments: initial.attachments,
59
+ };
60
+ try {
61
+ while (next && !turn.cancelled) {
62
+ const { text, attachments } = next;
63
+ next = null;
64
+ const abortController = new AbortController();
65
+ turn.abortController = abortController;
66
+ const timeoutHandle = setTimeout(() => {
67
+ console.log("[agent] Turn timed out after", host.turnTimeoutMs, "ms");
68
+ abortController.abort();
69
+ // Reject any pending interactive request for this turn too. A bridge
70
+ // that awaits askUser/approval without racing the abort signal would
71
+ // otherwise leave startTurn parked forever after abort(), leaking a
72
+ // concurrency slot until restart. Draining unblocks the unwind.
73
+ coordinator.drainPendingForTurn(turn, "Turn timed out");
74
+ }, host.turnTimeoutMs);
75
+ turn.timeoutHandle = timeoutHandle;
76
+ const bridge = makeBridge(host, turn, text, backend.id);
77
+ try {
78
+ await backend.startTurn({
79
+ prompt: text,
80
+ attachments,
81
+ sessionId: resumeId,
82
+ profileId,
83
+ signal: abortController.signal,
84
+ bridge,
85
+ });
86
+ }
87
+ catch (err) {
88
+ emitTurnError(host, turn, err);
89
+ }
90
+ finally {
91
+ clearTimeout(timeoutHandle);
92
+ }
93
+ // Subsequent (queued) turns resume the now-known session and keep its
94
+ // pin. turn.providerId is the resolved profile (set from initial routing
95
+ // and re-pinned by session_info); carrying it forward stops a queued
96
+ // follow-up from silently dropping onto the default profile/model/billing.
97
+ resumeId = turn.sessionId ?? resumeId;
98
+ profileId = turn.providerId ?? profileId;
99
+ if (!turn.cancelled && turn.queue.length > 0) {
100
+ next = turn.queue.shift();
101
+ // A queued follow-up is its own turn — give it a fresh identity.
102
+ turn.turnId = crypto.randomUUID();
103
+ }
104
+ }
105
+ }
106
+ finally {
107
+ if (turn.sessionId)
108
+ coordinator.bySession.delete(turn.sessionId);
109
+ coordinator.running.delete(turn);
110
+ coordinator.drainPendingForTurn(turn, "Session ended");
111
+ }
112
+ }
113
+ /** Dispatch a chat_message: follow-up to a running session, or a new session. */
114
+ export async function handleChatMessage(host, ws, text, sessionId, attachments, requestedProviderId) {
115
+ const { coordinator } = host;
116
+ const runningTurn = sessionId ? coordinator.bySession.get(sessionId) : undefined;
117
+ if (runningTurn && sessionId) {
118
+ const backend = runningTurn.backend;
119
+ // Message to a session whose turn is running = a follow-up.
120
+ if (backend.capabilities.followUp && backend.followUp) {
121
+ // Inject into the running turn; frames flow through its bridge.
122
+ backend.followUp({ sessionId, prompt: text, attachments }).catch((err) => {
123
+ host.sendToClients(withTurnScope({ type: "error", code: "FOLLOWUP_FAILED", message: err instanceof Error ? err.message : String(err) }, runningTurn));
124
+ });
125
+ }
126
+ else if (runningTurn.queue.length >= MAX_SESSION_QUEUE) {
127
+ host.sendMessage(ws,
128
+ // Session-scoped only: the rejected message would have become a
129
+ // FUTURE turn in this slot, not the one currently running.
130
+ withSessionId({ type: "error", code: "SESSION_QUEUE_FULL", message: `This session's queue is full (max ${MAX_SESSION_QUEUE}). Wait for it to catch up.` }, sessionId));
131
+ }
132
+ else {
133
+ // Queue it as the session's next turn; report queued immediately.
134
+ runningTurn.queue.push({ text, attachments });
135
+ host.sendToClients(withSessionId({ type: "status", status: "queued" }, sessionId));
136
+ }
137
+ return;
138
+ }
139
+ // New session (or resume of an idle one) — gated by the concurrency cap.
140
+ const cap = host.maxConcurrentSessions();
141
+ if (coordinator.running.size + coordinator.startingSessions >= cap) {
142
+ host.sendMessage(ws, {
143
+ type: "error",
144
+ code: "SESSION_LIMIT",
145
+ message: `Too many concurrent sessions (max ${cap}). Wait for one to finish.`,
146
+ ...(sessionId ? { sessionId } : {}),
147
+ });
148
+ return;
149
+ }
150
+ if (sessionId) {
151
+ host.sendToClients(withSessionId({ type: "status", status: "thinking" }, sessionId));
152
+ }
153
+ void runSession(host, {
154
+ text,
155
+ sessionId,
156
+ attachments,
157
+ providerId: requestedProviderId,
158
+ });
159
+ }
160
+ //# sourceMappingURL=run-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-session.js","sourceRoot":"","sources":["../../src/ws/run-session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import type { ServerResultMessage } from "@schlessera/brain-ui-sdk/protocol";
3
+ /**
4
+ * Persistence seam for session ownership + accounting. The ws coordinator only
5
+ * talks to this interface — swapping the store means implementing four
6
+ * methods, not editing the turn loop.
7
+ */
8
+ export interface SessionCatalog {
9
+ /** The provider/profile a stored session is pinned to, or null. */
10
+ getStoredProviderId(sessionId: string): string | null;
11
+ /** The backend that owns a stored session, or null for legacy/unknown rows. */
12
+ getStoredBackendId(sessionId: string): string | null;
13
+ /**
14
+ * Persist session ownership as soon as `session_info` names the session,
15
+ * with zero cost/turn deltas — the terminal `result` adds the accounting.
16
+ * Without this, a failed or cancelled first turn left the transcript
17
+ * unowned.
18
+ */
19
+ persistSessionStub(sessionId: string, promptText: string, providerId: string | null, backendId: string): void;
20
+ /** Persist session accounting on the terminal `result` frame. */
21
+ persistSession(msg: ServerResultMessage, promptText: string, providerId: string | null, backendId: string): void;
22
+ }
23
+ /** SQLite-backed catalog over the package's session database. */
24
+ export declare function createSessionCatalog(db?: () => Database): SessionCatalog;
25
+ //# sourceMappingURL=session-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-catalog.d.ts","sourceRoot":"","sources":["../../src/ws/session-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAG7E;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,mEAAmE;IACnE,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACtD,+EAA+E;IAC/E,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACrD;;;;;OAKG;IACH,kBAAkB,CAChB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,SAAS,EAAE,MAAM,GAChB,IAAI,CAAC;IACR,iEAAiE;IACjE,cAAc,CACZ,GAAG,EAAE,mBAAmB,EACxB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,SAAS,EAAE,MAAM,GAChB,IAAI,CAAC;CACT;AAWD,iEAAiE;AACjE,wBAAgB,oBAAoB,CAAC,EAAE,GAAE,MAAM,QAAgB,GAAG,cAAc,CAmD/E"}
@@ -0,0 +1,45 @@
1
+ import { getDb } from "../db/client.js";
2
+ const UPSERT_SESSION_SQL = `INSERT INTO sessions (id, title, created_at, last_active_at, total_cost_usd, num_turns, provider_id, backend_id)
3
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
4
+ ON CONFLICT(id) DO UPDATE SET
5
+ last_active_at = excluded.last_active_at,
6
+ total_cost_usd = total_cost_usd + excluded.total_cost_usd,
7
+ num_turns = num_turns + excluded.num_turns,
8
+ provider_id = COALESCE(excluded.provider_id, provider_id),
9
+ backend_id = COALESCE(excluded.backend_id, backend_id)`;
10
+ /** SQLite-backed catalog over the package's session database. */
11
+ export function createSessionCatalog(db = getDb) {
12
+ return {
13
+ getStoredProviderId(sessionId) {
14
+ const row = db()
15
+ .query("SELECT provider_id AS providerId FROM sessions WHERE id = ?")
16
+ .get(sessionId);
17
+ return row?.providerId ?? null;
18
+ },
19
+ getStoredBackendId(sessionId) {
20
+ const row = db()
21
+ .query("SELECT backend_id AS backendId FROM sessions WHERE id = ?")
22
+ .get(sessionId);
23
+ return row?.backendId ?? null;
24
+ },
25
+ persistSessionStub(sessionId, promptText, providerId, backendId) {
26
+ try {
27
+ db()
28
+ .prepare(UPSERT_SESSION_SQL)
29
+ .run(sessionId, promptText.slice(0, 100), Date.now(), Date.now(), 0, 0, providerId, backendId);
30
+ }
31
+ catch { }
32
+ },
33
+ persistSession(msg, promptText, providerId, backendId) {
34
+ try {
35
+ db()
36
+ .prepare(UPSERT_SESSION_SQL)
37
+ .run(msg.sessionId, promptText.slice(0, 100), Date.now(), Date.now(),
38
+ // costUsd is optional (rev 2): absent = unknown, accounted as 0.
39
+ msg.costUsd ?? 0, msg.numTurns, providerId, backendId);
40
+ }
41
+ catch { }
42
+ },
43
+ };
44
+ }
45
+ //# sourceMappingURL=session-catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-catalog.js","sourceRoot":"","sources":["../../src/ws/session-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAiCxC,MAAM,kBAAkB,GAAG;;;;;;;4DAOiC,CAAC;AAE7D,iEAAiE;AACjE,MAAM,UAAU,oBAAoB,CAAC,KAAqB,KAAK;IAC7D,OAAO;QACL,mBAAmB,CAAC,SAAS;YAC3B,MAAM,GAAG,GAAG,EAAE,EAAE;iBACb,KAAK,CAAC,6DAA6D,CAAC;iBACpE,GAAG,CAAC,SAAS,CAAyC,CAAC;YAC1D,OAAO,GAAG,EAAE,UAAU,IAAI,IAAI,CAAC;QACjC,CAAC;QAED,kBAAkB,CAAC,SAAS;YAC1B,MAAM,GAAG,GAAG,EAAE,EAAE;iBACb,KAAK,CAAC,2DAA2D,CAAC;iBAClE,GAAG,CAAC,SAAS,CAAwC,CAAC;YACzD,OAAO,GAAG,EAAE,SAAS,IAAI,IAAI,CAAC;QAChC,CAAC;QAED,kBAAkB,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS;YAC7D,IAAI,CAAC;gBACH,EAAE,EAAE;qBACD,OAAO,CAAC,kBAAkB,CAAC;qBAC3B,GAAG,CACF,SAAS,EACT,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EACxB,IAAI,CAAC,GAAG,EAAE,EACV,IAAI,CAAC,GAAG,EAAE,EACV,CAAC,EACD,CAAC,EACD,UAAU,EACV,SAAS,CACV,CAAC;YACN,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QACZ,CAAC;QAED,cAAc,CAAC,GAAG,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS;YACnD,IAAI,CAAC;gBACH,EAAE,EAAE;qBACD,OAAO,CAAC,kBAAkB,CAAC;qBAC3B,GAAG,CACF,GAAG,CAAC,SAAS,EACb,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EACxB,IAAI,CAAC,GAAG,EAAE,EACV,IAAI,CAAC,GAAG,EAAE;gBACV,iEAAiE;gBACjE,GAAG,CAAC,OAAO,IAAI,CAAC,EAChB,GAAG,CAAC,QAAQ,EACZ,UAAU,EACV,SAAS,CACV,CAAC;YACN,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QACZ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Size-bounding for outbound WebSocket frames.
3
+ *
4
+ * A single oversized frame — typically a multi-megabyte tool result (a big
5
+ * `Read`/`Bash`/`Grep` output), a tool_use with a large input, or a tool
6
+ * result whose content array holds thousands of blocks — can exceed the
7
+ * socket's per-frame limit, drop the connection, and (because the client
8
+ * reconnects and re-requests the same history) wedge into a reconnect loop.
9
+ *
10
+ * This helper bounds any JSON-serializable payload below
11
+ * {@link MAX_WS_MESSAGE_BYTES} by head-truncating long strings AND
12
+ * head-clipping long arrays, leaving small metadata (ids, types, statuses)
13
+ * untouched. Both axes matter: string truncation alone leaves the cap
14
+ * unenforced for a payload built of many short strings. The model already
15
+ * received the full tool output server-side — this only bounds what the
16
+ * browser renders.
17
+ *
18
+ * Ported from oh-my-pi's collab replication-shrink.
19
+ */
20
+ /** Per-frame ceiling for server → client messages. Well under Bun's 16 MB
21
+ * default and typical reverse-proxy WebSocket limits. */
22
+ export declare const MAX_WS_MESSAGE_BYTES = 512000;
23
+ /** Target byte size for one `session_history` chunk. Below the frame cap so a
24
+ * batch plus its wrapper stays comfortably under {@link MAX_WS_MESSAGE_BYTES}. */
25
+ export declare const HISTORY_CHUNK_BYTES = 400000;
26
+ /**
27
+ * Return `value` unchanged when its JSON serialization already fits
28
+ * {@link MAX_WS_MESSAGE_BYTES}; otherwise return a deep-cloned shadow shrunk
29
+ * along both string and array axes until it fits. The wire shape is preserved:
30
+ * only string leaves and array tails change — discriminator fields, ids, and
31
+ * other small metadata pass through untouched.
32
+ */
33
+ export declare function shrinkForReplication<T>(value: T): T;
34
+ //# sourceMappingURL=shrink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shrink.d.ts","sourceRoot":"","sources":["../../src/ws/shrink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;yDACyD;AACzD,eAAO,MAAM,oBAAoB,SAAU,CAAC;AAE5C;kFACkF;AAClF,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAqD3C;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAQnD"}