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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeck",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "description": "A browser-based UI for Claude Code — chat, run workflows, manage MCP servers, track costs, and orchestrate autonomous agents from a local web interface. Installable as a PWA.",
6
6
  "main": "server.js",
@@ -11,6 +11,7 @@
11
11
  "cli.js",
12
12
  "server.js",
13
13
  "db.js",
14
+ "db/",
14
15
  "server/",
15
16
  "public/",
16
17
  "config/",
@@ -3,63 +3,63 @@ import { listTodos, createTodo, updateTodo, archiveTodo, deleteTodo, createBrag,
3
3
 
4
4
  const router = Router();
5
5
 
6
- router.get("/", (req, res) => {
6
+ router.get("/", async (req, res) => {
7
7
  try {
8
8
  const archived = req.query.archived === "1";
9
- const todos = listTodos(archived);
9
+ const todos = await listTodos(archived);
10
10
  res.json(todos);
11
11
  } catch (err) {
12
12
  res.status(500).json({ error: err.message });
13
13
  }
14
14
  });
15
15
 
16
- router.get("/counts", (req, res) => {
16
+ router.get("/counts", async (req, res) => {
17
17
  try {
18
- res.json(getTodoCounts());
18
+ res.json(await getTodoCounts());
19
19
  } catch (err) {
20
20
  res.status(500).json({ error: err.message });
21
21
  }
22
22
  });
23
23
 
24
- router.post("/", (req, res) => {
24
+ router.post("/", async (req, res) => {
25
25
  try {
26
26
  const { text } = req.body;
27
27
  if (!text || typeof text !== "string") {
28
28
  return res.status(400).json({ error: "text is required" });
29
29
  }
30
- const info = createTodo(text.trim());
30
+ const info = await createTodo(text.trim());
31
31
  res.json({ id: info.lastInsertRowid });
32
32
  } catch (err) {
33
33
  res.status(500).json({ error: err.message });
34
34
  }
35
35
  });
36
36
 
37
- router.put("/:id", (req, res) => {
37
+ router.put("/:id", async (req, res) => {
38
38
  try {
39
39
  const id = Number(req.params.id);
40
40
  const { text, done, priority } = req.body;
41
- updateTodo(id, text ?? null, done ?? null, priority ?? null);
41
+ await updateTodo(id, text ?? null, done ?? null, priority ?? null);
42
42
  res.json({ ok: true });
43
43
  } catch (err) {
44
44
  res.status(500).json({ error: err.message });
45
45
  }
46
46
  });
47
47
 
48
- router.put("/:id/archive", (req, res) => {
48
+ router.put("/:id/archive", async (req, res) => {
49
49
  try {
50
50
  const id = Number(req.params.id);
51
51
  const { archived } = req.body;
52
- archiveTodo(id, archived ?? true);
52
+ await archiveTodo(id, archived ?? true);
53
53
  res.json({ ok: true });
54
54
  } catch (err) {
55
55
  res.status(500).json({ error: err.message });
56
56
  }
57
57
  });
58
58
 
59
- router.delete("/:id", (req, res) => {
59
+ router.delete("/:id", async (req, res) => {
60
60
  try {
61
61
  const id = Number(req.params.id);
62
- deleteTodo(id);
62
+ await deleteTodo(id);
63
63
  res.json({ ok: true });
64
64
  } catch (err) {
65
65
  res.status(500).json({ error: err.message });
@@ -67,15 +67,15 @@ router.delete("/:id", (req, res) => {
67
67
  });
68
68
 
69
69
  // ── Brags ──────────────────────────────────────────────
70
- router.get("/brags", (req, res) => {
70
+ router.get("/brags", async (req, res) => {
71
71
  try {
72
- res.json(listBrags());
72
+ res.json(await listBrags());
73
73
  } catch (err) {
74
74
  res.status(500).json({ error: err.message });
75
75
  }
76
76
  });
77
77
 
78
- router.post("/:id/brag", (req, res) => {
78
+ router.post("/:id/brag", async (req, res) => {
79
79
  try {
80
80
  const todoId = Number(req.params.id);
81
81
  const { summary } = req.body;
@@ -86,27 +86,27 @@ router.post("/:id/brag", (req, res) => {
86
86
  return res.status(400).json({ error: "summary must be 500 chars or less" });
87
87
  }
88
88
  // Get the todo text before archiving
89
- const todos = listTodos(false);
89
+ const todos = await listTodos(false);
90
90
  const todo = todos.find(t => t.id === todoId);
91
- const archivedTodos = listTodos(true);
91
+ const archivedTodos = await listTodos(true);
92
92
  const archivedTodo = archivedTodos.find(t => t.id === todoId);
93
93
  const foundTodo = todo || archivedTodo;
94
94
  if (!foundTodo) {
95
95
  return res.status(404).json({ error: "Todo not found" });
96
96
  }
97
- const info = createBrag(todoId, foundTodo.text, summary.trim());
97
+ const info = await createBrag(todoId, foundTodo.text, summary.trim());
98
98
  // Archive the todo
99
- archiveTodo(todoId, true);
99
+ await archiveTodo(todoId, true);
100
100
  res.json({ id: info.lastInsertRowid });
101
101
  } catch (err) {
102
102
  res.status(500).json({ error: err.message });
103
103
  }
104
104
  });
105
105
 
106
- router.delete("/brags/:id", (req, res) => {
106
+ router.delete("/brags/:id", async (req, res) => {
107
107
  try {
108
108
  const id = Number(req.params.id);
109
- deleteBrag(id);
109
+ await deleteBrag(id);
110
110
  res.json({ ok: true });
111
111
  } catch (err) {
112
112
  res.status(500).json({ error: err.message });
@@ -25,6 +25,31 @@
25
25
  gap: 18px;
26
26
  }
27
27
 
28
+ /* ── Load-more indicator (lazy loading) ─────────────── */
29
+ .load-more-indicator {
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ gap: 8px;
34
+ padding: 10px 0;
35
+ color: var(--text-muted);
36
+ font-size: 0.8rem;
37
+ }
38
+
39
+ .load-more-spinner {
40
+ display: inline-block;
41
+ width: 14px;
42
+ height: 14px;
43
+ border: 2px solid var(--border);
44
+ border-top-color: var(--accent);
45
+ border-radius: 50%;
46
+ animation: load-more-spin 0.6s linear infinite;
47
+ }
48
+
49
+ @keyframes load-more-spin {
50
+ to { transform: rotate(360deg); }
51
+ }
52
+
28
53
  .messages:empty::after {
29
54
  content: "";
30
55
  display: none;
@@ -36,18 +36,35 @@ export async function fetchActiveSessionIds() {
36
36
  return data.activeSessionIds || [];
37
37
  }
38
38
 
39
- export async function fetchMessages(sessionId) {
40
- const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/messages`);
39
+ function _appendPaginationParams(url, { limit, before } = {}) {
40
+ const params = new URLSearchParams();
41
+ if (limit) params.set("limit", limit);
42
+ if (before) params.set("before", before);
43
+ const qs = params.toString();
44
+ return qs ? `${url}?${qs}` : url;
45
+ }
46
+
47
+ export async function fetchMessages(sessionId, opts) {
48
+ const url = _appendPaginationParams(
49
+ `/api/sessions/${encodeURIComponent(sessionId)}/messages`, opts
50
+ );
51
+ const res = await fetch(url);
41
52
  return res.json();
42
53
  }
43
54
 
44
- export async function fetchMessagesByChatId(sessionId, chatId) {
45
- const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(chatId)}`);
55
+ export async function fetchMessagesByChatId(sessionId, chatId, opts) {
56
+ const url = _appendPaginationParams(
57
+ `/api/sessions/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(chatId)}`, opts
58
+ );
59
+ const res = await fetch(url);
46
60
  return res.json();
47
61
  }
48
62
 
49
- export async function fetchSingleMessages(sessionId) {
50
- const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/messages-single`);
63
+ export async function fetchSingleMessages(sessionId, opts) {
64
+ const url = _appendPaginationParams(
65
+ `/api/sessions/${encodeURIComponent(sessionId)}/messages-single`, opts
66
+ );
67
+ const res = await fetch(url);
51
68
  return res.json();
52
69
  }
53
70
 
@@ -3,6 +3,12 @@ import { $ } from './dom.js';
3
3
  import { getState, setState } from './store.js';
4
4
  import { emit } from './events.js';
5
5
 
6
+ export function subscribeToSession(sessionId) {
7
+ const ws = getState("ws");
8
+ if (!ws || ws.readyState !== 1 || !sessionId) return;
9
+ ws.send(JSON.stringify({ type: "subscribe", sessionId }));
10
+ }
11
+
6
12
  let backoffAttempt = 0;
7
13
  let hasConnectedBefore = false;
8
14
 
@@ -37,6 +43,12 @@ export function connectWebSocket() {
37
43
  hasConnectedBefore = true;
38
44
  emit("ws:connected");
39
45
  }
46
+
47
+ // Subscribe to current session for multi-client broadcast
48
+ const currentSession = getState("sessionId");
49
+ if (currentSession) {
50
+ ws.send(JSON.stringify({ type: "subscribe", sessionId: currentSession }));
51
+ }
40
52
  };
41
53
 
42
54
  ws.onmessage = (event) => {
@@ -399,6 +399,10 @@ function handleServerMessage(msg) {
399
399
  showThinking("Thinking...", pane);
400
400
  break;
401
401
 
402
+ case "user_message":
403
+ addUserMessage(msg.text, pane);
404
+ break;
405
+
402
406
  case "text":
403
407
  appendAssistantText(msg.text, pane);
404
408
  break;
@@ -5,8 +5,12 @@ import { CHAT_IDS } from '../core/constants.js';
5
5
  import { escapeHtml } from '../core/utils.js';
6
6
  import * as api from '../core/api.js';
7
7
  import { panes, enterParallelMode, exitParallelMode } from '../ui/parallel.js';
8
- import { renderMessagesIntoPane, showWhalyPlaceholder } from '../ui/messages.js';
8
+ import { renderMessagesIntoPane, prependOlderMessages, showWhalyPlaceholder, showLoadingIndicator, hideLoadingIndicator } from '../ui/messages.js';
9
9
  import { loadContextGauge } from '../ui/context-gauge.js';
10
+ import { subscribeToSession } from '../core/ws.js';
11
+
12
+ const MESSAGE_PAGE_SIZE = 30;
13
+ const SCROLL_LOAD_THRESHOLD = 150; // px from top to trigger load more
10
14
 
11
15
  const SESSION_STORAGE_KEY = "claudeck-session-id";
12
16
 
@@ -14,6 +18,7 @@ const SESSION_STORAGE_KEY = "claudeck-session-id";
14
18
  onState("sessionId", (val) => {
15
19
  if (val) {
16
20
  localStorage.setItem(SESSION_STORAGE_KEY, val);
21
+ subscribeToSession(val);
17
22
  } else {
18
23
  localStorage.removeItem(SESSION_STORAGE_KEY);
19
24
  }
@@ -228,16 +233,16 @@ export async function deleteSession(id) {
228
233
 
229
234
  export async function loadMessages(sid) {
230
235
  if (getState("parallelMode")) {
231
- for (const chatId of CHAT_IDS) {
232
- loadPaneMessages(sid, chatId);
233
- }
236
+ // Load all panes concurrently instead of sequentially
237
+ await Promise.all(CHAT_IDS.map(chatId => loadPaneMessages(sid, chatId)));
234
238
  return;
235
239
  }
236
240
 
237
241
  const pane = panes.get(null);
238
242
  try {
239
- const messages = await api.fetchSingleMessages(sid);
243
+ const messages = await api.fetchSingleMessages(sid, { limit: MESSAGE_PAGE_SIZE });
240
244
  renderMessagesIntoPane(messages, pane);
245
+ _initPanePagination(pane, messages, "single");
241
246
  loadContextGauge(sid);
242
247
  } catch (err) {
243
248
  console.error("Failed to load messages:", err);
@@ -248,22 +253,109 @@ export async function loadPaneMessages(sid, chatId) {
248
253
  const pane = panes.get(chatId);
249
254
  if (!pane) return;
250
255
  try {
251
- let messages = await api.fetchMessagesByChatId(sid, chatId);
252
-
256
+ let messages;
253
257
  // For Chat 1 (chat-0): also load single-mode messages as fallback
254
258
  if (chatId === CHAT_IDS[0]) {
255
- const singleMessages = await api.fetchSingleMessages(sid);
256
- if (singleMessages.length > 0) {
257
- messages = [...singleMessages, ...messages].sort((a, b) => a.id - b.id);
259
+ const [chatMsgs, singleMsgs] = await Promise.all([
260
+ api.fetchMessagesByChatId(sid, chatId, { limit: MESSAGE_PAGE_SIZE }),
261
+ api.fetchSingleMessages(sid, { limit: MESSAGE_PAGE_SIZE }),
262
+ ]);
263
+ if (singleMsgs.length > 0) {
264
+ messages = [...singleMsgs, ...chatMsgs].sort((a, b) => a.id - b.id);
265
+ } else {
266
+ messages = chatMsgs;
258
267
  }
268
+ } else {
269
+ messages = await api.fetchMessagesByChatId(sid, chatId, { limit: MESSAGE_PAGE_SIZE });
259
270
  }
260
271
 
261
272
  renderMessagesIntoPane(messages, pane);
273
+ _initPanePagination(pane, messages, chatId === CHAT_IDS[0] ? "chat0" : "chat");
262
274
  } catch (err) {
263
275
  console.error(`Failed to load messages for ${chatId}:`, err);
264
276
  }
265
277
  }
266
278
 
279
+ // ── Lazy-load pagination ────────────────────────────────
280
+
281
+ function _initPanePagination(pane, messages, mode) {
282
+ pane._hasMore = messages.length >= MESSAGE_PAGE_SIZE;
283
+ pane._oldestMessageId = messages.length > 0 ? messages[0].id : null;
284
+ pane._loadingMore = false;
285
+ pane._paginationMode = mode; // "single" | "chat" | "chat0"
286
+
287
+ // Remove any existing scroll listener
288
+ if (pane._scrollHandler) {
289
+ pane.messagesDiv.removeEventListener("scroll", pane._scrollHandler);
290
+ }
291
+
292
+ if (pane._hasMore) {
293
+ pane._scrollHandler = () => _onPaneScroll(pane);
294
+ pane.messagesDiv.addEventListener("scroll", pane._scrollHandler, { passive: true });
295
+ }
296
+ }
297
+
298
+ function _onPaneScroll(pane) {
299
+ if (
300
+ pane.messagesDiv.scrollTop < SCROLL_LOAD_THRESHOLD &&
301
+ pane._hasMore &&
302
+ !pane._loadingMore
303
+ ) {
304
+ _loadMoreMessages(pane);
305
+ }
306
+ }
307
+
308
+ async function _loadMoreMessages(pane) {
309
+ pane._loadingMore = true;
310
+ showLoadingIndicator(pane);
311
+
312
+ const sid = getState("sessionId");
313
+ const before = pane._oldestMessageId;
314
+ const opts = { limit: MESSAGE_PAGE_SIZE, before };
315
+
316
+ try {
317
+ let olderMessages;
318
+
319
+ switch (pane._paginationMode) {
320
+ case "single":
321
+ olderMessages = await api.fetchSingleMessages(sid, opts);
322
+ break;
323
+ case "chat0": {
324
+ // Chat 1 merges chatId + single messages
325
+ const [chatMsgs, singleMsgs] = await Promise.all([
326
+ api.fetchMessagesByChatId(sid, pane.chatId, opts),
327
+ api.fetchSingleMessages(sid, opts),
328
+ ]);
329
+ olderMessages = singleMsgs.length > 0
330
+ ? [...singleMsgs, ...chatMsgs].sort((a, b) => a.id - b.id)
331
+ : chatMsgs;
332
+ break;
333
+ }
334
+ default:
335
+ olderMessages = await api.fetchMessagesByChatId(sid, pane.chatId, opts);
336
+ }
337
+
338
+ if (olderMessages.length === 0) {
339
+ pane._hasMore = false;
340
+ } else {
341
+ pane._oldestMessageId = olderMessages[0].id;
342
+ pane._hasMore = olderMessages.length >= MESSAGE_PAGE_SIZE;
343
+ prependOlderMessages(olderMessages, pane);
344
+ }
345
+ } catch (err) {
346
+ console.error("Failed to load more messages:", err);
347
+ } finally {
348
+ hideLoadingIndicator(pane);
349
+ pane._loadingMore = false;
350
+
351
+ // Detach scroll listener if no more messages
352
+ if (!pane._hasMore && pane._scrollHandler) {
353
+ pane.messagesDiv.removeEventListener("scroll", pane._scrollHandler);
354
+ pane._scrollHandler = null;
355
+ }
356
+ }
357
+ }
358
+
267
359
  // ── Session Context Menu ────────────────────────────────
268
360
  let sessionCtxMenu = null;
269
361
 
@@ -443,3 +443,45 @@ function addForkButton(msgEl, messageId) {
443
443
  btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`;
444
444
  msgEl.appendChild(btn);
445
445
  }
446
+
447
+ // ── Lazy-loading helpers ────────────────────────────────
448
+
449
+ export function prependOlderMessages(messages, pane) {
450
+ if (!messages || messages.length === 0) return;
451
+
452
+ // Render older messages into a detached container using the same rendering logic
453
+ const tempContainer = document.createElement("div");
454
+ const tempPane = { messagesDiv: tempContainer, currentAssistantMsg: null };
455
+ renderMessagesIntoPane(messages, tempPane);
456
+
457
+ // Capture scroll position before DOM mutation
458
+ const scrollHeightBefore = pane.messagesDiv.scrollHeight;
459
+
460
+ // Move all rendered nodes into the real pane
461
+ const fragment = document.createDocumentFragment();
462
+ while (tempContainer.firstChild) {
463
+ fragment.appendChild(tempContainer.firstChild);
464
+ }
465
+
466
+ // Insert after loading indicator (if present) or at the top
467
+ const indicator = pane.messagesDiv.querySelector(".load-more-indicator");
468
+ const insertRef = indicator ? indicator.nextSibling : pane.messagesDiv.firstChild;
469
+ pane.messagesDiv.insertBefore(fragment, insertRef);
470
+
471
+ // Restore scroll position so the user's view doesn't jump
472
+ const scrollHeightAfter = pane.messagesDiv.scrollHeight;
473
+ pane.messagesDiv.scrollTop += (scrollHeightAfter - scrollHeightBefore);
474
+ }
475
+
476
+ export function showLoadingIndicator(pane) {
477
+ if (pane.messagesDiv.querySelector(".load-more-indicator")) return;
478
+ const el = document.createElement("div");
479
+ el.className = "load-more-indicator";
480
+ el.innerHTML = '<span class="load-more-spinner"></span> Loading older messages\u2026';
481
+ pane.messagesDiv.prepend(el);
482
+ }
483
+
484
+ export function hideLoadingIndicator(pane) {
485
+ const el = pane.messagesDiv.querySelector(".load-more-indicator");
486
+ if (el) el.remove();
487
+ }
@@ -138,11 +138,9 @@ export function enterParallelMode() {
138
138
 
139
139
  const sessionId = getState("sessionId");
140
140
  if (sessionId) {
141
- // Lazy import to avoid circular dependency
141
+ // Lazy import to avoid circular dependency — load all panes concurrently
142
142
  import('../features/sessions.js').then(({ loadPaneMessages }) => {
143
- for (const chatId of CHAT_IDS) {
144
- loadPaneMessages(sessionId, chatId);
145
- }
143
+ Promise.all(CHAT_IDS.map(chatId => loadPaneMessages(sessionId, chatId)));
146
144
  });
147
145
  }
148
146
  }
@@ -40,7 +40,7 @@ import { logNotification } from "./notification-logger.js";
40
40
  * Build the agent system prompt that instructs Claude to work autonomously
41
41
  * toward the given goal.
42
42
  */
43
- function buildAgentPrompt(agentDef, userContext, sharedContext, cwd) {
43
+ async function buildAgentPrompt(agentDef, userContext, sharedContext, cwd) {
44
44
  let prompt = `You are an autonomous AI agent. Work toward the following goal step by step, using any tools available to you.\n\n`;
45
45
  prompt += `## Goal\n${agentDef.goal}\n\n`;
46
46
  if (userContext) {
@@ -48,7 +48,7 @@ function buildAgentPrompt(agentDef, userContext, sharedContext, cwd) {
48
48
  }
49
49
  // Inject persistent memories from previous sessions
50
50
  if (cwd) {
51
- const memoryPrompt = buildAgentMemoryPrompt(cwd, 8);
51
+ const memoryPrompt = await buildAgentMemoryPrompt(cwd, 8);
52
52
  if (memoryPrompt) {
53
53
  prompt += memoryPrompt + '\n\n';
54
54
  console.log(`\n══════ AGENT MEMORY INJECTION ══════`);
@@ -105,7 +105,7 @@ export async function runAgent({
105
105
  const monitorRunId = runId || `single-${Date.now()}`;
106
106
  const effectiveRunType = runType || 'single';
107
107
  try {
108
- recordAgentRunStart(monitorRunId, agentId, agentDef.title, effectiveRunType, parentRunId);
108
+ await recordAgentRunStart(monitorRunId, agentId, agentDef.title, effectiveRunType, parentRunId);
109
109
  } catch (e) { /* ignore duplicates */ }
110
110
 
111
111
  function agentSend(payload) {
@@ -145,6 +145,7 @@ export async function runAgent({
145
145
  abortController,
146
146
  maxTurns,
147
147
  executable: execPath,
148
+ settingSources: ["user", "project", "local"],
148
149
  };
149
150
 
150
151
  if (!useBypass && !usePlan) {
@@ -160,8 +161,8 @@ export async function runAgent({
160
161
  if (resumeId) opts.resume = resumeId;
161
162
 
162
163
  // Load shared context from previous agents in this run
163
- const sharedContext = runId ? getAllAgentContext(runId) : [];
164
- const prompt = buildAgentPrompt(agentDef, userContext, sharedContext, cwd);
164
+ const sharedContext = runId ? await getAllAgentContext(runId) : [];
165
+ const prompt = await buildAgentPrompt(agentDef, userContext, sharedContext, cwd);
165
166
  let resolvedSid = clientSid;
166
167
  let claudeSessionId = null;
167
168
  let sessionModel = null;
@@ -184,15 +185,15 @@ export async function runAgent({
184
185
 
185
186
  sessionIds.set(ourSid, claudeSessionId);
186
187
 
187
- if (!getSession(ourSid)) {
188
- createSession(ourSid, claudeSessionId, projectName || "Agent Session", cwd || "");
189
- updateSessionTitle(ourSid, `Agent: ${agentDef.title}`);
188
+ if (!await getSession(ourSid)) {
189
+ await createSession(ourSid, claudeSessionId, projectName || "Agent Session", cwd || "");
190
+ await updateSessionTitle(ourSid, `Agent: ${agentDef.title}`);
190
191
  } else {
191
- updateClaudeSessionId(ourSid, claudeSessionId);
192
+ await updateClaudeSessionId(ourSid, claudeSessionId);
192
193
  }
193
194
 
194
195
  agentSend({ type: "session", sessionId: ourSid });
195
- addMessage(resolvedSid, "user", JSON.stringify({ text: `[Agent: ${agentDef.title}] ${agentDef.goal}` }), null);
196
+ await addMessage(resolvedSid, "user", JSON.stringify({ text: `[Agent: ${agentDef.title}] ${agentDef.goal}` }), null);
196
197
  continue;
197
198
  }
198
199
 
@@ -203,7 +204,7 @@ export async function runAgent({
203
204
  lastAssistantText += (lastAssistantText ? "\n\n" : "") + block.text;
204
205
  agentSend({ type: "text", text: block.text });
205
206
  if (resolvedSid) {
206
- addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), null);
207
+ await addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), null);
207
208
  }
208
209
  } else if (block.type === "tool_use") {
209
210
  turnCount++;
@@ -219,7 +220,7 @@ export async function runAgent({
219
220
  : "",
220
221
  });
221
222
  if (resolvedSid) {
222
- addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), null);
223
+ await addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), null);
223
224
  }
224
225
  }
225
226
  }
@@ -241,7 +242,7 @@ export async function runAgent({
241
242
  isError: block.is_error || false,
242
243
  });
243
244
  if (resolvedSid) {
244
- addMessage(resolvedSid, "tool_result", JSON.stringify({
245
+ await addMessage(resolvedSid, "tool_result", JSON.stringify({
245
246
  toolUseId: block.tool_use_id,
246
247
  content: text.slice(0, 10000),
247
248
  isError: block.is_error || false,
@@ -265,7 +266,7 @@ export async function runAgent({
265
266
  const resultModel = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
266
267
 
267
268
  if (resolvedSid) {
268
- addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
269
+ await addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
269
270
  model: resultModel,
270
271
  stopReason: sdkMsg.subtype,
271
272
  isError: 0,
@@ -279,7 +280,7 @@ export async function runAgent({
279
280
  duration_ms: durationMs,
280
281
  num_turns: numTurns,
281
282
  cost_usd: costUsd,
282
- totalCost: getTotalCost(),
283
+ totalCost: await getTotalCost(),
283
284
  input_tokens: inputTokens,
284
285
  output_tokens: outputTokens,
285
286
  cache_read_tokens: cacheReadTokens,
@@ -300,11 +301,11 @@ export async function runAgent({
300
301
 
301
302
  // Record completion for monitoring
302
303
  try {
303
- recordAgentRunComplete(monitorRunId, agentId, 'completed', numTurns, costUsd, durationMs, inputTokens, outputTokens);
304
+ await recordAgentRunComplete(monitorRunId, agentId, 'completed', numTurns, costUsd, durationMs, inputTokens, outputTokens);
304
305
  } catch (e) { /* ignore */ }
305
306
 
306
307
  // Log notification
307
- logNotification('agent', `Agent "${agentDef.title}" completed`,
308
+ await logNotification('agent', `Agent "${agentDef.title}" completed`,
308
309
  `${numTurns} turns · $${costUsd.toFixed(4)} · ${(durationMs / 1000).toFixed(1)}s`,
309
310
  JSON.stringify({ costUsd, durationMs, inputTokens, outputTokens, turns: numTurns }),
310
311
  resolvedSid, agentId);
@@ -314,7 +315,7 @@ export async function runAgent({
314
315
  const summary = lastAssistantText.length > 4000
315
316
  ? lastAssistantText.slice(0, 4000) + "\n\n[truncated]"
316
317
  : lastAssistantText;
317
- setAgentContext(runId, agentId, "output", summary);
318
+ await setAgentContext(runId, agentId, "output", summary);
318
319
  }
319
320
  } else if (sdkMsg.subtype?.startsWith("error")) {
320
321
  const errMsg = sdkMsg.errors?.join(", ") || "Unknown error";
@@ -328,14 +329,14 @@ export async function runAgent({
328
329
  const resultModel = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
329
330
 
330
331
  if (resolvedSid) {
331
- addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
332
+ await addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
332
333
  model: resultModel,
333
334
  stopReason: sdkMsg.subtype,
334
335
  isError: 1,
335
336
  cacheReadTokens,
336
337
  cacheCreationTokens,
337
338
  });
338
- addMessage(resolvedSid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype }), null);
339
+ await addMessage(resolvedSid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype }), null);
339
340
  }
340
341
 
341
342
  lastAgentMetrics = { durationMs, costUsd, inputTokens, outputTokens, model: resultModel, turns: numTurns, isError: true, error: errMsg };
@@ -344,11 +345,11 @@ export async function runAgent({
344
345
 
345
346
  // Record error for monitoring
346
347
  try {
347
- recordAgentRunComplete(monitorRunId, agentId, 'error', numTurns, costUsd, durationMs, inputTokens, outputTokens, errMsg);
348
+ await recordAgentRunComplete(monitorRunId, agentId, 'error', numTurns, costUsd, durationMs, inputTokens, outputTokens, errMsg);
348
349
  } catch (e) { /* ignore */ }
349
350
 
350
351
  // Log error notification
351
- logNotification('error', `Agent "${agentDef.title}" failed`,
352
+ await logNotification('error', `Agent "${agentDef.title}" failed`,
352
353
  errMsg.slice(0, 200),
353
354
  JSON.stringify({ costUsd, durationMs, error: errMsg }),
354
355
  resolvedSid, agentId);
@@ -360,11 +361,11 @@ export async function runAgent({
360
361
  if (err.name === "AbortError") {
361
362
  agentSend({ type: "agent_aborted", agentId, turn: turnCount });
362
363
  agentSend({ type: "aborted" });
363
- try { recordAgentRunComplete(monitorRunId, agentId, 'aborted', turnCount, 0, 0, 0, 0, 'Aborted'); } catch (e) { /* ignore */ }
364
+ try { await recordAgentRunComplete(monitorRunId, agentId, 'aborted', turnCount, 0, 0, 0, 0, 'Aborted'); } catch (e) { /* ignore */ }
364
365
  } else {
365
366
  agentSend({ type: "agent_error", agentId, error: err.message, turn: turnCount });
366
367
  agentSend({ type: "error", error: err.message });
367
- try { recordAgentRunComplete(monitorRunId, agentId, 'error', turnCount, 0, 0, 0, 0, err.message); } catch (e) { /* ignore */ }
368
+ try { await recordAgentRunComplete(monitorRunId, agentId, 'error', turnCount, 0, 0, 0, 0, err.message); } catch (e) { /* ignore */ }
368
369
  }
369
370
  throw err; // Re-throw so callers (chains, DAGs) know the agent failed
370
371
  } finally {
@@ -412,8 +413,8 @@ export async function runAgent({
412
413
  // Auto-capture memories from agent output
413
414
  if (cwd && lastAssistantText) {
414
415
  try {
415
- const explicitCount = saveExplicitMemories(cwd, lastAssistantText, resolvedSid);
416
- const autoCount = captureMemories(cwd, lastAssistantText, resolvedSid, agentId);
416
+ const explicitCount = await saveExplicitMemories(cwd, lastAssistantText, resolvedSid);
417
+ const autoCount = await captureMemories(cwd, lastAssistantText, resolvedSid, agentId);
417
418
  const totalCaptured = explicitCount + autoCount;
418
419
  if (totalCaptured > 0) {
419
420
  console.log(`Captured ${totalCaptured} memories (${explicitCount} explicit, ${autoCount} auto) from agent ${agentId}`);