@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 @@
1
+ {"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../../src/ws/attachments.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAQ7E,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,OAAO,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,mBAAmB,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAiElF"}
@@ -0,0 +1,63 @@
1
+ import { ALLOWED_IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, MAX_IMAGES_PER_MESSAGE, MAX_TOTAL_IMAGE_BYTES, } from "@schlessera/brain-ui-sdk/protocol";
2
+ export function estimateDecodedBase64Bytes(data) {
3
+ const normalized = data.replace(/\s/g, "");
4
+ const padding = normalized.endsWith("==")
5
+ ? 2
6
+ : normalized.endsWith("=")
7
+ ? 1
8
+ : 0;
9
+ return Math.floor((normalized.length * 3) / 4) - padding;
10
+ }
11
+ export function validateAttachments(attachments) {
12
+ if (attachments === undefined) {
13
+ return { ok: true, attachments: [] };
14
+ }
15
+ if (!Array.isArray(attachments)) {
16
+ return { ok: false, reason: "Attachments must be an array" };
17
+ }
18
+ if (attachments.length > MAX_IMAGES_PER_MESSAGE) {
19
+ return {
20
+ ok: false,
21
+ reason: `Too many images attached; maximum is ${MAX_IMAGES_PER_MESSAGE}`,
22
+ };
23
+ }
24
+ let totalBytes = 0;
25
+ const validated = [];
26
+ for (const attachment of attachments) {
27
+ if (!attachment ||
28
+ typeof attachment !== "object" ||
29
+ typeof attachment.data !== "string") {
30
+ return { ok: false, reason: "Each attachment must include base64 image data" };
31
+ }
32
+ const data = attachment.data;
33
+ if (data.length === 0 ||
34
+ data.length % 4 !== 0 ||
35
+ !/^[A-Za-z0-9+/]+={0,2}$/.test(data)) {
36
+ return { ok: false, reason: "Attachment data is not valid base64" };
37
+ }
38
+ const mediaType = attachment.mediaType;
39
+ if (!ALLOWED_IMAGE_MEDIA_TYPES.includes(mediaType)) {
40
+ return {
41
+ ok: false,
42
+ reason: `Unsupported image type: ${String(mediaType)}`,
43
+ };
44
+ }
45
+ const bytes = estimateDecodedBase64Bytes(attachment.data);
46
+ if (bytes > MAX_IMAGE_BYTES) {
47
+ return {
48
+ ok: false,
49
+ reason: `Image is too large; maximum decoded size is ${MAX_IMAGE_BYTES} bytes`,
50
+ };
51
+ }
52
+ totalBytes += bytes;
53
+ if (totalBytes > MAX_TOTAL_IMAGE_BYTES) {
54
+ return {
55
+ ok: false,
56
+ reason: `Attached images are too large; maximum total decoded size is ${MAX_TOTAL_IMAGE_BYTES} bytes`,
57
+ };
58
+ }
59
+ validated.push(attachment);
60
+ }
61
+ return { ok: true, attachments: validated };
62
+ }
63
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachments.js","sourceRoot":"","sources":["../../src/ws/attachments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { BackendBridge } from "@schlessera/brain-ui-sdk/server";
2
+ import type { RunningTurn } from "./turns.js";
3
+ import type { WsHost } from "./host.js";
4
+ /** Build the per-turn bridge the backend drives. */
5
+ export declare function makeBridge(host: WsHost, turn: RunningTurn, promptText: string, backendId: string): BackendBridge;
6
+ export declare function emitTurnError(host: WsHost, turn: RunningTurn, err: unknown): void;
7
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/ws/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAId,MAAM,iCAAiC,CAAC;AAIzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,oDAAoD;AACpD,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,aAAa,CAgGf;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI,CAsBjF"}
@@ -0,0 +1,86 @@
1
+ import { BackendBusyError, BackendRequestError } from "@schlessera/brain-ui-sdk/server";
2
+ import { hasClients } from "./clients.js";
3
+ import { withTurnScope } from "./frames.js";
4
+ /** Build the per-turn bridge the backend drives. */
5
+ export function makeBridge(host, turn, promptText, backendId) {
6
+ const { coordinator, catalog } = host;
7
+ // Capture the turn identity at construction: the slot's turnId is re-minted
8
+ // for each queued follow-up, and a backend can still emit late frames
9
+ // through this bridge after its startTurn resolved. Stamping from the live
10
+ // field would attribute those to the NEXT turn.
11
+ const turnId = turn.turnId;
12
+ return {
13
+ emit: (msg) => {
14
+ if (msg.type === "session_info") {
15
+ turn.sessionId = msg.sessionId;
16
+ if (msg.providerId)
17
+ turn.providerId = msg.providerId;
18
+ coordinator.bySession.set(msg.sessionId, turn);
19
+ // Persist ownership the moment the identity exists — a turn that
20
+ // later fails or is cancelled must not leave an unowned transcript.
21
+ catalog.persistSessionStub(msg.sessionId, promptText, turn.providerId, backendId);
22
+ }
23
+ host.sendToClients(withTurnScope(msg, turn, turnId));
24
+ if (msg.type === "result") {
25
+ catalog.persistSession(msg, promptText, turn.providerId, backendId);
26
+ }
27
+ },
28
+ requestPermission: (req) => {
29
+ host.sendToClients(withTurnScope({
30
+ type: "tool_approval_request",
31
+ toolUseId: req.toolUseId,
32
+ toolName: req.toolName,
33
+ input: req.input,
34
+ description: req.description,
35
+ }, turn, turnId));
36
+ host.sendToClients(withTurnScope({ type: "status", status: "tool_executing", detail: `Waiting for approval: ${req.toolName}` }, turn, turnId));
37
+ return new Promise((resolve) => {
38
+ if (coordinator.collidesAcrossTurns(coordinator.pendingApprovals, req.toolUseId, turn)) {
39
+ resolve({ behavior: "deny", message: "Duplicate tool-approval id" });
40
+ return;
41
+ }
42
+ coordinator.pendingApprovals.set(req.toolUseId, { turn, turnId, resolve });
43
+ });
44
+ },
45
+ askUser: (requestId, questions) => {
46
+ host.sendToClients(withTurnScope({ type: "ask_user_request", requestId, questions }, turn, turnId));
47
+ host.sendToClients(withTurnScope({ type: "status", status: "tool_executing", detail: "Waiting for your input" }, turn, turnId));
48
+ return new Promise((resolve, reject) => {
49
+ if (coordinator.collidesAcrossTurns(coordinator.pendingAskUser, requestId, turn)) {
50
+ reject(new Error("Duplicate ask-user request id"));
51
+ return;
52
+ }
53
+ coordinator.pendingAskUser.set(requestId, { turn, turnId, resolve, reject });
54
+ });
55
+ },
56
+ getLocation: (options) => {
57
+ if (!hasClients()) {
58
+ return Promise.reject(new Error(`No ${host.appName} client is connected to read the location from.`));
59
+ }
60
+ const requestId = coordinator.nextLocationRequestId();
61
+ host.sendToClients(withTurnScope({ type: "location_request", requestId, options }, turn, turnId));
62
+ host.sendToClients(withTurnScope({ type: "status", status: "tool_executing", detail: "Requesting your location" }, turn, turnId));
63
+ return new Promise((resolve, reject) => {
64
+ if (coordinator.collidesAcrossTurns(coordinator.pendingLocation, requestId, turn)) {
65
+ reject(new Error("Duplicate location request id"));
66
+ return;
67
+ }
68
+ coordinator.pendingLocation.set(requestId, { turn, turnId, resolve, reject });
69
+ });
70
+ },
71
+ };
72
+ }
73
+ export function emitTurnError(host, turn, err) {
74
+ // startTurn resolves for runtime failures (it emits its own error frame); it
75
+ // only rejects for caller errors.
76
+ if (err instanceof BackendBusyError) {
77
+ host.sendToClients(withTurnScope({ type: "error", code: "SESSION_BUSY", message: "That session already has a running turn." }, turn));
78
+ }
79
+ else if (err instanceof BackendRequestError) {
80
+ host.sendToClients(withTurnScope({ type: "error", code: "BACKEND_REQUEST_ERROR", message: err.message }, turn));
81
+ }
82
+ else {
83
+ host.sendToClients(withTurnScope({ type: "error", code: "BACKEND_ERROR", message: err instanceof Error ? err.message : String(err) }, turn));
84
+ }
85
+ }
86
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../../src/ws/bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { ServerMessage } from "@schlessera/brain-ui-sdk/protocol";
2
+ /** Minimal structural view of a live socket — all we need to write to it. */
3
+ export type WSContext = {
4
+ send: (data: string) => void;
5
+ };
6
+ export declare function addClient(ws: WSContext): void;
7
+ export declare function removeClient(ws: WSContext): void;
8
+ export declare function clientCount(): number;
9
+ export declare function hasClients(): boolean;
10
+ /** Test-only: drop all registered sockets. */
11
+ export declare function resetClientsForTests(): void;
12
+ /** Serialize + size-bound a frame, then send it to one specific socket. */
13
+ export declare function sendTo(ws: WSContext, msg: ServerMessage): void;
14
+ /**
15
+ * Broadcast a frame to every attached client. Serializes once. A failing
16
+ * socket is skipped (its `onClose` will prune it) so one dead peer can't
17
+ * block delivery to the others.
18
+ */
19
+ export declare function broadcast(msg: ServerMessage): void;
20
+ //# sourceMappingURL=clients.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clients.d.ts","sourceRoot":"","sources":["../../src/ws/clients.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAGvE,6EAA6E;AAC7E,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,CAAC;AAWzD,wBAAgB,SAAS,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAE7C;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAEhD;AAED,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED,8CAA8C;AAC9C,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED,2EAA2E;AAC3E,wBAAgB,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAUlD"}
@@ -0,0 +1,48 @@
1
+ import { shrinkForReplication } from "./shrink.js";
2
+ /**
3
+ * Every currently-attached client socket. The active agent turn is a
4
+ * process-global singleton (server/src/ws/handler.ts), so all sockets observe
5
+ * the same conversation; streamed output fans out to all of them.
6
+ * Previously the server held a single `activeWs`, so a second connection
7
+ * silently orphaned the first (it stayed open but never received output).
8
+ */
9
+ const clients = new Set();
10
+ export function addClient(ws) {
11
+ clients.add(ws);
12
+ }
13
+ export function removeClient(ws) {
14
+ clients.delete(ws);
15
+ }
16
+ export function clientCount() {
17
+ return clients.size;
18
+ }
19
+ export function hasClients() {
20
+ return clients.size > 0;
21
+ }
22
+ /** Test-only: drop all registered sockets. */
23
+ export function resetClientsForTests() {
24
+ clients.clear();
25
+ }
26
+ /** Serialize + size-bound a frame, then send it to one specific socket. */
27
+ export function sendTo(ws, msg) {
28
+ ws.send(JSON.stringify(shrinkForReplication(msg)));
29
+ }
30
+ /**
31
+ * Broadcast a frame to every attached client. Serializes once. A failing
32
+ * socket is skipped (its `onClose` will prune it) so one dead peer can't
33
+ * block delivery to the others.
34
+ */
35
+ export function broadcast(msg) {
36
+ if (clients.size === 0)
37
+ return;
38
+ const payload = JSON.stringify(shrinkForReplication(msg));
39
+ for (const ws of clients) {
40
+ try {
41
+ ws.send(payload);
42
+ }
43
+ catch {
44
+ // Drop; the socket's onClose handler removes it from the set.
45
+ }
46
+ }
47
+ }
48
+ //# sourceMappingURL=clients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clients.js","sourceRoot":"","sources":["../../src/ws/clients.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAKnD;;;;;;GAMG;AACH,MAAM,OAAO,GAAG,IAAI,GAAG,EAAa,CAAC;AAErC,MAAM,UAAU,SAAS,CAAC,EAAa;IACrC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAa;IACxC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,oBAAoB;IAClC,OAAO,CAAC,KAAK,EAAE,CAAC;AAClB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,MAAM,CAAC,EAAa,EAAE,GAAkB;IACtD,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAkB;IAC1C,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO;IAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACP,8DAA8D;QAChE,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { websocket } from "hono/bun";
2
+ import type { WsHost } from "./host.js";
3
+ export { websocket };
4
+ /** Build the Hono WebSocket upgrade handler bound to one host. */
5
+ export declare function createWsUpgrade(host: WsHost): import("hono").MiddlewareHandler<any, string, {
6
+ outputFormat: "ws";
7
+ }>;
8
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/ws/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,SAAS,EAAE,MAAM,UAAU,CAAC;AAQvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM;;GAsH3C"}
@@ -0,0 +1,116 @@
1
+ import { upgradeWebSocket, websocket } from "hono/bun";
2
+ import { PROTOCOL_REV } from "@schlessera/brain-ui-sdk/protocol";
3
+ import { parseClientMessage } from "@schlessera/brain-ui-sdk/schemas";
4
+ import { getBackendForSession } from "../agent/backend.js";
5
+ import { addClient, removeClient, hasClients } from "./clients.js";
6
+ import { withTurnScope } from "./frames.js";
7
+ import { sendSessionHistory } from "./history.js";
8
+ import { handleClientMessage } from "./dispatch.js";
9
+ export { websocket };
10
+ /** Build the Hono WebSocket upgrade handler bound to one host. */
11
+ export function createWsUpgrade(host) {
12
+ return upgradeWebSocket(() => ({
13
+ async onOpen(_evt, ws) {
14
+ console.log("[ws] Client connected");
15
+ const { coordinator, catalog } = host;
16
+ // Handshake first (rev 2, additive): protocol revision + coarse
17
+ // capabilities, so the client can gate behavior instead of sniffing.
18
+ host.sendMessage(ws, {
19
+ type: "server_hello",
20
+ protocolRev: PROTOCOL_REV,
21
+ capabilities: { multiSession: true, askUser: true, location: true },
22
+ });
23
+ // Snapshot-on-connect only for the single-running-session case (backward
24
+ // compatible). With zero or several running sessions the client rehydrates
25
+ // itself per-session via session_resume, and live frames (sessionId-scoped)
26
+ // fan out to it once it joins the broadcast set.
27
+ const runningTurns = [...coordinator.running].filter((t) => t.sessionId);
28
+ if (runningTurns.length === 1) {
29
+ const turn = runningTurns[0];
30
+ const sid = turn.sessionId;
31
+ host.sendMessage(ws,
32
+ // Stamp the live turn identity: a reconnecting client must learn the
33
+ // current turnId, or the correlation guarantee dies at the reconnect
34
+ // it exists for.
35
+ withTurnScope({
36
+ type: "session_info",
37
+ sessionId: sid,
38
+ isNew: false,
39
+ providerId: turn.providerId ?? catalog.getStoredProviderId(sid) ?? undefined,
40
+ }, turn));
41
+ try {
42
+ const backend = await getBackendForSession(catalog.getStoredBackendId(sid) ?? turn.backend.id);
43
+ const history = await backend.getHistory(sid);
44
+ if (history.length > 0)
45
+ sendSessionHistory(ws, sid, history);
46
+ }
47
+ catch (err) {
48
+ console.error("[ws] snapshot-on-connect failed:", err);
49
+ }
50
+ finally {
51
+ addClient(ws);
52
+ host.sendMessage(ws, withTurnScope({
53
+ type: "status",
54
+ status: "thinking",
55
+ detail: "Session in progress",
56
+ sessionId: sid,
57
+ }, turn));
58
+ }
59
+ return;
60
+ }
61
+ addClient(ws);
62
+ host.sendMessage(ws, {
63
+ type: "status",
64
+ status: "idle",
65
+ detail: `Connected to ${host.appName}`,
66
+ });
67
+ },
68
+ onMessage(evt, ws) {
69
+ // Boundary validation (rev 2): byte cap + JSON decode + schema, in one
70
+ // place. No more casting client JSON to ClientMessage.
71
+ //
72
+ // Text frames only: hono's Bun adapter hands binary frames over as the
73
+ // underlying POOLED ArrayBuffer (byteOffset/byteLength discarded), so a
74
+ // binary frame cannot be decoded correctly here. The protocol is JSON
75
+ // text; reject anything else rather than parse a slab.
76
+ const raw = evt.data;
77
+ if (typeof raw !== "string") {
78
+ host.sendMessage(ws, {
79
+ type: "error",
80
+ code: "PARSE_ERROR",
81
+ message: "Binary frames are not supported; send JSON text",
82
+ });
83
+ return;
84
+ }
85
+ const parsed = parseClientMessage(raw);
86
+ if (!parsed.ok) {
87
+ host.sendMessage(ws, { type: "error", code: "PARSE_ERROR", message: parsed.error });
88
+ return;
89
+ }
90
+ // handleClientMessage is async — a rejection must not escape as an
91
+ // unhandled rejection with no frame sent.
92
+ void Promise.resolve()
93
+ .then(() => handleClientMessage(host, ws, parsed.message))
94
+ .catch(() => {
95
+ host.sendMessage(ws, {
96
+ type: "error",
97
+ code: "INTERNAL_ERROR",
98
+ message: "Failed to handle message",
99
+ });
100
+ });
101
+ },
102
+ onClose(_evt, ws) {
103
+ console.log("[ws] Client disconnected");
104
+ removeClient(ws);
105
+ // Turns keep running in the background. Only reject pending interactive
106
+ // requests once the LAST client leaves — while another client remains it
107
+ // can still answer them.
108
+ if (hasClients())
109
+ return;
110
+ for (const turn of host.coordinator.running) {
111
+ host.coordinator.drainPendingForTurn(turn, "Client disconnected");
112
+ }
113
+ },
114
+ }));
115
+ }
116
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/ws/connection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { ClientMessage } from "@schlessera/brain-ui-sdk/protocol";
2
+ import type { WSContext } from "./clients.js";
3
+ import type { WsHost } from "./host.js";
4
+ export declare function handleClientMessage(host: WsHost, ws: WSContext, msg: ClientMessage): Promise<void>;
5
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/ws/dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAEvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAK9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAYxC,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,IAAI,CAAC,CA0If"}
@@ -0,0 +1,139 @@
1
+ import { getBackendForSession } from "../agent/backend.js";
2
+ import { locationErrorText } from "./frames.js";
3
+ import { sendSessionHistory } from "./history.js";
4
+ import { validateAttachments } from "./attachments.js";
5
+ import { handleChatMessage } from "./run-session.js";
6
+ /**
7
+ * A client MAY echo the request's turnId (rev 2). When it does, it must match
8
+ * the turn that raised the request — a stale echo from before a reconnect or
9
+ * a follow-up would otherwise resolve the wrong turn's pending promise.
10
+ * Absent turnId stays valid: the field is optional on the wire.
11
+ */
12
+ function turnIdMatches(pending, echoed) {
13
+ return echoed === undefined || echoed === pending.turnId;
14
+ }
15
+ export async function handleClientMessage(host, ws, msg) {
16
+ const { coordinator, catalog } = host;
17
+ switch (msg.type) {
18
+ case "chat_message": {
19
+ const attachmentResult = validateAttachments(msg.attachments);
20
+ if (!attachmentResult.ok) {
21
+ host.sendMessage(ws, {
22
+ type: "error",
23
+ code: "ATTACHMENT_REJECTED",
24
+ message: attachmentResult.reason,
25
+ ...(msg.sessionId ? { sessionId: msg.sessionId } : {}),
26
+ });
27
+ return;
28
+ }
29
+ await handleChatMessage(host, ws, msg.text, msg.sessionId, attachmentResult.attachments, msg.providerId);
30
+ break;
31
+ }
32
+ case "ask_user_response": {
33
+ const pending = coordinator.pendingAskUser.get(msg.requestId);
34
+ if (pending && turnIdMatches(pending, msg.turnId)) {
35
+ coordinator.pendingAskUser.delete(msg.requestId);
36
+ pending.resolve({ answers: msg.answers, annotations: msg.annotations });
37
+ }
38
+ break;
39
+ }
40
+ case "ask_user_cancel": {
41
+ const pending = coordinator.pendingAskUser.get(msg.requestId);
42
+ if (pending && turnIdMatches(pending, msg.turnId)) {
43
+ coordinator.pendingAskUser.delete(msg.requestId);
44
+ pending.reject(new Error(msg.reason || "User cancelled the question"));
45
+ }
46
+ break;
47
+ }
48
+ case "location_response": {
49
+ const pending = coordinator.pendingLocation.get(msg.requestId);
50
+ if (pending && turnIdMatches(pending, msg.turnId)) {
51
+ coordinator.pendingLocation.delete(msg.requestId);
52
+ pending.resolve({ coords: msg.coords, timestamp: msg.timestamp });
53
+ }
54
+ break;
55
+ }
56
+ case "location_error": {
57
+ const pending = coordinator.pendingLocation.get(msg.requestId);
58
+ if (pending && turnIdMatches(pending, msg.turnId)) {
59
+ coordinator.pendingLocation.delete(msg.requestId);
60
+ pending.reject(new Error(locationErrorText(msg.code, msg.message)));
61
+ }
62
+ break;
63
+ }
64
+ case "tool_approval": {
65
+ const pending = coordinator.pendingApprovals.get(msg.toolUseId);
66
+ if (pending && turnIdMatches(pending, msg.turnId)) {
67
+ coordinator.pendingApprovals.delete(msg.toolUseId);
68
+ pending.resolve(msg.updatedInput
69
+ ? { behavior: "allow", updatedInput: msg.updatedInput }
70
+ : { behavior: "allow" });
71
+ }
72
+ break;
73
+ }
74
+ case "tool_denial": {
75
+ const pending = coordinator.pendingApprovals.get(msg.toolUseId);
76
+ if (pending && turnIdMatches(pending, msg.turnId)) {
77
+ coordinator.pendingApprovals.delete(msg.toolUseId);
78
+ pending.resolve({ behavior: "deny", message: msg.message });
79
+ }
80
+ break;
81
+ }
82
+ case "cancel": {
83
+ if (msg.sessionId) {
84
+ const turn = coordinator.bySession.get(msg.sessionId);
85
+ if (turn)
86
+ coordinator.cancelTurn(turn, "Cancelled by user");
87
+ return;
88
+ }
89
+ // No sessionId: cancel the sole running session; ambiguous if several run.
90
+ if (coordinator.running.size === 0)
91
+ return;
92
+ if (coordinator.running.size > 1) {
93
+ host.sendMessage(ws, {
94
+ type: "error",
95
+ code: "CANCEL_AMBIGUOUS",
96
+ message: "Several sessions are running — specify which to cancel.",
97
+ });
98
+ return;
99
+ }
100
+ coordinator.cancelTurn([...coordinator.running][0], "Cancelled by user");
101
+ break;
102
+ }
103
+ case "session_resume": {
104
+ host.sendMessage(ws, {
105
+ type: "session_info",
106
+ sessionId: msg.sessionId,
107
+ isNew: false,
108
+ providerId: catalog.getStoredProviderId(msg.sessionId) ?? undefined,
109
+ });
110
+ try {
111
+ const backend = await getBackendForSession(catalog.getStoredBackendId(msg.sessionId));
112
+ const messages = await backend.getHistory(msg.sessionId);
113
+ sendSessionHistory(ws, msg.sessionId, messages);
114
+ // A resume of a RUNNING session (reattach) must not report idle: idle
115
+ // would clear the client's running badge and finish its streaming
116
+ // message mid-turn. Mirror the snapshot-on-connect status instead.
117
+ const runningTurn = coordinator.bySession.get(msg.sessionId);
118
+ host.sendMessage(ws, {
119
+ type: "status",
120
+ ...(runningTurn
121
+ ? { status: "thinking", detail: "Session in progress" }
122
+ : { status: "idle", detail: "Session loaded" }),
123
+ sessionId: msg.sessionId,
124
+ ...(runningTurn ? { turnId: runningTurn.turnId } : {}),
125
+ });
126
+ }
127
+ catch (err) {
128
+ host.sendMessage(ws, {
129
+ type: "error",
130
+ code: "SESSION_LOAD_ERROR",
131
+ message: err instanceof Error ? err.message : "Failed to load session",
132
+ sessionId: msg.sessionId,
133
+ });
134
+ }
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../src/ws/dispatch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { ServerMessage } from "@schlessera/brain-ui-sdk/protocol";
2
+ import type { RunningTurn } from "./turns.js";
3
+ /**
4
+ * Stamp a session-scoped frame with a sessionId for client demux. Every
5
+ * ServerMessage is session-scoped (or carries its own sessionId), so this only
6
+ * fills in a missing id and never overwrites one the backend already set.
7
+ */
8
+ export declare function withSessionId(msg: ServerMessage, sessionId: string | null): ServerMessage;
9
+ /**
10
+ * Stamp a frame with the turn's session AND turn identity (rev 2). turnId lets
11
+ * the client correlate interactive round-trips (approvals / ask-user /
12
+ * location) with the exact turn that raised them, even across a reconnect.
13
+ */
14
+ export declare function withTurnScope(msg: ServerMessage, turn: RunningTurn, turnId?: string): ServerMessage;
15
+ /** Human-readable reason for a GeolocationPositionError code. */
16
+ export declare function locationErrorText(code: number, fallback: string): string;
17
+ //# sourceMappingURL=frames.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.d.ts","sourceRoot":"","sources":["../../src/ws/frames.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,aAAa,CAIzF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,WAAW,EACjB,MAAM,GAAE,MAAoB,GAC3B,aAAa,CAIf;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWxE"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Stamp a session-scoped frame with a sessionId for client demux. Every
3
+ * ServerMessage is session-scoped (or carries its own sessionId), so this only
4
+ * fills in a missing id and never overwrites one the backend already set.
5
+ */
6
+ export function withSessionId(msg, sessionId) {
7
+ if (!sessionId)
8
+ return msg;
9
+ if (msg.sessionId)
10
+ return msg;
11
+ return { ...msg, sessionId };
12
+ }
13
+ /**
14
+ * Stamp a frame with the turn's session AND turn identity (rev 2). turnId lets
15
+ * the client correlate interactive round-trips (approvals / ask-user /
16
+ * location) with the exact turn that raised them, even across a reconnect.
17
+ */
18
+ export function withTurnScope(msg, turn, turnId = turn.turnId) {
19
+ const scoped = withSessionId(msg, turn.sessionId);
20
+ if (scoped.turnId)
21
+ return scoped;
22
+ return { ...scoped, turnId };
23
+ }
24
+ /** Human-readable reason for a GeolocationPositionError code. */
25
+ export function locationErrorText(code, fallback) {
26
+ switch (code) {
27
+ case 1:
28
+ return "The user denied permission to share their location.";
29
+ case 2:
30
+ return "The user's location is currently unavailable.";
31
+ case 3:
32
+ return "Timed out while retrieving the user's location.";
33
+ default:
34
+ return fallback || "Failed to retrieve the user's location.";
35
+ }
36
+ }
37
+ //# sourceMappingURL=frames.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/ws/frames.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,GAAkB,EAAE,SAAwB;IACxE,IAAI,CAAC,SAAS;QAAE,OAAO,GAAG,CAAC;IAC3B,IAAK,GAA8B,CAAC,SAAS;QAAE,OAAO,GAAG,CAAC;IAC1D,OAAO,EAAE,GAAG,GAAG,EAAE,SAAS,EAAmB,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,GAAkB,EAClB,IAAiB,EACjB,SAAiB,IAAI,CAAC,MAAM;IAE5B,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAClD,IAAK,MAA8B,CAAC,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1D,OAAO,EAAE,GAAG,MAAM,EAAE,MAAM,EAAmB,CAAC;AAChD,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,QAAgB;IAC9D,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,CAAC;YACJ,OAAO,qDAAqD,CAAC;QAC/D,KAAK,CAAC;YACJ,OAAO,+CAA+C,CAAC;QACzD,KAAK,CAAC;YACJ,OAAO,iDAAiD,CAAC;QAC3D;YACE,OAAO,QAAQ,IAAI,yCAAyC,CAAC;IACjE,CAAC;AACH,CAAC"}