claudeck 1.1.1 → 1.2.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 +30 -4
- package/config/skillsmp-config.json +5 -0
- package/db.js +248 -0
- package/package.json +11 -2
- package/public/css/panels/git-panel.css +220 -0
- package/public/css/panels/skills-manager.css +975 -0
- package/public/css/ui/input-history.css +109 -0
- package/public/css/ui/messages.css +51 -0
- package/public/css/ui/notification-bell.css +421 -0
- package/public/css/ui/sessions.css +41 -0
- package/public/css/ui/worktree.css +442 -0
- package/public/index.html +43 -10
- package/public/js/core/api.js +83 -0
- package/public/js/core/dom.js +15 -0
- package/public/js/features/background-sessions.js +11 -0
- package/public/js/features/chat.js +501 -3
- package/public/js/features/input-history.js +122 -0
- package/public/js/features/projects.js +16 -1
- package/public/js/features/sessions.js +77 -30
- package/public/js/main.js +3 -0
- package/public/js/panels/git-panel.js +385 -6
- package/public/js/panels/skills-manager.js +1005 -0
- package/public/js/ui/messages.js +58 -0
- package/public/js/ui/notification-bell.js +240 -0
- package/public/js/ui/notification-history.js +210 -0
- package/public/js/ui/parallel.js +11 -0
- package/public/js/ui/tab-sdk.js +1 -1
- package/public/style.css +4 -0
- package/server/agent-loop.js +13 -0
- package/server/notification-logger.js +27 -0
- package/server/routes/notifications.js +57 -1
- package/server/routes/sessions.js +41 -0
- package/server/routes/skills.js +454 -0
- package/server/routes/worktrees.js +93 -0
- package/server/utils/git-worktree.js +297 -0
- package/server/ws-handler.js +708 -629
- package/server.js +17 -1
package/server/ws-handler.js
CHANGED
|
@@ -14,18 +14,23 @@ import {
|
|
|
14
14
|
updateSessionTitle,
|
|
15
15
|
getTopMemories,
|
|
16
16
|
touchMemory,
|
|
17
|
+
createWorktreeRecord,
|
|
18
|
+
updateWorktreeStatus,
|
|
19
|
+
updateWorktreeSession,
|
|
17
20
|
} from "../db.js";
|
|
18
21
|
import { getProjectSystemPrompt } from "./routes/projects.js";
|
|
19
22
|
import { buildMemoryPrompt, parseRememberCommand, saveExplicitMemories } from "./memory-injector.js";
|
|
20
23
|
import { captureMemories, runMaintenance } from "./memory-extractor.js";
|
|
24
|
+
import { createWorktree, generateBranchName, getCurrentBranch, autoCommitWorktree, getWorktreeDiffStats } from "./utils/git-worktree.js";
|
|
25
|
+
import { logNotification } from "./notification-logger.js";
|
|
21
26
|
|
|
22
27
|
// Map short model names to current model IDs
|
|
23
|
-
const MODEL_MAP = {
|
|
28
|
+
export const MODEL_MAP = {
|
|
24
29
|
haiku: "claude-haiku-4-5-20251001",
|
|
25
30
|
sonnet: "claude-sonnet-4-6",
|
|
26
31
|
opus: "claude-opus-4-6",
|
|
27
32
|
};
|
|
28
|
-
function resolveModel(name) {
|
|
33
|
+
export function resolveModel(name) {
|
|
29
34
|
if (!name) return undefined;
|
|
30
35
|
return MODEL_MAP[name] || name;
|
|
31
36
|
}
|
|
@@ -38,14 +43,14 @@ import { runOrchestrator } from "./orchestrator.js";
|
|
|
38
43
|
import { runDag } from "./dag-executor.js";
|
|
39
44
|
|
|
40
45
|
// Tools that are read-only and safe to auto-approve in "confirmDangerous" mode
|
|
41
|
-
const READ_ONLY_TOOLS = new Set([
|
|
46
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
42
47
|
"Read", "Glob", "Grep", "WebSearch", "WebFetch", "Agent",
|
|
43
48
|
"TodoRead", "TaskRead", "NotebookRead", "LS", "View", "ListFiles",
|
|
44
49
|
"TaskList", "TaskGet",
|
|
45
50
|
]);
|
|
46
51
|
|
|
47
52
|
const DEFAULT_APPROVAL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes (web)
|
|
48
|
-
function getApprovalTimeoutMs() {
|
|
53
|
+
export function getApprovalTimeoutMs() {
|
|
49
54
|
const cfg = getTelegramConfig();
|
|
50
55
|
if (telegramEnabled()) {
|
|
51
56
|
return (cfg.afkTimeoutMinutes || 15) * 60 * 1000;
|
|
@@ -57,7 +62,7 @@ function getApprovalTimeoutMs() {
|
|
|
57
62
|
// Key: sessionId, Value: Set<queryKey>
|
|
58
63
|
const globalActiveQueries = new Map();
|
|
59
64
|
|
|
60
|
-
function registerGlobalQuery(sessionId, queryKey) {
|
|
65
|
+
export function registerGlobalQuery(sessionId, queryKey) {
|
|
61
66
|
if (!sessionId) return;
|
|
62
67
|
if (!globalActiveQueries.has(sessionId)) {
|
|
63
68
|
globalActiveQueries.set(sessionId, new Set());
|
|
@@ -65,7 +70,7 @@ function registerGlobalQuery(sessionId, queryKey) {
|
|
|
65
70
|
globalActiveQueries.get(sessionId).add(queryKey);
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
function unregisterGlobalQuery(sessionId, queryKey) {
|
|
73
|
+
export function unregisterGlobalQuery(sessionId, queryKey) {
|
|
69
74
|
if (!sessionId) return;
|
|
70
75
|
const set = globalActiveQueries.get(sessionId);
|
|
71
76
|
if (set) {
|
|
@@ -139,7 +144,7 @@ export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, ses
|
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
// Shared SDK stream processor — deduplicates chat and workflow message parsing
|
|
142
|
-
async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, chatId, cwd, projectName, isWorkflow, stepLabel, workflowId, stepIndex }) {
|
|
147
|
+
export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, chatId, cwd, projectName, isWorkflow, stepLabel, workflowId, stepIndex }) {
|
|
143
148
|
let claudeSessionId = null;
|
|
144
149
|
let resolvedSid = clientSid;
|
|
145
150
|
let sessionModel = null;
|
|
@@ -309,698 +314,772 @@ async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, chatId,
|
|
|
309
314
|
return { claudeSessionId, resolvedSid, lastMetrics };
|
|
310
315
|
}
|
|
311
316
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
});
|
|
317
|
+
// ── Pure function: build prompt with optional images ──────────────────────
|
|
318
|
+
export function buildPrompt(text, imgs) {
|
|
319
|
+
if (!imgs?.length) return text;
|
|
320
|
+
return (async function*() {
|
|
321
|
+
yield {
|
|
322
|
+
type: "user",
|
|
323
|
+
message: { role: "user", content: [
|
|
324
|
+
{ type: "text", text },
|
|
325
|
+
...imgs.map(img => ({
|
|
326
|
+
type: "image",
|
|
327
|
+
source: { type: "base64", media_type: img.mimeType, data: img.data },
|
|
328
|
+
})),
|
|
329
|
+
]},
|
|
330
|
+
parent_tool_use_id: null,
|
|
331
|
+
session_id: "",
|
|
332
|
+
};
|
|
333
|
+
})();
|
|
334
|
+
}
|
|
331
335
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
336
|
+
// ── Extracted handler: close ──────────────────────────────────────────────
|
|
337
|
+
export function handleClose({ activeQueries, pendingApprovals }) {
|
|
338
|
+
// Abort all active SDK streams first (they may be blocked on approval)
|
|
339
|
+
for (const [, q] of activeQueries) {
|
|
340
|
+
q.abort();
|
|
341
|
+
}
|
|
342
|
+
activeQueries.clear();
|
|
339
343
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
for (const q of activeQueries.values()) q.abort();
|
|
347
|
-
activeQueries.clear();
|
|
348
|
-
}
|
|
349
|
-
// Also deny any pending approvals on abort
|
|
350
|
-
for (const [id, { resolve, timer }] of pendingApprovals) {
|
|
351
|
-
clearTimeout(timer);
|
|
352
|
-
resolve({ behavior: "deny", message: "Aborted by user" });
|
|
353
|
-
}
|
|
354
|
-
pendingApprovals.clear();
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
344
|
+
for (const [id, { resolve, timer }] of pendingApprovals) {
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
resolve({ behavior: "deny", message: "Client disconnected" });
|
|
347
|
+
}
|
|
348
|
+
pendingApprovals.clear();
|
|
349
|
+
}
|
|
357
350
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
351
|
+
// ── Extracted handler: abort ──────────────────────────────────────────────
|
|
352
|
+
export function handleAbort(msg, { activeQueries, pendingApprovals }) {
|
|
353
|
+
if (msg.chatId) {
|
|
354
|
+
const q = activeQueries.get(msg.chatId);
|
|
355
|
+
if (q) { q.abort(); activeQueries.delete(msg.chatId); }
|
|
356
|
+
} else {
|
|
357
|
+
for (const q of activeQueries.values()) q.abort();
|
|
358
|
+
activeQueries.clear();
|
|
359
|
+
}
|
|
360
|
+
// Also deny any pending approvals on abort
|
|
361
|
+
for (const [id, { resolve, timer }] of pendingApprovals) {
|
|
362
|
+
clearTimeout(timer);
|
|
363
|
+
resolve({ behavior: "deny", message: "Aborted by user" });
|
|
364
|
+
}
|
|
365
|
+
pendingApprovals.clear();
|
|
366
|
+
}
|
|
374
367
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
368
|
+
// ── Extracted handler: permission response ────────────────────────────────
|
|
369
|
+
export function handlePermissionResponse(msg, { pendingApprovals }) {
|
|
370
|
+
const pending = pendingApprovals.get(msg.id);
|
|
371
|
+
if (pending) {
|
|
372
|
+
clearTimeout(pending.timer);
|
|
373
|
+
pendingApprovals.delete(msg.id);
|
|
374
|
+
if (msg.behavior === "allow") {
|
|
375
|
+
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
|
|
376
|
+
} else {
|
|
377
|
+
pending.resolve({ behavior: "deny", message: "Denied by user" });
|
|
378
|
+
}
|
|
379
|
+
// Update Telegram message to show it was resolved via web
|
|
380
|
+
markTelegramMessageResolved(msg.id, msg.behavior === "allow" ? "allow" : "deny").catch(() => {});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
379
383
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
+
// ── Extracted handler: workflow ────────────────────────────────────────────
|
|
385
|
+
export async function handleWorkflow(msg, { ws, sessionIds, activeQueries, pendingApprovals }) {
|
|
386
|
+
const { workflow, cwd, sessionId: clientSid, projectName, permissionMode: wfPermMode, model: wfModel } = msg;
|
|
387
|
+
if (!workflow || !workflow.steps) return;
|
|
384
388
|
|
|
385
|
-
|
|
389
|
+
function wfSend(payload) {
|
|
390
|
+
if (ws.readyState !== 1) return;
|
|
391
|
+
ws.send(JSON.stringify(payload));
|
|
392
|
+
}
|
|
386
393
|
|
|
387
|
-
|
|
388
|
-
const wfStepNames = workflow.steps.map((s, i) => ` ${i + 1}. ${s.label}`).join("\n");
|
|
389
|
-
sendTelegramNotification("start", "Workflow Started", `${workflow.title}\n\n${workflow.steps.length} steps:\n${wfStepNames}`);
|
|
394
|
+
wfSend({ type: "workflow_started", workflow: { id: workflow.id, title: workflow.title, steps: workflow.steps.map((s) => s.label) } });
|
|
390
395
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
let wfAborted = false;
|
|
396
|
+
// Telegram start notification
|
|
397
|
+
const wfStepNames = workflow.steps.map((s, i) => ` ${i + 1}. ${s.label}`).join("\n");
|
|
398
|
+
sendTelegramNotification("start", "Workflow Started", `${workflow.title}\n\n${workflow.steps.length} steps:\n${wfStepNames}`);
|
|
395
399
|
|
|
396
|
-
|
|
397
|
-
|
|
400
|
+
let resumeId = clientSid ? sessionIds.get(clientSid) : undefined;
|
|
401
|
+
let resolvedSid = clientSid;
|
|
402
|
+
const wfQueryKey = `wf-${workflow.id}-${Date.now()}`;
|
|
403
|
+
let wfAborted = false;
|
|
404
|
+
|
|
405
|
+
for (let i = 0; i < workflow.steps.length; i++) {
|
|
406
|
+
if (wfAborted || ws.readyState !== 1) break;
|
|
407
|
+
|
|
408
|
+
const step = workflow.steps[i];
|
|
409
|
+
wfSend({ type: "workflow_step", stepIndex: i, status: "running" });
|
|
410
|
+
|
|
411
|
+
const abortController = new AbortController();
|
|
412
|
+
activeQueries.set(wfQueryKey, { abort: () => abortController.abort() });
|
|
413
|
+
|
|
414
|
+
const effectivePermMode = wfPermMode || "bypass";
|
|
415
|
+
const useBypass = effectivePermMode === "bypass";
|
|
416
|
+
const usePlan = effectivePermMode === "plan";
|
|
417
|
+
const wfCwd = (cwd && existsSync(cwd)) ? cwd : homedir();
|
|
418
|
+
const stepOpts = {
|
|
419
|
+
cwd: wfCwd,
|
|
420
|
+
permissionMode: usePlan ? "plan" : (useBypass ? "bypassPermissions" : "default"),
|
|
421
|
+
abortController,
|
|
422
|
+
maxTurns: 30,
|
|
423
|
+
executable: execPath,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
if (!useBypass && !usePlan) {
|
|
427
|
+
stepOpts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, null, `Workflow: ${workflow.title}`);
|
|
428
|
+
}
|
|
429
|
+
if (wfModel) stepOpts.model = resolveModel(wfModel);
|
|
430
|
+
|
|
431
|
+
const projectPrompt = getProjectSystemPrompt(cwd);
|
|
432
|
+
if (projectPrompt) stepOpts.appendSystemPrompt = projectPrompt;
|
|
433
|
+
if (resumeId) stepOpts.resume = resumeId;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const q = query({ prompt: step.prompt, options: stepOpts });
|
|
437
|
+
const result = await processSdkStream(q, {
|
|
438
|
+
ws, wsSend: wfSend, sessionIds,
|
|
439
|
+
clientSid: resolvedSid, chatId: null,
|
|
440
|
+
cwd, projectName: projectName || "Workflow",
|
|
441
|
+
isWorkflow: true, stepLabel: step.label,
|
|
442
|
+
workflowId: workflow.id, stepIndex: i,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (result.resolvedSid) resolvedSid = result.resolvedSid;
|
|
446
|
+
if (result.claudeSessionId) resumeId = result.claudeSessionId;
|
|
447
|
+
|
|
448
|
+
if (i === 0 && result.resolvedSid && !clientSid) {
|
|
449
|
+
wfSend({ type: "session", sessionId: result.resolvedSid });
|
|
450
|
+
}
|
|
451
|
+
} catch (err) {
|
|
452
|
+
if (err.name === "AbortError" || abortController.signal.aborted) {
|
|
453
|
+
wfAborted = true;
|
|
454
|
+
wfSend({ type: "workflow_step", stepIndex: i, status: "aborted" });
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
wfSend({ type: "error", error: `Workflow step "${step.label}" failed: ${err.message}` });
|
|
458
|
+
sendTelegramNotification("error", "Workflow Step Failed", `${workflow.title}\n\nStep ${i + 1}/${workflow.steps.length}: ${step.label}\nError: ${err.message}`);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
398
461
|
|
|
399
|
-
|
|
400
|
-
|
|
462
|
+
wfSend({ type: "workflow_step", stepIndex: i, status: "completed" });
|
|
463
|
+
}
|
|
401
464
|
|
|
402
|
-
|
|
403
|
-
|
|
465
|
+
activeQueries.delete(wfQueryKey);
|
|
466
|
+
|
|
467
|
+
if (wfAborted) {
|
|
468
|
+
wfSend({ type: "workflow_completed", aborted: true });
|
|
469
|
+
wfSend({ type: "done" });
|
|
470
|
+
sendTelegramNotification("error", "Workflow Aborted", `${workflow.title}\nAborted during execution`);
|
|
471
|
+
} else {
|
|
472
|
+
wfSend({ type: "workflow_completed" });
|
|
473
|
+
wfSend({ type: "done" });
|
|
474
|
+
sendPushNotification("Claudeck", `Workflow "${workflow.title}" completed`, `wf-${resolvedSid}`);
|
|
475
|
+
const stepNames = workflow.steps.map((s, i) => ` ${i + 1}. ${s.label}`).join("\n");
|
|
476
|
+
sendTelegramNotification("workflow", "Workflow Completed", `${workflow.title}\n\nSteps:\n${stepNames}`, {
|
|
477
|
+
steps: workflow.steps.length,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
404
481
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
482
|
+
// ── Extracted handler: agent ──────────────────────────────────────────────
|
|
483
|
+
export async function handleAgent(msg, { ws, sessionIds, activeQueries, pendingApprovals }) {
|
|
484
|
+
const { agentDef, cwd, sessionId: clientSid, projectName, permissionMode: agentPermMode, model: agentModel, userContext } = msg;
|
|
485
|
+
if (!agentDef) return;
|
|
486
|
+
|
|
487
|
+
runAgent({
|
|
488
|
+
ws,
|
|
489
|
+
agentDef,
|
|
490
|
+
cwd,
|
|
491
|
+
sessionId: clientSid,
|
|
492
|
+
projectName,
|
|
493
|
+
permissionMode: agentPermMode,
|
|
494
|
+
model: agentModel,
|
|
495
|
+
sessionIds,
|
|
496
|
+
pendingApprovals,
|
|
497
|
+
makeCanUseTool,
|
|
498
|
+
userContext,
|
|
499
|
+
activeQueries,
|
|
500
|
+
runType: 'single',
|
|
501
|
+
}).catch(() => {}); // errors already handled inside runAgent
|
|
502
|
+
}
|
|
416
503
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const projectPrompt = getProjectSystemPrompt(cwd);
|
|
423
|
-
if (projectPrompt) stepOpts.appendSystemPrompt = projectPrompt;
|
|
424
|
-
if (resumeId) stepOpts.resume = resumeId;
|
|
425
|
-
|
|
426
|
-
try {
|
|
427
|
-
const q = query({ prompt: step.prompt, options: stepOpts });
|
|
428
|
-
const result = await processSdkStream(q, {
|
|
429
|
-
ws, wsSend: wfSend, sessionIds,
|
|
430
|
-
clientSid: resolvedSid, chatId: null,
|
|
431
|
-
cwd, projectName: projectName || "Workflow",
|
|
432
|
-
isWorkflow: true, stepLabel: step.label,
|
|
433
|
-
workflowId: workflow.id, stepIndex: i,
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
if (result.resolvedSid) resolvedSid = result.resolvedSid;
|
|
437
|
-
if (result.claudeSessionId) resumeId = result.claudeSessionId;
|
|
438
|
-
|
|
439
|
-
if (i === 0 && result.resolvedSid && !clientSid) {
|
|
440
|
-
wfSend({ type: "session", sessionId: result.resolvedSid });
|
|
441
|
-
}
|
|
442
|
-
} catch (err) {
|
|
443
|
-
if (err.name === "AbortError" || abortController.signal.aborted) {
|
|
444
|
-
wfAborted = true;
|
|
445
|
-
wfSend({ type: "workflow_step", stepIndex: i, status: "aborted" });
|
|
446
|
-
break;
|
|
447
|
-
}
|
|
448
|
-
wfSend({ type: "error", error: `Workflow step "${step.label}" failed: ${err.message}` });
|
|
449
|
-
sendTelegramNotification("error", "Workflow Step Failed", `${workflow.title}\n\nStep ${i + 1}/${workflow.steps.length}: ${step.label}\nError: ${err.message}`);
|
|
450
|
-
break;
|
|
451
|
-
}
|
|
504
|
+
// ── Extracted handler: agent chain ────────────────────────────────────────
|
|
505
|
+
export async function handleAgentChain(msg, { ws, sessionIds, activeQueries, pendingApprovals }) {
|
|
506
|
+
const { chain, agents: agentDefs, cwd, sessionId: clientSid, projectName, permissionMode: chainPermMode, model: chainModel } = msg;
|
|
507
|
+
if (!chain || !agentDefs?.length) return;
|
|
452
508
|
|
|
453
|
-
|
|
454
|
-
}
|
|
509
|
+
const runId = crypto.randomUUID();
|
|
455
510
|
|
|
456
|
-
|
|
511
|
+
function chainSend(payload) {
|
|
512
|
+
if (ws.readyState !== 1) return;
|
|
513
|
+
ws.send(JSON.stringify(payload));
|
|
514
|
+
}
|
|
457
515
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const stepNames = workflow.steps.map((s, i) => ` ${i + 1}. ${s.label}`).join("\n");
|
|
467
|
-
sendTelegramNotification("workflow", "Workflow Completed", `${workflow.title}\n\nSteps:\n${stepNames}`, {
|
|
468
|
-
steps: workflow.steps.length,
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
516
|
+
chainSend({
|
|
517
|
+
type: "agent_chain_started",
|
|
518
|
+
chainId: chain.id,
|
|
519
|
+
runId,
|
|
520
|
+
title: chain.title,
|
|
521
|
+
agents: agentDefs.map(a => ({ id: a.id, title: a.title })),
|
|
522
|
+
totalSteps: agentDefs.length,
|
|
523
|
+
});
|
|
473
524
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (!agentDef) return;
|
|
478
|
-
|
|
479
|
-
runAgent({
|
|
480
|
-
ws,
|
|
481
|
-
agentDef,
|
|
482
|
-
cwd,
|
|
483
|
-
sessionId: clientSid,
|
|
484
|
-
projectName,
|
|
485
|
-
permissionMode: agentPermMode,
|
|
486
|
-
model: agentModel,
|
|
487
|
-
sessionIds,
|
|
488
|
-
pendingApprovals,
|
|
489
|
-
makeCanUseTool,
|
|
490
|
-
userContext,
|
|
491
|
-
activeQueries,
|
|
492
|
-
runType: 'single',
|
|
493
|
-
}).catch(() => {}); // errors already handled inside runAgent
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
525
|
+
// Telegram start notification
|
|
526
|
+
const chainAgentNames = agentDefs.map((a, i) => ` ${i + 1}. ${a.title}`).join("\n");
|
|
527
|
+
sendTelegramNotification("start", "Chain Started", `${chain.title}\n\n${agentDefs.length} agents:\n${chainAgentNames}`);
|
|
496
528
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const { chain, agents: agentDefs, cwd, sessionId: clientSid, projectName, permissionMode: chainPermMode, model: chainModel } = msg;
|
|
500
|
-
if (!chain || !agentDefs?.length) return;
|
|
529
|
+
let chainResumeId = clientSid ? sessionIds.get(clientSid) : undefined;
|
|
530
|
+
let resolvedSid = clientSid;
|
|
501
531
|
|
|
502
|
-
|
|
532
|
+
for (let i = 0; i < agentDefs.length; i++) {
|
|
533
|
+
const agentDef = agentDefs[i];
|
|
534
|
+
if (ws.readyState !== 1) break;
|
|
503
535
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
536
|
+
chainSend({
|
|
537
|
+
type: "agent_chain_step",
|
|
538
|
+
chainId: chain.id,
|
|
539
|
+
stepIndex: i,
|
|
540
|
+
agentId: agentDef.id,
|
|
541
|
+
agentTitle: agentDef.title,
|
|
542
|
+
status: "running",
|
|
543
|
+
});
|
|
508
544
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
545
|
+
try {
|
|
546
|
+
const result = await runAgent({
|
|
547
|
+
ws,
|
|
548
|
+
agentDef,
|
|
549
|
+
cwd,
|
|
550
|
+
sessionId: resolvedSid,
|
|
551
|
+
projectName: projectName || `Chain: ${chain.title}`,
|
|
552
|
+
permissionMode: chainPermMode,
|
|
553
|
+
model: chainModel,
|
|
554
|
+
sessionIds,
|
|
555
|
+
pendingApprovals,
|
|
556
|
+
makeCanUseTool,
|
|
557
|
+
activeQueries,
|
|
558
|
+
chainResumeId,
|
|
559
|
+
runId,
|
|
560
|
+
runType: 'chain',
|
|
561
|
+
parentRunId: chain.id,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (result?.resolvedSid) resolvedSid = result.resolvedSid;
|
|
565
|
+
if (result?.claudeSessionId) chainResumeId = result.claudeSessionId;
|
|
566
|
+
|
|
567
|
+
chainSend({
|
|
568
|
+
type: "agent_chain_step",
|
|
569
|
+
chainId: chain.id,
|
|
570
|
+
stepIndex: i,
|
|
571
|
+
agentId: agentDef.id,
|
|
572
|
+
agentTitle: agentDef.title,
|
|
573
|
+
status: "completed",
|
|
574
|
+
});
|
|
575
|
+
} catch (err) {
|
|
576
|
+
chainSend({
|
|
577
|
+
type: "agent_chain_step",
|
|
578
|
+
chainId: chain.id,
|
|
579
|
+
stepIndex: i,
|
|
580
|
+
agentId: agentDef.id,
|
|
581
|
+
agentTitle: agentDef.title,
|
|
582
|
+
status: "error",
|
|
583
|
+
error: err.message,
|
|
584
|
+
});
|
|
585
|
+
sendTelegramNotification("error", "Chain Agent Failed", `${chain.title}\n\nAgent ${i + 1}/${agentDefs.length}: ${agentDef.title}\nError: ${err.message}`);
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
517
589
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
for (let i = 0; i < agentDefs.length; i++) {
|
|
526
|
-
const agentDef = agentDefs[i];
|
|
527
|
-
if (ws.readyState !== 1) break;
|
|
528
|
-
|
|
529
|
-
chainSend({
|
|
530
|
-
type: "agent_chain_step",
|
|
531
|
-
chainId: chain.id,
|
|
532
|
-
stepIndex: i,
|
|
533
|
-
agentId: agentDef.id,
|
|
534
|
-
agentTitle: agentDef.title,
|
|
535
|
-
status: "running",
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
try {
|
|
539
|
-
const result = await runAgent({
|
|
540
|
-
ws,
|
|
541
|
-
agentDef,
|
|
542
|
-
cwd,
|
|
543
|
-
sessionId: resolvedSid,
|
|
544
|
-
projectName: projectName || `Chain: ${chain.title}`,
|
|
545
|
-
permissionMode: chainPermMode,
|
|
546
|
-
model: chainModel,
|
|
547
|
-
sessionIds,
|
|
548
|
-
pendingApprovals,
|
|
549
|
-
makeCanUseTool,
|
|
550
|
-
activeQueries,
|
|
551
|
-
chainResumeId,
|
|
552
|
-
runId,
|
|
553
|
-
runType: 'chain',
|
|
554
|
-
parentRunId: chain.id,
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
if (result?.resolvedSid) resolvedSid = result.resolvedSid;
|
|
558
|
-
if (result?.claudeSessionId) chainResumeId = result.claudeSessionId;
|
|
559
|
-
|
|
560
|
-
chainSend({
|
|
561
|
-
type: "agent_chain_step",
|
|
562
|
-
chainId: chain.id,
|
|
563
|
-
stepIndex: i,
|
|
564
|
-
agentId: agentDef.id,
|
|
565
|
-
agentTitle: agentDef.title,
|
|
566
|
-
status: "completed",
|
|
567
|
-
});
|
|
568
|
-
} catch (err) {
|
|
569
|
-
chainSend({
|
|
570
|
-
type: "agent_chain_step",
|
|
571
|
-
chainId: chain.id,
|
|
572
|
-
stepIndex: i,
|
|
573
|
-
agentId: agentDef.id,
|
|
574
|
-
agentTitle: agentDef.title,
|
|
575
|
-
status: "error",
|
|
576
|
-
error: err.message,
|
|
577
|
-
});
|
|
578
|
-
sendTelegramNotification("error", "Chain Agent Failed", `${chain.title}\n\nAgent ${i + 1}/${agentDefs.length}: ${agentDef.title}\nError: ${err.message}`);
|
|
579
|
-
break;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
590
|
+
chainSend({ type: "agent_chain_completed", chainId: chain.id, runId });
|
|
591
|
+
sendPushNotification("Claudeck", `Chain "${chain.title}" completed`, `chain-${resolvedSid}`);
|
|
592
|
+
const agentNames = agentDefs.map((a, i) => ` ${i + 1}. ${a.title}`).join("\n");
|
|
593
|
+
sendTelegramNotification("chain", "Chain Completed", `${chain.title}\n\nAgents:\n${agentNames}`, {
|
|
594
|
+
steps: agentDefs.length,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
582
597
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
598
|
+
// ── Extracted handler: DAG ────────────────────────────────────────────────
|
|
599
|
+
export async function handleDag(msg, { ws, sessionIds, activeQueries, pendingApprovals }) {
|
|
600
|
+
const { dag, agents: agentDefs, cwd, sessionId: clientSid, projectName, permissionMode: dagPermMode, model: dagModel } = msg;
|
|
601
|
+
if (!dag || !agentDefs?.length) return;
|
|
602
|
+
|
|
603
|
+
runDag({
|
|
604
|
+
ws,
|
|
605
|
+
dag,
|
|
606
|
+
agents: agentDefs,
|
|
607
|
+
cwd,
|
|
608
|
+
sessionId: clientSid,
|
|
609
|
+
projectName,
|
|
610
|
+
permissionMode: dagPermMode,
|
|
611
|
+
model: dagModel,
|
|
612
|
+
sessionIds,
|
|
613
|
+
pendingApprovals,
|
|
614
|
+
makeCanUseTool,
|
|
615
|
+
activeQueries,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
591
618
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
sessionIds,
|
|
607
|
-
pendingApprovals,
|
|
608
|
-
makeCanUseTool,
|
|
609
|
-
activeQueries,
|
|
610
|
-
});
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
619
|
+
// ── Extracted handler: orchestrate ────────────────────────────────────────
|
|
620
|
+
export async function handleOrchestrate(msg, { ws, sessionIds, activeQueries, pendingApprovals }) {
|
|
621
|
+
const { task, cwd, sessionId: clientSid, projectName, permissionMode: orchPermMode, model: orchModel } = msg;
|
|
622
|
+
if (!task) return;
|
|
623
|
+
|
|
624
|
+
const { readFile } = await import("fs/promises");
|
|
625
|
+
const { configPath } = await import("./paths.js");
|
|
626
|
+
let agents;
|
|
627
|
+
try {
|
|
628
|
+
agents = JSON.parse(await readFile(configPath("agents.json"), "utf-8"));
|
|
629
|
+
} catch {
|
|
630
|
+
ws.send(JSON.stringify({ type: "error", error: "Failed to load agents" }));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
613
633
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
634
|
+
runOrchestrator({
|
|
635
|
+
ws,
|
|
636
|
+
task,
|
|
637
|
+
agents,
|
|
638
|
+
cwd,
|
|
639
|
+
sessionId: clientSid,
|
|
640
|
+
projectName,
|
|
641
|
+
permissionMode: orchPermMode,
|
|
642
|
+
model: orchModel,
|
|
643
|
+
sessionIds,
|
|
644
|
+
pendingApprovals,
|
|
645
|
+
makeCanUseTool,
|
|
646
|
+
activeQueries,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
618
649
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
650
|
+
// ── Extracted handler: chat ───────────────────────────────────────────────
|
|
651
|
+
export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingApprovals }) {
|
|
652
|
+
const { message, cwd, sessionId: clientSid, projectName, chatId, permissionMode: clientPermMode, model: chatModel, maxTurns: clientMaxTurns, images, systemPrompt, disabledTools, worktree } = msg;
|
|
653
|
+
|
|
654
|
+
// Handle /remember command — save memory and respond without calling Claude
|
|
655
|
+
if (message && message.trim().toLowerCase().startsWith('/remember ') && cwd) {
|
|
656
|
+
const result = parseRememberCommand(message, cwd, clientSid);
|
|
657
|
+
function remSend(payload) {
|
|
658
|
+
if (ws.readyState !== 1) return;
|
|
659
|
+
if (chatId) payload.chatId = chatId;
|
|
660
|
+
ws.send(JSON.stringify(payload));
|
|
661
|
+
}
|
|
662
|
+
if (result) {
|
|
663
|
+
remSend({ type: "text", text: result.saved
|
|
664
|
+
? `Saved memory [${result.category}]: ${result.content}`
|
|
665
|
+
: `Memory already exists: ${result.content}` });
|
|
666
|
+
remSend({ type: "memory_saved", category: result.category, content: result.content, isDuplicate: !result.saved });
|
|
667
|
+
} else {
|
|
668
|
+
remSend({ type: "text", text: "Usage: /remember [category] what to remember\nCategories: convention, decision, discovery, warning" });
|
|
669
|
+
}
|
|
670
|
+
remSend({ type: "done" });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
628
673
|
|
|
629
|
-
|
|
630
|
-
ws,
|
|
631
|
-
task,
|
|
632
|
-
agents,
|
|
633
|
-
cwd,
|
|
634
|
-
sessionId: clientSid,
|
|
635
|
-
projectName,
|
|
636
|
-
permissionMode: orchPermMode,
|
|
637
|
-
model: orchModel,
|
|
638
|
-
sessionIds,
|
|
639
|
-
pendingApprovals,
|
|
640
|
-
makeCanUseTool,
|
|
641
|
-
activeQueries,
|
|
642
|
-
});
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
674
|
+
const queryKey = chatId || "__default__";
|
|
645
675
|
|
|
646
|
-
|
|
647
|
-
|
|
676
|
+
const sessionKey = chatId ? `${clientSid}::${chatId}` : clientSid;
|
|
677
|
+
const resumeId = clientSid ? sessionIds.get(sessionKey) : undefined;
|
|
648
678
|
|
|
649
|
-
|
|
679
|
+
if (clientSid && getSession(clientSid)) {
|
|
680
|
+
touchSession(clientSid);
|
|
681
|
+
}
|
|
650
682
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
683
|
+
const abortController = new AbortController();
|
|
684
|
+
const effectivePermMode = clientPermMode || "bypass";
|
|
685
|
+
const useBypass = effectivePermMode === "bypass";
|
|
686
|
+
const usePlan = effectivePermMode === "plan";
|
|
687
|
+
const resolvedCwd = (cwd && existsSync(cwd)) ? cwd : homedir();
|
|
688
|
+
|
|
689
|
+
// ── Worktree mode: create isolated worktree before SDK query ──
|
|
690
|
+
let worktreeRecord = null;
|
|
691
|
+
let effectiveCwd = resolvedCwd;
|
|
692
|
+
|
|
693
|
+
console.log(`[worktree] flag=${worktree}, cwd=${cwd}, exists=${cwd ? existsSync(cwd) : 'n/a'}`);
|
|
694
|
+
|
|
695
|
+
if (worktree && cwd && existsSync(cwd)) {
|
|
696
|
+
try {
|
|
697
|
+
const branchName = generateBranchName(message);
|
|
698
|
+
console.log(`[worktree] Creating worktree: branch=${branchName}, project=${cwd}`);
|
|
699
|
+
const baseBranch = await getCurrentBranch(cwd);
|
|
700
|
+
const wtResult = await createWorktree(cwd, branchName);
|
|
701
|
+
const wtId = crypto.randomUUID();
|
|
702
|
+
|
|
703
|
+
createWorktreeRecord(wtId, clientSid || null, cwd, wtResult.worktreePath, branchName, baseBranch, (message || "").slice(0, 200));
|
|
704
|
+
worktreeRecord = { id: wtId, worktreePath: wtResult.worktreePath, branchName, baseBranch };
|
|
705
|
+
effectiveCwd = wtResult.worktreePath;
|
|
706
|
+
|
|
707
|
+
// Notify client of worktree creation
|
|
708
|
+
if (ws.readyState === 1) {
|
|
709
|
+
const wtPayload = { type: "worktree_created", worktreeId: wtId, branchName, baseBranch, worktreePath: wtResult.worktreePath };
|
|
710
|
+
if (chatId) wtPayload.chatId = chatId;
|
|
711
|
+
ws.send(JSON.stringify(wtPayload));
|
|
669
712
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
touchSession(clientSid);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
console.error("Worktree creation failed:", err.message, err.stack);
|
|
715
|
+
// Notify client of failure so they know it fell back to normal mode
|
|
716
|
+
if (ws.readyState === 1) {
|
|
717
|
+
const errPayload = { type: "worktree_error", error: err.message };
|
|
718
|
+
if (chatId) errPayload.chatId = chatId;
|
|
719
|
+
ws.send(JSON.stringify(errPayload));
|
|
678
720
|
}
|
|
721
|
+
// Fall back to normal cwd — don't block the query
|
|
722
|
+
}
|
|
723
|
+
}
|
|
679
724
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
abortController,
|
|
691
|
-
executable: execPath,
|
|
692
|
-
stderr: (text) => stderrChunks.push(text),
|
|
693
|
-
};
|
|
694
|
-
if (effectiveMaxTurns) opts.maxTurns = effectiveMaxTurns;
|
|
695
|
-
|
|
696
|
-
if (!useBypass && !usePlan) {
|
|
697
|
-
opts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, chatId, projectName || "Chat");
|
|
698
|
-
}
|
|
699
|
-
if (chatModel) opts.model = resolveModel(chatModel);
|
|
700
|
-
if (Array.isArray(disabledTools) && disabledTools.length > 0) {
|
|
701
|
-
opts.disallowedTools = disabledTools;
|
|
702
|
-
}
|
|
725
|
+
const stderrChunks = [];
|
|
726
|
+
const effectiveMaxTurns = clientMaxTurns > 0 ? clientMaxTurns : undefined;
|
|
727
|
+
const opts = {
|
|
728
|
+
cwd: effectiveCwd,
|
|
729
|
+
permissionMode: usePlan ? "plan" : (useBypass ? "bypassPermissions" : "default"),
|
|
730
|
+
abortController,
|
|
731
|
+
executable: execPath,
|
|
732
|
+
stderr: (text) => stderrChunks.push(text),
|
|
733
|
+
};
|
|
734
|
+
if (effectiveMaxTurns) opts.maxTurns = effectiveMaxTurns;
|
|
703
735
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (memPrompt) {
|
|
716
|
-
opts.appendSystemPrompt = (opts.appendSystemPrompt || '') +
|
|
717
|
-
(opts.appendSystemPrompt ? '\n\n' : '') + memPrompt;
|
|
718
|
-
|
|
719
|
-
// Log memories being passed to Claude SDK
|
|
720
|
-
console.log(`\n══════ MEMORY INJECTION ══════`);
|
|
721
|
-
console.log(`Project: ${cwd}`);
|
|
722
|
-
console.log(`User message: "${(message || '').slice(0, 100)}"`);
|
|
723
|
-
console.log(`Memories injected: ${memCount}`);
|
|
724
|
-
for (const m of memList) {
|
|
725
|
-
console.log(` [${m.category}] ${m.content.slice(0, 120)}`);
|
|
726
|
-
}
|
|
727
|
-
console.log(`Prompt appended to appendSystemPrompt (${memPrompt.length} chars)`);
|
|
728
|
-
console.log(`══════════════════════════════\n`);
|
|
736
|
+
if (!useBypass && !usePlan) {
|
|
737
|
+
opts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, chatId, projectName || "Chat");
|
|
738
|
+
}
|
|
739
|
+
if (chatModel) opts.model = resolveModel(chatModel);
|
|
740
|
+
if (Array.isArray(disabledTools) && disabledTools.length > 0) {
|
|
741
|
+
opts.disallowedTools = disabledTools;
|
|
742
|
+
}
|
|
743
|
+
// Prevent SDK from creating nested worktrees when Claudeck manages them
|
|
744
|
+
if (worktreeRecord) {
|
|
745
|
+
opts.disallowedTools = [...(opts.disallowedTools || []), "EnterWorktree"];
|
|
746
|
+
}
|
|
729
747
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
748
|
+
const projectPrompt = getProjectSystemPrompt(cwd);
|
|
749
|
+
if (projectPrompt) opts.appendSystemPrompt = projectPrompt;
|
|
750
|
+
if (systemPrompt) {
|
|
751
|
+
opts.appendSystemPrompt = (opts.appendSystemPrompt || '') +
|
|
752
|
+
(opts.appendSystemPrompt ? '\n\n' : '') + systemPrompt;
|
|
753
|
+
}
|
|
754
|
+
// Run memory maintenance (decay stale, clean expired) on each session
|
|
755
|
+
if (cwd) runMaintenance(cwd);
|
|
756
|
+
// Inject persistent memories for this project (smart: uses user message for relevance)
|
|
757
|
+
if (cwd) {
|
|
758
|
+
const { prompt: memPrompt, count: memCount, memories: memList } = buildMemoryPrompt(cwd, 10, message);
|
|
759
|
+
if (memPrompt) {
|
|
760
|
+
opts.appendSystemPrompt = (opts.appendSystemPrompt || '') +
|
|
761
|
+
(opts.appendSystemPrompt ? '\n\n' : '') + memPrompt;
|
|
762
|
+
|
|
763
|
+
// Log memories being passed to Claude SDK
|
|
764
|
+
console.log(`\n══════ MEMORY INJECTION ══════`);
|
|
765
|
+
console.log(`Project: ${cwd}`);
|
|
766
|
+
console.log(`User message: "${(message || '').slice(0, 100)}"`);
|
|
767
|
+
console.log(`Memories injected: ${memCount}`);
|
|
768
|
+
for (const m of memList) {
|
|
769
|
+
console.log(` [${m.category}] ${m.content.slice(0, 120)}`);
|
|
742
770
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
let resolvedSid = clientSid;
|
|
771
|
+
console.log(`Prompt appended to appendSystemPrompt (${memPrompt.length} chars)`);
|
|
772
|
+
console.log(`══════════════════════════════\n`);
|
|
746
773
|
|
|
747
|
-
|
|
748
|
-
|
|
774
|
+
// Notify client that memories were injected (include content for display)
|
|
775
|
+
if (ws.readyState === 1) {
|
|
776
|
+
const payload = { type: "memories_injected", count: memCount, memories: memList };
|
|
749
777
|
if (chatId) payload.chatId = chatId;
|
|
750
|
-
if (resolvedSid) payload.sessionId = resolvedSid;
|
|
751
778
|
ws.send(JSON.stringify(payload));
|
|
752
779
|
}
|
|
780
|
+
} else {
|
|
781
|
+
console.log(`\n══════ MEMORY INJECTION ══════`);
|
|
782
|
+
console.log(`Project: ${cwd}`);
|
|
783
|
+
console.log(`No memories found for this project`);
|
|
784
|
+
console.log(`══════════════════════════════\n`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (resumeId) opts.resume = resumeId;
|
|
753
788
|
|
|
754
|
-
|
|
755
|
-
if (clientSid) registerGlobalQuery(clientSid, queryKey);
|
|
756
|
-
|
|
757
|
-
function buildPrompt(text, imgs) {
|
|
758
|
-
if (!imgs?.length) return text;
|
|
759
|
-
return (async function*() {
|
|
760
|
-
yield {
|
|
761
|
-
type: "user",
|
|
762
|
-
message: { role: "user", content: [
|
|
763
|
-
{ type: "text", text },
|
|
764
|
-
...imgs.map(img => ({
|
|
765
|
-
type: "image",
|
|
766
|
-
source: { type: "base64", media_type: img.mimeType, data: img.data },
|
|
767
|
-
})),
|
|
768
|
-
]},
|
|
769
|
-
parent_tool_use_id: null,
|
|
770
|
-
session_id: "",
|
|
771
|
-
};
|
|
772
|
-
})();
|
|
773
|
-
}
|
|
789
|
+
const state = { resolvedSid: clientSid, lastAssistantText: "", lastChatMetrics: {} };
|
|
774
790
|
|
|
775
|
-
|
|
776
|
-
|
|
791
|
+
function wsSend(payload) {
|
|
792
|
+
if (ws.readyState !== 1) return;
|
|
793
|
+
if (chatId) payload.chatId = chatId;
|
|
794
|
+
if (state.resolvedSid) payload.sessionId = state.resolvedSid;
|
|
795
|
+
ws.send(JSON.stringify(payload));
|
|
796
|
+
}
|
|
777
797
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
activeQueries.set(queryKey, { abort: () => abortController.abort() });
|
|
798
|
+
// Register for global tracking if we already know the session
|
|
799
|
+
if (clientSid) registerGlobalQuery(clientSid, queryKey);
|
|
781
800
|
|
|
782
|
-
|
|
783
|
-
|
|
801
|
+
async function runChatQuery(queryOpts) {
|
|
802
|
+
const q = query({ prompt: buildPrompt(message, images), options: queryOpts });
|
|
803
|
+
activeQueries.set(queryKey, { abort: () => abortController.abort() });
|
|
784
804
|
|
|
785
|
-
|
|
786
|
-
|
|
805
|
+
let claudeSessionId = null;
|
|
806
|
+
let sessionModel = null;
|
|
787
807
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if (sdkMsg.model) sessionModel = sdkMsg.model;
|
|
791
|
-
const ourSid = clientSid || crypto.randomUUID();
|
|
792
|
-
resolvedSid = ourSid;
|
|
808
|
+
for await (const sdkMsg of q) {
|
|
809
|
+
if (ws.readyState !== 1) break;
|
|
793
810
|
|
|
794
|
-
|
|
795
|
-
|
|
811
|
+
if (sdkMsg.type === "system" && sdkMsg.subtype === "init") {
|
|
812
|
+
claudeSessionId = sdkMsg.session_id;
|
|
813
|
+
if (sdkMsg.model) sessionModel = sdkMsg.model;
|
|
814
|
+
const ourSid = clientSid || crypto.randomUUID();
|
|
815
|
+
state.resolvedSid = ourSid;
|
|
796
816
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
} else {
|
|
800
|
-
updateClaudeSessionId(ourSid, claudeSessionId);
|
|
801
|
-
}
|
|
817
|
+
const sKey = chatId ? `${ourSid}::${chatId}` : ourSid;
|
|
818
|
+
sessionIds.set(sKey, claudeSessionId);
|
|
802
819
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
820
|
+
if (!getSession(ourSid)) {
|
|
821
|
+
createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
|
|
822
|
+
} else {
|
|
823
|
+
updateClaudeSessionId(ourSid, claudeSessionId);
|
|
824
|
+
}
|
|
806
825
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
userMsgData.images = images.map(i => ({ name: i.name, data: i.data, mimeType: i.mimeType }));
|
|
811
|
-
}
|
|
812
|
-
addMessage(resolvedSid, "user", JSON.stringify(userMsgData), chatId || null);
|
|
826
|
+
if (chatId) {
|
|
827
|
+
setClaudeSession(ourSid, chatId, claudeSessionId);
|
|
828
|
+
}
|
|
813
829
|
|
|
814
|
-
|
|
815
|
-
|
|
830
|
+
wsSend({ type: "session", sessionId: ourSid });
|
|
831
|
+
const userMsgData = { text: message };
|
|
832
|
+
if (images?.length) {
|
|
833
|
+
userMsgData.images = images.map(i => ({ name: i.name, data: i.data, mimeType: i.mimeType }));
|
|
834
|
+
}
|
|
835
|
+
addMessage(state.resolvedSid, "user", JSON.stringify(userMsgData), chatId || null);
|
|
816
836
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const title = message.slice(0, 100).split("\n")[0];
|
|
820
|
-
updateSessionTitle(ourSid, title);
|
|
821
|
-
}
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
837
|
+
// Register global query tracking now that we know the session
|
|
838
|
+
if (!clientSid) registerGlobalQuery(state.resolvedSid, queryKey);
|
|
824
839
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
840
|
+
const existingSession = getSession(ourSid);
|
|
841
|
+
if (existingSession && !existingSession.title) {
|
|
842
|
+
const title = message.slice(0, 100).split("\n")[0];
|
|
843
|
+
updateSessionTitle(ourSid, title);
|
|
844
|
+
}
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (sdkMsg.type === "assistant" && sdkMsg.message?.content) {
|
|
849
|
+
for (const block of sdkMsg.message.content) {
|
|
850
|
+
if (block.type === "text" && block.text) {
|
|
851
|
+
state.lastAssistantText += (state.lastAssistantText ? "\n\n" : "") + block.text;
|
|
852
|
+
wsSend({ type: "text", text: block.text });
|
|
853
|
+
if (state.resolvedSid) addMessage(state.resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null);
|
|
854
|
+
} else if (block.type === "tool_use") {
|
|
855
|
+
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);
|
|
837
857
|
}
|
|
858
|
+
}
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
838
861
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
wsSend({ type: "error", error: errMsg });
|
|
880
|
-
}
|
|
881
|
-
continue;
|
|
862
|
+
if (sdkMsg.type === "result") {
|
|
863
|
+
if (sdkMsg.subtype === "success") {
|
|
864
|
+
const sid = state.resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
|
|
865
|
+
const inputTokens = sdkMsg.usage?.input_tokens || 0;
|
|
866
|
+
const outputTokens = sdkMsg.usage?.output_tokens || 0;
|
|
867
|
+
const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
|
|
868
|
+
const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
|
|
869
|
+
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" });
|
|
872
|
+
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);
|
|
874
|
+
} else if (sdkMsg.subtype === "error_max_turns") {
|
|
875
|
+
// Max turns reached — treat as a normal completion with a notice
|
|
876
|
+
const sid = state.resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
|
|
877
|
+
const inputTokens = sdkMsg.usage?.input_tokens || 0;
|
|
878
|
+
const outputTokens = sdkMsg.usage?.output_tokens || 0;
|
|
879
|
+
const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
|
|
880
|
+
const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
|
|
881
|
+
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" });
|
|
884
|
+
wsSend({ type: "error", error: `Reached max turns limit (${sdkMsg.num_turns}). Send another message to continue.` });
|
|
885
|
+
} else if (sdkMsg.subtype?.startsWith("error")) {
|
|
886
|
+
const errMsg = sdkMsg.errors?.join(", ") || sdkMsg.error || sdkMsg.message || "Unknown error";
|
|
887
|
+
console.error("SDK result error:", JSON.stringify(sdkMsg));
|
|
888
|
+
const costUsd = sdkMsg.total_cost_usd || 0;
|
|
889
|
+
const durationMs = sdkMsg.duration_ms || 0;
|
|
890
|
+
const numTurns = sdkMsg.num_turns || 0;
|
|
891
|
+
const inputTokens = sdkMsg.usage?.input_tokens || 0;
|
|
892
|
+
const outputTokens = sdkMsg.usage?.output_tokens || 0;
|
|
893
|
+
const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
|
|
894
|
+
const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
|
|
895
|
+
const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
|
|
896
|
+
const sid = state.resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
|
|
897
|
+
state.lastChatMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: true, error: errMsg };
|
|
898
|
+
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);
|
|
882
901
|
}
|
|
902
|
+
wsSend({ type: "error", error: errMsg });
|
|
903
|
+
}
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
883
906
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
}
|
|
907
|
+
if (sdkMsg.type === "user" && sdkMsg.message?.content) {
|
|
908
|
+
const blocks = Array.isArray(sdkMsg.message.content) ? sdkMsg.message.content : [];
|
|
909
|
+
for (const block of blocks) {
|
|
910
|
+
if (block.type === "tool_result") {
|
|
911
|
+
const text = Array.isArray(block.content) ? block.content.map(c => c.type === "text" ? c.text : "").join("") : typeof block.content === "string" ? block.content : "";
|
|
912
|
+
const wirePayload = { toolUseId: block.tool_use_id, content: text.slice(0, 2000), isError: block.is_error || false };
|
|
913
|
+
wsSend({ type: "tool_result", ...wirePayload });
|
|
914
|
+
if (state.resolvedSid) {
|
|
915
|
+
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);
|
|
896
917
|
}
|
|
897
|
-
continue;
|
|
898
918
|
}
|
|
899
919
|
}
|
|
920
|
+
continue;
|
|
900
921
|
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
901
924
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
} else {
|
|
925
|
-
console.error("Query retry error:", retryErr.message);
|
|
926
|
-
wsSend({ type: "error", error: retryErr.message });
|
|
927
|
-
}
|
|
928
|
-
}
|
|
925
|
+
try {
|
|
926
|
+
await runChatQuery(opts);
|
|
927
|
+
wsSend({ type: "done" });
|
|
928
|
+
} catch (err) {
|
|
929
|
+
if (err.name === "AbortError") {
|
|
930
|
+
if (state.resolvedSid) addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
|
|
931
|
+
wsSend({ type: "aborted" });
|
|
932
|
+
} else {
|
|
933
|
+
const stderrOutput = stderrChunks.join("");
|
|
934
|
+
// Retry without resume if the Claude session no longer exists
|
|
935
|
+
if (opts.resume && stderrOutput.includes("No conversation found")) {
|
|
936
|
+
console.warn("Stale session", opts.resume, "— retrying without resume");
|
|
937
|
+
delete opts.resume;
|
|
938
|
+
sessionIds.delete(sessionKey);
|
|
939
|
+
stderrChunks.length = 0;
|
|
940
|
+
try {
|
|
941
|
+
await runChatQuery(opts);
|
|
942
|
+
wsSend({ type: "done" });
|
|
943
|
+
} catch (retryErr) {
|
|
944
|
+
if (retryErr.name === "AbortError") {
|
|
945
|
+
if (state.resolvedSid) addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
|
|
946
|
+
wsSend({ type: "aborted" });
|
|
929
947
|
} else {
|
|
930
|
-
console.error("Query error:",
|
|
931
|
-
wsSend({ type: "error", error:
|
|
948
|
+
console.error("Query retry error:", retryErr.message);
|
|
949
|
+
wsSend({ type: "error", error: retryErr.message });
|
|
932
950
|
}
|
|
933
951
|
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
952
|
+
} else {
|
|
953
|
+
console.error("Query error:", err.message, stderrOutput ? "\nstderr: " + stderrOutput : "");
|
|
954
|
+
wsSend({ type: "error", error: err.message });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
} finally {
|
|
958
|
+
activeQueries.delete(queryKey);
|
|
959
|
+
unregisterGlobalQuery(state.resolvedSid, queryKey);
|
|
960
|
+
// Send push notification when query completes
|
|
961
|
+
const session = state.resolvedSid ? getSession(state.resolvedSid) : null;
|
|
962
|
+
const pushTitle = session?.title || "Session complete";
|
|
963
|
+
sendPushNotification("Claudeck", pushTitle, `chat-${state.resolvedSid}`);
|
|
964
|
+
|
|
965
|
+
// Rich Telegram notification — meaningful for AFK developer
|
|
966
|
+
const userQuery = (message || "").slice(0, 150).split("\n")[0];
|
|
967
|
+
const answerSnippet = state.lastAssistantText
|
|
968
|
+
? state.lastAssistantText.slice(0, 300).replace(/\n{2,}/g, "\n")
|
|
969
|
+
: "";
|
|
970
|
+
|
|
971
|
+
if (state.lastChatMetrics.isError) {
|
|
972
|
+
const errorBody = [
|
|
973
|
+
userQuery ? `Q: ${userQuery}` : "",
|
|
974
|
+
`Error: ${state.lastChatMetrics.error || "Unknown error"}`,
|
|
975
|
+
].filter(Boolean).join("\n");
|
|
976
|
+
sendTelegramNotification("error", "Session Failed", errorBody, {
|
|
977
|
+
durationMs: state.lastChatMetrics.durationMs,
|
|
978
|
+
costUsd: state.lastChatMetrics.costUsd,
|
|
979
|
+
inputTokens: state.lastChatMetrics.inputTokens,
|
|
980
|
+
outputTokens: state.lastChatMetrics.outputTokens,
|
|
981
|
+
model: state.lastChatMetrics.model,
|
|
982
|
+
});
|
|
983
|
+
} else {
|
|
984
|
+
const body = [
|
|
985
|
+
userQuery ? `Q: ${userQuery}` : pushTitle,
|
|
986
|
+
answerSnippet ? `\nA: ${answerSnippet}` : "",
|
|
987
|
+
].filter(Boolean).join("\n");
|
|
988
|
+
sendTelegramNotification("session", "Session Complete", body, {
|
|
989
|
+
durationMs: state.lastChatMetrics.durationMs,
|
|
990
|
+
costUsd: state.lastChatMetrics.costUsd,
|
|
991
|
+
inputTokens: state.lastChatMetrics.inputTokens,
|
|
992
|
+
outputTokens: state.lastChatMetrics.outputTokens,
|
|
993
|
+
model: state.lastChatMetrics.model,
|
|
994
|
+
turns: state.lastChatMetrics.turns,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
974
997
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
998
|
+
// Fire-and-forget summary generation
|
|
999
|
+
if (state.resolvedSid) {
|
|
1000
|
+
generateSessionSummary(state.resolvedSid).catch(err =>
|
|
1001
|
+
console.error("Summary generation error:", err.message)
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Auto-capture memories from assistant output
|
|
1006
|
+
if (cwd && state.lastAssistantText) {
|
|
1007
|
+
try {
|
|
1008
|
+
// 1. Parse explicit ```memory blocks (Claude-requested saves)
|
|
1009
|
+
const explicitCount = saveExplicitMemories(cwd, state.lastAssistantText, state.resolvedSid);
|
|
1010
|
+
|
|
1011
|
+
// 2. Heuristic extraction from assistant text
|
|
1012
|
+
const autoCount = captureMemories(cwd, state.lastAssistantText, state.resolvedSid, null);
|
|
1013
|
+
|
|
1014
|
+
const totalCaptured = explicitCount + autoCount;
|
|
1015
|
+
if (totalCaptured > 0) {
|
|
1016
|
+
console.log(`Captured ${totalCaptured} memories (${explicitCount} explicit, ${autoCount} auto) from session ${state.resolvedSid}`);
|
|
1017
|
+
// Notify client
|
|
1018
|
+
if (ws.readyState === 1) {
|
|
1019
|
+
const payload = { type: "memories_captured", count: totalCaptured, explicit: explicitCount, auto: autoCount };
|
|
1020
|
+
if (chatId) payload.chatId = chatId;
|
|
1021
|
+
ws.send(JSON.stringify(payload));
|
|
1022
|
+
}
|
|
980
1023
|
}
|
|
1024
|
+
} catch (e) { console.error("Memory capture error:", e.message); }
|
|
1025
|
+
}
|
|
981
1026
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1027
|
+
// Worktree post-completion: auto-commit, diff stats, notify
|
|
1028
|
+
if (worktreeRecord) {
|
|
1029
|
+
try {
|
|
1030
|
+
if (state.resolvedSid) updateWorktreeSession(worktreeRecord.id, state.resolvedSid);
|
|
1031
|
+
|
|
1032
|
+
const commitMsg = `claudeck: ${(message || "worktree changes").slice(0, 72)}`;
|
|
1033
|
+
await autoCommitWorktree(worktreeRecord.worktreePath, commitMsg);
|
|
1034
|
+
|
|
1035
|
+
const stats = await getWorktreeDiffStats(worktreeRecord.worktreePath, worktreeRecord.baseBranch);
|
|
1036
|
+
updateWorktreeStatus(worktreeRecord.id, "completed");
|
|
1037
|
+
|
|
1038
|
+
if (ws.readyState === 1) {
|
|
1039
|
+
const wtPayload = {
|
|
1040
|
+
type: "worktree_completed",
|
|
1041
|
+
worktreeId: worktreeRecord.id,
|
|
1042
|
+
branchName: worktreeRecord.branchName,
|
|
1043
|
+
stats,
|
|
1044
|
+
};
|
|
1045
|
+
if (chatId) wtPayload.chatId = chatId;
|
|
1046
|
+
ws.send(JSON.stringify(wtPayload));
|
|
1002
1047
|
}
|
|
1048
|
+
|
|
1049
|
+
logNotification(
|
|
1050
|
+
"worktree",
|
|
1051
|
+
`Worktree "${worktreeRecord.branchName}" ready`,
|
|
1052
|
+
`+${stats.insertions} -${stats.deletions} lines in ${stats.files} file(s)`,
|
|
1053
|
+
JSON.stringify({ worktreeId: worktreeRecord.id, branchName: worktreeRecord.branchName }),
|
|
1054
|
+
state.resolvedSid,
|
|
1055
|
+
null
|
|
1056
|
+
);
|
|
1057
|
+
} catch (e) {
|
|
1058
|
+
console.error("Worktree post-completion error:", e.message);
|
|
1003
1059
|
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ── Main entry point: thin router ─────────────────────────────────────────
|
|
1065
|
+
export function setupWebSocket(wss, sessionIds) {
|
|
1066
|
+
wss.on("connection", (ws) => {
|
|
1067
|
+
const ctx = { ws, sessionIds, activeQueries: new Map(), pendingApprovals: new Map() };
|
|
1068
|
+
|
|
1069
|
+
ws.on("close", () => handleClose(ctx));
|
|
1070
|
+
|
|
1071
|
+
ws.on("message", async (raw) => {
|
|
1072
|
+
let msg;
|
|
1073
|
+
try { msg = JSON.parse(raw); } catch { return; }
|
|
1074
|
+
|
|
1075
|
+
if (msg.type === "abort") return handleAbort(msg, ctx);
|
|
1076
|
+
if (msg.type === "permission_response") return handlePermissionResponse(msg, ctx);
|
|
1077
|
+
if (msg.type === "workflow") return handleWorkflow(msg, ctx);
|
|
1078
|
+
if (msg.type === "agent") return handleAgent(msg, ctx);
|
|
1079
|
+
if (msg.type === "agent_chain") return handleAgentChain(msg, ctx);
|
|
1080
|
+
if (msg.type === "agent_dag") return handleDag(msg, ctx);
|
|
1081
|
+
if (msg.type === "orchestrate") return handleOrchestrate(msg, ctx);
|
|
1082
|
+
if (msg.type === "chat") return handleChat(msg, ctx);
|
|
1004
1083
|
});
|
|
1005
1084
|
});
|
|
1006
1085
|
}
|