@usejarvis/brain 0.1.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 (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,788 @@
1
+ /**
2
+ * WebSocket Service — The Mouth
3
+ *
4
+ * Wraps WebSocketServer and StreamRelay. Routes incoming messages
5
+ * to the AgentService and relays streamed responses back to clients.
6
+ */
7
+
8
+ import type { ServerWebSocket } from 'bun';
9
+ import type { Service, ServiceStatus } from './services.ts';
10
+ import type { AgentService } from './agent-service.ts';
11
+ import type { CommitmentExecutor } from './commitment-executor.ts';
12
+ import type { ChannelService } from './channel-service.ts';
13
+ import type { Commitment } from '../vault/commitments.ts';
14
+ import type { ContentItem } from '../vault/content-pipeline.ts';
15
+ import type { STTProvider, TTSProvider } from '../comms/voice.ts';
16
+ import type { ApprovalRequest } from '../authority/approval.ts';
17
+ import type { EmergencyState } from '../authority/emergency.ts';
18
+ import { createCommitment, updateCommitmentStatus, updateCommitmentAssignee } from '../vault/commitments.ts';
19
+ import { WebSocketServer, type WSMessage } from '../comms/websocket.ts';
20
+ import { StreamRelay } from '../comms/streaming.ts';
21
+ import { getOrCreateConversation, addMessage } from '../vault/conversations.ts';
22
+
23
+ type VoiceSession = {
24
+ requestId: string;
25
+ chunks: Buffer[];
26
+ startedAt: number;
27
+ };
28
+
29
+ export class WebSocketService implements Service {
30
+ name = 'websocket';
31
+ private _status: ServiceStatus = 'stopped';
32
+ private port: number;
33
+ private agentService: AgentService;
34
+ private wsServer: WebSocketServer;
35
+ private streamRelay: StreamRelay;
36
+ /** Tracks the commitment ID for the currently processing chat message */
37
+ private activeTaskId: string | null = null;
38
+ private commitmentExecutor: CommitmentExecutor | null = null;
39
+ private channelService: ChannelService | null = null;
40
+ private ttsProvider: TTSProvider | null = null;
41
+ private sttProvider: STTProvider | null = null;
42
+ private voiceSessions = new Map<ServerWebSocket<unknown>, VoiceSession>();
43
+
44
+ constructor(port: number, agentService: AgentService) {
45
+ this.port = port;
46
+ this.agentService = agentService;
47
+ this.wsServer = new WebSocketServer(port);
48
+ this.streamRelay = new StreamRelay(this.wsServer);
49
+
50
+ // Wire delegation callback: when PA delegates to a specialist,
51
+ // update the active task's assigned_to on the task board
52
+ this.agentService.setDelegationCallback((specialistName) => {
53
+ if (!this.activeTaskId) return;
54
+ try {
55
+ const updated = updateCommitmentAssignee(this.activeTaskId, specialistName);
56
+ if (updated) this.broadcastTaskUpdate(updated, 'updated');
57
+ } catch (err) {
58
+ console.error('[WSService] Failed to update task assignee:', err);
59
+ }
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Set the commitment executor for handling cancel commands.
65
+ */
66
+ setCommitmentExecutor(executor: CommitmentExecutor): void {
67
+ this.commitmentExecutor = executor;
68
+ }
69
+
70
+ /**
71
+ * Set the channel service for cross-channel broadcasts.
72
+ */
73
+ setChannelService(channelService: ChannelService): void {
74
+ this.channelService = channelService;
75
+ }
76
+
77
+ /**
78
+ * Set the TTS provider for voice responses.
79
+ */
80
+ setTTSProvider(provider: TTSProvider): void {
81
+ this.ttsProvider = provider;
82
+ console.log('[WSService] TTS provider set');
83
+ }
84
+
85
+ /**
86
+ * Set the STT provider for voice input transcription.
87
+ */
88
+ setSTTProvider(provider: STTProvider): void {
89
+ this.sttProvider = provider;
90
+ console.log('[WSService] STT provider set');
91
+ }
92
+
93
+ /**
94
+ * Get the underlying WebSocket server for direct broadcasting.
95
+ */
96
+ getServer(): WebSocketServer {
97
+ return this.wsServer;
98
+ }
99
+
100
+ /**
101
+ * Register API route handlers on the underlying WebSocket server.
102
+ * Must be called before start().
103
+ */
104
+ setApiRoutes(routes: Record<string, any>): void {
105
+ this.wsServer.setApiRoutes(routes);
106
+ }
107
+
108
+ /**
109
+ * Set directory for serving pre-built dashboard files.
110
+ * Must be called before start().
111
+ */
112
+ setStaticDir(dir: string): void {
113
+ this.wsServer.setStaticDir(dir);
114
+ }
115
+
116
+ setPublicDir(dir: string): void {
117
+ this.wsServer.setPublicDir(dir);
118
+ }
119
+
120
+ setAuthToken(token: string): void {
121
+ this.wsServer.setAuthToken(token);
122
+ }
123
+
124
+ async start(): Promise<void> {
125
+ this._status = 'starting';
126
+
127
+ try {
128
+ // Set up message handler
129
+ this.wsServer.setHandler({
130
+ onMessage: (msg, ws) => this.routeMessage(msg, ws),
131
+ onBinaryMessage: (data, ws) => this.handleVoiceAudio(data, ws),
132
+ onConnect: (_ws) => {
133
+ console.log('[WSService] Client connected');
134
+ },
135
+ onDisconnect: (ws) => {
136
+ // Clean up any pending voice session for this client
137
+ this.voiceSessions.delete(ws);
138
+ console.log('[WSService] Client disconnected');
139
+ },
140
+ });
141
+
142
+ // Start the server
143
+ this.wsServer.start();
144
+ this._status = 'running';
145
+ console.log(`[WSService] Started on port ${this.port}`);
146
+ } catch (error) {
147
+ this._status = 'error';
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ async stop(): Promise<void> {
153
+ this._status = 'stopping';
154
+ this.wsServer.stop();
155
+ this._status = 'stopped';
156
+ console.log('[WSService] Stopped');
157
+ }
158
+
159
+ status(): ServiceStatus {
160
+ return this._status;
161
+ }
162
+
163
+ /**
164
+ * Broadcast a proactive heartbeat message to all connected clients
165
+ * and external channels.
166
+ */
167
+ broadcastHeartbeat(text: string): void {
168
+ const message: WSMessage = {
169
+ type: 'chat',
170
+ payload: {
171
+ text,
172
+ source: 'heartbeat',
173
+ },
174
+ priority: 'normal',
175
+ timestamp: Date.now(),
176
+ };
177
+ this.wsServer.broadcast(message);
178
+
179
+ // Also push to external channels
180
+ if (this.channelService) {
181
+ this.channelService.broadcastToAll(text).catch(err =>
182
+ console.error('[WSService] Channel heartbeat broadcast error:', err)
183
+ );
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Broadcast a notification with priority level.
189
+ * Used by EventReactor for immediate event reactions.
190
+ * Urgent notifications are also pushed to all external channels.
191
+ */
192
+ broadcastNotification(text: string, priority: 'urgent' | 'normal' | 'low'): void {
193
+ const message: WSMessage = {
194
+ type: 'chat',
195
+ payload: {
196
+ text,
197
+ source: 'proactive',
198
+ },
199
+ priority,
200
+ timestamp: Date.now(),
201
+ };
202
+ this.wsServer.broadcast(message);
203
+
204
+ // Push urgent notifications to external channels (Telegram, Discord)
205
+ if (priority === 'urgent' && this.channelService) {
206
+ this.channelService.broadcastToAll(`[URGENT] ${text}`).catch(err =>
207
+ console.error('[WSService] Channel broadcast error:', err)
208
+ );
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Broadcast task (commitment) changes to all connected clients.
214
+ * Used for real-time task board updates.
215
+ */
216
+ broadcastTaskUpdate(task: Commitment, action: 'created' | 'updated' | 'deleted'): void {
217
+ const message: WSMessage = {
218
+ type: 'notification',
219
+ payload: {
220
+ source: 'task_update',
221
+ action,
222
+ task,
223
+ },
224
+ timestamp: Date.now(),
225
+ };
226
+ this.wsServer.broadcast(message);
227
+ }
228
+
229
+ /**
230
+ * Broadcast content pipeline changes to all connected clients.
231
+ * Used for real-time content pipeline updates.
232
+ */
233
+ broadcastContentUpdate(item: ContentItem, action: 'created' | 'updated' | 'deleted'): void {
234
+ const message: WSMessage = {
235
+ type: 'notification',
236
+ payload: {
237
+ source: 'content_update',
238
+ action,
239
+ item,
240
+ },
241
+ timestamp: Date.now(),
242
+ };
243
+ this.wsServer.broadcast(message);
244
+ }
245
+
246
+ /**
247
+ * Broadcast sub-agent progress events to all connected clients.
248
+ * Used by the delegation system for real-time visibility.
249
+ */
250
+ broadcastSubAgentProgress(event: {
251
+ type: 'text' | 'tool_call' | 'done';
252
+ agentName: string;
253
+ agentId: string;
254
+ data: unknown;
255
+ }): void {
256
+ const message: WSMessage = {
257
+ type: 'stream',
258
+ payload: {
259
+ ...event,
260
+ source: 'sub-agent',
261
+ },
262
+ timestamp: Date.now(),
263
+ };
264
+ this.wsServer.broadcast(message);
265
+ }
266
+
267
+ /**
268
+ * Broadcast an approval request to all connected dashboard clients.
269
+ * Always pushed via WS; urgent requests are also sent to external channels.
270
+ */
271
+ broadcastApprovalRequest(request: ApprovalRequest): void {
272
+ const shortId = request.id.slice(0, 8);
273
+ const message: WSMessage = {
274
+ type: 'notification',
275
+ payload: {
276
+ source: 'approval_request',
277
+ request,
278
+ shortId,
279
+ },
280
+ priority: request.urgency === 'urgent' ? 'urgent' : 'normal',
281
+ timestamp: Date.now(),
282
+ };
283
+ this.wsServer.broadcast(message);
284
+
285
+ // Push urgent approvals to external channels
286
+ if (request.urgency === 'urgent' && this.channelService) {
287
+ const text = `[APPROVAL NEEDED] ${request.agent_name} wants to run ${request.tool_name} (${request.action_category}).\nReason: ${request.reason}\nReply: approve ${shortId} / deny ${shortId}`;
288
+ this.channelService.broadcastToAll(text).catch(err =>
289
+ console.error('[WSService] Approval channel broadcast error:', err)
290
+ );
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Broadcast emergency state changes to all connected clients.
296
+ */
297
+ broadcastEmergencyState(state: EmergencyState): void {
298
+ const message: WSMessage = {
299
+ type: 'notification',
300
+ payload: {
301
+ source: 'emergency_state',
302
+ state,
303
+ },
304
+ priority: 'urgent',
305
+ timestamp: Date.now(),
306
+ };
307
+ this.wsServer.broadcast(message);
308
+ }
309
+
310
+ /**
311
+ * Broadcast an approval resolution (approved/denied/executed) to all clients.
312
+ */
313
+ /**
314
+ * Broadcast an awareness event to all connected clients.
315
+ */
316
+ /**
317
+ * Broadcast a sidecar event to all connected clients.
318
+ */
319
+ broadcastSidecarEvent(sidecarId: string, event: { type: string; data: Record<string, unknown>; timestamp: number }): void {
320
+ const message: WSMessage = {
321
+ type: 'notification',
322
+ payload: {
323
+ source: 'sidecar_event',
324
+ sidecarId,
325
+ event,
326
+ },
327
+ timestamp: event.timestamp,
328
+ };
329
+ this.wsServer.broadcast(message);
330
+ }
331
+
332
+ broadcastAwarenessEvent(event: { type: string; data: Record<string, unknown>; timestamp: number }): void {
333
+ const message: WSMessage = {
334
+ type: 'notification',
335
+ payload: {
336
+ source: 'awareness_event',
337
+ event,
338
+ },
339
+ timestamp: event.timestamp,
340
+ };
341
+ this.wsServer.broadcast(message);
342
+ }
343
+
344
+ /**
345
+ * Synthesize TTS for a proactive message and broadcast audio to all clients.
346
+ * Used for awareness suggestions and other unsolicited voice notifications.
347
+ */
348
+ /**
349
+ * Synthesize TTS for a proactive message and broadcast audio to all clients.
350
+ * Used for awareness suggestions and other unsolicited voice notifications.
351
+ */
352
+ async broadcastProactiveVoice(text: string): Promise<void> {
353
+ if (!this.ttsProvider || !text) {
354
+ console.log(`[WSService] Proactive TTS skipped: ${!this.ttsProvider ? 'no TTS provider' : 'empty text'}`);
355
+ return;
356
+ }
357
+
358
+ if (this.wsServer.getClientCount() === 0) {
359
+ console.log('[WSService] Proactive TTS skipped: no connected clients');
360
+ return;
361
+ }
362
+
363
+ try {
364
+ const requestId = `proactive-${Date.now()}`;
365
+
366
+ // Signal TTS start to all clients
367
+ const startMsg: WSMessage = {
368
+ type: 'tts_start',
369
+ payload: { requestId },
370
+ timestamp: Date.now(),
371
+ };
372
+ this.wsServer.broadcast(startMsg);
373
+
374
+ let chunkCount = 0;
375
+ for await (const chunk of this.ttsProvider.synthesizeStream(text)) {
376
+ // Send binary audio to all connected clients
377
+ for (const ws of this.wsServer.getClients()) {
378
+ try {
379
+ ws.sendBinary(chunk);
380
+ } catch { /* client may have disconnected */ }
381
+ }
382
+ chunkCount++;
383
+ }
384
+
385
+ // Signal TTS end
386
+ const endMsg: WSMessage = {
387
+ type: 'tts_end',
388
+ payload: { requestId },
389
+ timestamp: Date.now(),
390
+ };
391
+ this.wsServer.broadcast(endMsg);
392
+ console.log(`[WSService] Proactive TTS complete: "${text.slice(0, 60)}..." (${chunkCount} chunks)`);
393
+ } catch (err) {
394
+ console.error('[WSService] Proactive TTS error:', err instanceof Error ? err.message : err);
395
+ // Still send tts_end so client doesn't get stuck
396
+ try {
397
+ this.wsServer.broadcast({ type: 'tts_end', payload: {}, timestamp: Date.now() });
398
+ } catch { /* ignore */ }
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Broadcast a workflow execution event to all connected clients.
404
+ */
405
+ broadcastWorkflowEvent(event: { type: string; workflowId: string; executionId?: string; nodeId?: string; data: Record<string, unknown>; timestamp: number }): void {
406
+ const message: WSMessage = {
407
+ type: 'workflow_event',
408
+ payload: event,
409
+ timestamp: event.timestamp,
410
+ };
411
+ this.wsServer.broadcast(message);
412
+ }
413
+
414
+ /**
415
+ * Broadcast a goal event to all connected clients.
416
+ */
417
+ broadcastGoalEvent(event: { type: string; goalId?: string; data: Record<string, unknown>; timestamp: number }): void {
418
+ const message: WSMessage = {
419
+ type: 'goal_event',
420
+ payload: event,
421
+ timestamp: event.timestamp,
422
+ };
423
+ this.wsServer.broadcast(message);
424
+ }
425
+
426
+ broadcastApprovalUpdate(request: ApprovalRequest): void {
427
+ const message: WSMessage = {
428
+ type: 'notification',
429
+ payload: {
430
+ source: 'approval_update',
431
+ request,
432
+ },
433
+ timestamp: Date.now(),
434
+ };
435
+ this.wsServer.broadcast(message);
436
+ }
437
+
438
+ /**
439
+ * Route incoming WebSocket messages to the appropriate handler.
440
+ */
441
+ private async routeMessage(msg: WSMessage, ws: ServerWebSocket<unknown>): Promise<WSMessage | void> {
442
+ switch (msg.type) {
443
+ case 'chat':
444
+ return this.handleChat(msg, ws);
445
+
446
+ case 'command':
447
+ return this.handleCommand(msg);
448
+
449
+ case 'status':
450
+ return this.handleStatus();
451
+
452
+ case 'voice_start': {
453
+ const { requestId } = msg.payload as { requestId: string };
454
+ this.voiceSessions.set(ws, { requestId, chunks: [], startedAt: Date.now() });
455
+ return undefined;
456
+ }
457
+
458
+ case 'voice_end': {
459
+ const session = this.voiceSessions.get(ws);
460
+ if (!session) return undefined;
461
+ this.voiceSessions.delete(ws);
462
+ // Fire-and-forget: transcribe → process → TTS response
463
+ this.handleVoiceSession(session, ws).catch(err =>
464
+ console.error('[WSService] Voice session error:', err)
465
+ );
466
+ return undefined;
467
+ }
468
+
469
+ default:
470
+ return {
471
+ type: 'error',
472
+ payload: { message: `Unknown message type: ${msg.type}` },
473
+ timestamp: Date.now(),
474
+ };
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Handle chat messages — stream response via StreamRelay.
480
+ * Auto-creates a task for non-trivial messages so the task board tracks agent work.
481
+ */
482
+ private async handleChat(msg: WSMessage, ws?: ServerWebSocket<unknown>): Promise<WSMessage | void> {
483
+ const payload = msg.payload as { text?: string; channel?: string };
484
+ const text = payload?.text;
485
+
486
+ if (!text) {
487
+ return {
488
+ type: 'error',
489
+ payload: { message: 'Missing text in chat payload' },
490
+ id: msg.id,
491
+ timestamp: Date.now(),
492
+ };
493
+ }
494
+
495
+ const channel = payload.channel ?? 'websocket';
496
+ const requestId = msg.id ?? crypto.randomUUID();
497
+
498
+ // Auto-create a task for non-trivial messages
499
+ const isTrivial = text.trim().length < 10;
500
+ let taskCommitment: Commitment | null = null;
501
+
502
+ if (!isTrivial) {
503
+ try {
504
+ const taskLabel = text.length > 80 ? text.slice(0, 77) + '...' : text;
505
+ taskCommitment = createCommitment(taskLabel, {
506
+ assigned_to: 'jarvis',
507
+ created_from: 'user',
508
+ });
509
+ updateCommitmentStatus(taskCommitment.id, 'active');
510
+ taskCommitment.status = 'active';
511
+ this.activeTaskId = taskCommitment.id;
512
+ this.broadcastTaskUpdate(taskCommitment, 'created');
513
+ } catch (err) {
514
+ console.error('[WSService] Failed to auto-create task:', err);
515
+ }
516
+ }
517
+
518
+ // Persist user message
519
+ try {
520
+ const conversation = getOrCreateConversation(channel);
521
+ addMessage(conversation.id, { role: 'user', content: text });
522
+
523
+ const { stream, onComplete } = this.agentService.streamMessage(text, channel);
524
+
525
+ // Set up streaming TTS: speak sentences as they arrive
526
+ const ttsActive = !!(this.ttsProvider && ws);
527
+ let ttsSentenceQueue: string[] = [];
528
+ let ttsSpeaking = false;
529
+ let ttsStartSent = false;
530
+ let ttsStreamFullyDone = false; // set AFTER relayStream returns, not per-turn 'done'
531
+ let ttsSentenceCount = 0;
532
+ let ttsChunkCount = 0;
533
+
534
+ const speakNextSentence = async () => {
535
+ if (ttsSpeaking || !ttsActive || !ws) return;
536
+ const sentence = ttsSentenceQueue.shift();
537
+ if (!sentence) {
538
+ // Queue empty — send tts_end only if stream is fully done
539
+ if (ttsStreamFullyDone && ttsStartSent) {
540
+ console.log(`[WSService] TTS complete: ${ttsSentenceCount} sentences, ${ttsChunkCount} audio chunks`);
541
+ this.wsServer.sendToClient(ws, {
542
+ type: 'tts_end',
543
+ payload: { requestId },
544
+ id: requestId,
545
+ timestamp: Date.now(),
546
+ });
547
+ ttsStartSent = false; // prevent duplicate tts_end
548
+ }
549
+ return;
550
+ }
551
+
552
+ // Send tts_start exactly once before the first audio chunk
553
+ if (!ttsStartSent) {
554
+ ttsStartSent = true;
555
+ this.wsServer.sendToClient(ws, {
556
+ type: 'tts_start',
557
+ payload: { requestId },
558
+ id: requestId,
559
+ timestamp: Date.now(),
560
+ });
561
+ }
562
+
563
+ ttsSpeaking = true;
564
+ ttsSentenceCount++;
565
+ try {
566
+ if (this.ttsProvider) {
567
+ for await (const chunk of this.ttsProvider.synthesizeStream(sentence)) {
568
+ ttsChunkCount++;
569
+ this.wsServer.sendBinary(ws, chunk);
570
+ }
571
+ }
572
+ } catch (err) {
573
+ console.error('[WSService] TTS sentence error:', err);
574
+ }
575
+ ttsSpeaking = false;
576
+ speakNextSentence();
577
+ };
578
+
579
+ // Relay stream to all WebSocket clients, collect full text.
580
+ // onSentence fires for each complete sentence during streaming.
581
+ // NOTE: onTextDone fires per LLM turn (tool loop), NOT once at the end.
582
+ // We ignore onTextDone and use the relayStream return to mark stream completion.
583
+ const fullText = await this.streamRelay.relayStream(stream, requestId, ttsActive ? {
584
+ onSentence: (sentence) => {
585
+ ttsSentenceQueue.push(sentence);
586
+ speakNextSentence();
587
+ },
588
+ } : undefined);
589
+
590
+ // Stream is now fully done (all tool loop turns complete)
591
+ ttsStreamFullyDone = true;
592
+ if (ttsActive) {
593
+ if (!ttsSpeaking && ttsSentenceQueue.length === 0 && ttsStartSent) {
594
+ // Everything already played, send tts_end now
595
+ this.wsServer.sendToClient(ws!, {
596
+ type: 'tts_end',
597
+ payload: { requestId },
598
+ id: requestId,
599
+ timestamp: Date.now(),
600
+ });
601
+ ttsStartSent = false;
602
+ }
603
+ // Otherwise speakNextSentence will send tts_end when queue drains
604
+ }
605
+
606
+ // Persist assistant response
607
+ addMessage(conversation.id, { role: 'assistant', content: fullText });
608
+
609
+ // Mark task as completed
610
+ if (taskCommitment) {
611
+ try {
612
+ const resultSummary = fullText.length > 200 ? fullText.slice(0, 197) + '...' : fullText;
613
+ const updated = updateCommitmentStatus(taskCommitment.id, 'completed', resultSummary);
614
+ if (updated) this.broadcastTaskUpdate(updated, 'updated');
615
+ } catch (err) {
616
+ console.error('[WSService] Failed to complete task:', err);
617
+ } finally {
618
+ this.activeTaskId = null;
619
+ }
620
+ }
621
+
622
+ // Fire-and-forget: run post-processing (extraction, personality)
623
+ onComplete(fullText).catch((err) =>
624
+ console.error('[WSService] onComplete error:', err)
625
+ );
626
+
627
+ // Don't return a direct response — StreamRelay already broadcast everything
628
+ return undefined;
629
+ } catch (error) {
630
+ console.error('[WSService] Chat error:', error);
631
+
632
+ // Mark task as failed
633
+ if (taskCommitment) {
634
+ try {
635
+ const reason = error instanceof Error ? error.message : 'Processing failed';
636
+ const updated = updateCommitmentStatus(taskCommitment.id, 'failed', reason);
637
+ if (updated) this.broadcastTaskUpdate(updated, 'updated');
638
+ } catch (err) {
639
+ console.error('[WSService] Failed to fail task:', err);
640
+ } finally {
641
+ this.activeTaskId = null;
642
+ }
643
+ }
644
+
645
+ return {
646
+ type: 'error',
647
+ payload: {
648
+ message: error instanceof Error ? error.message : 'Chat processing failed',
649
+ },
650
+ id: requestId,
651
+ timestamp: Date.now(),
652
+ };
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Handle binary audio data from voice recording.
658
+ * Accumulates chunks into the active voice session for this client.
659
+ */
660
+ private async handleVoiceAudio(data: Buffer, ws: ServerWebSocket<unknown>): Promise<void> {
661
+ const session = this.voiceSessions.get(ws);
662
+ if (!session) {
663
+ console.warn('[WSService] Binary audio received with no active voice session');
664
+ return;
665
+ }
666
+ session.chunks.push(data);
667
+ }
668
+
669
+ /**
670
+ * Process a completed voice session: STT → chat → TTS response.
671
+ */
672
+ private async handleVoiceSession(session: VoiceSession, ws: ServerWebSocket<unknown>): Promise<void> {
673
+ if (!this.sttProvider) {
674
+ this.wsServer.sendToClient(ws, {
675
+ type: 'error',
676
+ payload: { message: 'STT not configured. Enable it in Settings > Channels.' },
677
+ timestamp: Date.now(),
678
+ });
679
+ return;
680
+ }
681
+
682
+ const audioBuffer = Buffer.concat(session.chunks);
683
+ if (audioBuffer.length === 0) return;
684
+
685
+ try {
686
+ const transcript = await this.sttProvider.transcribe(audioBuffer);
687
+ if (!transcript.trim()) return;
688
+
689
+ console.log('[WSService] Voice transcript:', transcript);
690
+
691
+ // Echo transcript back so the UI shows it as a user message
692
+ this.wsServer.sendToClient(ws, {
693
+ type: 'chat',
694
+ payload: { text: transcript, source: 'voice_transcript' },
695
+ id: session.requestId,
696
+ timestamp: Date.now(),
697
+ });
698
+
699
+ // Reuse existing chat flow
700
+ await this.handleChat({
701
+ type: 'chat',
702
+ payload: { text: transcript },
703
+ id: session.requestId,
704
+ timestamp: Date.now(),
705
+ }, ws);
706
+ } catch (err) {
707
+ console.error('[WSService] STT error:', err);
708
+ this.wsServer.sendToClient(ws, {
709
+ type: 'error',
710
+ payload: { message: 'Voice transcription failed' },
711
+ timestamp: Date.now(),
712
+ });
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Handle system commands.
718
+ */
719
+ private async handleCommand(msg: WSMessage): Promise<WSMessage> {
720
+ const payload = msg.payload as { command?: string };
721
+ const command = payload?.command;
722
+
723
+ switch (command) {
724
+ case 'health':
725
+ return {
726
+ type: 'status',
727
+ payload: {
728
+ status: 'ok',
729
+ service: this.name,
730
+ clients: this.wsServer.getClientCount(),
731
+ },
732
+ id: msg.id,
733
+ timestamp: Date.now(),
734
+ };
735
+
736
+ case 'ping':
737
+ return {
738
+ type: 'status',
739
+ payload: { pong: true },
740
+ id: msg.id,
741
+ timestamp: Date.now(),
742
+ };
743
+
744
+ case 'cancel_execution': {
745
+ const commitmentId = (msg.payload as any)?.commitmentId;
746
+ if (this.commitmentExecutor && commitmentId) {
747
+ const cancelled = this.commitmentExecutor.cancelExecution(commitmentId);
748
+ return {
749
+ type: 'status',
750
+ payload: { cancelled, commitmentId },
751
+ id: msg.id,
752
+ timestamp: Date.now(),
753
+ };
754
+ }
755
+ return {
756
+ type: 'error',
757
+ payload: { message: 'No executor available or missing commitmentId' },
758
+ id: msg.id,
759
+ timestamp: Date.now(),
760
+ };
761
+ }
762
+
763
+ default:
764
+ return {
765
+ type: 'error',
766
+ payload: { message: `Unknown command: ${command}` },
767
+ id: msg.id,
768
+ timestamp: Date.now(),
769
+ };
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Handle status requests.
775
+ */
776
+ private handleStatus(): WSMessage {
777
+ return {
778
+ type: 'status',
779
+ payload: {
780
+ service: this.name,
781
+ status: this._status,
782
+ clients: this.wsServer.getClientCount(),
783
+ port: this.port,
784
+ },
785
+ timestamp: Date.now(),
786
+ };
787
+ }
788
+ }