@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,91 @@
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 const MAX_WS_MESSAGE_BYTES = 512_000;
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 const HISTORY_CHUNK_BYTES = 400_000;
26
+ /**
27
+ * Progressive shrink passes. Each tightens both the per-string cap and the
28
+ * per-array head limit; the loop stops at the first pass whose output fits
29
+ * {@link MAX_WS_MESSAGE_BYTES}. The final pass clamps every string to 64 B and
30
+ * every array to one element, so even pathological mixes converge.
31
+ */
32
+ const SHRINK_PASSES = [
33
+ { stringCap: 64 * 1024, arrayLimit: 256 },
34
+ { stringCap: 16 * 1024, arrayLimit: 128 },
35
+ { stringCap: 4 * 1024, arrayLimit: 64 },
36
+ { stringCap: 1 * 1024, arrayLimit: 32 },
37
+ { stringCap: 256, arrayLimit: 16 },
38
+ { stringCap: 256, arrayLimit: 4 },
39
+ { stringCap: 64, arrayLimit: 1 },
40
+ ];
41
+ const STRING_ELISION_RESERVE = 80;
42
+ /**
43
+ * Recursively walk `value`, head-truncating any string longer than
44
+ * `stringCap` and head-clipping any array longer than `arrayLimit`. Returns a
45
+ * freshly built deep clone; short strings/numbers/booleans pass through.
46
+ */
47
+ function shrinkWalk(value, stringCap, arrayLimit) {
48
+ if (typeof value === "string") {
49
+ if (value.length <= stringCap)
50
+ return value;
51
+ const headLen = Math.max(0, stringCap - STRING_ELISION_RESERVE);
52
+ return `${value.slice(0, headLen)}\n…[${value.length - headLen} chars elided]`;
53
+ }
54
+ if (Array.isArray(value)) {
55
+ const keep = Math.min(value.length, arrayLimit);
56
+ const elided = value.length - keep;
57
+ const out = new Array(elided > 0 ? keep + 1 : keep);
58
+ for (let i = 0; i < keep; i++)
59
+ out[i] = shrinkWalk(value[i], stringCap, arrayLimit);
60
+ if (elided > 0)
61
+ out[keep] = `…[${elided} items elided]`;
62
+ return out;
63
+ }
64
+ if (value && typeof value === "object") {
65
+ const src = value;
66
+ const out = {};
67
+ for (const k in src)
68
+ out[k] = shrinkWalk(src[k], stringCap, arrayLimit);
69
+ return out;
70
+ }
71
+ return value;
72
+ }
73
+ /**
74
+ * Return `value` unchanged when its JSON serialization already fits
75
+ * {@link MAX_WS_MESSAGE_BYTES}; otherwise return a deep-cloned shadow shrunk
76
+ * along both string and array axes until it fits. The wire shape is preserved:
77
+ * only string leaves and array tails change — discriminator fields, ids, and
78
+ * other small metadata pass through untouched.
79
+ */
80
+ export function shrinkForReplication(value) {
81
+ if (JSON.stringify(value).length <= MAX_WS_MESSAGE_BYTES)
82
+ return value;
83
+ let shrunk = value;
84
+ for (const pass of SHRINK_PASSES) {
85
+ shrunk = shrinkWalk(value, pass.stringCap, pass.arrayLimit);
86
+ if (JSON.stringify(shrunk).length <= MAX_WS_MESSAGE_BYTES)
87
+ return shrunk;
88
+ }
89
+ return shrunk;
90
+ }
91
+ //# sourceMappingURL=shrink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shrink.js","sourceRoot":"","sources":["../../src/ws/shrink.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import type { AgentBackend, PermissionDecision, AskUserResult, LocationFix } from "@schlessera/brain-ui-sdk/server";
2
+ import type { ChatImageAttachment } from "@schlessera/brain-ui-sdk/protocol";
3
+ export interface QueuedFollowUp {
4
+ text: string;
5
+ attachments: ChatImageAttachment[];
6
+ }
7
+ /**
8
+ * One RUNNING session slot. `abortController`/`timeoutHandle` belong to the
9
+ * turn currently executing; a queued follow-up runs as the next turn in the
10
+ * same slot (same sessionId) after the current one resolves.
11
+ */
12
+ export interface RunningTurn {
13
+ sessionId: string | null;
14
+ /** Host-minted id of the CURRENT turn in this slot; re-minted per queued follow-up. */
15
+ turnId: string;
16
+ providerId: string | null;
17
+ backend: AgentBackend;
18
+ abortController: AbortController;
19
+ timeoutHandle: ReturnType<typeof setTimeout>;
20
+ queue: QueuedFollowUp[];
21
+ cancelled: boolean;
22
+ }
23
+ export interface PendingApproval {
24
+ turn: RunningTurn;
25
+ turnId: string;
26
+ resolve: (decision: PermissionDecision) => void;
27
+ }
28
+ export interface PendingAskUser {
29
+ turn: RunningTurn;
30
+ turnId: string;
31
+ resolve: (result: AskUserResult) => void;
32
+ reject: (err: Error) => void;
33
+ }
34
+ export interface PendingLocation {
35
+ turn: RunningTurn;
36
+ turnId: string;
37
+ resolve: (result: LocationFix) => void;
38
+ reject: (err: Error) => void;
39
+ }
40
+ /**
41
+ * Mutable turn state for one ws host: the running session slots and every
42
+ * pending interactive round-trip. This used to be six module-level globals
43
+ * inside a 976-line handler; as instance state it is testable and owned by an
44
+ * explicit object with a defined lifecycle.
45
+ */
46
+ export declare class TurnCoordinator {
47
+ readonly running: Set<RunningTurn>;
48
+ readonly bySession: Map<string, RunningTurn>;
49
+ startingSessions: number;
50
+ readonly pendingApprovals: Map<string, PendingApproval>;
51
+ readonly pendingAskUser: Map<string, PendingAskUser>;
52
+ readonly pendingLocation: Map<string, PendingLocation>;
53
+ private locationCounter;
54
+ nextLocationRequestId(): string;
55
+ /** True while any session has a running turn. */
56
+ isTurnActive(): boolean;
57
+ /** Cancel every running turn (used on shutdown). */
58
+ cancelAll(reason: string): boolean;
59
+ /** Cancel a session's current turn and drop its queued follow-ups. */
60
+ cancelTurn(turn: RunningTurn, reason: string): void;
61
+ /** Resolve/reject every pending interactive request belonging to one turn. */
62
+ drainPendingForTurn(turn: RunningTurn, reason: string): void;
63
+ /**
64
+ * Interactive-request ids (tool-approval / ask-user / location) must be
65
+ * globally unique so an inbound response resolves the correct turn's
66
+ * promise. The current backends mint unique ids (SDK tool-use ids,
67
+ * crypto.randomUUID, a host counter), so a collision across live turns means
68
+ * a future backend broke that invariant — fail the new request loudly rather
69
+ * than silently overwriting (and thus resolving) another turn's entry.
70
+ */
71
+ collidesAcrossTurns<T extends {
72
+ turn: RunningTurn;
73
+ }>(map: Map<string, T>, id: string, turn: RunningTurn): boolean;
74
+ /** Test seam: abort everything and clear all turn/pending state. */
75
+ reset(): void;
76
+ }
77
+ //# sourceMappingURL=turns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turns.d.ts","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,WAAW,EACZ,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAE7E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,mBAAmB,EAAE,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uFAAuF;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,EAAE,eAAe,CAAC;IACjC,aAAa,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC7C,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;CACpB;AAKD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAC9B;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,QAAQ,CAAC,OAAO,mBAA0B;IAC1C,QAAQ,CAAC,SAAS,2BAAkC;IACpD,gBAAgB,SAAK;IAErB,QAAQ,CAAC,gBAAgB,+BAAsC;IAC/D,QAAQ,CAAC,cAAc,8BAAqC;IAC5D,QAAQ,CAAC,eAAe,+BAAsC;IAE9D,OAAO,CAAC,eAAe,CAAK;IAE5B,qBAAqB,IAAI,MAAM;IAI/B,iDAAiD;IACjD,YAAY,IAAI,OAAO;IAIvB,oDAAoD;IACpD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAMlC,sEAAsE;IACtE,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAQnD,8EAA8E;IAC9E,mBAAmB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAkB5D;;;;;;;OAOG;IACH,mBAAmB,CAAC,CAAC,SAAS;QAAE,IAAI,EAAE,WAAW,CAAA;KAAE,EACjD,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EACnB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,WAAW,GAChB,OAAO;IAWV,oEAAoE;IACpE,KAAK,IAAI,IAAI;CAYd"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Mutable turn state for one ws host: the running session slots and every
3
+ * pending interactive round-trip. This used to be six module-level globals
4
+ * inside a 976-line handler; as instance state it is testable and owned by an
5
+ * explicit object with a defined lifecycle.
6
+ */
7
+ export class TurnCoordinator {
8
+ running = new Set();
9
+ bySession = new Map();
10
+ startingSessions = 0;
11
+ pendingApprovals = new Map();
12
+ pendingAskUser = new Map();
13
+ pendingLocation = new Map();
14
+ locationCounter = 0;
15
+ nextLocationRequestId() {
16
+ return `loc-${Date.now().toString(36)}-${++this.locationCounter}`;
17
+ }
18
+ /** True while any session has a running turn. */
19
+ isTurnActive() {
20
+ return this.running.size > 0;
21
+ }
22
+ /** Cancel every running turn (used on shutdown). */
23
+ cancelAll(reason) {
24
+ if (this.running.size === 0)
25
+ return false;
26
+ for (const turn of [...this.running])
27
+ this.cancelTurn(turn, reason);
28
+ return true;
29
+ }
30
+ /** Cancel a session's current turn and drop its queued follow-ups. */
31
+ cancelTurn(turn, reason) {
32
+ turn.cancelled = true;
33
+ turn.queue.length = 0;
34
+ clearTimeout(turn.timeoutHandle);
35
+ turn.abortController.abort();
36
+ this.drainPendingForTurn(turn, reason);
37
+ }
38
+ /** Resolve/reject every pending interactive request belonging to one turn. */
39
+ drainPendingForTurn(turn, reason) {
40
+ for (const [id, p] of this.pendingApprovals) {
41
+ if (p.turn !== turn)
42
+ continue;
43
+ p.resolve({ behavior: "deny", message: reason });
44
+ this.pendingApprovals.delete(id);
45
+ }
46
+ for (const [id, p] of this.pendingAskUser) {
47
+ if (p.turn !== turn)
48
+ continue;
49
+ p.reject(new Error(reason));
50
+ this.pendingAskUser.delete(id);
51
+ }
52
+ for (const [id, p] of this.pendingLocation) {
53
+ if (p.turn !== turn)
54
+ continue;
55
+ p.reject(new Error(reason));
56
+ this.pendingLocation.delete(id);
57
+ }
58
+ }
59
+ /**
60
+ * Interactive-request ids (tool-approval / ask-user / location) must be
61
+ * globally unique so an inbound response resolves the correct turn's
62
+ * promise. The current backends mint unique ids (SDK tool-use ids,
63
+ * crypto.randomUUID, a host counter), so a collision across live turns means
64
+ * a future backend broke that invariant — fail the new request loudly rather
65
+ * than silently overwriting (and thus resolving) another turn's entry.
66
+ */
67
+ collidesAcrossTurns(map, id, turn) {
68
+ const existing = map.get(id);
69
+ if (existing && existing.turn !== turn) {
70
+ console.error(`[ws] interactive id collision: "${id}" is already pending for another turn`);
71
+ return true;
72
+ }
73
+ return false;
74
+ }
75
+ /** Test seam: abort everything and clear all turn/pending state. */
76
+ reset() {
77
+ for (const turn of [...this.running]) {
78
+ clearTimeout(turn.timeoutHandle);
79
+ turn.abortController.abort();
80
+ }
81
+ this.running.clear();
82
+ this.bySession.clear();
83
+ this.pendingApprovals.clear();
84
+ this.pendingAskUser.clear();
85
+ this.pendingLocation.clear();
86
+ this.startingSessions = 0;
87
+ }
88
+ }
89
+ //# sourceMappingURL=turns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turns.js","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ -- Chat session metadata (Claude SDK stores actual messages in JSONL)
2
+ CREATE TABLE IF NOT EXISTS sessions (
3
+ id TEXT PRIMARY KEY,
4
+ title TEXT,
5
+ created_at INTEGER NOT NULL,
6
+ last_active_at INTEGER NOT NULL,
7
+ total_cost_usd REAL DEFAULT 0,
8
+ num_turns INTEGER DEFAULT 0
9
+ );
10
+
11
+ -- Cron job run history
12
+ CREATE TABLE IF NOT EXISTS cron_runs (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ job_name TEXT NOT NULL,
15
+ started_at INTEGER NOT NULL,
16
+ finished_at INTEGER,
17
+ status TEXT NOT NULL CHECK (status IN ('running', 'success', 'error')),
18
+ error_message TEXT,
19
+ duration_ms INTEGER
20
+ );
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_cron_runs_job ON cron_runs(job_name, started_at DESC);
23
+
24
+ -- Migrations tracking
25
+ CREATE TABLE IF NOT EXISTS _migrations (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ filename TEXT NOT NULL UNIQUE,
28
+ applied_at INTEGER NOT NULL
29
+ );
@@ -0,0 +1 @@
1
+ ALTER TABLE sessions ADD COLUMN provider_id TEXT;
@@ -0,0 +1,5 @@
1
+ -- Which AgentBackend owns a session's transcript (e.g. "claude" | "pi").
2
+ -- One backend is active per deployment in v1, but the column is added now so a
3
+ -- later per-session-backend change needs no migration. Populated on session
4
+ -- creation from the active backend id.
5
+ ALTER TABLE sessions ADD COLUMN backend_id TEXT;
@@ -0,0 +1,20 @@
1
+ -- Phase-5 hardening: the native Gemini backend was removed. Reconcile sessions
2
+ -- that were pinned to it so they resume cleanly on the default (Claude) backend
3
+ -- instead of dangling on a backend id that no longer resolves.
4
+ --
5
+ -- Rollback note: this rewrites data. Redeploying a Gemini-aware image does NOT
6
+ -- restore these ids; a rollback that needs them must reinstate them separately.
7
+
8
+ -- Clear the profile pin for any session that ran on the removed backend — those
9
+ -- profile ids do not exist under the default backend.
10
+ UPDATE sessions SET provider_id = NULL WHERE backend_id = 'gemini';
11
+
12
+ -- Point those sessions at the default backend (getBackendForSession resolves a
13
+ -- NULL/unknown backend_id to the default).
14
+ UPDATE sessions SET backend_id = NULL WHERE backend_id = 'gemini';
15
+
16
+ -- Migrate the pre-extraction Claude profile id to its renamed, byte-identical
17
+ -- successor so those sessions keep their pin instead of silently dropping to the
18
+ -- default model.
19
+ UPDATE sessions SET provider_id = 'claude-sonnet-4-6'
20
+ WHERE provider_id = 'anthropic-sonnet';
@@ -0,0 +1,18 @@
1
+ -- WebAuthn passkey credentials (single-user app: no user_id column).
2
+ -- rp_id scopes each credential to the relying party it was registered on
3
+ -- (localhost dev credentials and prod-domain credentials coexist).
4
+ CREATE TABLE passkey_credentials (
5
+ id TEXT PRIMARY KEY, -- base64url credential id
6
+ public_key BLOB NOT NULL, -- COSE public key bytes
7
+ counter INTEGER NOT NULL DEFAULT 0, -- signature counter (0 forever for cloud passkeys)
8
+ transports TEXT, -- JSON array, e.g. ["internal","hybrid"], or NULL
9
+ rp_id TEXT NOT NULL,
10
+ aaguid TEXT,
11
+ device_type TEXT, -- 'singleDevice' | 'multiDevice'
12
+ backed_up INTEGER NOT NULL DEFAULT 0,
13
+ label TEXT NOT NULL DEFAULT '',
14
+ created_at INTEGER NOT NULL,
15
+ last_used_at INTEGER
16
+ );
17
+
18
+ CREATE INDEX idx_passkey_credentials_rp_id ON passkey_credentials(rp_id);
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@schlessera/brain-ui-server",
3
+ "version": "0.4.0",
4
+ "description": "brain-kit chat-UI server: Hono app factory, WebSocket turn coordinator, auth (password/passkeys/tailscale/proxy), session catalog, and brain/files/voice routes",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "brain-kit",
8
+ "server",
9
+ "hono",
10
+ "websocket",
11
+ "ai-agents"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/schlessera/brain-kit.git",
16
+ "directory": "packages/ui-server"
17
+ },
18
+ "homepage": "https://github.com/schlessera/brain-kit",
19
+ "bugs": {
20
+ "url": "https://github.com/schlessera/brain-kit/issues"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "engines": {
26
+ "bun": ">=1.3.0"
27
+ },
28
+ "sideEffects": false,
29
+ "type": "module",
30
+ "exports": {
31
+ ".": {
32
+ "bun": "./src/index.ts",
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "src",
39
+ "dist",
40
+ "migrations",
41
+ "README.md"
42
+ ],
43
+ "scripts": {
44
+ "test": "bun test tests",
45
+ "prepublishOnly": "bun ../../scripts/check-dist.ts"
46
+ },
47
+ "dependencies": {
48
+ "@schlessera/brain-backend-claude": "0.4.0",
49
+ "@schlessera/brain-ui-sdk": "0.4.0",
50
+ "@simplewebauthn/server": "^13.3.2",
51
+ "hono": "^4",
52
+ "ignore": "^7.0.5",
53
+ "marked": "^18.0.4",
54
+ "zod": "^4.4.3"
55
+ },
56
+ "peerDependencies": {
57
+ "@schlessera/brain-backend-pi": "0.4.0",
58
+ "@types/bun": ">=1.3.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@schlessera/brain-backend-pi": {
62
+ "optional": true
63
+ },
64
+ "@types/bun": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "@types/bun": "^1.3.14",
70
+ "typescript": "^6.0.3"
71
+ }
72
+ }