@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,151 @@
1
+ import type {
2
+ AgentBackend,
3
+ PermissionDecision,
4
+ AskUserResult,
5
+ LocationFix,
6
+ } from "@schlessera/brain-ui-sdk/server";
7
+ import type { ChatImageAttachment } from "@schlessera/brain-ui-sdk/protocol";
8
+
9
+ export interface QueuedFollowUp {
10
+ text: string;
11
+ attachments: ChatImageAttachment[];
12
+ }
13
+
14
+ /**
15
+ * One RUNNING session slot. `abortController`/`timeoutHandle` belong to the
16
+ * turn currently executing; a queued follow-up runs as the next turn in the
17
+ * same slot (same sessionId) after the current one resolves.
18
+ */
19
+ export interface RunningTurn {
20
+ sessionId: string | null; // null until session_info resolves it (new session)
21
+ /** Host-minted id of the CURRENT turn in this slot; re-minted per queued follow-up. */
22
+ turnId: string;
23
+ providerId: string | null;
24
+ backend: AgentBackend;
25
+ abortController: AbortController;
26
+ timeoutHandle: ReturnType<typeof setTimeout>;
27
+ queue: QueuedFollowUp[];
28
+ cancelled: boolean;
29
+ }
30
+
31
+ // Pending interactive requests are tagged with their turn so a per-session
32
+ // cancel/disconnect can drain only that session's pending entries, and with
33
+ // the turnId that raised them (the slot's id mutates across queued follow-ups).
34
+ export interface PendingApproval {
35
+ turn: RunningTurn;
36
+ turnId: string;
37
+ resolve: (decision: PermissionDecision) => void;
38
+ }
39
+
40
+ export interface PendingAskUser {
41
+ turn: RunningTurn;
42
+ turnId: string;
43
+ resolve: (result: AskUserResult) => void;
44
+ reject: (err: Error) => void;
45
+ }
46
+
47
+ export interface PendingLocation {
48
+ turn: RunningTurn;
49
+ turnId: string;
50
+ resolve: (result: LocationFix) => void;
51
+ reject: (err: Error) => void;
52
+ }
53
+
54
+ /**
55
+ * Mutable turn state for one ws host: the running session slots and every
56
+ * pending interactive round-trip. This used to be six module-level globals
57
+ * inside a 976-line handler; as instance state it is testable and owned by an
58
+ * explicit object with a defined lifecycle.
59
+ */
60
+ export class TurnCoordinator {
61
+ readonly running = new Set<RunningTurn>();
62
+ readonly bySession = new Map<string, RunningTurn>();
63
+ startingSessions = 0;
64
+
65
+ readonly pendingApprovals = new Map<string, PendingApproval>();
66
+ readonly pendingAskUser = new Map<string, PendingAskUser>();
67
+ readonly pendingLocation = new Map<string, PendingLocation>();
68
+
69
+ private locationCounter = 0;
70
+
71
+ nextLocationRequestId(): string {
72
+ return `loc-${Date.now().toString(36)}-${++this.locationCounter}`;
73
+ }
74
+
75
+ /** True while any session has a running turn. */
76
+ isTurnActive(): boolean {
77
+ return this.running.size > 0;
78
+ }
79
+
80
+ /** Cancel every running turn (used on shutdown). */
81
+ cancelAll(reason: string): boolean {
82
+ if (this.running.size === 0) return false;
83
+ for (const turn of [...this.running]) this.cancelTurn(turn, reason);
84
+ return true;
85
+ }
86
+
87
+ /** Cancel a session's current turn and drop its queued follow-ups. */
88
+ cancelTurn(turn: RunningTurn, reason: string): void {
89
+ turn.cancelled = true;
90
+ turn.queue.length = 0;
91
+ clearTimeout(turn.timeoutHandle);
92
+ turn.abortController.abort();
93
+ this.drainPendingForTurn(turn, reason);
94
+ }
95
+
96
+ /** Resolve/reject every pending interactive request belonging to one turn. */
97
+ drainPendingForTurn(turn: RunningTurn, reason: string): void {
98
+ for (const [id, p] of this.pendingApprovals) {
99
+ if (p.turn !== turn) continue;
100
+ p.resolve({ behavior: "deny", message: reason });
101
+ this.pendingApprovals.delete(id);
102
+ }
103
+ for (const [id, p] of this.pendingAskUser) {
104
+ if (p.turn !== turn) continue;
105
+ p.reject(new Error(reason));
106
+ this.pendingAskUser.delete(id);
107
+ }
108
+ for (const [id, p] of this.pendingLocation) {
109
+ if (p.turn !== turn) continue;
110
+ p.reject(new Error(reason));
111
+ this.pendingLocation.delete(id);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Interactive-request ids (tool-approval / ask-user / location) must be
117
+ * globally unique so an inbound response resolves the correct turn's
118
+ * promise. The current backends mint unique ids (SDK tool-use ids,
119
+ * crypto.randomUUID, a host counter), so a collision across live turns means
120
+ * a future backend broke that invariant — fail the new request loudly rather
121
+ * than silently overwriting (and thus resolving) another turn's entry.
122
+ */
123
+ collidesAcrossTurns<T extends { turn: RunningTurn }>(
124
+ map: Map<string, T>,
125
+ id: string,
126
+ turn: RunningTurn
127
+ ): boolean {
128
+ const existing = map.get(id);
129
+ if (existing && existing.turn !== turn) {
130
+ console.error(
131
+ `[ws] interactive id collision: "${id}" is already pending for another turn`
132
+ );
133
+ return true;
134
+ }
135
+ return false;
136
+ }
137
+
138
+ /** Test seam: abort everything and clear all turn/pending state. */
139
+ reset(): void {
140
+ for (const turn of [...this.running]) {
141
+ clearTimeout(turn.timeoutHandle);
142
+ turn.abortController.abort();
143
+ }
144
+ this.running.clear();
145
+ this.bySession.clear();
146
+ this.pendingApprovals.clear();
147
+ this.pendingAskUser.clear();
148
+ this.pendingLocation.clear();
149
+ this.startingSessions = 0;
150
+ }
151
+ }