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.
- package/README.md +7 -3
- package/db/sqlite.js +1697 -0
- package/db.js +3 -1645
- package/package.json +2 -1
- package/plugins/tasks/server.js +21 -21
- package/public/css/ui/messages.css +25 -0
- package/public/js/core/api.js +23 -6
- package/public/js/core/ws.js +12 -0
- package/public/js/features/chat.js +4 -0
- package/public/js/features/sessions.js +102 -10
- package/public/js/ui/messages.js +42 -0
- package/public/js/ui/parallel.js +2 -4
- package/server/agent-loop.js +26 -26
- package/server/memory-extractor.js +4 -4
- package/server/memory-injector.js +11 -11
- package/server/memory-optimizer.js +2 -2
- package/server/notification-logger.js +5 -5
- package/server/orchestrator.js +15 -15
- package/server/push-sender.js +2 -2
- package/server/routes/agents.js +2 -2
- package/server/routes/memory.js +20 -20
- package/server/routes/messages.js +41 -10
- package/server/routes/notifications.js +20 -20
- package/server/routes/sessions.js +17 -17
- package/server/routes/stats.js +37 -37
- package/server/routes/worktrees.js +9 -9
- package/server/summarizer.js +3 -3
- package/server/ws-handler.js +151 -53
- package/server.js +2 -2
package/server/ws-handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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", () =>
|
|
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", () => {
|