claudeck 1.3.0 → 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 +27 -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 +16 -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 +153 -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) } });
|
|
@@ -421,10 +488,11 @@ export async function handleWorkflow(msg, { ws, sessionIds, activeQueries, pendi
|
|
|
421
488
|
abortController,
|
|
422
489
|
maxTurns: 30,
|
|
423
490
|
executable: execPath,
|
|
491
|
+
settingSources: ["user", "project", "local"],
|
|
424
492
|
};
|
|
425
493
|
|
|
426
494
|
if (!useBypass && !usePlan) {
|
|
427
|
-
stepOpts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, null, `Workflow: ${workflow.title}
|
|
495
|
+
stepOpts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, null, `Workflow: ${workflow.title}`, () => clientSid);
|
|
428
496
|
}
|
|
429
497
|
if (wfModel) stepOpts.model = resolveModel(wfModel);
|
|
430
498
|
|
|
@@ -511,6 +579,7 @@ export async function handleAgentChain(msg, { ws, sessionIds, activeQueries, pen
|
|
|
511
579
|
function chainSend(payload) {
|
|
512
580
|
if (ws.readyState !== 1) return;
|
|
513
581
|
ws.send(JSON.stringify(payload));
|
|
582
|
+
if (clientSid) broadcastToSession(clientSid, payload, ws);
|
|
514
583
|
}
|
|
515
584
|
|
|
516
585
|
chainSend({
|
|
@@ -627,7 +696,9 @@ export async function handleOrchestrate(msg, { ws, sessionIds, activeQueries, pe
|
|
|
627
696
|
try {
|
|
628
697
|
agents = JSON.parse(await readFile(configPath("agents.json"), "utf-8"));
|
|
629
698
|
} catch {
|
|
630
|
-
|
|
699
|
+
const errPayload = { type: "error", error: "Failed to load agents" };
|
|
700
|
+
ws.send(JSON.stringify(errPayload));
|
|
701
|
+
if (clientSid) broadcastToSession(clientSid, errPayload, ws);
|
|
631
702
|
return;
|
|
632
703
|
}
|
|
633
704
|
|
|
@@ -653,11 +724,13 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
653
724
|
|
|
654
725
|
// Handle /remember command — save memory and respond without calling Claude
|
|
655
726
|
if (message && message.trim().toLowerCase().startsWith('/remember ') && cwd) {
|
|
656
|
-
const result = parseRememberCommand(message, cwd, clientSid);
|
|
727
|
+
const result = await parseRememberCommand(message, cwd, clientSid);
|
|
657
728
|
function remSend(payload) {
|
|
658
729
|
if (ws.readyState !== 1) return;
|
|
659
730
|
if (chatId) payload.chatId = chatId;
|
|
731
|
+
if (clientSid) payload.sessionId = clientSid;
|
|
660
732
|
ws.send(JSON.stringify(payload));
|
|
733
|
+
if (clientSid) broadcastToSession(clientSid, payload, ws);
|
|
661
734
|
}
|
|
662
735
|
if (result) {
|
|
663
736
|
remSend({ type: "text", text: result.saved
|
|
@@ -676,8 +749,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
676
749
|
const sessionKey = chatId ? `${clientSid}::${chatId}` : clientSid;
|
|
677
750
|
const resumeId = clientSid ? sessionIds.get(sessionKey) : undefined;
|
|
678
751
|
|
|
679
|
-
if (clientSid && getSession(clientSid)) {
|
|
680
|
-
touchSession(clientSid);
|
|
752
|
+
if (clientSid && await getSession(clientSid)) {
|
|
753
|
+
await touchSession(clientSid);
|
|
681
754
|
}
|
|
682
755
|
|
|
683
756
|
const abortController = new AbortController();
|
|
@@ -700,7 +773,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
700
773
|
const wtResult = await createWorktree(cwd, branchName);
|
|
701
774
|
const wtId = crypto.randomUUID();
|
|
702
775
|
|
|
703
|
-
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));
|
|
704
777
|
worktreeRecord = { id: wtId, worktreePath: wtResult.worktreePath, branchName, baseBranch };
|
|
705
778
|
effectiveCwd = wtResult.worktreePath;
|
|
706
779
|
|
|
@@ -709,6 +782,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
709
782
|
const wtPayload = { type: "worktree_created", worktreeId: wtId, branchName, baseBranch, worktreePath: wtResult.worktreePath };
|
|
710
783
|
if (chatId) wtPayload.chatId = chatId;
|
|
711
784
|
ws.send(JSON.stringify(wtPayload));
|
|
785
|
+
if (clientSid) broadcastToSession(clientSid, wtPayload, ws);
|
|
712
786
|
}
|
|
713
787
|
} catch (err) {
|
|
714
788
|
console.error("Worktree creation failed:", err.message, err.stack);
|
|
@@ -730,11 +804,12 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
730
804
|
abortController,
|
|
731
805
|
executable: execPath,
|
|
732
806
|
stderr: (text) => stderrChunks.push(text),
|
|
807
|
+
settingSources: ["user", "project", "local"],
|
|
733
808
|
};
|
|
734
809
|
if (effectiveMaxTurns) opts.maxTurns = effectiveMaxTurns;
|
|
735
810
|
|
|
736
811
|
if (!useBypass && !usePlan) {
|
|
737
|
-
opts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, chatId, projectName || "Chat");
|
|
812
|
+
opts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, chatId, projectName || "Chat", () => state.resolvedSid);
|
|
738
813
|
}
|
|
739
814
|
if (chatModel) opts.model = resolveModel(chatModel);
|
|
740
815
|
if (Array.isArray(disabledTools) && disabledTools.length > 0) {
|
|
@@ -752,10 +827,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
752
827
|
(opts.appendSystemPrompt ? '\n\n' : '') + systemPrompt;
|
|
753
828
|
}
|
|
754
829
|
// Run memory maintenance (decay stale, clean expired) on each session
|
|
755
|
-
if (cwd) runMaintenance(cwd);
|
|
830
|
+
if (cwd) await runMaintenance(cwd);
|
|
756
831
|
// Inject persistent memories for this project (smart: uses user message for relevance)
|
|
757
832
|
if (cwd) {
|
|
758
|
-
const { prompt: memPrompt, count: memCount, memories: memList } = buildMemoryPrompt(cwd, 10, message);
|
|
833
|
+
const { prompt: memPrompt, count: memCount, memories: memList } = await buildMemoryPrompt(cwd, 10, message);
|
|
759
834
|
if (memPrompt) {
|
|
760
835
|
opts.appendSystemPrompt = (opts.appendSystemPrompt || '') +
|
|
761
836
|
(opts.appendSystemPrompt ? '\n\n' : '') + memPrompt;
|
|
@@ -776,6 +851,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
776
851
|
const payload = { type: "memories_injected", count: memCount, memories: memList };
|
|
777
852
|
if (chatId) payload.chatId = chatId;
|
|
778
853
|
ws.send(JSON.stringify(payload));
|
|
854
|
+
if (clientSid) broadcastToSession(clientSid, payload, ws);
|
|
779
855
|
}
|
|
780
856
|
} else {
|
|
781
857
|
console.log(`\n══════ MEMORY INJECTION ══════`);
|
|
@@ -793,6 +869,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
793
869
|
if (chatId) payload.chatId = chatId;
|
|
794
870
|
if (state.resolvedSid) payload.sessionId = state.resolvedSid;
|
|
795
871
|
ws.send(JSON.stringify(payload));
|
|
872
|
+
// Broadcast to other clients watching this session
|
|
873
|
+
if (state.resolvedSid) broadcastToSession(state.resolvedSid, payload, ws);
|
|
796
874
|
}
|
|
797
875
|
|
|
798
876
|
// Register for global tracking if we already know the session
|
|
@@ -817,14 +895,14 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
817
895
|
const sKey = chatId ? `${ourSid}::${chatId}` : ourSid;
|
|
818
896
|
sessionIds.set(sKey, claudeSessionId);
|
|
819
897
|
|
|
820
|
-
if (!getSession(ourSid)) {
|
|
821
|
-
createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
|
|
898
|
+
if (!await getSession(ourSid)) {
|
|
899
|
+
await createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
|
|
822
900
|
} else {
|
|
823
|
-
updateClaudeSessionId(ourSid, claudeSessionId);
|
|
901
|
+
await updateClaudeSessionId(ourSid, claudeSessionId);
|
|
824
902
|
}
|
|
825
903
|
|
|
826
904
|
if (chatId) {
|
|
827
|
-
setClaudeSession(ourSid, chatId, claudeSessionId);
|
|
905
|
+
await setClaudeSession(ourSid, chatId, claudeSessionId);
|
|
828
906
|
}
|
|
829
907
|
|
|
830
908
|
wsSend({ type: "session", sessionId: ourSid });
|
|
@@ -832,15 +910,21 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
832
910
|
if (images?.length) {
|
|
833
911
|
userMsgData.images = images.map(i => ({ name: i.name, data: i.data, mimeType: i.mimeType }));
|
|
834
912
|
}
|
|
835
|
-
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);
|
|
836
920
|
|
|
837
921
|
// Register global query tracking now that we know the session
|
|
838
922
|
if (!clientSid) registerGlobalQuery(state.resolvedSid, queryKey);
|
|
839
923
|
|
|
840
|
-
const existingSession = getSession(ourSid);
|
|
924
|
+
const existingSession = await getSession(ourSid);
|
|
841
925
|
if (existingSession && !existingSession.title) {
|
|
842
926
|
const title = message.slice(0, 100).split("\n")[0];
|
|
843
|
-
updateSessionTitle(ourSid, title);
|
|
927
|
+
await updateSessionTitle(ourSid, title);
|
|
844
928
|
}
|
|
845
929
|
continue;
|
|
846
930
|
}
|
|
@@ -850,10 +934,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
850
934
|
if (block.type === "text" && block.text) {
|
|
851
935
|
state.lastAssistantText += (state.lastAssistantText ? "\n\n" : "") + block.text;
|
|
852
936
|
wsSend({ type: "text", text: block.text });
|
|
853
|
-
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);
|
|
854
938
|
} else if (block.type === "tool_use") {
|
|
855
939
|
wsSend({ type: "tool", id: block.id, name: block.name, input: block.input });
|
|
856
|
-
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);
|
|
857
941
|
}
|
|
858
942
|
}
|
|
859
943
|
continue;
|
|
@@ -867,10 +951,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
867
951
|
const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
|
|
868
952
|
const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
|
|
869
953
|
const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
|
|
870
|
-
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 });
|
|
871
|
-
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" });
|
|
872
956
|
state.lastChatMetrics = { durationMs: sdkMsg.duration_ms, costUsd: sdkMsg.total_cost_usd, inputTokens, outputTokens, model, turns: sdkMsg.num_turns, isError: false };
|
|
873
|
-
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);
|
|
874
958
|
} else if (sdkMsg.subtype === "error_max_turns") {
|
|
875
959
|
// Max turns reached — treat as a normal completion with a notice
|
|
876
960
|
const sid = state.resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
|
|
@@ -879,8 +963,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
879
963
|
const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
|
|
880
964
|
const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
|
|
881
965
|
const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
|
|
882
|
-
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 });
|
|
883
|
-
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" });
|
|
884
968
|
wsSend({ type: "error", error: `Reached max turns limit (${sdkMsg.num_turns}). Send another message to continue.` });
|
|
885
969
|
} else if (sdkMsg.subtype?.startsWith("error")) {
|
|
886
970
|
const errMsg = sdkMsg.errors?.join(", ") || sdkMsg.error || sdkMsg.message || "Unknown error";
|
|
@@ -896,8 +980,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
896
980
|
const sid = state.resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
|
|
897
981
|
state.lastChatMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: true, error: errMsg };
|
|
898
982
|
if (sid) {
|
|
899
|
-
addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
|
|
900
|
-
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);
|
|
901
985
|
}
|
|
902
986
|
wsSend({ type: "error", error: errMsg });
|
|
903
987
|
}
|
|
@@ -913,7 +997,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
913
997
|
wsSend({ type: "tool_result", ...wirePayload });
|
|
914
998
|
if (state.resolvedSid) {
|
|
915
999
|
const dbPayload = { toolUseId: block.tool_use_id, content: text.slice(0, 10000), isError: block.is_error || false };
|
|
916
|
-
addMessage(state.resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null);
|
|
1000
|
+
await addMessage(state.resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null);
|
|
917
1001
|
}
|
|
918
1002
|
}
|
|
919
1003
|
}
|
|
@@ -927,7 +1011,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
927
1011
|
wsSend({ type: "done" });
|
|
928
1012
|
} catch (err) {
|
|
929
1013
|
if (err.name === "AbortError") {
|
|
930
|
-
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);
|
|
931
1015
|
wsSend({ type: "aborted" });
|
|
932
1016
|
} else {
|
|
933
1017
|
const stderrOutput = stderrChunks.join("");
|
|
@@ -942,7 +1026,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
942
1026
|
wsSend({ type: "done" });
|
|
943
1027
|
} catch (retryErr) {
|
|
944
1028
|
if (retryErr.name === "AbortError") {
|
|
945
|
-
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);
|
|
946
1030
|
wsSend({ type: "aborted" });
|
|
947
1031
|
} else {
|
|
948
1032
|
console.error("Query retry error:", retryErr.message);
|
|
@@ -958,7 +1042,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
958
1042
|
activeQueries.delete(queryKey);
|
|
959
1043
|
unregisterGlobalQuery(state.resolvedSid, queryKey);
|
|
960
1044
|
// Send push notification when query completes
|
|
961
|
-
const session = state.resolvedSid ? getSession(state.resolvedSid) : null;
|
|
1045
|
+
const session = state.resolvedSid ? await getSession(state.resolvedSid) : null;
|
|
962
1046
|
const pushTitle = session?.title || "Session complete";
|
|
963
1047
|
sendPushNotification("Claudeck", pushTitle, `chat-${state.resolvedSid}`);
|
|
964
1048
|
|
|
@@ -1006,10 +1090,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
1006
1090
|
if (cwd && state.lastAssistantText) {
|
|
1007
1091
|
try {
|
|
1008
1092
|
// 1. Parse explicit ```memory blocks (Claude-requested saves)
|
|
1009
|
-
const explicitCount = saveExplicitMemories(cwd, state.lastAssistantText, state.resolvedSid);
|
|
1093
|
+
const explicitCount = await saveExplicitMemories(cwd, state.lastAssistantText, state.resolvedSid);
|
|
1010
1094
|
|
|
1011
1095
|
// 2. Heuristic extraction from assistant text
|
|
1012
|
-
const autoCount = captureMemories(cwd, state.lastAssistantText, state.resolvedSid, null);
|
|
1096
|
+
const autoCount = await captureMemories(cwd, state.lastAssistantText, state.resolvedSid, null);
|
|
1013
1097
|
|
|
1014
1098
|
const totalCaptured = explicitCount + autoCount;
|
|
1015
1099
|
if (totalCaptured > 0) {
|
|
@@ -1019,6 +1103,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
1019
1103
|
const payload = { type: "memories_captured", count: totalCaptured, explicit: explicitCount, auto: autoCount };
|
|
1020
1104
|
if (chatId) payload.chatId = chatId;
|
|
1021
1105
|
ws.send(JSON.stringify(payload));
|
|
1106
|
+
if (state.resolvedSid) broadcastToSession(state.resolvedSid, payload, ws);
|
|
1022
1107
|
}
|
|
1023
1108
|
}
|
|
1024
1109
|
} catch (e) { console.error("Memory capture error:", e.message); }
|
|
@@ -1027,13 +1112,13 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
1027
1112
|
// Worktree post-completion: auto-commit, diff stats, notify
|
|
1028
1113
|
if (worktreeRecord) {
|
|
1029
1114
|
try {
|
|
1030
|
-
if (state.resolvedSid) updateWorktreeSession(worktreeRecord.id, state.resolvedSid);
|
|
1115
|
+
if (state.resolvedSid) await updateWorktreeSession(worktreeRecord.id, state.resolvedSid);
|
|
1031
1116
|
|
|
1032
1117
|
const commitMsg = `claudeck: ${(message || "worktree changes").slice(0, 72)}`;
|
|
1033
1118
|
await autoCommitWorktree(worktreeRecord.worktreePath, commitMsg);
|
|
1034
1119
|
|
|
1035
1120
|
const stats = await getWorktreeDiffStats(worktreeRecord.worktreePath, worktreeRecord.baseBranch);
|
|
1036
|
-
updateWorktreeStatus(worktreeRecord.id, "completed");
|
|
1121
|
+
await updateWorktreeStatus(worktreeRecord.id, "completed");
|
|
1037
1122
|
|
|
1038
1123
|
if (ws.readyState === 1) {
|
|
1039
1124
|
const wtPayload = {
|
|
@@ -1044,9 +1129,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
1044
1129
|
};
|
|
1045
1130
|
if (chatId) wtPayload.chatId = chatId;
|
|
1046
1131
|
ws.send(JSON.stringify(wtPayload));
|
|
1132
|
+
if (state.resolvedSid) broadcastToSession(state.resolvedSid, wtPayload, ws);
|
|
1047
1133
|
}
|
|
1048
1134
|
|
|
1049
|
-
logNotification(
|
|
1135
|
+
await logNotification(
|
|
1050
1136
|
"worktree",
|
|
1051
1137
|
`Worktree "${worktreeRecord.branchName}" ready`,
|
|
1052
1138
|
`+${stats.insertions} -${stats.deletions} lines in ${stats.files} file(s)`,
|
|
@@ -1066,12 +1152,26 @@ export function setupWebSocket(wss, sessionIds) {
|
|
|
1066
1152
|
wss.on("connection", (ws) => {
|
|
1067
1153
|
const ctx = { ws, sessionIds, activeQueries: new Map(), pendingApprovals: new Map() };
|
|
1068
1154
|
|
|
1069
|
-
ws.on("close", () =>
|
|
1155
|
+
ws.on("close", () => {
|
|
1156
|
+
leaveRoom(ws);
|
|
1157
|
+
handleClose(ctx);
|
|
1158
|
+
});
|
|
1070
1159
|
|
|
1071
1160
|
ws.on("message", async (raw) => {
|
|
1072
1161
|
let msg;
|
|
1073
1162
|
try { msg = JSON.parse(raw); } catch { return; }
|
|
1074
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
|
+
|
|
1075
1175
|
if (msg.type === "abort") return handleAbort(msg, ctx);
|
|
1076
1176
|
if (msg.type === "permission_response") return handlePermissionResponse(msg, ctx);
|
|
1077
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", () => {
|