claudeck 1.3.1 → 1.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.
@@ -42,6 +42,37 @@ import { runAgent } from "./agent-loop.js";
42
42
  import { runOrchestrator } from "./orchestrator.js";
43
43
  import { runDag } from "./dag-executor.js";
44
44
 
45
+ // ── Session broadcast rooms ───────────────────────────────────────────────
46
+ // Maps sessionId → Set of WebSocket clients watching that session
47
+ const sessionRooms = new Map();
48
+
49
+ function joinRoom(sessionId, ws) {
50
+ if (!sessionRooms.has(sessionId)) sessionRooms.set(sessionId, new Set());
51
+ sessionRooms.get(sessionId).add(ws);
52
+ }
53
+
54
+ function leaveRoom(ws) {
55
+ for (const [sessionId, clients] of sessionRooms) {
56
+ clients.delete(ws);
57
+ if (clients.size === 0) sessionRooms.delete(sessionId);
58
+ }
59
+ }
60
+
61
+ function broadcastToSession(sessionId, message, excludeWs = null) {
62
+ const clients = sessionRooms.get(sessionId);
63
+ if (!clients) return;
64
+ const payload = JSON.stringify({ ...message, _broadcast: true });
65
+ for (const client of clients) {
66
+ if (client !== excludeWs && client.readyState === 1) {
67
+ client.send(payload);
68
+ }
69
+ }
70
+ }
71
+
72
+ // Global pending approvals — enables cross-connection approval (any client can approve)
73
+ // Key: approval ID, Value: { resolve, timer, toolInput, ws, localMap, sessionId }
74
+ const globalPendingApprovals = new Map();
75
+
45
76
  // Tools that are read-only and safe to auto-approve in "confirmDangerous" mode
46
77
  export const READ_ONLY_TOOLS = new Set([
47
78
  "Read", "Glob", "Grep", "WebSearch", "WebFetch", "Agent",
@@ -87,7 +118,7 @@ export function getActiveSessionIds() {
87
118
  * Creates a canUseTool callback that sends permission requests over WebSocket
88
119
  * AND Telegram (for AFK approval). Whichever channel responds first wins.
89
120
  */
90
- export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, sessionTitle) {
121
+ export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, sessionTitle, getSessionId = null) {
91
122
  return async (toolName, toolInput, options) => {
92
123
  // Bypass mode — auto-approve everything
93
124
  if (permissionMode === "bypass") {
@@ -109,6 +140,8 @@ export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, ses
109
140
  }
110
141
 
111
142
  ws.send(JSON.stringify(payload));
143
+ const permSid = getSessionId?.();
144
+ if (permSid) broadcastToSession(permSid, payload, ws);
112
145
 
113
146
  // Also send to Telegram for AFK approval
114
147
  if (telegramEnabled()) {
@@ -122,8 +155,11 @@ export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, ses
122
155
  const timeoutMs = getApprovalTimeoutMs();
123
156
 
124
157
  return new Promise((resolve) => {
158
+ const permSidForApproval = getSessionId?.();
159
+
125
160
  const timer = setTimeout(() => {
126
161
  pendingApprovals.delete(id);
162
+ globalPendingApprovals.delete(id);
127
163
  markTelegramMessageResolved(id, "timeout").catch(() => {});
128
164
  resolve({ behavior: "deny", message: `Approval timed out (${Math.round(timeoutMs / 60000)}min)` });
129
165
  }, timeoutMs);
@@ -133,12 +169,14 @@ export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, ses
133
169
  options.signal.addEventListener("abort", () => {
134
170
  clearTimeout(timer);
135
171
  pendingApprovals.delete(id);
172
+ globalPendingApprovals.delete(id);
136
173
  markTelegramMessageResolved(id, "abort").catch(() => {});
137
174
  resolve({ behavior: "deny", message: "Aborted by user" });
138
175
  }, { once: true });
139
176
  }
140
177
 
141
178
  pendingApprovals.set(id, { resolve, timer, toolInput, ws });
179
+ globalPendingApprovals.set(id, { resolve, timer, toolInput, ws, localMap: pendingApprovals, sessionId: permSidForApproval });
142
180
  });
143
181
  };
144
182
  }
@@ -164,17 +202,17 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
164
202
  const sKey = chatId ? `${ourSid}::${chatId}` : ourSid;
165
203
  sessionIds.set(sKey, claudeSessionId);
166
204
 
167
- if (!getSession(ourSid)) {
168
- createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
205
+ if (!await getSession(ourSid)) {
206
+ await createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
169
207
  if (isWorkflow) {
170
- updateSessionTitle(ourSid, `Workflow: ${stepLabel}`);
208
+ await updateSessionTitle(ourSid, `Workflow: ${stepLabel}`);
171
209
  }
172
210
  } else {
173
- updateClaudeSessionId(ourSid, claudeSessionId);
211
+ await updateClaudeSessionId(ourSid, claudeSessionId);
174
212
  }
175
213
 
176
214
  if (chatId) {
177
- setClaudeSession(ourSid, chatId, claudeSessionId);
215
+ await setClaudeSession(ourSid, chatId, claudeSessionId);
178
216
  }
179
217
 
180
218
  wsSend({ type: "session", sessionId: ourSid });
@@ -185,12 +223,12 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
185
223
  // user message saved by caller for chat; for workflow, save with step label
186
224
  }
187
225
  if (isWorkflow) {
188
- addMessage(resolvedSid, "user", JSON.stringify({ text: msgText }), null, wfMeta);
226
+ await addMessage(resolvedSid, "user", JSON.stringify({ text: msgText }), null, wfMeta);
189
227
  }
190
228
 
191
229
  if (!isWorkflow) {
192
230
  // Auto-set session title from first user message
193
- const existingSession = getSession(ourSid);
231
+ const existingSession = await getSession(ourSid);
194
232
  if (existingSession && !existingSession.title) {
195
233
  // Title is set by caller
196
234
  }
@@ -204,12 +242,12 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
204
242
  if (block.type === "text" && block.text) {
205
243
  wsSend({ type: "text", text: block.text });
206
244
  if (resolvedSid) {
207
- addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null, wfMeta);
245
+ await addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null, wfMeta);
208
246
  }
209
247
  } else if (block.type === "tool_use") {
210
248
  wsSend({ type: "tool", id: block.id, name: block.name, input: block.input });
211
249
  if (resolvedSid) {
212
- addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null, wfMeta);
250
+ await addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null, wfMeta);
213
251
  }
214
252
  }
215
253
  }
@@ -231,7 +269,7 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
231
269
  ([, v]) => v === claudeSessionId
232
270
  )?.[0];
233
271
  if (sid) {
234
- addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
272
+ await addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
235
273
  }
236
274
 
237
275
  wsSend({
@@ -239,7 +277,7 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
239
277
  duration_ms: sdkMsg.duration_ms,
240
278
  num_turns: sdkMsg.num_turns,
241
279
  cost_usd: sdkMsg.total_cost_usd,
242
- totalCost: getTotalCost(),
280
+ totalCost: await getTotalCost(),
243
281
  input_tokens: inputTokens,
244
282
  output_tokens: outputTokens,
245
283
  cache_read_tokens: cacheReadTokens,
@@ -251,7 +289,7 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
251
289
  lastMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: false };
252
290
 
253
291
  if (resolvedSid) {
254
- addMessage(resolvedSid, "result", JSON.stringify({
292
+ await addMessage(resolvedSid, "result", JSON.stringify({
255
293
  duration_ms: sdkMsg.duration_ms,
256
294
  num_turns: sdkMsg.num_turns,
257
295
  cost_usd: sdkMsg.total_cost_usd,
@@ -273,8 +311,8 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
273
311
  ([, v]) => v === claudeSessionId
274
312
  )?.[0];
275
313
  if (sid) {
276
- addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
277
- addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null, wfMeta);
314
+ await addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
315
+ await addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null, wfMeta);
278
316
  }
279
317
  lastMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: true, error: errMsg };
280
318
  wsSend({ type: "error", error: errMsg });
@@ -303,7 +341,7 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
303
341
  content: text.slice(0, 10000),
304
342
  isError: block.is_error || false,
305
343
  };
306
- addMessage(resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null, wfMeta);
344
+ await addMessage(resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null, wfMeta);
307
345
  }
308
346
  }
309
347
  }
@@ -344,6 +382,7 @@ export function handleClose({ activeQueries, pendingApprovals }) {
344
382
  for (const [id, { resolve, timer }] of pendingApprovals) {
345
383
  clearTimeout(timer);
346
384
  resolve({ behavior: "deny", message: "Client disconnected" });
385
+ globalPendingApprovals.delete(id);
347
386
  }
348
387
  pendingApprovals.clear();
349
388
  }
@@ -361,21 +400,48 @@ export function handleAbort(msg, { activeQueries, pendingApprovals }) {
361
400
  for (const [id, { resolve, timer }] of pendingApprovals) {
362
401
  clearTimeout(timer);
363
402
  resolve({ behavior: "deny", message: "Aborted by user" });
403
+ globalPendingApprovals.delete(id);
364
404
  }
365
405
  pendingApprovals.clear();
366
406
  }
367
407
 
368
408
  // ── Extracted handler: permission response ────────────────────────────────
369
- export function handlePermissionResponse(msg, { pendingApprovals }) {
370
- const pending = pendingApprovals.get(msg.id);
409
+ export function handlePermissionResponse(msg, { pendingApprovals, ws: responderWs }) {
410
+ // Check local first (same connection that initiated the request)
411
+ let pending = pendingApprovals.get(msg.id);
412
+ let isLocal = !!pending;
413
+
414
+ // If not found locally, check global (cross-connection approval from another client)
415
+ if (!pending) {
416
+ pending = globalPendingApprovals.get(msg.id);
417
+ }
418
+
371
419
  if (pending) {
420
+ // Get sessionId before deleting (local pending doesn't have it, global does)
421
+ const sessionId = pending.sessionId || globalPendingApprovals.get(msg.id)?.sessionId;
422
+
372
423
  clearTimeout(pending.timer);
373
424
  pendingApprovals.delete(msg.id);
425
+ globalPendingApprovals.delete(msg.id);
426
+ // Also clean up from the originating connection's local map
427
+ if (!isLocal && pending.localMap) pending.localMap.delete(msg.id);
428
+
374
429
  if (msg.behavior === "allow") {
375
430
  pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
376
431
  } else {
377
432
  pending.resolve({ behavior: "deny", message: "Denied by user" });
378
433
  }
434
+
435
+ // Broadcast permission_response_external to dismiss modals on all other clients
436
+ if (sessionId) {
437
+ broadcastToSession(sessionId, {
438
+ type: "permission_response_external",
439
+ id: msg.id,
440
+ behavior: msg.behavior,
441
+ source: "broadcast",
442
+ }, responderWs);
443
+ }
444
+
379
445
  // Update Telegram message to show it was resolved via web
380
446
  markTelegramMessageResolved(msg.id, msg.behavior === "allow" ? "allow" : "deny").catch(() => {});
381
447
  }
@@ -389,6 +455,7 @@ export async function handleWorkflow(msg, { ws, sessionIds, activeQueries, pendi
389
455
  function wfSend(payload) {
390
456
  if (ws.readyState !== 1) return;
391
457
  ws.send(JSON.stringify(payload));
458
+ if (clientSid) broadcastToSession(clientSid, payload, ws);
392
459
  }
393
460
 
394
461
  wfSend({ type: "workflow_started", workflow: { id: workflow.id, title: workflow.title, steps: workflow.steps.map((s) => s.label) } });
@@ -425,7 +492,7 @@ export async function handleWorkflow(msg, { ws, sessionIds, activeQueries, pendi
425
492
  };
426
493
 
427
494
  if (!useBypass && !usePlan) {
428
- stepOpts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, null, `Workflow: ${workflow.title}`);
495
+ stepOpts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, null, `Workflow: ${workflow.title}`, () => clientSid);
429
496
  }
430
497
  if (wfModel) stepOpts.model = resolveModel(wfModel);
431
498
 
@@ -512,6 +579,7 @@ export async function handleAgentChain(msg, { ws, sessionIds, activeQueries, pen
512
579
  function chainSend(payload) {
513
580
  if (ws.readyState !== 1) return;
514
581
  ws.send(JSON.stringify(payload));
582
+ if (clientSid) broadcastToSession(clientSid, payload, ws);
515
583
  }
516
584
 
517
585
  chainSend({
@@ -628,7 +696,9 @@ export async function handleOrchestrate(msg, { ws, sessionIds, activeQueries, pe
628
696
  try {
629
697
  agents = JSON.parse(await readFile(configPath("agents.json"), "utf-8"));
630
698
  } catch {
631
- ws.send(JSON.stringify({ type: "error", error: "Failed to load agents" }));
699
+ const errPayload = { type: "error", error: "Failed to load agents" };
700
+ ws.send(JSON.stringify(errPayload));
701
+ if (clientSid) broadcastToSession(clientSid, errPayload, ws);
632
702
  return;
633
703
  }
634
704
 
@@ -654,11 +724,13 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
654
724
 
655
725
  // Handle /remember command — save memory and respond without calling Claude
656
726
  if (message && message.trim().toLowerCase().startsWith('/remember ') && cwd) {
657
- const result = parseRememberCommand(message, cwd, clientSid);
727
+ const result = await parseRememberCommand(message, cwd, clientSid);
658
728
  function remSend(payload) {
659
729
  if (ws.readyState !== 1) return;
660
730
  if (chatId) payload.chatId = chatId;
731
+ if (clientSid) payload.sessionId = clientSid;
661
732
  ws.send(JSON.stringify(payload));
733
+ if (clientSid) broadcastToSession(clientSid, payload, ws);
662
734
  }
663
735
  if (result) {
664
736
  remSend({ type: "text", text: result.saved
@@ -677,8 +749,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
677
749
  const sessionKey = chatId ? `${clientSid}::${chatId}` : clientSid;
678
750
  const resumeId = clientSid ? sessionIds.get(sessionKey) : undefined;
679
751
 
680
- if (clientSid && getSession(clientSid)) {
681
- touchSession(clientSid);
752
+ if (clientSid && await getSession(clientSid)) {
753
+ await touchSession(clientSid);
682
754
  }
683
755
 
684
756
  const abortController = new AbortController();
@@ -701,7 +773,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
701
773
  const wtResult = await createWorktree(cwd, branchName);
702
774
  const wtId = crypto.randomUUID();
703
775
 
704
- createWorktreeRecord(wtId, clientSid || null, cwd, wtResult.worktreePath, branchName, baseBranch, (message || "").slice(0, 200));
776
+ await createWorktreeRecord(wtId, clientSid || null, cwd, wtResult.worktreePath, branchName, baseBranch, (message || "").slice(0, 200));
705
777
  worktreeRecord = { id: wtId, worktreePath: wtResult.worktreePath, branchName, baseBranch };
706
778
  effectiveCwd = wtResult.worktreePath;
707
779
 
@@ -710,6 +782,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
710
782
  const wtPayload = { type: "worktree_created", worktreeId: wtId, branchName, baseBranch, worktreePath: wtResult.worktreePath };
711
783
  if (chatId) wtPayload.chatId = chatId;
712
784
  ws.send(JSON.stringify(wtPayload));
785
+ if (clientSid) broadcastToSession(clientSid, wtPayload, ws);
713
786
  }
714
787
  } catch (err) {
715
788
  console.error("Worktree creation failed:", err.message, err.stack);
@@ -736,7 +809,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
736
809
  if (effectiveMaxTurns) opts.maxTurns = effectiveMaxTurns;
737
810
 
738
811
  if (!useBypass && !usePlan) {
739
- opts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, chatId, projectName || "Chat");
812
+ opts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, chatId, projectName || "Chat", () => state.resolvedSid);
740
813
  }
741
814
  if (chatModel) opts.model = resolveModel(chatModel);
742
815
  if (Array.isArray(disabledTools) && disabledTools.length > 0) {
@@ -754,10 +827,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
754
827
  (opts.appendSystemPrompt ? '\n\n' : '') + systemPrompt;
755
828
  }
756
829
  // Run memory maintenance (decay stale, clean expired) on each session
757
- if (cwd) runMaintenance(cwd);
830
+ if (cwd) await runMaintenance(cwd);
758
831
  // Inject persistent memories for this project (smart: uses user message for relevance)
759
832
  if (cwd) {
760
- const { prompt: memPrompt, count: memCount, memories: memList } = buildMemoryPrompt(cwd, 10, message);
833
+ const { prompt: memPrompt, count: memCount, memories: memList } = await buildMemoryPrompt(cwd, 10, message);
761
834
  if (memPrompt) {
762
835
  opts.appendSystemPrompt = (opts.appendSystemPrompt || '') +
763
836
  (opts.appendSystemPrompt ? '\n\n' : '') + memPrompt;
@@ -778,6 +851,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
778
851
  const payload = { type: "memories_injected", count: memCount, memories: memList };
779
852
  if (chatId) payload.chatId = chatId;
780
853
  ws.send(JSON.stringify(payload));
854
+ if (clientSid) broadcastToSession(clientSid, payload, ws);
781
855
  }
782
856
  } else {
783
857
  console.log(`\n══════ MEMORY INJECTION ══════`);
@@ -795,6 +869,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
795
869
  if (chatId) payload.chatId = chatId;
796
870
  if (state.resolvedSid) payload.sessionId = state.resolvedSid;
797
871
  ws.send(JSON.stringify(payload));
872
+ // Broadcast to other clients watching this session
873
+ if (state.resolvedSid) broadcastToSession(state.resolvedSid, payload, ws);
798
874
  }
799
875
 
800
876
  // Register for global tracking if we already know the session
@@ -819,14 +895,14 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
819
895
  const sKey = chatId ? `${ourSid}::${chatId}` : ourSid;
820
896
  sessionIds.set(sKey, claudeSessionId);
821
897
 
822
- if (!getSession(ourSid)) {
823
- createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
898
+ if (!await getSession(ourSid)) {
899
+ await createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
824
900
  } else {
825
- updateClaudeSessionId(ourSid, claudeSessionId);
901
+ await updateClaudeSessionId(ourSid, claudeSessionId);
826
902
  }
827
903
 
828
904
  if (chatId) {
829
- setClaudeSession(ourSid, chatId, claudeSessionId);
905
+ await setClaudeSession(ourSid, chatId, claudeSessionId);
830
906
  }
831
907
 
832
908
  wsSend({ type: "session", sessionId: ourSid });
@@ -834,15 +910,21 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
834
910
  if (images?.length) {
835
911
  userMsgData.images = images.map(i => ({ name: i.name, data: i.data, mimeType: i.mimeType }));
836
912
  }
837
- addMessage(state.resolvedSid, "user", JSON.stringify(userMsgData), chatId || null);
913
+ await addMessage(state.resolvedSid, "user", JSON.stringify(userMsgData), chatId || null);
914
+
915
+ // Broadcast user message to observers (sender already rendered it locally)
916
+ const userBroadcast = { type: "user_message", text: message, sessionId: state.resolvedSid };
917
+ if (chatId) userBroadcast.chatId = chatId;
918
+ if (images?.length) userBroadcast.images = images.map(i => ({ name: i.name, mimeType: i.mimeType }));
919
+ broadcastToSession(state.resolvedSid, userBroadcast, ws);
838
920
 
839
921
  // Register global query tracking now that we know the session
840
922
  if (!clientSid) registerGlobalQuery(state.resolvedSid, queryKey);
841
923
 
842
- const existingSession = getSession(ourSid);
924
+ const existingSession = await getSession(ourSid);
843
925
  if (existingSession && !existingSession.title) {
844
926
  const title = message.slice(0, 100).split("\n")[0];
845
- updateSessionTitle(ourSid, title);
927
+ await updateSessionTitle(ourSid, title);
846
928
  }
847
929
  continue;
848
930
  }
@@ -852,10 +934,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
852
934
  if (block.type === "text" && block.text) {
853
935
  state.lastAssistantText += (state.lastAssistantText ? "\n\n" : "") + block.text;
854
936
  wsSend({ type: "text", text: block.text });
855
- if (state.resolvedSid) addMessage(state.resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null);
937
+ if (state.resolvedSid) await addMessage(state.resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null);
856
938
  } else if (block.type === "tool_use") {
857
939
  wsSend({ type: "tool", id: block.id, name: block.name, input: block.input });
858
- if (state.resolvedSid) addMessage(state.resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null);
940
+ if (state.resolvedSid) await addMessage(state.resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null);
859
941
  }
860
942
  }
861
943
  continue;
@@ -869,10 +951,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
869
951
  const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
870
952
  const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
871
953
  const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
872
- if (sid) addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
873
- wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" });
954
+ if (sid) await addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
955
+ wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: await getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" });
874
956
  state.lastChatMetrics = { durationMs: sdkMsg.duration_ms, costUsd: sdkMsg.total_cost_usd, inputTokens, outputTokens, model, turns: sdkMsg.num_turns, isError: false };
875
- if (state.resolvedSid) addMessage(state.resolvedSid, "result", JSON.stringify({ duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" }), chatId || null);
957
+ if (state.resolvedSid) await addMessage(state.resolvedSid, "result", JSON.stringify({ duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" }), chatId || null);
876
958
  } else if (sdkMsg.subtype === "error_max_turns") {
877
959
  // Max turns reached — treat as a normal completion with a notice
878
960
  const sid = state.resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
@@ -881,8 +963,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
881
963
  const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
882
964
  const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
883
965
  const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
884
- if (sid) addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "error_max_turns", isError: 0, cacheReadTokens, cacheCreationTokens });
885
- wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "error_max_turns" });
966
+ if (sid) await addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "error_max_turns", isError: 0, cacheReadTokens, cacheCreationTokens });
967
+ wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: await getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "error_max_turns" });
886
968
  wsSend({ type: "error", error: `Reached max turns limit (${sdkMsg.num_turns}). Send another message to continue.` });
887
969
  } else if (sdkMsg.subtype?.startsWith("error")) {
888
970
  const errMsg = sdkMsg.errors?.join(", ") || sdkMsg.error || sdkMsg.message || "Unknown error";
@@ -898,8 +980,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
898
980
  const sid = state.resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
899
981
  state.lastChatMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: true, error: errMsg };
900
982
  if (sid) {
901
- addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
902
- addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null);
983
+ await addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
984
+ await addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null);
903
985
  }
904
986
  wsSend({ type: "error", error: errMsg });
905
987
  }
@@ -915,7 +997,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
915
997
  wsSend({ type: "tool_result", ...wirePayload });
916
998
  if (state.resolvedSid) {
917
999
  const dbPayload = { toolUseId: block.tool_use_id, content: text.slice(0, 10000), isError: block.is_error || false };
918
- addMessage(state.resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null);
1000
+ await addMessage(state.resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null);
919
1001
  }
920
1002
  }
921
1003
  }
@@ -929,7 +1011,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
929
1011
  wsSend({ type: "done" });
930
1012
  } catch (err) {
931
1013
  if (err.name === "AbortError") {
932
- if (state.resolvedSid) addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
1014
+ if (state.resolvedSid) await addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
933
1015
  wsSend({ type: "aborted" });
934
1016
  } else {
935
1017
  const stderrOutput = stderrChunks.join("");
@@ -944,7 +1026,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
944
1026
  wsSend({ type: "done" });
945
1027
  } catch (retryErr) {
946
1028
  if (retryErr.name === "AbortError") {
947
- if (state.resolvedSid) addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
1029
+ if (state.resolvedSid) await addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
948
1030
  wsSend({ type: "aborted" });
949
1031
  } else {
950
1032
  console.error("Query retry error:", retryErr.message);
@@ -960,7 +1042,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
960
1042
  activeQueries.delete(queryKey);
961
1043
  unregisterGlobalQuery(state.resolvedSid, queryKey);
962
1044
  // Send push notification when query completes
963
- const session = state.resolvedSid ? getSession(state.resolvedSid) : null;
1045
+ const session = state.resolvedSid ? await getSession(state.resolvedSid) : null;
964
1046
  const pushTitle = session?.title || "Session complete";
965
1047
  sendPushNotification("Claudeck", pushTitle, `chat-${state.resolvedSid}`);
966
1048
 
@@ -1008,10 +1090,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
1008
1090
  if (cwd && state.lastAssistantText) {
1009
1091
  try {
1010
1092
  // 1. Parse explicit ```memory blocks (Claude-requested saves)
1011
- const explicitCount = saveExplicitMemories(cwd, state.lastAssistantText, state.resolvedSid);
1093
+ const explicitCount = await saveExplicitMemories(cwd, state.lastAssistantText, state.resolvedSid);
1012
1094
 
1013
1095
  // 2. Heuristic extraction from assistant text
1014
- const autoCount = captureMemories(cwd, state.lastAssistantText, state.resolvedSid, null);
1096
+ const autoCount = await captureMemories(cwd, state.lastAssistantText, state.resolvedSid, null);
1015
1097
 
1016
1098
  const totalCaptured = explicitCount + autoCount;
1017
1099
  if (totalCaptured > 0) {
@@ -1021,6 +1103,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
1021
1103
  const payload = { type: "memories_captured", count: totalCaptured, explicit: explicitCount, auto: autoCount };
1022
1104
  if (chatId) payload.chatId = chatId;
1023
1105
  ws.send(JSON.stringify(payload));
1106
+ if (state.resolvedSid) broadcastToSession(state.resolvedSid, payload, ws);
1024
1107
  }
1025
1108
  }
1026
1109
  } catch (e) { console.error("Memory capture error:", e.message); }
@@ -1029,13 +1112,13 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
1029
1112
  // Worktree post-completion: auto-commit, diff stats, notify
1030
1113
  if (worktreeRecord) {
1031
1114
  try {
1032
- if (state.resolvedSid) updateWorktreeSession(worktreeRecord.id, state.resolvedSid);
1115
+ if (state.resolvedSid) await updateWorktreeSession(worktreeRecord.id, state.resolvedSid);
1033
1116
 
1034
1117
  const commitMsg = `claudeck: ${(message || "worktree changes").slice(0, 72)}`;
1035
1118
  await autoCommitWorktree(worktreeRecord.worktreePath, commitMsg);
1036
1119
 
1037
1120
  const stats = await getWorktreeDiffStats(worktreeRecord.worktreePath, worktreeRecord.baseBranch);
1038
- updateWorktreeStatus(worktreeRecord.id, "completed");
1121
+ await updateWorktreeStatus(worktreeRecord.id, "completed");
1039
1122
 
1040
1123
  if (ws.readyState === 1) {
1041
1124
  const wtPayload = {
@@ -1046,9 +1129,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
1046
1129
  };
1047
1130
  if (chatId) wtPayload.chatId = chatId;
1048
1131
  ws.send(JSON.stringify(wtPayload));
1132
+ if (state.resolvedSid) broadcastToSession(state.resolvedSid, wtPayload, ws);
1049
1133
  }
1050
1134
 
1051
- logNotification(
1135
+ await logNotification(
1052
1136
  "worktree",
1053
1137
  `Worktree "${worktreeRecord.branchName}" ready`,
1054
1138
  `+${stats.insertions} -${stats.deletions} lines in ${stats.files} file(s)`,
@@ -1068,12 +1152,26 @@ export function setupWebSocket(wss, sessionIds) {
1068
1152
  wss.on("connection", (ws) => {
1069
1153
  const ctx = { ws, sessionIds, activeQueries: new Map(), pendingApprovals: new Map() };
1070
1154
 
1071
- ws.on("close", () => handleClose(ctx));
1155
+ ws.on("close", () => {
1156
+ leaveRoom(ws);
1157
+ handleClose(ctx);
1158
+ });
1072
1159
 
1073
1160
  ws.on("message", async (raw) => {
1074
1161
  let msg;
1075
1162
  try { msg = JSON.parse(raw); } catch { return; }
1076
1163
 
1164
+ // Session broadcast: subscribe/unsubscribe
1165
+ if (msg.type === "subscribe") {
1166
+ leaveRoom(ws); // leave any previous room first
1167
+ if (msg.sessionId) joinRoom(msg.sessionId, ws);
1168
+ return;
1169
+ }
1170
+ if (msg.type === "unsubscribe") {
1171
+ leaveRoom(ws);
1172
+ return;
1173
+ }
1174
+
1077
1175
  if (msg.type === "abort") return handleAbort(msg, ctx);
1078
1176
  if (msg.type === "permission_response") return handlePermissionResponse(msg, ctx);
1079
1177
  if (msg.type === "workflow") return handleWorkflow(msg, ctx);
package/server.js CHANGED
@@ -88,7 +88,7 @@ const sessionIds = new Map();
88
88
  for (const row of rows) {
89
89
  sessionIds.set(row.id, row.claude_session_id);
90
90
  }
91
- const csRows = allClaudeSessions();
91
+ const csRows = await allClaudeSessions();
92
92
  for (const row of csRows) {
93
93
  const key = row.chat_id ? `${row.session_id}::${row.chat_id}` : row.session_id;
94
94
  sessionIds.set(key, row.claude_session_id);
@@ -214,7 +214,7 @@ ${isAuthEnabled() ? ` \x1b[2m➜ Auth:\x1b[0m \x1b[33menabled\x1b[0m\n \x1
214
214
  });
215
215
 
216
216
  // Purge old notifications once per day
217
- setInterval(() => purgeOldNotifications(90), 24 * 60 * 60 * 1000);
217
+ setInterval(async () => { try { await purgeOldNotifications(90); } catch {} }, 24 * 60 * 60 * 1000);
218
218
 
219
219
  // Graceful shutdown
220
220
  process.on("SIGINT", () => {