@runcore-sh/runcore 0.5.6 → 0.5.7

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.
Files changed (174) hide show
  1. package/dictionary.json +2 -2
  2. package/dist/.extensions/ext-byok.json +1 -0
  3. package/dist/.extensions/ext-hosted.json +1 -0
  4. package/dist/.extensions/ext-spawn.json +1 -0
  5. package/dist/agents/autonomous.d.ts.map +1 -1
  6. package/dist/agents/runtime/bus.d.ts +1 -0
  7. package/dist/agents/runtime/bus.d.ts.map +1 -1
  8. package/dist/agents/spawn.d.ts.map +1 -1
  9. package/dist/auth/middleware.d.ts.map +1 -1
  10. package/dist/auth/middleware.js +3 -2
  11. package/dist/auth/middleware.js.map +1 -1
  12. package/dist/calendar/routes.d.ts.map +1 -1
  13. package/dist/calendar/routes.js +8 -7
  14. package/dist/calendar/routes.js.map +1 -1
  15. package/dist/files/registry.d.ts +4 -5
  16. package/dist/files/registry.d.ts.map +1 -1
  17. package/dist/files/registry.js +4 -5
  18. package/dist/files/registry.js.map +1 -1
  19. package/dist/files/store.d.ts +2 -0
  20. package/dist/files/store.d.ts.map +1 -1
  21. package/dist/files/store.js +5 -1
  22. package/dist/files/store.js.map +1 -1
  23. package/dist/instance.d.ts +2 -0
  24. package/dist/instance.d.ts.map +1 -1
  25. package/dist/instance.js +2 -0
  26. package/dist/instance.js.map +1 -1
  27. package/dist/lib/paths.d.ts.map +1 -1
  28. package/dist/lib/paths.js +15 -1
  29. package/dist/lib/paths.js.map +1 -1
  30. package/dist/library/brain-shadow.d.ts.map +1 -1
  31. package/dist/library/brain-shadow.js +5 -4
  32. package/dist/library/brain-shadow.js.map +1 -1
  33. package/dist/library/routes.d.ts.map +1 -1
  34. package/dist/library/routes.js +10 -9
  35. package/dist/library/routes.js.map +1 -1
  36. package/dist/llm/cache.d.ts.map +1 -1
  37. package/dist/llm/cache.js +7 -5
  38. package/dist/llm/cache.js.map +1 -1
  39. package/dist/llm/complete.js +2 -0
  40. package/dist/llm/complete.js.map +1 -1
  41. package/dist/llm/providers/ollama.d.ts.map +1 -1
  42. package/dist/llm/providers/ollama.js +32 -7
  43. package/dist/llm/providers/ollama.js.map +1 -1
  44. package/dist/llm/providers/openrouter.d.ts.map +1 -1
  45. package/dist/llm/providers/openrouter.js +105 -12
  46. package/dist/llm/providers/openrouter.js.map +1 -1
  47. package/dist/llm/providers/types.d.ts +18 -0
  48. package/dist/llm/providers/types.d.ts.map +1 -1
  49. package/dist/llm/retry.d.ts.map +1 -1
  50. package/dist/llm/retry.js +6 -0
  51. package/dist/llm/retry.js.map +1 -1
  52. package/dist/llm/tools/handlers.d.ts +27 -0
  53. package/dist/llm/tools/handlers.d.ts.map +1 -0
  54. package/dist/llm/tools/handlers.js +842 -0
  55. package/dist/llm/tools/handlers.js.map +1 -0
  56. package/dist/llm/tools/index.d.ts +12 -0
  57. package/dist/llm/tools/index.d.ts.map +1 -0
  58. package/dist/llm/tools/index.js +10 -0
  59. package/dist/llm/tools/index.js.map +1 -0
  60. package/dist/llm/tools/loop.d.ts +47 -0
  61. package/dist/llm/tools/loop.d.ts.map +1 -0
  62. package/dist/llm/tools/loop.js +126 -0
  63. package/dist/llm/tools/loop.js.map +1 -0
  64. package/dist/llm/tools/registry.d.ts +27 -0
  65. package/dist/llm/tools/registry.d.ts.map +1 -0
  66. package/dist/llm/tools/registry.js +60 -0
  67. package/dist/llm/tools/registry.js.map +1 -0
  68. package/dist/llm/tools/schemas.d.ts +92 -0
  69. package/dist/llm/tools/schemas.d.ts.map +1 -0
  70. package/dist/llm/tools/schemas.js +154 -0
  71. package/dist/llm/tools/schemas.js.map +1 -0
  72. package/dist/llm/tools/types.d.ts +44 -0
  73. package/dist/llm/tools/types.d.ts.map +1 -0
  74. package/dist/llm/tools/types.js +9 -0
  75. package/dist/llm/tools/types.js.map +1 -0
  76. package/dist/mcp-server.d.ts +1 -1
  77. package/dist/mcp-server.js +249 -5
  78. package/dist/mcp-server.js.map +1 -1
  79. package/dist/memory/visual.d.ts.map +1 -1
  80. package/dist/memory/visual.js +3 -6
  81. package/dist/memory/visual.js.map +1 -1
  82. package/dist/openloop/foldback.js +1 -1
  83. package/dist/openloop/foldback.js.map +1 -1
  84. package/dist/openloop/resolution-scanner.d.ts.map +1 -1
  85. package/dist/openloop/resolution-scanner.js +76 -63
  86. package/dist/openloop/resolution-scanner.js.map +1 -1
  87. package/dist/plugins/github/index.d.ts +49 -0
  88. package/dist/plugins/github/index.d.ts.map +1 -0
  89. package/dist/plugins/github/index.js +153 -0
  90. package/dist/plugins/github/index.js.map +1 -0
  91. package/dist/plugins/index.d.ts +1 -2
  92. package/dist/plugins/index.d.ts.map +1 -1
  93. package/dist/plugins/index.js +79 -2
  94. package/dist/plugins/index.js.map +1 -1
  95. package/dist/plugins/slack/index.d.ts +43 -0
  96. package/dist/plugins/slack/index.d.ts.map +1 -0
  97. package/dist/plugins/slack/index.js +158 -0
  98. package/dist/plugins/slack/index.js.map +1 -0
  99. package/dist/plugins/twilio/index.d.ts +41 -0
  100. package/dist/plugins/twilio/index.d.ts.map +1 -0
  101. package/dist/plugins/twilio/index.js +102 -0
  102. package/dist/plugins/twilio/index.js.map +1 -0
  103. package/dist/pulse/tier.d.ts +1 -1
  104. package/dist/pulse/tier.js +2 -2
  105. package/dist/pulse/tier.js.map +1 -1
  106. package/dist/search/gemini.d.ts +27 -0
  107. package/dist/search/gemini.d.ts.map +1 -0
  108. package/dist/search/gemini.js +103 -0
  109. package/dist/search/gemini.js.map +1 -0
  110. package/dist/server.d.ts.map +1 -1
  111. package/dist/server.js +850 -536
  112. package/dist/server.js.map +1 -1
  113. package/dist/services/routine-patterns.d.ts.map +1 -1
  114. package/dist/services/routine-patterns.js +6 -0
  115. package/dist/services/routine-patterns.js.map +1 -1
  116. package/dist/services/traceInsights.d.ts +5 -0
  117. package/dist/services/traceInsights.d.ts.map +1 -1
  118. package/dist/services/traceInsights.js +18 -1
  119. package/dist/services/traceInsights.js.map +1 -1
  120. package/dist/settings.d.ts +1 -1
  121. package/dist/settings.d.ts.map +1 -1
  122. package/dist/types.d.ts +26 -2
  123. package/dist/types.d.ts.map +1 -1
  124. package/dist/vault/store.d.ts +1 -1
  125. package/dist/vault/store.d.ts.map +1 -1
  126. package/dist/webhooks/mount.d.ts.map +1 -1
  127. package/dist/whiteboard/store.d.ts +40 -0
  128. package/dist/whiteboard/store.d.ts.map +1 -0
  129. package/dist/whiteboard/store.js +280 -0
  130. package/dist/whiteboard/store.js.map +1 -0
  131. package/dist/whiteboard/types.d.ts +55 -0
  132. package/dist/whiteboard/types.d.ts.map +1 -0
  133. package/dist/whiteboard/types.js +9 -0
  134. package/dist/whiteboard/types.js.map +1 -0
  135. package/dist/whiteboard/weight.d.ts +23 -0
  136. package/dist/whiteboard/weight.d.ts.map +1 -0
  137. package/dist/whiteboard/weight.js +126 -0
  138. package/dist/whiteboard/weight.js.map +1 -0
  139. package/package.json +2 -2
  140. package/public/avatar/cache/0cdaf9c41eff4347.mp4 +0 -0
  141. package/public/avatar/cache/3dacc4ea1082ae36.mp4 +0 -0
  142. package/public/avatar/cache/44f5db0bfdde93c6.mp4 +0 -0
  143. package/public/avatar/cache/5628fd10fe55e529.mp4 +0 -0
  144. package/public/avatar/cache/7ee2ab1577690c8a.mp4 +0 -0
  145. package/public/avatar/cache/8c470929e814b6b0.mp4 +0 -0
  146. package/public/avatar/cache/8c908421ce52bf91.mp4 +0 -0
  147. package/public/avatar/cache/9532f8782a42a89c.mp4 +0 -0
  148. package/public/avatar/cache/9ce0dddd0cc4d7a1.mp4 +0 -0
  149. package/public/avatar/cache/a6508e00b6711143.mp4 +0 -0
  150. package/public/avatar/cache/ba61810a8915e0c7.mp4 +0 -0
  151. package/public/avatar/cache/c07bee3a10c917cf.mp4 +0 -0
  152. package/public/avatar/cache/d69175900ea4ea2a.mp4 +0 -0
  153. package/public/avatar/cache/e61039bc8d39cb93.mp4 +0 -0
  154. package/public/avatar/cache/e61c6b7047e2cbdb.mp4 +0 -0
  155. package/public/avatar/cache/efd93c9b18930cf6.mp4 +0 -0
  156. package/public/avatar/cache/f052d74f5c4abab7.mp4 +0 -0
  157. package/public/avatar/cache/f7c0be3429a4ef97.mp4 +0 -0
  158. package/public/avatar/cache/fc8e480f63fe4e35.mp4 +0 -0
  159. package/public/index.html +225 -51
  160. package/public/search-flyout.js +324 -0
  161. package/public/whiteboard.html +915 -0
  162. package/public/avatar/cache/06fa55aececcc478.mp4 +0 -0
  163. package/public/avatar/cache/07a65738ba170827.mp4 +0 -0
  164. package/public/avatar/cache/1185fd491f413406.mp4 +0 -0
  165. package/public/avatar/cache/272c004a41087de5.mp4 +0 -0
  166. package/public/avatar/cache/332384e088ca214b.mp4 +0 -0
  167. package/public/avatar/cache/5d9a960bbf71732c.mp4 +0 -0
  168. package/public/avatar/cache/5e0954401e15af89.mp4 +0 -0
  169. package/public/avatar/cache/b35f7a3d558f22cb.mp4 +0 -0
  170. package/public/avatar/cache/be89f49970672374.mp4 +0 -0
  171. package/public/avatar/cache/c900811e3382ac6d.mp4 +0 -0
  172. package/public/avatar/cache/d42a73667acf5716.mp4 +0 -0
  173. package/public/avatar/cache/e539f247a8908603.mp4 +0 -0
  174. package/public/avatar/cache/ec95af57d33b3f07.mp4 +0 -0
package/dist/server.js CHANGED
@@ -10,6 +10,9 @@ import { join, dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { readFile, writeFile, mkdir } from "node:fs/promises";
12
12
  import { acquireLock, releaseLock } from "./runtime-lock.js";
13
+ import { ToolRegistry } from "./llm/tools/registry.js";
14
+ import { createToolHandlers } from "./llm/tools/handlers.js";
15
+ import { streamWithTools } from "./llm/tools/loop.js";
13
16
  // Package root — works whether run from CWD or npx
14
17
  const __filename = fileURLToPath(import.meta.url);
15
18
  const __dirname = dirname(__filename);
@@ -146,7 +149,7 @@ import { brainShadowRoutes, initBrainShadow } from "./library/brain-shadow.js";
146
149
  import { createCalendarStore } from "./calendar/store.js";
147
150
  import { calendarRoutes } from "./calendar/routes.js";
148
151
  import { getGoogleCalendarAdapter } from "./calendar/google-adapter.js";
149
- import { saveVisualMemory, hydrateVisualMemories, isVisualMemory, searchVisualMemories } from "./memory/visual.js";
152
+ import { saveVisualMemory, isVisualMemory, searchVisualMemories } from "./memory/visual.js";
150
153
  // --- LLM provider selection (via settings) ---
151
154
  function pickStreamFn() {
152
155
  const providerName = resolveProvider();
@@ -284,6 +287,42 @@ async function getOrCreateChatSession(sessionId, name) {
284
287
  }
285
288
  }
286
289
  catch { }
290
+ // Fetch whiteboard status for system prompt injection
291
+ let whiteboardContext = "";
292
+ try {
293
+ const { WhiteboardStore } = await import("./whiteboard/store.js");
294
+ const wbStore = new WhiteboardStore(BRAIN_DIR);
295
+ const summary = await wbStore.getSummary();
296
+ if (summary.total > 0) {
297
+ const parts = [`Whiteboard: ${summary.total} items (${summary.open} open, ${summary.done} done, ${summary.openQuestions} open questions)`];
298
+ // Open questions from human
299
+ const openQs = await wbStore.getOpenQuestions();
300
+ const humanQs = openQs.filter((q) => q.plantedBy === "human");
301
+ if (humanQs.length > 0) {
302
+ parts.push("Questions from human (answer these):");
303
+ for (const q of humanQs)
304
+ parts.push(` → "${q.question || q.title}" (id: ${q.id})`);
305
+ }
306
+ // Answered questions to act on
307
+ const allNodes = await wbStore.list();
308
+ const answered = allNodes.filter((n) => n.type === "question" && n.answer && n.status === "done");
309
+ if (answered.length > 0) {
310
+ parts.push("Answered questions (act on these — the answer IS the instruction):");
311
+ for (const a of answered)
312
+ parts.push(` Q: "${a.question || a.title}" → A: ${a.answer}`);
313
+ }
314
+ // Top weighted
315
+ if (summary.topWeighted.length > 0) {
316
+ parts.push("Top attention items:");
317
+ for (const n of summary.topWeighted) {
318
+ const icon = n.type === "question" ? "?" : n.type === "decision" ? "!" : "-";
319
+ parts.push(` ${icon} [${n.weight}] ${n.title}`);
320
+ }
321
+ }
322
+ whiteboardContext = parts.join("\n");
323
+ }
324
+ }
325
+ catch { }
287
326
  const encryptionKey = sessionKeys.get(sessionId) ?? null;
288
327
  const ltm = new FileSystemLongTermMemory(MEMORY_DIR, encryptionKey ?? undefined);
289
328
  await ltm.init();
@@ -398,6 +437,7 @@ async function getOrCreateChatSession(sessionId, name) {
398
437
  ...(TIER_CAPS[activeTier].spawning ? [
399
438
  `## Agent spawning (CRITICAL — follow exactly)`,
400
439
  `When a task requires code editing, file operations, or shell commands, you MUST spawn a Claude Code agent.`,
440
+ `EXCEPTION: Your built-in tools (whiteboard_plant, whiteboard_status, memory_learn, memory_retrieve, files_search, etc.) are NOT agent tasks. Call them directly via function calling — NEVER spawn an agent to call a tool you already have.`,
401
441
  `Do NOT describe what you would do — actually spawn the agent by including the block below.`,
402
442
  `The block content MUST be valid JSON with "label" and "prompt" keys. No markdown, no backticks, no explanation inside the block.`,
403
443
  ``,
@@ -424,9 +464,31 @@ async function getOrCreateChatSession(sessionId, name) {
424
464
  `Agent failures are normal (auth issues, timeouts, environment mismatches). Never stop spawning agents because of past failures.`,
425
465
  `IMPORTANT: Do NOT announce agent spawns in your visible response text. No "Agent spawned to...", no "I'll spawn an agent...", no "Let me run an agent...". The UI shows agent status automatically. Just include the [AGENT_REQUEST] block silently at the end. Your visible text should answer the user's question or continue the conversation naturally.`,
426
466
  ] : []), // end spawning gate
467
+ ``,
468
+ `## Tools`,
469
+ `You have function-calling tools available. Use them directly — they execute server-side and return results inline.`,
470
+ `Key tools: whiteboard_plant, whiteboard_status, memory_retrieve, memory_learn, files_search, read_brain_file.`,
471
+ `Call tools whenever you need to read or write to the brain, whiteboard, or memory. Do NOT describe what you would do — call the tool.`,
472
+ ``,
473
+ `## Whiteboard (shared collaboration surface)`,
474
+ `You and ${name} share a whiteboard — a tree of goals, tasks, questions, decisions, and notes at /whiteboard.`,
475
+ `Use the whiteboard_plant tool to plant nodes. Use whiteboard_status to check the board.`,
476
+ ``,
477
+ ...(whiteboardContext ? [
478
+ `### Current whiteboard state`,
479
+ whiteboardContext,
480
+ ``,
481
+ ] : []),
482
+ `### When to plant questions (IMPORTANT)`,
483
+ `Before spawning an agent for an ambiguous task, plant a question on the whiteboard using whiteboard_plant.`,
484
+ `If a task has multiple valid approaches and you're unsure which ${name} wants, plant a question and wait for their answer.`,
485
+ ``,
486
+ `### Answered questions`,
487
+ `If you see answered questions above, ACT ON THEM IMMEDIATELY. Do not ask "which one should I focus on?" or "what would you like me to do?" — the answer IS the instruction. Read it, do the work, report what you did. ${name} already told you what to do by answering — don't make them say it twice.`,
488
+ ``,
427
489
  // Inject instance-readable vault values (CORE_*/DASH_* prefixed only — never secrets)
428
490
  ...(() => {
429
- const readable = (_vaultStore ? _vaultStore.getDashReadableVault() : []);
491
+ const readable = (_vaultStore ? _vaultStore.getInstanceReadableVault() : []);
430
492
  if (readable.length === 0)
431
493
  return [];
432
494
  const lines = readable.map((r) => `- ${r.name}: ${r.value}`);
@@ -507,7 +569,7 @@ import { TIER_CAPS } from "./tier/types.js";
507
569
  let activeTier = "local";
508
570
  const app = new Hono();
509
571
  // Global error handler — structured JSON errors
510
- import { errorHandler, ApiError } from "./middleware/error-handler.js";
572
+ import { errorHandler, ApiError, badRequest, unauthorized, forbidden, notFound } from "./middleware/error-handler.js";
511
573
  app.onError((err, c) => {
512
574
  // Use structured handler for ApiErrors; preserve original behavior for others
513
575
  if (err instanceof ApiError)
@@ -671,13 +733,13 @@ app.post("/api/pair", async (c) => {
671
733
  // Backward compat: accept "safeWord" from old clients
672
734
  const pw = password || safeWord;
673
735
  if (!code || !name || !pw) {
674
- return c.json({ error: "Name and password required" }, 400);
736
+ return badRequest("Name and password required");
675
737
  }
676
738
  // Auto-token bypass: startup token already proved the user is local.
677
739
  const skipCodeCheck = code === "__auto_token__";
678
740
  const result = await pair({ code, name, password: pw, recoveryQuestion, recoveryAnswer, skipCodeCheck });
679
741
  if ("error" in result) {
680
- return c.json({ error: result.error }, 400);
742
+ return badRequest(result.error);
681
743
  }
682
744
  sessionKeys.set(result.session.id, result.sessionKey);
683
745
  setEncryptionKey(result.sessionKey);
@@ -706,11 +768,11 @@ app.post("/api/auth", async (c) => {
706
768
  // Backward compat: accept "safeWord" from old clients
707
769
  const pw = password || safeWord;
708
770
  if (!pw) {
709
- return c.json({ error: "Password required" }, 400);
771
+ return badRequest("Password required");
710
772
  }
711
773
  const result = await authenticate(pw);
712
774
  if ("error" in result) {
713
- return c.json({ error: result.error }, 401);
775
+ return unauthorized(result.error);
714
776
  }
715
777
  sessionKeys.set(result.session.id, result.sessionKey);
716
778
  setEncryptionKey(result.sessionKey);
@@ -755,7 +817,7 @@ app.get("/api/auth/token", async (c) => {
755
817
  app.get("/api/recover", async (c) => {
756
818
  const question = await getRecoveryQuestion();
757
819
  if (!question) {
758
- return c.json({ error: "Not paired yet" }, 400);
820
+ return badRequest("Not paired yet");
759
821
  }
760
822
  return c.json({ question });
761
823
  });
@@ -766,11 +828,11 @@ app.post("/api/recover", async (c) => {
766
828
  // Backward compat: accept "newSafeWord" from old clients
767
829
  const newPw = newPassword || newSafeWord;
768
830
  if (!answer || !newPw) {
769
- return c.json({ error: "Answer and new password required" }, 400);
831
+ return badRequest("Answer and new password required");
770
832
  }
771
833
  const result = await recover(answer, newPw);
772
834
  if ("error" in result) {
773
- return c.json({ error: result.error }, 401);
835
+ return unauthorized(result.error);
774
836
  }
775
837
  sessionKeys.set(result.session.id, result.sessionKey);
776
838
  setEncryptionKey(result.sessionKey);
@@ -808,7 +870,7 @@ async function savePairedDevices() {
808
870
  app.post("/api/mobile/voucher", async (c) => {
809
871
  const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
810
872
  if (!sessionId || !validateSession(sessionId)) {
811
- return c.json({ error: "Unauthorized" }, 401);
873
+ return unauthorized("Unauthorized");
812
874
  }
813
875
  const { randomBytes: rng } = await import("node:crypto");
814
876
  const { createHash } = await import("node:crypto");
@@ -867,7 +929,7 @@ app.get("/api/mobile/info/:token", async (c) => {
867
929
  const token = c.req.param("token");
868
930
  const voucher = deviceVouchers.get(token);
869
931
  if (!voucher || voucher.consumed || voucher.expiresAt < Date.now()) {
870
- return c.json({ error: "Invalid or expired voucher" }, 404);
932
+ return notFound("Invalid or expired voucher");
871
933
  }
872
934
  return c.json({
873
935
  instanceName: voucher.instanceName,
@@ -879,16 +941,16 @@ app.post("/api/mobile/redeem", async (c) => {
879
941
  const body = await c.req.json();
880
942
  const { token, password } = body;
881
943
  if (!token || !password) {
882
- return c.json({ error: "Voucher token and password required" }, 400);
944
+ return badRequest("Voucher token and password required");
883
945
  }
884
946
  const voucher = deviceVouchers.get(token);
885
947
  if (!voucher || voucher.consumed || voucher.expiresAt < Date.now()) {
886
- return c.json({ error: "Invalid or expired voucher" }, 404);
948
+ return notFound("Invalid or expired voucher");
887
949
  }
888
950
  // Validate safe word via existing auth
889
951
  const authResult = await authenticate(password);
890
952
  if ("error" in authResult) {
891
- return c.json({ error: "Invalid safe word" }, 401);
953
+ return unauthorized("Invalid safe word");
892
954
  }
893
955
  // Consume voucher
894
956
  voucher.consumed = true;
@@ -919,7 +981,7 @@ app.post("/api/mobile/redeem", async (c) => {
919
981
  app.get("/api/mobile/devices", async (c) => {
920
982
  const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
921
983
  if (!sessionId || !validateSession(sessionId)) {
922
- return c.json({ error: "Unauthorized" }, 401);
984
+ return unauthorized("Unauthorized");
923
985
  }
924
986
  return c.json({
925
987
  devices: [...pairedDevices.values()].map((d) => ({
@@ -933,7 +995,7 @@ app.get("/api/mobile/devices", async (c) => {
933
995
  app.delete("/api/mobile/devices/:label", async (c) => {
934
996
  const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
935
997
  if (!sessionId || !validateSession(sessionId)) {
936
- return c.json({ error: "Unauthorized" }, 401);
998
+ return unauthorized("Unauthorized");
937
999
  }
938
1000
  const label = c.req.param("label");
939
1001
  for (const [token, d] of pairedDevices) {
@@ -943,7 +1005,7 @@ app.delete("/api/mobile/devices/:label", async (c) => {
943
1005
  return c.json({ ok: true });
944
1006
  }
945
1007
  }
946
- return c.json({ error: "Device not found" }, 404);
1008
+ return notFound("Device not found");
947
1009
  });
948
1010
  // --- Relay polling (receive messages from paired phones) ---
949
1011
  let relayPollInterval = null;
@@ -1156,28 +1218,28 @@ async function handleRelayPair(token, password, label, instanceHash) {
1156
1218
  app.get("/api/vault", async (c) => {
1157
1219
  const sessionId = c.req.query("sessionId");
1158
1220
  if (!sessionId)
1159
- return c.json({ error: "sessionId required" }, 400);
1221
+ return badRequest("sessionId required");
1160
1222
  const session = validateSession(sessionId);
1161
1223
  if (!session)
1162
- return c.json({ error: "Invalid or expired session" }, 401);
1224
+ return unauthorized("Invalid or expired session");
1163
1225
  return c.json({ keys: (_vaultStore ? _vaultStore.listVaultKeys() : []) });
1164
1226
  });
1165
1227
  // Add or update a vault key
1166
1228
  app.put("/api/vault/:name", async (c) => {
1167
1229
  const sessionId = c.req.query("sessionId");
1168
1230
  if (!sessionId)
1169
- return c.json({ error: "sessionId required" }, 400);
1231
+ return badRequest("sessionId required");
1170
1232
  const session = validateSession(sessionId);
1171
1233
  if (!session)
1172
- return c.json({ error: "Invalid or expired session" }, 401);
1234
+ return unauthorized("Invalid or expired session");
1173
1235
  const key = sessionKeys.get(sessionId);
1174
1236
  if (!key)
1175
- return c.json({ error: "Session key not found" }, 401);
1237
+ return unauthorized("Session key not found");
1176
1238
  const name = c.req.param("name");
1177
1239
  const body = await c.req.json();
1178
1240
  const { value, label } = body;
1179
1241
  if (!value)
1180
- return c.json({ error: "value required" }, 400);
1242
+ return badRequest("value required");
1181
1243
  await _vaultStore.setVaultKey(name, value, key, label);
1182
1244
  return c.json({ ok: true });
1183
1245
  });
@@ -1185,13 +1247,13 @@ app.put("/api/vault/:name", async (c) => {
1185
1247
  app.delete("/api/vault/:name", async (c) => {
1186
1248
  const sessionId = c.req.query("sessionId");
1187
1249
  if (!sessionId)
1188
- return c.json({ error: "sessionId required" }, 400);
1250
+ return badRequest("sessionId required");
1189
1251
  const session = validateSession(sessionId);
1190
1252
  if (!session)
1191
- return c.json({ error: "Invalid or expired session" }, 401);
1253
+ return unauthorized("Invalid or expired session");
1192
1254
  const key = sessionKeys.get(sessionId);
1193
1255
  if (!key)
1194
- return c.json({ error: "Session key not found" }, 401);
1256
+ return unauthorized("Session key not found");
1195
1257
  const name = c.req.param("name");
1196
1258
  await _vaultStore.deleteVaultKey(name, key);
1197
1259
  return c.json({ ok: true });
@@ -1200,13 +1262,13 @@ app.delete("/api/vault/:name", async (c) => {
1200
1262
  app.post("/api/vault/export", async (c) => {
1201
1263
  const sessionId = c.req.query("sessionId");
1202
1264
  if (!sessionId)
1203
- return c.json({ error: "sessionId required" }, 400);
1265
+ return badRequest("sessionId required");
1204
1266
  const session = validateSession(sessionId);
1205
1267
  if (!session)
1206
- return c.json({ error: "Invalid or expired session" }, 401);
1268
+ return unauthorized("Invalid or expired session");
1207
1269
  const body = await c.req.json();
1208
1270
  if (!body.passphrase || body.passphrase.length < 8) {
1209
- return c.json({ error: "Passphrase required (min 8 characters)" }, 400);
1271
+ return badRequest("Passphrase required (min 8 characters)");
1210
1272
  }
1211
1273
  try {
1212
1274
  const result = await _vaultTransfer.exportVault(body.passphrase);
@@ -1220,16 +1282,16 @@ app.post("/api/vault/export", async (c) => {
1220
1282
  app.post("/api/vault/import", async (c) => {
1221
1283
  const sessionId = c.req.query("sessionId");
1222
1284
  if (!sessionId)
1223
- return c.json({ error: "sessionId required" }, 400);
1285
+ return badRequest("sessionId required");
1224
1286
  const session = validateSession(sessionId);
1225
1287
  if (!session)
1226
- return c.json({ error: "Invalid or expired session" }, 401);
1288
+ return unauthorized("Invalid or expired session");
1227
1289
  const key = sessionKeys.get(sessionId);
1228
1290
  if (!key)
1229
- return c.json({ error: "Session key not found" }, 401);
1291
+ return unauthorized("Session key not found");
1230
1292
  const body = await c.req.json();
1231
1293
  if (!body.filePath || !body.passphrase) {
1232
- return c.json({ error: "filePath and passphrase required" }, 400);
1294
+ return badRequest("filePath and passphrase required");
1233
1295
  }
1234
1296
  const strategy = body.strategy ?? "skip";
1235
1297
  try {
@@ -1237,27 +1299,27 @@ app.post("/api/vault/import", async (c) => {
1237
1299
  return c.json({ ok: true, stats: result.stats });
1238
1300
  }
1239
1301
  catch (e) {
1240
- return c.json({ error: e.message }, 400);
1302
+ return badRequest(e.message);
1241
1303
  }
1242
1304
  });
1243
1305
  // Verify a vault export file without importing
1244
1306
  app.post("/api/vault/verify-export", async (c) => {
1245
1307
  const sessionId = c.req.query("sessionId");
1246
1308
  if (!sessionId)
1247
- return c.json({ error: "sessionId required" }, 400);
1309
+ return badRequest("sessionId required");
1248
1310
  const session = validateSession(sessionId);
1249
1311
  if (!session)
1250
- return c.json({ error: "Invalid or expired session" }, 401);
1312
+ return unauthorized("Invalid or expired session");
1251
1313
  const body = await c.req.json();
1252
1314
  if (!body.filePath || !body.passphrase) {
1253
- return c.json({ error: "filePath and passphrase required" }, 400);
1315
+ return badRequest("filePath and passphrase required");
1254
1316
  }
1255
1317
  try {
1256
1318
  const result = await _vaultTransfer.verifyExport(body.filePath, body.passphrase);
1257
1319
  return c.json({ ok: true, message: result.message, stats: result.stats });
1258
1320
  }
1259
1321
  catch (e) {
1260
- return c.json({ error: e.message }, 400);
1322
+ return badRequest(e.message);
1261
1323
  }
1262
1324
  });
1263
1325
  // --- Google OAuth2 routes ---
@@ -1325,16 +1387,16 @@ app.get("/api/google/status", async (c) => {
1325
1387
  app.post("/api/google/send-email", async (c) => {
1326
1388
  const sessionId = c.req.query("sessionId");
1327
1389
  if (!sessionId)
1328
- return c.json({ error: "sessionId required" }, 400);
1390
+ return badRequest("sessionId required");
1329
1391
  const session = validateSession(sessionId);
1330
1392
  if (!session)
1331
- return c.json({ error: "Invalid or expired session" }, 401);
1393
+ return unauthorized("Invalid or expired session");
1332
1394
  if (!(_googleAuth?.isGoogleAuthenticated() ?? false)) {
1333
- return c.json({ error: "Google not authenticated" }, 400);
1395
+ return badRequest("Google not authenticated");
1334
1396
  }
1335
1397
  const body = await c.req.json();
1336
1398
  if (!body.to || !body.subject || !body.body) {
1337
- return c.json({ error: "to, subject, and body are required" }, 400);
1399
+ return badRequest("to, subject, and body are required");
1338
1400
  }
1339
1401
  const result = await _googleGmailSend.sendEmail(body);
1340
1402
  if (!result.ok)
@@ -1347,12 +1409,12 @@ app.post("/api/google/send-email", async (c) => {
1347
1409
  app.get("/api/google/gmail/inbox-summary", async (c) => {
1348
1410
  const sessionId = c.req.query("sessionId");
1349
1411
  if (!sessionId)
1350
- return c.json({ error: "sessionId required" }, 400);
1412
+ return badRequest("sessionId required");
1351
1413
  const session = validateSession(sessionId);
1352
1414
  if (!session)
1353
- return c.json({ error: "Invalid or expired session" }, 401);
1415
+ return unauthorized("Invalid or expired session");
1354
1416
  if (!(_googleGmail?.isGmailAvailable() ?? false))
1355
- return c.json({ error: "Google not authenticated" }, 400);
1417
+ return badRequest("Google not authenticated");
1356
1418
  const hours = parseInt(c.req.query("hours") ?? "24", 10);
1357
1419
  const result = await _googleGmail.getInboxSummary(hours);
1358
1420
  if (!result.ok)
@@ -1363,12 +1425,12 @@ app.get("/api/google/gmail/inbox-summary", async (c) => {
1363
1425
  app.get("/api/google/gmail/categorize", async (c) => {
1364
1426
  const sessionId = c.req.query("sessionId");
1365
1427
  if (!sessionId)
1366
- return c.json({ error: "sessionId required" }, 400);
1428
+ return badRequest("sessionId required");
1367
1429
  const session = validateSession(sessionId);
1368
1430
  if (!session)
1369
- return c.json({ error: "Invalid or expired session" }, 401);
1431
+ return unauthorized("Invalid or expired session");
1370
1432
  if (!(_googleGmail?.isGmailAvailable() ?? false))
1371
- return c.json({ error: "Google not authenticated" }, 400);
1433
+ return badRequest("Google not authenticated");
1372
1434
  const hours = parseInt(c.req.query("hours") ?? "24", 10);
1373
1435
  const result = await _googleGmail.categorizeMessages(hours);
1374
1436
  if (!result.ok)
@@ -1379,12 +1441,12 @@ app.get("/api/google/gmail/categorize", async (c) => {
1379
1441
  app.get("/api/google/gmail/prioritize", async (c) => {
1380
1442
  const sessionId = c.req.query("sessionId");
1381
1443
  if (!sessionId)
1382
- return c.json({ error: "sessionId required" }, 400);
1444
+ return badRequest("sessionId required");
1383
1445
  const session = validateSession(sessionId);
1384
1446
  if (!session)
1385
- return c.json({ error: "Invalid or expired session" }, 401);
1447
+ return unauthorized("Invalid or expired session");
1386
1448
  if (!(_googleGmail?.isGmailAvailable() ?? false))
1387
- return c.json({ error: "Google not authenticated" }, 400);
1449
+ return badRequest("Google not authenticated");
1388
1450
  const hours = parseInt(c.req.query("hours") ?? "24", 10);
1389
1451
  const result = await _googleGmail.prioritizeInbox(hours);
1390
1452
  if (!result.ok)
@@ -1395,12 +1457,12 @@ app.get("/api/google/gmail/prioritize", async (c) => {
1395
1457
  app.post("/api/google/gmail/mark-read", async (c) => {
1396
1458
  const sessionId = c.req.query("sessionId");
1397
1459
  if (!sessionId)
1398
- return c.json({ error: "sessionId required" }, 400);
1460
+ return badRequest("sessionId required");
1399
1461
  const session = validateSession(sessionId);
1400
1462
  if (!session)
1401
- return c.json({ error: "Invalid or expired session" }, 401);
1463
+ return unauthorized("Invalid or expired session");
1402
1464
  if (!(_googleGmail?.isGmailAvailable() ?? false))
1403
- return c.json({ error: "Google not authenticated" }, 400);
1465
+ return badRequest("Google not authenticated");
1404
1466
  const body = await c.req.json();
1405
1467
  if (body.messageIds && body.messageIds.length > 0) {
1406
1468
  const result = await _googleGmail.batchMarkAsRead(body.messageIds);
@@ -1409,7 +1471,7 @@ app.post("/api/google/gmail/mark-read", async (c) => {
1409
1471
  return c.json(result);
1410
1472
  }
1411
1473
  if (!body.messageId)
1412
- return c.json({ error: "messageId or messageIds required" }, 400);
1474
+ return badRequest("messageId or messageIds required");
1413
1475
  const result = await _googleGmail.markAsRead(body.messageId);
1414
1476
  if (!result.ok)
1415
1477
  return c.json({ error: result.message }, 500);
@@ -1419,12 +1481,12 @@ app.post("/api/google/gmail/mark-read", async (c) => {
1419
1481
  app.post("/api/google/gmail/mark-unread", async (c) => {
1420
1482
  const sessionId = c.req.query("sessionId");
1421
1483
  if (!sessionId)
1422
- return c.json({ error: "sessionId required" }, 400);
1484
+ return badRequest("sessionId required");
1423
1485
  const session = validateSession(sessionId);
1424
1486
  if (!session)
1425
- return c.json({ error: "Invalid or expired session" }, 401);
1487
+ return unauthorized("Invalid or expired session");
1426
1488
  if (!(_googleGmail?.isGmailAvailable() ?? false))
1427
- return c.json({ error: "Google not authenticated" }, 400);
1489
+ return badRequest("Google not authenticated");
1428
1490
  const body = await c.req.json();
1429
1491
  if (body.messageIds && body.messageIds.length > 0) {
1430
1492
  const result = await _googleGmail.batchMarkAsUnread(body.messageIds);
@@ -1433,7 +1495,7 @@ app.post("/api/google/gmail/mark-unread", async (c) => {
1433
1495
  return c.json(result);
1434
1496
  }
1435
1497
  if (!body.messageId)
1436
- return c.json({ error: "messageId or messageIds required" }, 400);
1498
+ return badRequest("messageId or messageIds required");
1437
1499
  const result = await _googleGmail.markAsUnread(body.messageId);
1438
1500
  if (!result.ok)
1439
1501
  return c.json({ error: result.message }, 500);
@@ -1443,7 +1505,7 @@ app.post("/api/google/gmail/mark-unread", async (c) => {
1443
1505
  // Get today's schedule
1444
1506
  app.get("/api/google/calendar/today", async (c) => {
1445
1507
  if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1446
- return c.json({ error: "Google not authenticated" }, 400);
1508
+ return badRequest("Google not authenticated");
1447
1509
  const result = await _googleCalendar.getTodaySchedule();
1448
1510
  if (!result.ok)
1449
1511
  return c.json({ error: result.message }, 500);
@@ -1452,7 +1514,7 @@ app.get("/api/google/calendar/today", async (c) => {
1452
1514
  // Get upcoming events
1453
1515
  app.get("/api/google/calendar/upcoming", async (c) => {
1454
1516
  if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1455
- return c.json({ error: "Google not authenticated" }, 400);
1517
+ return badRequest("Google not authenticated");
1456
1518
  const hours = parseInt(c.req.query("hours") ?? "4", 10);
1457
1519
  const result = await _googleCalendar.getUpcomingEvents(hours);
1458
1520
  if (!result.ok)
@@ -1462,10 +1524,10 @@ app.get("/api/google/calendar/upcoming", async (c) => {
1462
1524
  // Get free/busy
1463
1525
  app.post("/api/google/calendar/freebusy", async (c) => {
1464
1526
  if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1465
- return c.json({ error: "Google not authenticated" }, 400);
1527
+ return badRequest("Google not authenticated");
1466
1528
  const body = await c.req.json();
1467
1529
  if (!body.start || !body.end)
1468
- return c.json({ error: "start and end are required" }, 400);
1530
+ return badRequest("start and end are required");
1469
1531
  const result = await _googleCalendar.getFreeBusy(body.start, body.end);
1470
1532
  if (!result.ok)
1471
1533
  return c.json({ error: result.message }, 500);
@@ -1474,10 +1536,10 @@ app.post("/api/google/calendar/freebusy", async (c) => {
1474
1536
  // Create calendar event
1475
1537
  app.post("/api/google/calendar/events", async (c) => {
1476
1538
  if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1477
- return c.json({ error: "Google not authenticated" }, 400);
1539
+ return badRequest("Google not authenticated");
1478
1540
  const body = await c.req.json();
1479
1541
  if (!body.title || !body.start || !body.end) {
1480
- return c.json({ error: "title, start, and end are required" }, 400);
1542
+ return badRequest("title, start, and end are required");
1481
1543
  }
1482
1544
  // Temporal validation: catch day-of-week mismatches before creating events (ts_temporal_mismatch_01)
1483
1545
  const temporalCheck = _googleTemporal.validateCalendarEntry(body.start, body.expectedDayOfWeek);
@@ -1503,7 +1565,7 @@ app.post("/api/google/calendar/events", async (c) => {
1503
1565
  // List events with flexible filtering
1504
1566
  app.get("/api/google/calendar/events", async (c) => {
1505
1567
  if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1506
- return c.json({ error: "Google not authenticated" }, 400);
1568
+ return badRequest("Google not authenticated");
1507
1569
  const result = await _googleCalendar.listEvents({
1508
1570
  timeMin: c.req.query("timeMin"),
1509
1571
  timeMax: c.req.query("timeMax"),
@@ -1518,7 +1580,7 @@ app.get("/api/google/calendar/events", async (c) => {
1518
1580
  // Update calendar event
1519
1581
  app.patch("/api/google/calendar/events/:eventId", async (c) => {
1520
1582
  if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1521
- return c.json({ error: "Google not authenticated" }, 400);
1583
+ return badRequest("Google not authenticated");
1522
1584
  const eventId = c.req.param("eventId");
1523
1585
  const body = await c.req.json();
1524
1586
  // Temporal validation on updated start date (ts_temporal_mismatch_01)
@@ -1541,7 +1603,7 @@ app.patch("/api/google/calendar/events/:eventId", async (c) => {
1541
1603
  // Delete calendar event
1542
1604
  app.delete("/api/google/calendar/events/:eventId", async (c) => {
1543
1605
  if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1544
- return c.json({ error: "Google not authenticated" }, 400);
1606
+ return badRequest("Google not authenticated");
1545
1607
  const eventId = c.req.param("eventId");
1546
1608
  const sendUpdates = c.req.query("sendUpdates");
1547
1609
  const result = await _googleCalendar.deleteEvent(eventId, { sendUpdates });
@@ -1553,10 +1615,10 @@ app.delete("/api/google/calendar/events/:eventId", async (c) => {
1553
1615
  // Search calendar events by text
1554
1616
  app.get("/api/google/calendar/search", async (c) => {
1555
1617
  if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1556
- return c.json({ error: "Google not authenticated" }, 400);
1618
+ return badRequest("Google not authenticated");
1557
1619
  const query = c.req.query("q");
1558
1620
  if (!query)
1559
- return c.json({ error: "q (search query) is required" }, 400);
1621
+ return badRequest("q (search query) is required");
1560
1622
  const result = await _googleCalendar.searchEvents(query, {
1561
1623
  timeMin: c.req.query("timeMin"),
1562
1624
  timeMax: c.req.query("timeMax"),
@@ -1571,12 +1633,12 @@ app.get("/api/google/calendar/search", async (c) => {
1571
1633
  app.get("/api/google/tasks/lists", async (c) => {
1572
1634
  const sessionId = c.req.query("sessionId");
1573
1635
  if (!sessionId)
1574
- return c.json({ error: "sessionId required" }, 400);
1636
+ return badRequest("sessionId required");
1575
1637
  const session = validateSession(sessionId);
1576
1638
  if (!session)
1577
- return c.json({ error: "Invalid or expired session" }, 401);
1639
+ return unauthorized("Invalid or expired session");
1578
1640
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1579
- return c.json({ error: "Google not authenticated" }, 400);
1641
+ return badRequest("Google not authenticated");
1580
1642
  const result = await _googleTasks.listTaskLists();
1581
1643
  if (!result.ok)
1582
1644
  return c.json({ error: result.message }, 500);
@@ -1586,15 +1648,15 @@ app.get("/api/google/tasks/lists", async (c) => {
1586
1648
  app.post("/api/google/tasks/lists", async (c) => {
1587
1649
  const sessionId = c.req.query("sessionId");
1588
1650
  if (!sessionId)
1589
- return c.json({ error: "sessionId required" }, 400);
1651
+ return badRequest("sessionId required");
1590
1652
  const session = validateSession(sessionId);
1591
1653
  if (!session)
1592
- return c.json({ error: "Invalid or expired session" }, 401);
1654
+ return unauthorized("Invalid or expired session");
1593
1655
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1594
- return c.json({ error: "Google not authenticated" }, 400);
1656
+ return badRequest("Google not authenticated");
1595
1657
  const body = await c.req.json();
1596
1658
  if (!body.title)
1597
- return c.json({ error: "title is required" }, 400);
1659
+ return badRequest("title is required");
1598
1660
  const result = await _googleTasks.createTaskList(body.title);
1599
1661
  if (!result.ok)
1600
1662
  return c.json({ error: result.message }, 500);
@@ -1605,16 +1667,16 @@ app.post("/api/google/tasks/lists", async (c) => {
1605
1667
  app.patch("/api/google/tasks/lists/:listId", async (c) => {
1606
1668
  const sessionId = c.req.query("sessionId");
1607
1669
  if (!sessionId)
1608
- return c.json({ error: "sessionId required" }, 400);
1670
+ return badRequest("sessionId required");
1609
1671
  const session = validateSession(sessionId);
1610
1672
  if (!session)
1611
- return c.json({ error: "Invalid or expired session" }, 401);
1673
+ return unauthorized("Invalid or expired session");
1612
1674
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1613
- return c.json({ error: "Google not authenticated" }, 400);
1675
+ return badRequest("Google not authenticated");
1614
1676
  const listId = c.req.param("listId");
1615
1677
  const body = await c.req.json();
1616
1678
  if (!body.title)
1617
- return c.json({ error: "title is required" }, 400);
1679
+ return badRequest("title is required");
1618
1680
  const result = await _googleTasks.updateTaskList(listId, body.title);
1619
1681
  if (!result.ok)
1620
1682
  return c.json({ error: result.message }, 500);
@@ -1624,12 +1686,12 @@ app.patch("/api/google/tasks/lists/:listId", async (c) => {
1624
1686
  app.delete("/api/google/tasks/lists/:listId", async (c) => {
1625
1687
  const sessionId = c.req.query("sessionId");
1626
1688
  if (!sessionId)
1627
- return c.json({ error: "sessionId required" }, 400);
1689
+ return badRequest("sessionId required");
1628
1690
  const session = validateSession(sessionId);
1629
1691
  if (!session)
1630
- return c.json({ error: "Invalid or expired session" }, 401);
1692
+ return unauthorized("Invalid or expired session");
1631
1693
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1632
- return c.json({ error: "Google not authenticated" }, 400);
1694
+ return badRequest("Google not authenticated");
1633
1695
  const listId = c.req.param("listId");
1634
1696
  const result = await _googleTasks.deleteTaskList(listId);
1635
1697
  if (!result.ok)
@@ -1641,19 +1703,19 @@ app.delete("/api/google/tasks/lists/:listId", async (c) => {
1641
1703
  app.post("/api/google/tasks/recurring", async (c) => {
1642
1704
  const sessionId = c.req.query("sessionId");
1643
1705
  if (!sessionId)
1644
- return c.json({ error: "sessionId required" }, 400);
1706
+ return badRequest("sessionId required");
1645
1707
  const session = validateSession(sessionId);
1646
1708
  if (!session)
1647
- return c.json({ error: "Invalid or expired session" }, 401);
1709
+ return unauthorized("Invalid or expired session");
1648
1710
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1649
- return c.json({ error: "Google not authenticated" }, 400);
1711
+ return badRequest("Google not authenticated");
1650
1712
  const body = await c.req.json();
1651
1713
  if (!body.title)
1652
- return c.json({ error: "title is required" }, 400);
1714
+ return badRequest("title is required");
1653
1715
  if (body.dayOfWeek === undefined)
1654
- return c.json({ error: "dayOfWeek is required (0=Sun, 6=Sat)" }, 400);
1716
+ return badRequest("dayOfWeek is required (0=Sun, 6=Sat)");
1655
1717
  if (body.hour === undefined)
1656
- return c.json({ error: "hour is required (0-23)" }, 400);
1718
+ return badRequest("hour is required (0-23)");
1657
1719
  // Temporal validation: cross-check dayOfWeek number against expectedDayName (ts_temporal_mismatch_01)
1658
1720
  if (body.expectedDayName) {
1659
1721
  const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
@@ -1679,12 +1741,12 @@ app.post("/api/google/tasks/recurring", async (c) => {
1679
1741
  app.get("/api/google/tasks/:listId", async (c) => {
1680
1742
  const sessionId = c.req.query("sessionId");
1681
1743
  if (!sessionId)
1682
- return c.json({ error: "sessionId required" }, 400);
1744
+ return badRequest("sessionId required");
1683
1745
  const session = validateSession(sessionId);
1684
1746
  if (!session)
1685
- return c.json({ error: "Invalid or expired session" }, 401);
1747
+ return unauthorized("Invalid or expired session");
1686
1748
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1687
- return c.json({ error: "Google not authenticated" }, 400);
1749
+ return badRequest("Google not authenticated");
1688
1750
  const listId = c.req.param("listId");
1689
1751
  const showCompleted = c.req.query("showCompleted") === "true";
1690
1752
  const dueMin = c.req.query("dueMin");
@@ -1702,12 +1764,12 @@ app.get("/api/google/tasks/:listId", async (c) => {
1702
1764
  app.get("/api/google/tasks/:listId/:taskId", async (c) => {
1703
1765
  const sessionId = c.req.query("sessionId");
1704
1766
  if (!sessionId)
1705
- return c.json({ error: "sessionId required" }, 400);
1767
+ return badRequest("sessionId required");
1706
1768
  const session = validateSession(sessionId);
1707
1769
  if (!session)
1708
- return c.json({ error: "Invalid or expired session" }, 401);
1770
+ return unauthorized("Invalid or expired session");
1709
1771
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1710
- return c.json({ error: "Google not authenticated" }, 400);
1772
+ return badRequest("Google not authenticated");
1711
1773
  const listId = c.req.param("listId");
1712
1774
  const taskId = c.req.param("taskId");
1713
1775
  const result = await _googleTasks.getTask(listId, taskId);
@@ -1719,16 +1781,16 @@ app.get("/api/google/tasks/:listId/:taskId", async (c) => {
1719
1781
  app.post("/api/google/tasks/:listId", async (c) => {
1720
1782
  const sessionId = c.req.query("sessionId");
1721
1783
  if (!sessionId)
1722
- return c.json({ error: "sessionId required" }, 400);
1784
+ return badRequest("sessionId required");
1723
1785
  const session = validateSession(sessionId);
1724
1786
  if (!session)
1725
- return c.json({ error: "Invalid or expired session" }, 401);
1787
+ return unauthorized("Invalid or expired session");
1726
1788
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1727
- return c.json({ error: "Google not authenticated" }, 400);
1789
+ return badRequest("Google not authenticated");
1728
1790
  const listId = c.req.param("listId");
1729
1791
  const body = await c.req.json();
1730
1792
  if (!body.title)
1731
- return c.json({ error: "title is required" }, 400);
1793
+ return badRequest("title is required");
1732
1794
  const result = await _googleTasks.createTask(listId, body);
1733
1795
  if (!result.ok)
1734
1796
  return c.json({ error: result.message }, 500);
@@ -1739,12 +1801,12 @@ app.post("/api/google/tasks/:listId", async (c) => {
1739
1801
  app.patch("/api/google/tasks/:listId/:taskId", async (c) => {
1740
1802
  const sessionId = c.req.query("sessionId");
1741
1803
  if (!sessionId)
1742
- return c.json({ error: "sessionId required" }, 400);
1804
+ return badRequest("sessionId required");
1743
1805
  const session = validateSession(sessionId);
1744
1806
  if (!session)
1745
- return c.json({ error: "Invalid or expired session" }, 401);
1807
+ return unauthorized("Invalid or expired session");
1746
1808
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1747
- return c.json({ error: "Google not authenticated" }, 400);
1809
+ return badRequest("Google not authenticated");
1748
1810
  const listId = c.req.param("listId");
1749
1811
  const taskId = c.req.param("taskId");
1750
1812
  const body = await c.req.json();
@@ -1758,12 +1820,12 @@ app.patch("/api/google/tasks/:listId/:taskId", async (c) => {
1758
1820
  app.post("/api/google/tasks/:listId/:taskId/complete", async (c) => {
1759
1821
  const sessionId = c.req.query("sessionId");
1760
1822
  if (!sessionId)
1761
- return c.json({ error: "sessionId required" }, 400);
1823
+ return badRequest("sessionId required");
1762
1824
  const session = validateSession(sessionId);
1763
1825
  if (!session)
1764
- return c.json({ error: "Invalid or expired session" }, 401);
1826
+ return unauthorized("Invalid or expired session");
1765
1827
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1766
- return c.json({ error: "Google not authenticated" }, 400);
1828
+ return badRequest("Google not authenticated");
1767
1829
  const listId = c.req.param("listId");
1768
1830
  const taskId = c.req.param("taskId");
1769
1831
  const result = await _googleTasks.completeTask(listId, taskId);
@@ -1776,12 +1838,12 @@ app.post("/api/google/tasks/:listId/:taskId/complete", async (c) => {
1776
1838
  app.post("/api/google/tasks/:listId/:taskId/uncomplete", async (c) => {
1777
1839
  const sessionId = c.req.query("sessionId");
1778
1840
  if (!sessionId)
1779
- return c.json({ error: "sessionId required" }, 400);
1841
+ return badRequest("sessionId required");
1780
1842
  const session = validateSession(sessionId);
1781
1843
  if (!session)
1782
- return c.json({ error: "Invalid or expired session" }, 401);
1844
+ return unauthorized("Invalid or expired session");
1783
1845
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1784
- return c.json({ error: "Google not authenticated" }, 400);
1846
+ return badRequest("Google not authenticated");
1785
1847
  const listId = c.req.param("listId");
1786
1848
  const taskId = c.req.param("taskId");
1787
1849
  const result = await _googleTasks.uncompleteTask(listId, taskId);
@@ -1793,12 +1855,12 @@ app.post("/api/google/tasks/:listId/:taskId/uncomplete", async (c) => {
1793
1855
  app.delete("/api/google/tasks/:listId/:taskId", async (c) => {
1794
1856
  const sessionId = c.req.query("sessionId");
1795
1857
  if (!sessionId)
1796
- return c.json({ error: "sessionId required" }, 400);
1858
+ return badRequest("sessionId required");
1797
1859
  const session = validateSession(sessionId);
1798
1860
  if (!session)
1799
- return c.json({ error: "Invalid or expired session" }, 401);
1861
+ return unauthorized("Invalid or expired session");
1800
1862
  if (!(_googleTasks?.isTasksAvailable() ?? false))
1801
- return c.json({ error: "Google not authenticated" }, 400);
1863
+ return badRequest("Google not authenticated");
1802
1864
  const listId = c.req.param("listId");
1803
1865
  const taskId = c.req.param("taskId");
1804
1866
  const result = await _googleTasks.deleteTask(listId, taskId);
@@ -1812,10 +1874,10 @@ app.delete("/api/google/tasks/:listId/:taskId", async (c) => {
1812
1874
  app.get("/api/prompt", async (c) => {
1813
1875
  const sessionId = c.req.query("sessionId");
1814
1876
  if (!sessionId)
1815
- return c.json({ error: "sessionId required" }, 400);
1877
+ return badRequest("sessionId required");
1816
1878
  const session = validateSession(sessionId);
1817
1879
  if (!session)
1818
- return c.json({ error: "Invalid or expired session" }, 401);
1880
+ return unauthorized("Invalid or expired session");
1819
1881
  let prompt = "";
1820
1882
  try {
1821
1883
  prompt = await readBrainFile(PERSONALITY_PATH);
@@ -1828,10 +1890,10 @@ app.put("/api/prompt", async (c) => {
1828
1890
  const body = await c.req.json();
1829
1891
  const { sessionId, prompt } = body;
1830
1892
  if (!sessionId)
1831
- return c.json({ error: "sessionId required" }, 400);
1893
+ return badRequest("sessionId required");
1832
1894
  const session = validateSession(sessionId);
1833
1895
  if (!session)
1834
- return c.json({ error: "Invalid or expired session" }, 401);
1896
+ return unauthorized("Invalid or expired session");
1835
1897
  await mkdir(join(BRAIN_DIR, "identity"), { recursive: true });
1836
1898
  await writeBrainFile(PERSONALITY_PATH, prompt ?? "");
1837
1899
  return c.json({ ok: true });
@@ -1839,32 +1901,47 @@ app.put("/api/prompt", async (c) => {
1839
1901
  // --- Model discovery ---
1840
1902
  app.get("/api/models", async (c) => {
1841
1903
  const ollamaUrl = process.env.OLLAMA_URL ?? "http://localhost:11434";
1904
+ const results = [];
1905
+ // Fetch local Ollama models
1842
1906
  try {
1843
1907
  const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });
1844
- if (!res.ok)
1845
- return c.json({ models: [], error: "Ollama not responding" });
1846
- const data = await res.json();
1847
- const models = (data.models ?? []).map((m) => ({
1848
- name: m.name,
1849
- size: m.size,
1850
- modified: m.modified_at,
1851
- }));
1852
- return c.json({ models });
1908
+ if (res.ok) {
1909
+ const data = await res.json();
1910
+ for (const m of data.models ?? []) {
1911
+ results.push({ name: m.name, size: m.size, modified: m.modified_at, source: "ollama" });
1912
+ }
1913
+ }
1853
1914
  }
1854
- catch {
1855
- return c.json({ models: [], error: "Ollama not reachable" });
1915
+ catch { /* Ollama not reachable */ }
1916
+ // Fetch OpenRouter models if API key is set
1917
+ const orKey = process.env.OPENROUTER_API_KEY;
1918
+ if (orKey) {
1919
+ try {
1920
+ const res = await fetch("https://openrouter.ai/api/v1/models", {
1921
+ headers: { Authorization: `Bearer ${orKey}` },
1922
+ signal: AbortSignal.timeout(5000),
1923
+ });
1924
+ if (res.ok) {
1925
+ const data = await res.json();
1926
+ for (const m of data.data ?? []) {
1927
+ results.push({ name: m.id, source: "openrouter" });
1928
+ }
1929
+ }
1930
+ }
1931
+ catch { /* OpenRouter not reachable */ }
1856
1932
  }
1933
+ return c.json({ models: results });
1857
1934
  });
1858
1935
  // --- Sensitivity trainer ---
1859
1936
  app.post("/api/sensitive/flag", async (c) => {
1860
1937
  const body = await c.req.json().catch(() => null);
1861
1938
  if (!body?.value || typeof body.value !== "string") {
1862
- return c.json({ error: "value required" }, 400);
1939
+ return badRequest("value required");
1863
1940
  }
1864
1941
  const category = (body.category || "FLAGGED").toUpperCase();
1865
1942
  const value = body.value.trim();
1866
1943
  if (value.length < 2) {
1867
- return c.json({ error: "value too short" }, 400);
1944
+ return badRequest("value too short");
1868
1945
  }
1869
1946
  if (!activeSensitiveRegistry) {
1870
1947
  return c.json({ error: "registry not initialized" }, 503);
@@ -1984,7 +2061,7 @@ app.get("/api/voice-status", async (c) => {
1984
2061
  app.get("/api/tts", async (c) => {
1985
2062
  const text = c.req.query("text");
1986
2063
  if (!text)
1987
- return c.json({ error: "text query param required" }, 400);
2064
+ return badRequest("text query param required");
1988
2065
  if (!(_ttsClient?.isTtsAvailable() ?? false))
1989
2066
  return c.json({ error: "TTS not available" }, 503);
1990
2067
  const wav = await _ttsClient.synthesize(text);
@@ -2003,7 +2080,7 @@ app.post("/api/stt", async (c) => {
2003
2080
  return c.json({ error: "STT not available" }, 503);
2004
2081
  const body = await c.req.arrayBuffer();
2005
2082
  if (!body || body.byteLength === 0)
2006
- return c.json({ error: "Audio body required" }, 400);
2083
+ return badRequest("Audio body required");
2007
2084
  const text = await _sttClient.transcribe(Buffer.from(body));
2008
2085
  if (!text)
2009
2086
  return c.json({ error: "Transcription failed" }, 502);
@@ -2036,7 +2113,7 @@ app.get("/api/avatar/video/:hash", async (c) => {
2036
2113
  const hash = c.req.param("hash");
2037
2114
  // Sanitize: only allow alphanumeric + .mp4
2038
2115
  if (!/^[a-f0-9]+\.mp4$/.test(hash)) {
2039
- return c.json({ error: "Invalid hash" }, 400);
2116
+ return badRequest("Invalid hash");
2040
2117
  }
2041
2118
  const filePath = join(UI_DIR, "avatar", "cache", hash);
2042
2119
  try {
@@ -2050,22 +2127,22 @@ app.get("/api/avatar/video/:hash", async (c) => {
2050
2127
  });
2051
2128
  }
2052
2129
  catch {
2053
- return c.json({ error: "Not found" }, 404);
2130
+ return notFound();
2054
2131
  }
2055
2132
  });
2056
2133
  // Upload a new reference photo, re-prepare, clear cache
2057
2134
  app.post("/api/avatar/photo", async (c) => {
2058
2135
  const sessionId = c.req.query("sessionId");
2059
2136
  if (!sessionId)
2060
- return c.json({ error: "sessionId required" }, 400);
2137
+ return badRequest("sessionId required");
2061
2138
  const session = validateSession(sessionId);
2062
2139
  if (!session)
2063
- return c.json({ error: "Invalid or expired session" }, 401);
2140
+ return unauthorized("Invalid or expired session");
2064
2141
  if (!(_avatarSidecar?.isAvatarAvailable() ?? false))
2065
2142
  return c.json({ error: "Avatar not available" }, 503);
2066
2143
  const body = await c.req.arrayBuffer();
2067
2144
  if (!body || body.byteLength === 0)
2068
- return c.json({ error: "Photo body required" }, 400);
2145
+ return badRequest("Photo body required");
2069
2146
  const avatarConfig = _settingsVoice ? _settingsVoice.getAvatarConfig() : { photoPath: "public/avatar/photo.png", port: 0, enabled: false };
2070
2147
  const photoPath = join(process.cwd(), avatarConfig.photoPath);
2071
2148
  await mkdir(join(UI_DIR, "avatar"), { recursive: true });
@@ -2084,7 +2161,7 @@ app.post("/api/extract", async (c) => {
2084
2161
  const formData = await c.req.formData();
2085
2162
  const file = formData.get("file");
2086
2163
  if (!file)
2087
- return c.json({ error: "No file provided" }, 400);
2164
+ return badRequest("No file provided");
2088
2165
  const buffer = Buffer.from(await file.arrayBuffer());
2089
2166
  const name = file.name.toLowerCase();
2090
2167
  let text = "";
@@ -2116,16 +2193,16 @@ app.post("/api/extract", async (c) => {
2116
2193
  app.get("/api/history", async (c) => {
2117
2194
  const sessionId = c.req.query("sessionId");
2118
2195
  if (!sessionId)
2119
- return c.json({ error: "sessionId required" }, 400);
2196
+ return badRequest("sessionId required");
2120
2197
  const session = validateSession(sessionId);
2121
2198
  if (!session)
2122
- return c.json({ error: "Invalid or expired session" }, 401);
2199
+ return unauthorized("Invalid or expired session");
2123
2200
  const cs = await getOrCreateChatSession(sessionId, session.name);
2124
2201
  // Always return main history (not active thread's)
2125
2202
  const historySource = cs.activeThreadId ? cs.mainHistory : cs.history;
2126
2203
  const messages = historySource
2127
2204
  .filter((m) => m.role === "user" || m.role === "assistant")
2128
- .map((m) => ({ role: m.role, content: m.content }));
2205
+ .map((m) => ({ role: m.role, content: m.content, ...(m.toolsUsed ? { toolsUsed: m.toolsUsed } : {}), ...(m.agentsUsed ? { agentsUsed: m.agentsUsed } : {}) }));
2129
2206
  return c.json({ messages });
2130
2207
  });
2131
2208
  // Persist intro message so it appears in all tabs/devices
@@ -2133,10 +2210,10 @@ app.post("/api/history/intro", async (c) => {
2133
2210
  const body = await c.req.json();
2134
2211
  const { sessionId, message } = body;
2135
2212
  if (!sessionId || !message)
2136
- return c.json({ error: "sessionId and message required" }, 400);
2213
+ return badRequest("sessionId and message required");
2137
2214
  const session = validateSession(sessionId);
2138
2215
  if (!session)
2139
- return c.json({ error: "Invalid session" }, 401);
2216
+ return unauthorized("Invalid session");
2140
2217
  const cs = await getOrCreateChatSession(sessionId, session.name);
2141
2218
  // Only add if history is empty (first run)
2142
2219
  if (cs.history.length === 0) {
@@ -2148,10 +2225,10 @@ app.post("/api/history/intro", async (c) => {
2148
2225
  app.get("/api/threads", async (c) => {
2149
2226
  const sessionId = c.req.query("sessionId");
2150
2227
  if (!sessionId)
2151
- return c.json({ error: "sessionId required" }, 400);
2228
+ return badRequest("sessionId required");
2152
2229
  const session = validateSession(sessionId);
2153
2230
  if (!session)
2154
- return c.json({ error: "Invalid session" }, 401);
2231
+ return unauthorized("Invalid session");
2155
2232
  const threads = getThreadsForSession(sessionId);
2156
2233
  const list = [...threads.values()]
2157
2234
  .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
@@ -2162,10 +2239,10 @@ app.post("/api/threads", async (c) => {
2162
2239
  const body = await c.req.json();
2163
2240
  const sessionId = body.sessionId;
2164
2241
  if (!sessionId)
2165
- return c.json({ error: "sessionId required" }, 400);
2242
+ return badRequest("sessionId required");
2166
2243
  const session = validateSession(sessionId);
2167
2244
  if (!session)
2168
- return c.json({ error: "Invalid session" }, 401);
2245
+ return unauthorized("Invalid session");
2169
2246
  const threads = getThreadsForSession(sessionId);
2170
2247
  const now = new Date().toISOString();
2171
2248
  const thread = {
@@ -2177,8 +2254,7 @@ app.post("/api/threads", async (c) => {
2177
2254
  updatedAt: now,
2178
2255
  };
2179
2256
  threads.set(thread.id, thread);
2180
- // New thread starts blank. Main chat keeps its history.
2181
- // If currently on a thread, save it back first.
2257
+ // Save current thread's history before creating new one
2182
2258
  const cs = chatSessions.get(sessionId);
2183
2259
  if (cs && cs.activeThreadId) {
2184
2260
  const prevThread = threads.get(cs.activeThreadId);
@@ -2186,44 +2262,41 @@ app.post("/api/threads", async (c) => {
2186
2262
  prevThread.history = cs.history;
2187
2263
  prevThread.historySummary = cs.historySummary;
2188
2264
  }
2189
- cs.history = cs.mainHistory;
2190
- cs.historySummary = cs.mainHistorySummary;
2191
- cs.activeThreadId = null;
2192
2265
  }
2193
2266
  return c.json({ thread: { id: thread.id, title: thread.title, createdAt: thread.createdAt, updatedAt: thread.updatedAt } });
2194
2267
  });
2195
2268
  app.get("/api/threads/:id/history", async (c) => {
2196
2269
  const sessionId = c.req.query("sessionId");
2197
2270
  if (!sessionId)
2198
- return c.json({ error: "sessionId required" }, 400);
2271
+ return badRequest("sessionId required");
2199
2272
  const session = validateSession(sessionId);
2200
2273
  if (!session)
2201
- return c.json({ error: "Invalid session" }, 401);
2274
+ return unauthorized("Invalid session");
2202
2275
  const threadId = c.req.param("id");
2203
2276
  const threads = getThreadsForSession(sessionId);
2204
2277
  const thread = threads.get(threadId);
2205
2278
  if (!thread)
2206
- return c.json({ error: "Thread not found" }, 404);
2279
+ return notFound("Thread not found");
2207
2280
  // If this thread is currently active on cs, its live history is in cs.history
2208
2281
  const cs = chatSessions.get(sessionId);
2209
2282
  const historySource = (cs && cs.activeThreadId === threadId) ? cs.history : thread.history;
2210
2283
  const messages = historySource
2211
2284
  .filter((m) => m.role === "user" || m.role === "assistant")
2212
- .map((m) => ({ role: m.role, content: m.content }));
2285
+ .map((m) => ({ role: m.role, content: m.content, ...(m.toolsUsed ? { toolsUsed: m.toolsUsed } : {}), ...(m.agentsUsed ? { agentsUsed: m.agentsUsed } : {}) }));
2213
2286
  return c.json({ messages });
2214
2287
  });
2215
2288
  app.patch("/api/threads/:id", async (c) => {
2216
2289
  const body = await c.req.json();
2217
2290
  const sessionId = body.sessionId;
2218
2291
  if (!sessionId)
2219
- return c.json({ error: "sessionId required" }, 400);
2292
+ return badRequest("sessionId required");
2220
2293
  const session = validateSession(sessionId);
2221
2294
  if (!session)
2222
- return c.json({ error: "Invalid session" }, 401);
2295
+ return unauthorized("Invalid session");
2223
2296
  const threads = getThreadsForSession(sessionId);
2224
2297
  const thread = threads.get(c.req.param("id"));
2225
2298
  if (!thread)
2226
- return c.json({ error: "Thread not found" }, 404);
2299
+ return notFound("Thread not found");
2227
2300
  if (body.title)
2228
2301
  thread.title = body.title;
2229
2302
  thread.updatedAt = new Date().toISOString();
@@ -2232,16 +2305,18 @@ app.patch("/api/threads/:id", async (c) => {
2232
2305
  app.delete("/api/threads/:id", async (c) => {
2233
2306
  const sessionId = c.req.query("sessionId");
2234
2307
  if (!sessionId)
2235
- return c.json({ error: "sessionId required" }, 400);
2308
+ return badRequest("sessionId required");
2236
2309
  const session = validateSession(sessionId);
2237
2310
  if (!session)
2238
- return c.json({ error: "Invalid session" }, 401);
2311
+ return unauthorized("Invalid session");
2239
2312
  const threadId = c.req.param("id");
2240
2313
  const threads = getThreadsForSession(sessionId);
2241
- // If deleting the active thread, switch back to main first
2314
+ // If deleting the active thread, clear it (frontend creates a new one)
2242
2315
  const cs = chatSessions.get(sessionId);
2243
2316
  if (cs && cs.activeThreadId === threadId) {
2244
- switchSessionThread(cs, null, sessionId);
2317
+ cs.history = [];
2318
+ cs.historySummary = "";
2319
+ cs.activeThreadId = null;
2245
2320
  }
2246
2321
  threads.delete(threadId);
2247
2322
  return c.json({ ok: true });
@@ -2251,10 +2326,10 @@ app.delete("/api/threads/:id", async (c) => {
2251
2326
  app.get("/api/activity/stream", async (c) => {
2252
2327
  const sessionId = c.req.query("sessionId");
2253
2328
  if (!sessionId)
2254
- return c.json({ error: "sessionId required" }, 400);
2329
+ return badRequest("sessionId required");
2255
2330
  const session = validateSession(sessionId);
2256
2331
  if (!session)
2257
- return c.json({ error: "Invalid or expired session" }, 401);
2332
+ return unauthorized("Invalid or expired session");
2258
2333
  const { onActivity } = await import("./activity/log.js");
2259
2334
  return streamSSE(c, async (stream) => {
2260
2335
  // Send heartbeat immediately
@@ -2279,10 +2354,10 @@ app.get("/api/activity/stream", async (c) => {
2279
2354
  app.get("/api/activity", async (c) => {
2280
2355
  const sessionId = c.req.query("sessionId");
2281
2356
  if (!sessionId)
2282
- return c.json({ error: "sessionId required" }, 400);
2357
+ return badRequest("sessionId required");
2283
2358
  const session = validateSession(sessionId);
2284
2359
  if (!session)
2285
- return c.json({ error: "Invalid or expired session" }, 401);
2360
+ return unauthorized("Invalid or expired session");
2286
2361
  const since = parseInt(c.req.query("since") ?? "0", 10) || 0;
2287
2362
  return c.json({ activities: await getActivities(since) });
2288
2363
  });
@@ -2291,14 +2366,14 @@ app.post("/api/branch", async (c) => {
2291
2366
  const body = await c.req.json();
2292
2367
  const { sessionId, entryIds, question } = body;
2293
2368
  if (!sessionId || !entryIds?.length || !question) {
2294
- return c.json({ error: "sessionId, entryIds, and question required" }, 400);
2369
+ return badRequest("sessionId, entryIds, and question required");
2295
2370
  }
2296
2371
  const session = validateSession(sessionId);
2297
2372
  if (!session)
2298
- return c.json({ error: "Invalid or expired session" }, 401);
2373
+ return unauthorized("Invalid or expired session");
2299
2374
  const selected = await getActivitiesByIds(entryIds);
2300
2375
  if (selected.length === 0) {
2301
- return c.json({ error: "No matching activity entries" }, 404);
2376
+ return notFound("No matching activity entries");
2302
2377
  }
2303
2378
  // Generate a trace for this branch, backreffing the first selected entry
2304
2379
  const branchTraceId = generateTraceId();
@@ -2436,7 +2511,7 @@ app.post("/api/agents/tasks", async (c) => {
2436
2511
  const body = await c.req.json();
2437
2512
  const { label, prompt, cwd, origin, sessionId: sid, timeoutMs } = body;
2438
2513
  if (!prompt)
2439
- return c.json({ error: "prompt required" }, 400);
2514
+ return badRequest("prompt required");
2440
2515
  try {
2441
2516
  const task = await submitTask({
2442
2517
  label: label || prompt.slice(0, 60),
@@ -2459,7 +2534,7 @@ app.get("/api/agents/tasks", async (c) => {
2459
2534
  app.get("/api/agents/tasks/:id", async (c) => {
2460
2535
  const task = await getTask(c.req.param("id"));
2461
2536
  if (!task)
2462
- return c.json({ error: "Not found" }, 404);
2537
+ return notFound();
2463
2538
  return c.json(task);
2464
2539
  });
2465
2540
  app.get("/api/agents/tasks/:id/output", async (c) => {
@@ -2469,7 +2544,7 @@ app.get("/api/agents/tasks/:id/output", async (c) => {
2469
2544
  app.post("/api/agents/tasks/:id/cancel", async (c) => {
2470
2545
  const ok = await cancelTask(c.req.param("id"));
2471
2546
  if (!ok)
2472
- return c.json({ error: "Not found" }, 404);
2547
+ return notFound();
2473
2548
  return c.json({ ok: true });
2474
2549
  });
2475
2550
  // --- File lock routes ---
@@ -2481,7 +2556,7 @@ app.post("/api/agents/locks/acquire", async (c) => {
2481
2556
  const body = await c.req.json();
2482
2557
  const { agentId, agentLabel, filePaths, timeoutMs } = body;
2483
2558
  if (!agentId || !filePaths || !Array.isArray(filePaths)) {
2484
- return c.json({ error: "agentId and filePaths[] required" }, 400);
2559
+ return badRequest("agentId and filePaths[] required");
2485
2560
  }
2486
2561
  const result = await _agentLocks.acquireLocks(agentId, agentLabel || agentId, filePaths, timeoutMs);
2487
2562
  return c.json(result, result.acquired ? 200 : 409);
@@ -2490,7 +2565,7 @@ app.post("/api/agents/locks/release", async (c) => {
2490
2565
  const body = await c.req.json();
2491
2566
  const { agentId, filePath } = body;
2492
2567
  if (!agentId)
2493
- return c.json({ error: "agentId required" }, 400);
2568
+ return badRequest("agentId required");
2494
2569
  if (filePath) {
2495
2570
  const ok = await _agentLocks.releaseFileLock(agentId, filePath);
2496
2571
  return c.json({ released: ok ? 1 : 0 });
@@ -2502,7 +2577,7 @@ app.post("/api/agents/locks/force-release", async (c) => {
2502
2577
  const body = await c.req.json();
2503
2578
  const { filePath } = body;
2504
2579
  if (!filePath)
2505
- return c.json({ error: "filePath required" }, 400);
2580
+ return badRequest("filePath required");
2506
2581
  const ok = await _agentLocks.forceReleaseLock(filePath);
2507
2582
  return c.json({ released: ok });
2508
2583
  });
@@ -2510,7 +2585,7 @@ app.post("/api/agents/locks/check", async (c) => {
2510
2585
  const body = await c.req.json();
2511
2586
  const { filePaths } = body;
2512
2587
  if (!filePaths || !Array.isArray(filePaths)) {
2513
- return c.json({ error: "filePaths[] required" }, 400);
2588
+ return badRequest("filePaths[] required");
2514
2589
  }
2515
2590
  const conflicts = await _agentLocks.checkLocks(filePaths);
2516
2591
  return c.json({ locked: conflicts.length > 0, conflicts });
@@ -2549,7 +2624,7 @@ app.get("/api/runtime/instances/:id", async (c) => {
2549
2624
  return c.json({ error: "Runtime not initialized" }, 503);
2550
2625
  const inst = rt.getInstance(c.req.param("id"));
2551
2626
  if (!inst)
2552
- return c.json({ error: "Not found" }, 404);
2627
+ return notFound();
2553
2628
  return c.json(inst);
2554
2629
  });
2555
2630
  app.post("/api/runtime/spawn", async (c) => {
@@ -2559,7 +2634,7 @@ app.post("/api/runtime/spawn", async (c) => {
2559
2634
  const body = await c.req.json();
2560
2635
  const { taskId, label, prompt, cwd, origin, parentId, tags, config, resources } = body;
2561
2636
  if (!prompt)
2562
- return c.json({ error: "prompt required" }, 400);
2637
+ return badRequest("prompt required");
2563
2638
  try {
2564
2639
  // Create the underlying AgentTask first
2565
2640
  const { createTask } = await import("./agents/store.js");
@@ -2640,7 +2715,7 @@ app.post("/api/runtime/instances/:id/message", async (c) => {
2640
2715
  const body = await c.req.json();
2641
2716
  const { to, type, payload } = body;
2642
2717
  if (!to || !type)
2643
- return c.json({ error: "to and type required" }, 400);
2718
+ return badRequest("to and type required");
2644
2719
  const msg = rt.sendMessage(c.req.param("id"), to, type, payload);
2645
2720
  return c.json(msg);
2646
2721
  });
@@ -2733,7 +2808,7 @@ app.get("/api/workflows/:id", async (c) => {
2733
2808
  return c.json({ error: "Workflow engine not initialized" }, 503);
2734
2809
  const wf = engine.getOrchestrator().getWorkflow(c.req.param("id"));
2735
2810
  if (!wf)
2736
- return c.json({ error: "Workflow not found" }, 404);
2811
+ return notFound("Workflow not found");
2737
2812
  // Serialize Map<string, WorkflowTask> to plain object
2738
2813
  const tasks = {};
2739
2814
  for (const [key, task] of wf.tasks) {
@@ -2750,7 +2825,7 @@ app.get("/api/workflows/:id/results", async (c) => {
2750
2825
  return c.json(result);
2751
2826
  }
2752
2827
  catch {
2753
- return c.json({ error: "Workflow not found" }, 404);
2828
+ return notFound("Workflow not found");
2754
2829
  }
2755
2830
  });
2756
2831
  // --- Tracing routes ---
@@ -2762,16 +2837,16 @@ app.get("/api/traces", async (c) => {
2762
2837
  app.get("/api/traces/:traceId", async (c) => {
2763
2838
  const detail = tracer.getTraceDetail(c.req.param("traceId"));
2764
2839
  if (!detail)
2765
- return c.json({ error: "Trace not found" }, 404);
2840
+ return notFound("Trace not found");
2766
2841
  return c.json(detail);
2767
2842
  });
2768
2843
  app.get("/api/traces/agent/:agentId", async (c) => {
2769
2844
  const traceId = tracer.getAgentTraceId(c.req.param("agentId"));
2770
2845
  if (!traceId)
2771
- return c.json({ error: "No trace for agent" }, 404);
2846
+ return notFound("No trace for agent");
2772
2847
  const detail = tracer.getTraceDetail(traceId);
2773
2848
  if (!detail)
2774
- return c.json({ error: "Trace not found" }, 404);
2849
+ return notFound("Trace not found");
2775
2850
  return c.json(detail);
2776
2851
  });
2777
2852
  // --- GitHub routes ---
@@ -2784,7 +2859,7 @@ app.post("/api/github/webhooks", async (c) => {
2784
2859
  const secret = process.env.GITHUB_WEBHOOK_SECRET;
2785
2860
  if (secret && signature) {
2786
2861
  if (!_githubWebhooks.verifyWebhookSignature(rawBody, signature, secret)) {
2787
- return c.json({ error: "Invalid signature" }, 401);
2862
+ return unauthorized("Invalid signature");
2788
2863
  }
2789
2864
  }
2790
2865
  let payload;
@@ -2792,7 +2867,7 @@ app.post("/api/github/webhooks", async (c) => {
2792
2867
  payload = JSON.parse(rawBody);
2793
2868
  }
2794
2869
  catch {
2795
- return c.json({ error: "Invalid JSON" }, 400);
2870
+ return badRequest("Invalid JSON");
2796
2871
  }
2797
2872
  const result = await _integrationsGithub.processWebhook(eventType, payload);
2798
2873
  return c.json(result);
@@ -2806,14 +2881,14 @@ app.get("/api/github/status", async (c) => {
2806
2881
  app.post("/api/github/pr/review", async (c) => {
2807
2882
  const sessionId = c.req.query("sessionId");
2808
2883
  if (!sessionId)
2809
- return c.json({ error: "sessionId required" }, 400);
2884
+ return badRequest("sessionId required");
2810
2885
  const session = validateSession(sessionId);
2811
2886
  if (!session)
2812
- return c.json({ error: "Invalid or expired session" }, 401);
2887
+ return unauthorized("Invalid or expired session");
2813
2888
  const body = await c.req.json();
2814
2889
  const { prNumber, repo, postComment } = body;
2815
2890
  if (!prNumber)
2816
- return c.json({ error: "prNumber required" }, 400);
2891
+ return badRequest("prNumber required");
2817
2892
  const result = postComment
2818
2893
  ? await _integrationsGithub.reviewAndCommentPR(prNumber, repo)
2819
2894
  : await _integrationsGithub.reviewPullRequest(prNumber, repo);
@@ -2825,14 +2900,14 @@ app.post("/api/github/pr/review", async (c) => {
2825
2900
  app.post("/api/github/issues/triage", async (c) => {
2826
2901
  const sessionId = c.req.query("sessionId");
2827
2902
  if (!sessionId)
2828
- return c.json({ error: "sessionId required" }, 400);
2903
+ return badRequest("sessionId required");
2829
2904
  const session = validateSession(sessionId);
2830
2905
  if (!session)
2831
- return c.json({ error: "Invalid or expired session" }, 401);
2906
+ return unauthorized("Invalid or expired session");
2832
2907
  const body = await c.req.json();
2833
2908
  const { issueNumber, repo, apply } = body;
2834
2909
  if (!issueNumber)
2835
- return c.json({ error: "issueNumber required" }, 400);
2910
+ return badRequest("issueNumber required");
2836
2911
  const result = apply
2837
2912
  ? await _integrationsGithub.triageAndLabelIssue(issueNumber, repo)
2838
2913
  : await _integrationsGithub.triageGitHubIssue(issueNumber, repo);
@@ -2844,10 +2919,10 @@ app.post("/api/github/issues/triage", async (c) => {
2844
2919
  app.post("/api/github/issues/triage/batch", async (c) => {
2845
2920
  const sessionId = c.req.query("sessionId");
2846
2921
  if (!sessionId)
2847
- return c.json({ error: "sessionId required" }, 400);
2922
+ return badRequest("sessionId required");
2848
2923
  const session = validateSession(sessionId);
2849
2924
  if (!session)
2850
- return c.json({ error: "Invalid or expired session" }, 401);
2925
+ return unauthorized("Invalid or expired session");
2851
2926
  const body = await c.req.json();
2852
2927
  const { repo, apply } = body;
2853
2928
  const results = await _integrationsGithub.batchTriageIssues(repo, { apply });
@@ -2857,10 +2932,10 @@ app.post("/api/github/issues/triage/batch", async (c) => {
2857
2932
  app.post("/api/github/commits/analyze", async (c) => {
2858
2933
  const sessionId = c.req.query("sessionId");
2859
2934
  if (!sessionId)
2860
- return c.json({ error: "sessionId required" }, 400);
2935
+ return badRequest("sessionId required");
2861
2936
  const session = validateSession(sessionId);
2862
2937
  if (!session)
2863
- return c.json({ error: "Invalid or expired session" }, 401);
2938
+ return unauthorized("Invalid or expired session");
2864
2939
  const body = await c.req.json();
2865
2940
  const { sha, repo, count, since } = body;
2866
2941
  if (sha) {
@@ -2958,16 +3033,16 @@ app.get("/api/slack/status", async (c) => {
2958
3033
  app.post("/api/slack/send", async (c) => {
2959
3034
  const sessionId = c.req.query("sessionId");
2960
3035
  if (!sessionId)
2961
- return c.json({ error: "sessionId required" }, 400);
3036
+ return badRequest("sessionId required");
2962
3037
  const session = validateSession(sessionId);
2963
3038
  if (!session)
2964
- return c.json({ error: "Invalid or expired session" }, 401);
3039
+ return unauthorized("Invalid or expired session");
2965
3040
  const client = (_slackClient?.getClient() ?? null);
2966
3041
  if (!client)
2967
3042
  return c.json({ error: "Slack not available" }, 503);
2968
3043
  const body = await c.req.json();
2969
3044
  if (!body.channel || !body.text)
2970
- return c.json({ error: "channel and text required" }, 400);
3045
+ return badRequest("channel and text required");
2971
3046
  const result = await client.sendMessage(body.channel, body.text, {
2972
3047
  thread_ts: body.thread_ts,
2973
3048
  blocks: body.blocks,
@@ -2981,16 +3056,16 @@ app.post("/api/slack/send", async (c) => {
2981
3056
  app.post("/api/slack/dm", async (c) => {
2982
3057
  const sessionId = c.req.query("sessionId");
2983
3058
  if (!sessionId)
2984
- return c.json({ error: "sessionId required" }, 400);
3059
+ return badRequest("sessionId required");
2985
3060
  const session = validateSession(sessionId);
2986
3061
  if (!session)
2987
- return c.json({ error: "Invalid or expired session" }, 401);
3062
+ return unauthorized("Invalid or expired session");
2988
3063
  const client = (_slackClient?.getClient() ?? null);
2989
3064
  if (!client)
2990
3065
  return c.json({ error: "Slack not available" }, 503);
2991
3066
  const body = await c.req.json();
2992
3067
  if (!body.user || !body.text)
2993
- return c.json({ error: "user and text required" }, 400);
3068
+ return badRequest("user and text required");
2994
3069
  const result = await client.sendDm(body.user, body.text, { blocks: body.blocks });
2995
3070
  if (!result.ok)
2996
3071
  return c.json({ error: result.error }, 500);
@@ -3001,10 +3076,10 @@ app.post("/api/slack/dm", async (c) => {
3001
3076
  app.get("/api/slack/channels", async (c) => {
3002
3077
  const sessionId = c.req.query("sessionId");
3003
3078
  if (!sessionId)
3004
- return c.json({ error: "sessionId required" }, 400);
3079
+ return badRequest("sessionId required");
3005
3080
  const session = validateSession(sessionId);
3006
3081
  if (!session)
3007
- return c.json({ error: "Invalid or expired session" }, 401);
3082
+ return unauthorized("Invalid or expired session");
3008
3083
  const types = c.req.query("types") || undefined;
3009
3084
  const result = await _slackChannels.listChannels({ types });
3010
3085
  if (!result.ok)
@@ -3015,10 +3090,10 @@ app.get("/api/slack/channels", async (c) => {
3015
3090
  app.get("/api/slack/channels/:id", async (c) => {
3016
3091
  const sessionId = c.req.query("sessionId");
3017
3092
  if (!sessionId)
3018
- return c.json({ error: "sessionId required" }, 400);
3093
+ return badRequest("sessionId required");
3019
3094
  const session = validateSession(sessionId);
3020
3095
  if (!session)
3021
- return c.json({ error: "Invalid or expired session" }, 401);
3096
+ return unauthorized("Invalid or expired session");
3022
3097
  const result = await _slackChannels.getChannelInfo(c.req.param("id"));
3023
3098
  if (!result.ok)
3024
3099
  return c.json({ error: result.message }, 502);
@@ -3028,10 +3103,10 @@ app.get("/api/slack/channels/:id", async (c) => {
3028
3103
  app.post("/api/slack/channels/:id/join", async (c) => {
3029
3104
  const sessionId = c.req.query("sessionId");
3030
3105
  if (!sessionId)
3031
- return c.json({ error: "sessionId required" }, 400);
3106
+ return badRequest("sessionId required");
3032
3107
  const session = validateSession(sessionId);
3033
3108
  if (!session)
3034
- return c.json({ error: "Invalid or expired session" }, 401);
3109
+ return unauthorized("Invalid or expired session");
3035
3110
  const result = await _slackChannels.joinChannel(c.req.param("id"));
3036
3111
  if (!result.ok)
3037
3112
  return c.json({ error: result.message }, 502);
@@ -3041,10 +3116,10 @@ app.post("/api/slack/channels/:id/join", async (c) => {
3041
3116
  app.get("/api/slack/channels/:id/history", async (c) => {
3042
3117
  const sessionId = c.req.query("sessionId");
3043
3118
  if (!sessionId)
3044
- return c.json({ error: "sessionId required" }, 400);
3119
+ return badRequest("sessionId required");
3045
3120
  const session = validateSession(sessionId);
3046
3121
  if (!session)
3047
- return c.json({ error: "Invalid or expired session" }, 401);
3122
+ return unauthorized("Invalid or expired session");
3048
3123
  const limit = c.req.query("limit") ? parseInt(c.req.query("limit"), 10) : undefined;
3049
3124
  const result = await _slackChannels.getChannelHistory(c.req.param("id"), { limit });
3050
3125
  if (!result.ok)
@@ -3055,10 +3130,10 @@ app.get("/api/slack/channels/:id/history", async (c) => {
3055
3130
  app.get("/api/slack/users/:id", async (c) => {
3056
3131
  const sessionId = c.req.query("sessionId");
3057
3132
  if (!sessionId)
3058
- return c.json({ error: "sessionId required" }, 400);
3133
+ return badRequest("sessionId required");
3059
3134
  const session = validateSession(sessionId);
3060
3135
  if (!session)
3061
- return c.json({ error: "Invalid or expired session" }, 401);
3136
+ return unauthorized("Invalid or expired session");
3062
3137
  const client = (_slackClient?.getClient() ?? null);
3063
3138
  if (!client)
3064
3139
  return c.json({ error: "Slack not available" }, 503);
@@ -3071,7 +3146,7 @@ app.get("/api/slack/users/:id", async (c) => {
3071
3146
  // Routed through the generic webhook system with challenge response handling.
3072
3147
  app.post("/api/slack/events", async (c) => {
3073
3148
  if (!_webhooksMount)
3074
- return c.json({ error: "Webhooks require BYOK tier" }, 403);
3149
+ return forbidden("Webhooks require BYOK tier");
3075
3150
  return _webhooksMount.createWebhookRoute({
3076
3151
  provider: "slack-events",
3077
3152
  transformResponse: (result, ctx) => {
@@ -3084,30 +3159,30 @@ app.post("/api/slack/events", async (c) => {
3084
3159
  // Slack slash commands: routed through the generic webhook system.
3085
3160
  app.post("/api/slack/commands", async (c) => {
3086
3161
  if (!_webhooksMount)
3087
- return c.json({ error: "Webhooks require BYOK tier" }, 403);
3162
+ return forbidden("Webhooks require BYOK tier");
3088
3163
  return _webhooksMount.createWebhookRoute({ provider: "slack-commands" })(c);
3089
3164
  });
3090
3165
  // Slack interactions: routed through the generic webhook system.
3091
3166
  app.post("/api/slack/interactions", async (c) => {
3092
3167
  if (!_webhooksMount)
3093
- return c.json({ error: "Webhooks require BYOK tier" }, 403);
3168
+ return forbidden("Webhooks require BYOK tier");
3094
3169
  return _webhooksMount.createWebhookRoute({ provider: "slack-interactions" })(c);
3095
3170
  });
3096
3171
  // --- Resend inbound email ---
3097
3172
  // Resend webhook: receive inbound emails via Svix-signed webhooks (direct path).
3098
3173
  app.post("/api/resend/webhooks", async (c) => {
3099
3174
  if (!_webhooksMount)
3100
- return c.json({ error: "Webhooks require BYOK tier" }, 403);
3175
+ return forbidden("Webhooks require BYOK tier");
3101
3176
  return _webhooksMount.createWebhookRoute({ provider: "resend" })(c);
3102
3177
  });
3103
3178
  // Resend inbox: manually trigger inbox check (pulls from Worker KV).
3104
3179
  app.post("/api/resend/check-inbox", async (c) => {
3105
3180
  const sessionId = c.req.query("sessionId");
3106
3181
  if (!sessionId)
3107
- return c.json({ error: "sessionId required" }, 400);
3182
+ return badRequest("sessionId required");
3108
3183
  const session = validateSession(sessionId);
3109
3184
  if (!session)
3110
- return c.json({ error: "Invalid or expired session" }, 401);
3185
+ return unauthorized("Invalid or expired session");
3111
3186
  const { forceCheckResendInbox } = await import("./resend/inbox.js");
3112
3187
  const count = await forceCheckResendInbox();
3113
3188
  return c.json({ ok: true, processed: count });
@@ -3168,7 +3243,7 @@ app.post("/api/relay/whatsapp", async (c) => {
3168
3243
  const relaySecret = process.env.RELAY_SECRET ?? "";
3169
3244
  const verification = _webhooksRelay.verifyRelaySignature(rawBody, headers, relaySecret);
3170
3245
  if (!verification.valid) {
3171
- return c.json({ error: verification.error ?? "Invalid relay signature" }, 401);
3246
+ return unauthorized(verification.error ?? "Invalid relay signature");
3172
3247
  }
3173
3248
  const params = _webhooksTwilio.parseFormBody(rawBody);
3174
3249
  const payload = params;
@@ -3195,14 +3270,14 @@ app.post("/api/relay/resend", async (c) => {
3195
3270
  const relaySecret = process.env.RELAY_SECRET ?? "";
3196
3271
  const verification = _webhooksRelay.verifyRelaySignature(rawBody, headers, relaySecret);
3197
3272
  if (!verification.valid) {
3198
- return c.json({ error: verification.error ?? "Invalid relay signature" }, 401);
3273
+ return unauthorized(verification.error ?? "Invalid relay signature");
3199
3274
  }
3200
3275
  let payload;
3201
3276
  try {
3202
3277
  payload = JSON.parse(rawBody);
3203
3278
  }
3204
3279
  catch {
3205
- return c.json({ error: "Invalid JSON" }, 400);
3280
+ return badRequest("Invalid JSON");
3206
3281
  }
3207
3282
  if (payload.type !== "email.received" || !payload.body?.trim()) {
3208
3283
  return c.json({ ok: true, message: "No actionable content" });
@@ -3240,16 +3315,16 @@ app.post("/api/relay/resend", async (c) => {
3240
3315
  app.post("/api/whatsapp/send", async (c) => {
3241
3316
  const sessionId = c.req.query("sessionId");
3242
3317
  if (!sessionId)
3243
- return c.json({ error: "sessionId required" }, 400);
3318
+ return badRequest("sessionId required");
3244
3319
  const session = validateSession(sessionId);
3245
3320
  if (!session)
3246
- return c.json({ error: "Invalid or expired session" }, 401);
3321
+ return unauthorized("Invalid or expired session");
3247
3322
  const client = (_channelsWhatsapp?.getClient() ?? null);
3248
3323
  if (!client)
3249
3324
  return c.json({ error: "WhatsApp not configured. Add TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER to vault." }, 503);
3250
3325
  const body = await c.req.json();
3251
3326
  if (!body.to || !body.message)
3252
- return c.json({ error: "to and message required" }, 400);
3327
+ return badRequest("to and message required");
3253
3328
  const result = await client.sendMessage(body.to, body.message);
3254
3329
  if (!result.ok)
3255
3330
  return c.json({ error: result.message }, 502);
@@ -3259,10 +3334,10 @@ app.post("/api/whatsapp/send", async (c) => {
3259
3334
  app.get("/api/whatsapp/contacts", async (c) => {
3260
3335
  const sessionId = c.req.query("sessionId");
3261
3336
  if (!sessionId)
3262
- return c.json({ error: "sessionId required" }, 400);
3337
+ return badRequest("sessionId required");
3263
3338
  const session = validateSession(sessionId);
3264
3339
  if (!session)
3265
- return c.json({ error: "Invalid or expired session" }, 401);
3340
+ return unauthorized("Invalid or expired session");
3266
3341
  const client = (_channelsWhatsapp?.getClient() ?? null);
3267
3342
  if (!client)
3268
3343
  return c.json({ error: "WhatsApp not configured" }, 503);
@@ -3273,10 +3348,10 @@ app.get("/api/whatsapp/contacts", async (c) => {
3273
3348
  app.get("/api/whatsapp/history", async (c) => {
3274
3349
  const sessionId = c.req.query("sessionId");
3275
3350
  if (!sessionId)
3276
- return c.json({ error: "sessionId required" }, 400);
3351
+ return badRequest("sessionId required");
3277
3352
  const session = validateSession(sessionId);
3278
3353
  if (!session)
3279
- return c.json({ error: "Invalid or expired session" }, 401);
3354
+ return unauthorized("Invalid or expired session");
3280
3355
  const client = (_channelsWhatsapp?.getClient() ?? null);
3281
3356
  if (!client)
3282
3357
  return c.json({ error: "WhatsApp not configured" }, 503);
@@ -3323,10 +3398,10 @@ app.get("/api/board/status", async (c) => {
3323
3398
  app.get("/api/board/teams", async (c) => {
3324
3399
  const sessionId = c.req.query("sessionId");
3325
3400
  if (!sessionId)
3326
- return c.json({ error: "sessionId required" }, 400);
3401
+ return badRequest("sessionId required");
3327
3402
  const session = validateSession(sessionId);
3328
3403
  if (!session)
3329
- return c.json({ error: "Invalid or expired session" }, 401);
3404
+ return unauthorized("Invalid or expired session");
3330
3405
  const board = getBoardProvider();
3331
3406
  if (!board || !board.isAvailable())
3332
3407
  return c.json({ teams: [] });
@@ -3337,10 +3412,10 @@ app.get("/api/board/teams", async (c) => {
3337
3412
  app.get("/api/board/issues", async (c) => {
3338
3413
  const sessionId = c.req.query("sessionId");
3339
3414
  if (!sessionId)
3340
- return c.json({ error: "sessionId required" }, 400);
3415
+ return badRequest("sessionId required");
3341
3416
  const session = validateSession(sessionId);
3342
3417
  if (!session)
3343
- return c.json({ error: "Invalid or expired session" }, 401);
3418
+ return unauthorized("Invalid or expired session");
3344
3419
  const board = getBoardProvider();
3345
3420
  if (!board || !board.isAvailable())
3346
3421
  return c.json({ issues: [] });
@@ -3353,17 +3428,17 @@ app.get("/api/board/issues", async (c) => {
3353
3428
  app.post("/api/board/issues", async (c) => {
3354
3429
  const sessionId = c.req.query("sessionId");
3355
3430
  if (!sessionId)
3356
- return c.json({ error: "sessionId required" }, 400);
3431
+ return badRequest("sessionId required");
3357
3432
  const session = validateSession(sessionId);
3358
3433
  if (!session)
3359
- return c.json({ error: "Invalid or expired session" }, 401);
3434
+ return unauthorized("Invalid or expired session");
3360
3435
  const board = getBoardProvider();
3361
3436
  if (!board || !board.isAvailable())
3362
3437
  return c.json({ error: "No board provider configured" }, 503);
3363
3438
  const body = await c.req.json();
3364
3439
  const { title, description, teamId, priority } = body;
3365
3440
  if (!title)
3366
- return c.json({ error: "title required" }, 400);
3441
+ return badRequest("title required");
3367
3442
  const issue = await board.createIssue(title, { description, teamId, priority });
3368
3443
  if (!issue)
3369
3444
  return c.json({ error: "Failed to create issue" }, 502);
@@ -3373,10 +3448,10 @@ app.post("/api/board/issues", async (c) => {
3373
3448
  app.patch("/api/board/issues/:id", async (c) => {
3374
3449
  const sessionId = c.req.query("sessionId");
3375
3450
  if (!sessionId)
3376
- return c.json({ error: "sessionId required" }, 400);
3451
+ return badRequest("sessionId required");
3377
3452
  const session = validateSession(sessionId);
3378
3453
  if (!session)
3379
- return c.json({ error: "Invalid or expired session" }, 401);
3454
+ return unauthorized("Invalid or expired session");
3380
3455
  const board = getBoardProvider();
3381
3456
  if (!board || !board.isAvailable())
3382
3457
  return c.json({ error: "No board provider configured" }, 503);
@@ -3392,10 +3467,10 @@ app.patch("/api/board/issues/:id", async (c) => {
3392
3467
  app.post("/api/board/issues/:id/comments", async (c) => {
3393
3468
  const sessionId = c.req.query("sessionId");
3394
3469
  if (!sessionId)
3395
- return c.json({ error: "sessionId required" }, 400);
3470
+ return badRequest("sessionId required");
3396
3471
  const session = validateSession(sessionId);
3397
3472
  if (!session)
3398
- return c.json({ error: "Invalid or expired session" }, 401);
3473
+ return unauthorized("Invalid or expired session");
3399
3474
  const board = getBoardProvider();
3400
3475
  if (!board || !board.isAvailable())
3401
3476
  return c.json({ error: "No board provider configured" }, 503);
@@ -3403,7 +3478,7 @@ app.post("/api/board/issues/:id/comments", async (c) => {
3403
3478
  const body = await c.req.json();
3404
3479
  const { body: commentBody } = body;
3405
3480
  if (!commentBody)
3406
- return c.json({ error: "body required" }, 400);
3481
+ return badRequest("body required");
3407
3482
  const ok = await board.addComment(id, commentBody);
3408
3483
  return ok ? c.json({ ok: true }) : c.json({ error: "Failed to add comment" }, 502);
3409
3484
  });
@@ -3411,10 +3486,10 @@ app.post("/api/board/issues/:id/comments", async (c) => {
3411
3486
  app.get("/api/board/issues/:id/exchanges", async (c) => {
3412
3487
  const sessionId = c.req.query("sessionId");
3413
3488
  if (!sessionId)
3414
- return c.json({ error: "sessionId required" }, 400);
3489
+ return badRequest("sessionId required");
3415
3490
  const session = validateSession(sessionId);
3416
3491
  if (!session)
3417
- return c.json({ error: "Invalid or expired session" }, 401);
3492
+ return unauthorized("Invalid or expired session");
3418
3493
  const board = getBoardProvider();
3419
3494
  const store = board?.getStore?.();
3420
3495
  if (!store)
@@ -3426,10 +3501,10 @@ app.get("/api/board/issues/:id/exchanges", async (c) => {
3426
3501
  app.post("/api/board/issues/:id/exchanges", async (c) => {
3427
3502
  const sessionId = c.req.query("sessionId");
3428
3503
  if (!sessionId)
3429
- return c.json({ error: "sessionId required" }, 400);
3504
+ return badRequest("sessionId required");
3430
3505
  const session = validateSession(sessionId);
3431
3506
  if (!session)
3432
- return c.json({ error: "Invalid or expired session" }, 401);
3507
+ return unauthorized("Invalid or expired session");
3433
3508
  const board = getBoardProvider();
3434
3509
  const store = board?.getStore?.();
3435
3510
  if (!store)
@@ -3438,7 +3513,7 @@ app.post("/api/board/issues/:id/exchanges", async (c) => {
3438
3513
  const body = await c.req.json();
3439
3514
  const { author, body: exBody, source } = body;
3440
3515
  if (!author || !exBody)
3441
- return c.json({ error: "author and body required" }, 400);
3516
+ return badRequest("author and body required");
3442
3517
  const exchange = await store.addExchange(id, {
3443
3518
  author,
3444
3519
  body: exBody,
@@ -3448,7 +3523,7 @@ app.post("/api/board/issues/:id/exchanges", async (c) => {
3448
3523
  logActivity({ source: "board", summary: `Exchange on issue ${id} from ${author}`, actionLabel: "PROMPTED", reason: "user added board exchange" });
3449
3524
  return c.json({ exchange });
3450
3525
  }
3451
- return c.json({ error: "Task not found or archived" }, 404);
3526
+ return notFound("Task not found or archived");
3452
3527
  });
3453
3528
  // --- Weekly backlog review endpoints (DASH-59) ---
3454
3529
  // Get the last backlog review report.
@@ -3466,6 +3541,158 @@ app.post("/api/board/review/trigger", async (c) => {
3466
3541
  return c.json({ ok: true, report });
3467
3542
  });
3468
3543
  // ---------------------------------------------------------------------------
3544
+ // Gemini Search
3545
+ // ---------------------------------------------------------------------------
3546
+ app.post("/api/search/gemini", async (c) => {
3547
+ const { query } = await c.req.json();
3548
+ if (!query?.trim())
3549
+ return c.json({ ok: false, message: "Query is required" }, 400);
3550
+ const { geminiSearch, isGeminiAvailable } = await import("./search/gemini.js");
3551
+ if (!isGeminiAvailable()) {
3552
+ return c.json({ ok: false, message: "GEMINI_API_KEY not configured" }, 503);
3553
+ }
3554
+ const result = await geminiSearch(query.trim());
3555
+ return c.json(result);
3556
+ });
3557
+ app.get("/api/search/gemini/status", async (c) => {
3558
+ const { isGeminiAvailable } = await import("./search/gemini.js");
3559
+ return c.json({ available: isGeminiAvailable() });
3560
+ });
3561
+ // ---------------------------------------------------------------------------
3562
+ // Whiteboard routes
3563
+ // ---------------------------------------------------------------------------
3564
+ import { WhiteboardStore } from "./whiteboard/store.js";
3565
+ let _whiteboardStore = null;
3566
+ function getWhiteboardStore() {
3567
+ if (!_whiteboardStore)
3568
+ _whiteboardStore = new WhiteboardStore(BRAIN_DIR);
3569
+ return _whiteboardStore;
3570
+ }
3571
+ // Whiteboard is always accessible — it's the human-agent collaboration surface
3572
+ // List / tree / questions / weighted view
3573
+ app.get("/api/whiteboard", async (c) => {
3574
+ const store = getWhiteboardStore();
3575
+ const view = c.req.query("view") ?? "tree";
3576
+ const root = c.req.query("root");
3577
+ const status = c.req.query("status");
3578
+ const type = c.req.query("type");
3579
+ const tagsParam = c.req.query("tags");
3580
+ const search = c.req.query("search");
3581
+ if (view === "tree") {
3582
+ const tree = await store.getTree(root ?? undefined);
3583
+ return c.json({ nodes: tree });
3584
+ }
3585
+ if (view === "questions") {
3586
+ const questions = await store.getOpenQuestions();
3587
+ // Attach breadcrumb path to each question
3588
+ const withPaths = await Promise.all(questions.map(async (q) => {
3589
+ const ancestors = await store.getAncestors(q.id);
3590
+ return { ...q, path: ancestors.map((a) => ({ id: a.id, title: a.title })) };
3591
+ }));
3592
+ return c.json({ questions: withPaths });
3593
+ }
3594
+ if (view === "weighted") {
3595
+ const weighted = await store.getWeighted();
3596
+ return c.json({ nodes: weighted });
3597
+ }
3598
+ // Flat view with filters
3599
+ const tags = tagsParam ? tagsParam.split(",").map((t) => t.trim()) : undefined;
3600
+ const nodes = await store.list({ type, status, tags, search });
3601
+ return c.json({ nodes });
3602
+ });
3603
+ // Summary
3604
+ app.get("/api/whiteboard/summary", async (c) => {
3605
+ const store = getWhiteboardStore();
3606
+ const summary = await store.getSummary();
3607
+ return c.json(summary);
3608
+ });
3609
+ // Recently answered questions (for agent goals loop)
3610
+ app.get("/api/whiteboard/answered", async (c) => {
3611
+ const store = getWhiteboardStore();
3612
+ const since = c.req.query("since");
3613
+ if (!since)
3614
+ return c.json({ error: "Missing ?since= parameter" }, 400);
3615
+ const answered = await store.getAnsweredSince(since);
3616
+ return c.json({ answered });
3617
+ });
3618
+ // Get single node
3619
+ app.get("/api/whiteboard/:id", async (c) => {
3620
+ const store = getWhiteboardStore();
3621
+ const node = await store.get(c.req.param("id"));
3622
+ if (!node)
3623
+ return c.json({ error: "Node not found" }, 404);
3624
+ return c.json(node);
3625
+ });
3626
+ // Get ancestors (breadcrumb path)
3627
+ app.get("/api/whiteboard/:id/path", async (c) => {
3628
+ const store = getWhiteboardStore();
3629
+ const node = await store.get(c.req.param("id"));
3630
+ if (!node)
3631
+ return c.json({ error: "Node not found" }, 404);
3632
+ const ancestors = await store.getAncestors(c.req.param("id"));
3633
+ return c.json({ path: [...ancestors, node].map((n) => ({ id: n.id, title: n.title, type: n.type })) });
3634
+ });
3635
+ // Create node
3636
+ app.post("/api/whiteboard", async (c) => {
3637
+ const body = await c.req.json();
3638
+ if (!body.title?.trim())
3639
+ return c.json({ error: "Title is required" }, 400);
3640
+ if (!body.type)
3641
+ return c.json({ error: "Type is required (goal, task, question, decision, note)" }, 400);
3642
+ if (!body.plantedBy)
3643
+ return c.json({ error: "plantedBy is required (agent, human)" }, 400);
3644
+ const store = getWhiteboardStore();
3645
+ // Validate parentId exists if provided
3646
+ if (body.parentId) {
3647
+ const parent = await store.get(body.parentId);
3648
+ if (!parent)
3649
+ return c.json({ error: `Parent node not found: ${body.parentId}` }, 400);
3650
+ }
3651
+ const node = await store.create({
3652
+ title: body.title.trim(),
3653
+ type: body.type,
3654
+ parentId: body.parentId ?? null,
3655
+ tags: body.tags ?? [],
3656
+ plantedBy: body.plantedBy,
3657
+ body: body.body,
3658
+ question: body.question,
3659
+ boardTaskId: body.boardTaskId,
3660
+ });
3661
+ return c.json(node, 201);
3662
+ });
3663
+ // Update node
3664
+ app.patch("/api/whiteboard/:id", async (c) => {
3665
+ const body = await c.req.json();
3666
+ const store = getWhiteboardStore();
3667
+ const updated = await store.update(c.req.param("id"), body);
3668
+ if (!updated)
3669
+ return c.json({ error: "Node not found" }, 404);
3670
+ return c.json(updated);
3671
+ });
3672
+ // Answer a question
3673
+ app.post("/api/whiteboard/:id/answer", async (c) => {
3674
+ const { answer } = await c.req.json();
3675
+ if (!answer?.trim())
3676
+ return c.json({ error: "Answer is required" }, 400);
3677
+ const store = getWhiteboardStore();
3678
+ const node = await store.get(c.req.param("id"));
3679
+ if (!node)
3680
+ return c.json({ error: "Node not found" }, 404);
3681
+ if (node.type !== "question")
3682
+ return c.json({ error: "Only question nodes can be answered" }, 400);
3683
+ const updated = await store.answerQuestion(c.req.param("id"), answer.trim());
3684
+ return c.json(updated);
3685
+ });
3686
+ // Archive node
3687
+ app.delete("/api/whiteboard/:id", async (c) => {
3688
+ const cascade = c.req.query("cascade") === "true";
3689
+ const store = getWhiteboardStore();
3690
+ const result = await store.archive(c.req.param("id"), cascade);
3691
+ if (!result.ok)
3692
+ return c.json({ error: result.message }, 404);
3693
+ return c.json(result);
3694
+ });
3695
+ // ---------------------------------------------------------------------------
3469
3696
  // Skills routes
3470
3697
  // ---------------------------------------------------------------------------
3471
3698
  // List all registered skills (metadata only)
@@ -3487,7 +3714,7 @@ app.get("/api/skills/:name", async (c) => {
3487
3714
  const name = c.req.param("name");
3488
3715
  const skill = await _skillRegistry.get(name);
3489
3716
  if (!skill)
3490
- return c.json({ error: "Skill not found" }, 404);
3717
+ return notFound("Skill not found");
3491
3718
  const content = await _skillRegistry.getContent(name);
3492
3719
  return c.json({
3493
3720
  ...skill,
@@ -3498,10 +3725,10 @@ app.get("/api/skills/:name", async (c) => {
3498
3725
  app.post("/api/skills/resolve", async (c) => {
3499
3726
  const { trigger } = await c.req.json();
3500
3727
  if (!trigger)
3501
- return c.json({ error: "trigger is required" }, 400);
3728
+ return badRequest("trigger is required");
3502
3729
  const skill = await _skillRegistry.findByTrigger(trigger);
3503
3730
  if (!skill)
3504
- return c.json({ error: "No matching skill" }, 404);
3731
+ return notFound("No matching skill");
3505
3732
  return c.json({
3506
3733
  id: skill.id,
3507
3734
  name: skill.name,
@@ -3514,9 +3741,19 @@ app.post("/api/skills/resolve", async (c) => {
3514
3741
  import { getPluginStatusSummary } from "./plugins/status.js";
3515
3742
  import { initPlugins, shutdownPlugins } from "./plugins/index.js";
3516
3743
  // --- File management routes ---
3517
- import { fileRegistry, computeChecksum } from "./files/registry.js";
3518
- import { validateUpload } from "./files/validate.js";
3519
- import { slugify } from "./files/validate.js";
3744
+ import { FileStore, computeChecksum } from "./files/store.js";
3745
+ import { validateUpload, slugify } from "./files/validate.js";
3746
+ // Shared file store instance — initialized lazily on first use
3747
+ let _fileStore = null;
3748
+ function getFileStore() {
3749
+ if (!_fileStore)
3750
+ _fileStore = new FileStore(BRAIN_DIR);
3751
+ return _fileStore;
3752
+ }
3753
+ /** Map FileEntry to API response shape (adds `filename` alias for backward compat). */
3754
+ function toFileResponse(entry) {
3755
+ return { ...entry, filename: entry.name };
3756
+ }
3520
3757
  app.get("/api/plugins", (c) => {
3521
3758
  return c.json(getPluginStatusSummary());
3522
3759
  });
@@ -3524,10 +3761,11 @@ app.get("/api/plugins", (c) => {
3524
3761
  // Upload file — persist to brain/files/data/, register in JSONL
3525
3762
  app.post("/api/files/upload", async (c) => {
3526
3763
  try {
3764
+ const store = getFileStore();
3527
3765
  const formData = await c.req.formData();
3528
3766
  const file = formData.get("file");
3529
3767
  if (!file)
3530
- return c.json({ error: "No file provided" }, 400);
3768
+ return badRequest("No file provided");
3531
3769
  const source = formData.get("source") || "user-upload";
3532
3770
  const tagsRaw = formData.get("tags");
3533
3771
  const tags = tagsRaw ? tagsRaw.split(",").map(t => t.trim()).filter(Boolean) : [];
@@ -3540,17 +3778,17 @@ app.post("/api/files/upload", async (c) => {
3540
3778
  // Validate: extension allowlist, magic bytes, size, content scan
3541
3779
  const validation = await validateUpload(buffer, file.name, file.type, maxUploadBytes);
3542
3780
  if (!validation.valid) {
3543
- return c.json({ error: validation.rejected }, 400);
3781
+ return badRequest(validation.rejected ?? "Upload rejected");
3544
3782
  }
3545
3783
  // Generate storage path: brain/files/data/YYYY-MM-DD/slug_id.ext
3546
3784
  const dateDir = new Date().toISOString().slice(0, 10);
3547
3785
  const slug = slugify(file.name.replace(/\.[^.]+$/, ""));
3548
3786
  const checksum = computeChecksum(buffer);
3549
3787
  // Check for duplicate by checksum
3550
- const existing = await fileRegistry.list({});
3788
+ const existing = await store.list({});
3551
3789
  const dup = existing.find(r => r.checksum === checksum && r.status === "active");
3552
3790
  if (dup) {
3553
- return c.json({ file: dup, duplicate: true });
3791
+ return c.json({ file: toFileResponse(dup), duplicate: true });
3554
3792
  }
3555
3793
  const storageDir = join(BRAIN_DIR, "files", "data", dateDir);
3556
3794
  await mkdir(storageDir, { recursive: true });
@@ -3571,20 +3809,29 @@ app.post("/api/files/upload", async (c) => {
3571
3809
  }
3572
3810
  catch { /* PDF extraction optional */ }
3573
3811
  }
3574
- // Register in file registry
3575
- const record = await fileRegistry.register({
3576
- filename: validation.sanitizedName,
3812
+ // Register in file store
3813
+ const record = await store.create({
3814
+ name: validation.sanitizedName,
3815
+ slug,
3577
3816
  storagePath,
3578
3817
  mimeType: validation.detectedMime || file.type,
3579
3818
  sizeBytes: buffer.length,
3580
3819
  checksum,
3581
3820
  tags,
3582
- source,
3821
+ origin: source,
3822
+ ownerId: null,
3823
+ taskId: null,
3824
+ parentId: null,
3825
+ version: 1,
3826
+ encrypted: false,
3827
+ visibility: "private",
3583
3828
  status: "active",
3829
+ category: "upload",
3830
+ textPreview,
3584
3831
  });
3585
3832
  // Trigger volume replication (on-write event)
3586
3833
  volumeManager.handleEvent({ type: "write", fileId: record.id, volume: "primary" }).catch((err) => log.warn("Volume on-write event failed", { error: String(err) }));
3587
- return c.json({ file: record, duplicate: false });
3834
+ return c.json({ file: toFileResponse(record), duplicate: false });
3588
3835
  }
3589
3836
  catch (err) {
3590
3837
  const msg = err instanceof Error ? err.message : String(err);
@@ -3594,68 +3841,86 @@ app.post("/api/files/upload", async (c) => {
3594
3841
  });
3595
3842
  // Download / serve a stored file
3596
3843
  app.get("/api/files/:id/download", async (c) => {
3597
- const record = await fileRegistry.get(c.req.param("id"));
3844
+ const record = await getFileStore().get(c.req.param("id"));
3598
3845
  if (!record)
3599
- return c.json({ error: "File not found" }, 404);
3846
+ return notFound("File not found");
3600
3847
  const fullPath = join(BRAIN_DIR, record.storagePath);
3601
3848
  try {
3602
3849
  const data = await readFile(fullPath);
3603
3850
  return c.newResponse(data, 200, {
3604
3851
  "Content-Type": record.mimeType,
3605
- "Content-Disposition": `inline; filename="${record.filename}"`,
3852
+ "Content-Disposition": `inline; filename="${record.name}"`,
3606
3853
  "Content-Length": String(data.length),
3607
3854
  });
3608
3855
  }
3609
3856
  catch {
3610
- return c.json({ error: "File data not found on disk" }, 404);
3857
+ return notFound("File data not found on disk");
3611
3858
  }
3612
3859
  });
3613
3860
  // List virtual folders (must be before :id route)
3614
3861
  app.get("/api/files/folders", async (c) => {
3615
- const folders = await fileRegistry.getFolders();
3616
- return c.json({ folders });
3862
+ const store = getFileStore();
3863
+ const all = await store.list({ status: "active" });
3864
+ const folders = new Set();
3865
+ for (const f of all) {
3866
+ for (const t of f.tags ?? []) {
3867
+ if (t.startsWith("folder:"))
3868
+ folders.add(t.slice(7));
3869
+ }
3870
+ }
3871
+ return c.json({ folders: [...folders].sort() });
3617
3872
  });
3618
3873
  app.get("/api/files", async (c) => {
3874
+ const store = getFileStore();
3619
3875
  const status = c.req.query("status");
3620
3876
  const source = c.req.query("source");
3621
3877
  const q = c.req.query("q");
3622
- if (q) {
3623
- const results = await fileRegistry.search(q);
3624
- return c.json({ files: results, total: results.length });
3625
- }
3626
- const results = await fileRegistry.list({ status, source });
3627
- return c.json({ files: results, total: results.length });
3878
+ const filter = {};
3879
+ if (status)
3880
+ filter.status = status;
3881
+ if (source)
3882
+ filter.origin = source;
3883
+ if (q)
3884
+ filter.search = q;
3885
+ const results = await store.list(filter);
3886
+ const mapped = results.map(toFileResponse);
3887
+ return c.json({ files: mapped, total: mapped.length });
3628
3888
  });
3629
3889
  app.get("/api/files/:id", async (c) => {
3630
- const record = await fileRegistry.get(c.req.param("id"));
3890
+ const record = await getFileStore().get(c.req.param("id"));
3631
3891
  if (!record)
3632
- return c.json({ error: "File not found", code: "NOT_FOUND", status: 404 }, 404);
3633
- return c.json(record);
3892
+ return notFound("File not found");
3893
+ return c.json(toFileResponse(record));
3634
3894
  });
3635
3895
  app.post("/api/files/:id/archive", async (c) => {
3636
- const result = await fileRegistry.archive(c.req.param("id"));
3637
- if (!result)
3638
- return c.json({ error: "File not found", code: "NOT_FOUND", status: 404 }, 404);
3896
+ const result = await getFileStore().archive(c.req.param("id"), "user");
3897
+ if (!result.ok)
3898
+ return notFound(result.message);
3639
3899
  return c.json(result);
3640
3900
  });
3641
3901
  // Update file tags / move to virtual folder
3642
3902
  app.put("/api/files/:id", async (c) => {
3903
+ const store = getFileStore();
3643
3904
  const body = await c.req.json();
3644
3905
  const { tags, source, folder } = body;
3645
3906
  const id = c.req.param("id");
3646
- const record = await fileRegistry.get(id);
3907
+ const record = await store.get(id);
3647
3908
  if (!record)
3648
- return c.json({ error: "File not found" }, 404);
3909
+ return notFound("File not found");
3649
3910
  // Handle virtual folder: stored as tag "folder:Name"
3650
3911
  let updatedTags = tags ?? [...(record.tags ?? [])];
3651
3912
  if (folder !== undefined) {
3652
- // Remove existing folder tags, add new one
3653
3913
  updatedTags = updatedTags.filter(t => !t.startsWith("folder:"));
3654
3914
  if (folder)
3655
3915
  updatedTags.push("folder:" + folder);
3656
3916
  }
3657
- const result = await fileRegistry.update(id, { tags: updatedTags, ...(source ? { source } : {}) });
3658
- return c.json(result);
3917
+ const patch = { tags: updatedTags };
3918
+ if (source)
3919
+ patch.origin = source;
3920
+ const result = await store.update(id, patch);
3921
+ if (!result)
3922
+ return notFound("File not found");
3923
+ return c.json(toFileResponse(result));
3659
3924
  });
3660
3925
  // --- Volume management routes ---
3661
3926
  app.get("/api/volumes", async (c) => {
@@ -3676,7 +3941,7 @@ app.post("/api/volumes/probe", async (c) => {
3676
3941
  app.post("/api/volumes/event", async (c) => {
3677
3942
  const event = await c.req.json();
3678
3943
  if (!event?.type)
3679
- return c.json({ error: "Missing event type" }, 400);
3944
+ return badRequest("Missing event type");
3680
3945
  await volumeManager.handleEvent(event);
3681
3946
  return c.json({ ok: true });
3682
3947
  });
@@ -3736,7 +4001,7 @@ app.get("/api/alerts/history", (c) => {
3736
4001
  app.get("/api/alerts/:id", (c) => {
3737
4002
  const alert = alertManager.getAlert(c.req.param("id"));
3738
4003
  if (!alert)
3739
- return c.json({ error: "alert not found" }, 404);
4004
+ return notFound("alert not found");
3740
4005
  return c.json(alert);
3741
4006
  });
3742
4007
  // Acknowledge a firing alert.
@@ -3745,14 +4010,14 @@ app.post("/api/alerts/:id/acknowledge", async (c) => {
3745
4010
  const by = body.by;
3746
4011
  const ok = alertManager.acknowledge(c.req.param("id"), by);
3747
4012
  if (!ok)
3748
- return c.json({ error: "alert not found or not in firing state" }, 404);
4013
+ return notFound("alert not found or not in firing state");
3749
4014
  return c.json({ acknowledged: true });
3750
4015
  });
3751
4016
  // Manually resolve an alert.
3752
4017
  app.post("/api/alerts/:id/resolve", (c) => {
3753
4018
  const ok = alertManager.resolve(c.req.param("id"));
3754
4019
  if (!ok)
3755
- return c.json({ error: "alert not found" }, 404);
4020
+ return notFound("alert not found");
3756
4021
  return c.json({ resolved: true });
3757
4022
  });
3758
4023
  // Trigger manual evaluation of alert thresholds.
@@ -3838,7 +4103,7 @@ app.post("/api/scheduling/blocks", async (c) => {
3838
4103
  return c.json({ error: "Scheduling not initialized" }, 503);
3839
4104
  const body = await c.req.json();
3840
4105
  if (!body.type || !body.title) {
3841
- return c.json({ error: "type and title required" }, 400);
4106
+ return badRequest("type and title required");
3842
4107
  }
3843
4108
  const block = await store.create(body);
3844
4109
  return c.json(block, 201);
@@ -3852,7 +4117,7 @@ app.patch("/api/scheduling/blocks/:id", async (c) => {
3852
4117
  const body = await c.req.json();
3853
4118
  const updated = await store.update(id, body);
3854
4119
  if (!updated)
3855
- return c.json({ error: "Block not found" }, 404);
4120
+ return notFound("Block not found");
3856
4121
  return c.json(updated);
3857
4122
  });
3858
4123
  // Get today's schedule.
@@ -3880,7 +4145,7 @@ app.post("/api/contacts/entities", async (c) => {
3880
4145
  return c.json({ error: "Contacts not initialized" }, 503);
3881
4146
  const body = await c.req.json();
3882
4147
  if (!body.type || !body.name) {
3883
- return c.json({ error: "type and name required" }, 400);
4148
+ return badRequest("type and name required");
3884
4149
  }
3885
4150
  const entity = await store.createEntity(body);
3886
4151
  return c.json(entity, 201);
@@ -3894,7 +4159,7 @@ app.patch("/api/contacts/entities/:id", async (c) => {
3894
4159
  const body = await c.req.json();
3895
4160
  const updated = await store.updateEntity(id, body);
3896
4161
  if (!updated)
3897
- return c.json({ error: "Entity not found" }, 404);
4162
+ return notFound("Entity not found");
3898
4163
  return c.json(updated);
3899
4164
  });
3900
4165
  // Get an entity's relationships (all edges where entity is from or to).
@@ -3913,7 +4178,7 @@ app.post("/api/contacts/edges", async (c) => {
3913
4178
  return c.json({ error: "Contacts not initialized" }, 503);
3914
4179
  const body = await c.req.json();
3915
4180
  if (!body.from || !body.to || !body.type) {
3916
- return c.json({ error: "from, to, and type required" }, 400);
4181
+ return badRequest("from, to, and type required");
3917
4182
  }
3918
4183
  const edge = await store.createEdge(body);
3919
4184
  return c.json(edge, 201);
@@ -3947,7 +4212,7 @@ app.get("/api/credentials/:id", async (c) => {
3947
4212
  return c.json({ error: "Credentials not initialized" }, 503);
3948
4213
  const cred = await store.get(c.req.param("id"));
3949
4214
  if (!cred)
3950
- return c.json({ error: "Credential not found" }, 404);
4215
+ return notFound("Credential not found");
3951
4216
  return c.json(cred);
3952
4217
  });
3953
4218
  // Create credential.
@@ -3957,7 +4222,7 @@ app.post("/api/credentials", async (c) => {
3957
4222
  return c.json({ error: "Credentials not initialized" }, 503);
3958
4223
  const body = await c.req.json();
3959
4224
  if (!body.name || !body.service || !body.type || !body.value) {
3960
- return c.json({ error: "name, service, type, and value required" }, 400);
4225
+ return badRequest("name, service, type, and value required");
3961
4226
  }
3962
4227
  const cred = await store.create(body);
3963
4228
  // Hydrate immediately if envVar set
@@ -3975,7 +4240,7 @@ app.patch("/api/credentials/:id", async (c) => {
3975
4240
  const body = await c.req.json();
3976
4241
  const updated = await store.update(id, body);
3977
4242
  if (!updated)
3978
- return c.json({ error: "Credential not found" }, 404);
4243
+ return notFound("Credential not found");
3979
4244
  // Re-hydrate if envVar changed
3980
4245
  if (updated.envVar && updated.value && updated.status === "active") {
3981
4246
  process.env[updated.envVar] = updated.value;
@@ -3990,7 +4255,7 @@ app.delete("/api/credentials/:id", async (c) => {
3990
4255
  const id = c.req.param("id");
3991
4256
  const archived = await store.archive(id);
3992
4257
  if (!archived)
3993
- return c.json({ error: "Credential not found" }, 404);
4258
+ return notFound("Credential not found");
3994
4259
  // Remove from process.env
3995
4260
  if (archived.envVar) {
3996
4261
  delete process.env[archived.envVar];
@@ -4116,17 +4381,17 @@ app.put("/api/open-loops/:id/resolve", async (c) => {
4116
4381
  const body = await c.req.json().catch(() => ({}));
4117
4382
  const updated = await transitionLoop(id, "resonant", body.resolvedBy);
4118
4383
  if (!updated)
4119
- return c.json({ error: "Loop not found" }, 404);
4384
+ return notFound("Loop not found");
4120
4385
  return c.json({ ok: true, loop: updated });
4121
4386
  });
4122
4387
  // Trigger fold-back for a session.
4123
4388
  app.post("/api/open-loops/foldback", async (c) => {
4124
4389
  const body = await c.req.json();
4125
4390
  if (!body.sessionId)
4126
- return c.json({ error: "sessionId required" }, 400);
4391
+ return badRequest("sessionId required");
4127
4392
  const cs = chatSessions.get(body.sessionId);
4128
4393
  if (!cs)
4129
- return c.json({ error: "Session not found" }, 404);
4394
+ return notFound("Session not found");
4130
4395
  const result = await foldBack({
4131
4396
  history: cs.history,
4132
4397
  historySummary: cs.historySummary || undefined,
@@ -4162,7 +4427,7 @@ app.get("/api/metrics", async (c) => {
4162
4427
  app.get("/api/metrics/summary", async (c) => {
4163
4428
  const name = c.req.query("name");
4164
4429
  if (!name)
4165
- return c.json({ error: "name parameter required" }, 400);
4430
+ return badRequest("name parameter required");
4166
4431
  const windowMs = parseInt(c.req.query("window") ?? "60000", 10);
4167
4432
  const summary = await metricsStore.summarize(name, { windowMs });
4168
4433
  return c.json({ summary });
@@ -4176,7 +4441,7 @@ app.get("/api/metrics/names", async (c) => {
4176
4441
  app.get("/api/metrics/series", async (c) => {
4177
4442
  const name = c.req.query("name");
4178
4443
  if (!name)
4179
- return c.json({ error: "name parameter required" }, 400);
4444
+ return badRequest("name parameter required");
4180
4445
  const now = Date.now();
4181
4446
  const since = c.req.query("since") ?? new Date(now - 60 * 60 * 1000).toISOString();
4182
4447
  const until = c.req.query("until") ?? new Date(now).toISOString();
@@ -4257,7 +4522,7 @@ app.get("/api/metrics/firewall/compare", async (c) => {
4257
4522
  const afterSince = c.req.query("after_since");
4258
4523
  const afterUntil = c.req.query("after_until");
4259
4524
  if (!beforeSince || !beforeUntil || !afterSince) {
4260
- return c.json({ error: "Required: before_since, before_until, after_since" }, 400);
4525
+ return badRequest("Required: before_since, before_until, after_since");
4261
4526
  }
4262
4527
  const report = await generateComparisonReport(metricsStore, { since: beforeSince, until: beforeUntil }, { since: afterSince, until: afterUntil ?? new Date().toISOString() });
4263
4528
  return c.text(report);
@@ -4322,6 +4587,10 @@ app.get("/board", requireSurface("pages"), async (c) => {
4322
4587
  const html = await serveHtmlTemplate(join(UI_DIR, "board.html"));
4323
4588
  return c.html(html);
4324
4589
  });
4590
+ app.get("/whiteboard", async (c) => {
4591
+ const html = await serveHtmlTemplate(join(UI_DIR, "whiteboard.html"));
4592
+ return c.html(html);
4593
+ });
4325
4594
  app.get("/library", requireSurface("pages"), async (c) => {
4326
4595
  const html = await serveHtmlTemplate(join(UI_DIR, "library.html"));
4327
4596
  return c.html(html);
@@ -4356,7 +4625,7 @@ app.get("/api/roadmap", async (c) => {
4356
4625
  app.get("/api/roadmap/recent", async (c) => {
4357
4626
  const hours = parseInt(c.req.query("hours") || "24", 10);
4358
4627
  if (isNaN(hours) || hours < 1 || hours > 168) {
4359
- return c.json({ error: "hours must be between 1 and 168" }, 400);
4628
+ return badRequest("hours must be between 1 and 168");
4360
4629
  }
4361
4630
  try {
4362
4631
  const { gitAvailable } = await import("./utils/git.js");
@@ -4425,12 +4694,12 @@ app.get("/life", async (c) => {
4425
4694
  app.get("/api/browse", async (c) => {
4426
4695
  const url = c.req.query("url");
4427
4696
  if (!url)
4428
- return c.json({ error: "url parameter required" }, 400);
4697
+ return badRequest("url parameter required");
4429
4698
  try {
4430
4699
  new URL(url);
4431
4700
  }
4432
4701
  catch {
4433
- return c.json({ error: "Invalid URL" }, 400);
4702
+ return badRequest("Invalid URL");
4434
4703
  }
4435
4704
  const result = await _browse.browseUrl(url);
4436
4705
  if (!result) {
@@ -4442,7 +4711,7 @@ app.get("/api/browse", async (c) => {
4442
4711
  app.post("/api/share", async (c) => {
4443
4712
  const { email, note } = await c.req.json();
4444
4713
  if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
4445
- return c.json({ error: "Valid email address required" }, 400);
4714
+ return badRequest("Valid email address required");
4446
4715
  }
4447
4716
  const resendKey = process.env.RESEND_API_KEY;
4448
4717
  if (!resendKey) {
@@ -4604,7 +4873,7 @@ app.post("/api/ops/sidecars/:name/restart", async (c) => {
4604
4873
  ok = await _avatarSidecar?.startAvatarSidecar() ?? false;
4605
4874
  break;
4606
4875
  default:
4607
- return c.json({ error: `Unknown sidecar: ${name}` }, 400);
4876
+ return badRequest(`Unknown sidecar: ${name}`);
4608
4877
  }
4609
4878
  logActivity({ source: "system", summary: `Restarted sidecar: ${name} (${ok ? "up" : "failed"})` });
4610
4879
  return c.json({ name, available: ok });
@@ -4693,7 +4962,7 @@ app.patch("/api/ops/queue/:id", async (c) => {
4693
4962
  const body = await c.req.json();
4694
4963
  const updated = await store.update(id, body);
4695
4964
  if (!updated)
4696
- return c.json({ error: "Task not found" }, 404);
4965
+ return notFound("Task not found");
4697
4966
  return c.json(updated);
4698
4967
  });
4699
4968
  // --- Project endpoints ---
@@ -4713,7 +4982,7 @@ app.post("/api/ops/projects", async (c) => {
4713
4982
  const body = await c.req.json();
4714
4983
  const { name, prefix, description } = body;
4715
4984
  if (!name || !prefix)
4716
- return c.json({ error: "name and prefix required" }, 400);
4985
+ return badRequest("name and prefix required");
4717
4986
  try {
4718
4987
  const project = await projectStore.create({ name, prefix, description });
4719
4988
  return c.json(project, 201);
@@ -4731,7 +5000,7 @@ app.patch("/api/ops/projects/:id", async (c) => {
4731
5000
  const body = await c.req.json();
4732
5001
  const updated = await projectStore.update(id, body);
4733
5002
  if (!updated)
4734
- return c.json({ error: "Project not found" }, 404);
5003
+ return notFound("Project not found");
4735
5004
  return c.json(updated);
4736
5005
  });
4737
5006
  app.delete("/api/ops/projects/:id", async (c) => {
@@ -4751,7 +5020,7 @@ app.delete("/api/ops/projects/:id", async (c) => {
4751
5020
  }
4752
5021
  const ok = await projectStore.delete(id);
4753
5022
  if (!ok)
4754
- return c.json({ error: "Project not found" }, 404);
5023
+ return notFound("Project not found");
4755
5024
  return c.json({ ok: true });
4756
5025
  });
4757
5026
  // --- Posture API (UI surface assembly) ---
@@ -4817,7 +5086,7 @@ app.post("/api/nerve/subscribe", async (c) => {
4817
5086
  const body = await c.req.json();
4818
5087
  const { subscription, label } = body;
4819
5088
  if (!subscription?.endpoint || !subscription?.keys) {
4820
- return c.json({ error: "Invalid subscription" }, 400);
5089
+ return badRequest("Invalid subscription");
4821
5090
  }
4822
5091
  const id = await addSubscription(subscription, label);
4823
5092
  return c.json({ id });
@@ -4876,7 +5145,7 @@ app.post("/api/nerve/accept-update", async (c) => {
4876
5145
  app.get("/api/chat/poll", async (c) => {
4877
5146
  const sessionId = c.req.query("sessionId") || c.req.header("x-session-id");
4878
5147
  if (!sessionId)
4879
- return c.json({ error: "sessionId required" }, 400);
5148
+ return badRequest("sessionId required");
4880
5149
  const since = parseInt(c.req.query("since") || "0", 10);
4881
5150
  const cs = chatSessions.get(sessionId) || (chatSessions.size > 0 ? chatSessions.values().next().value : null);
4882
5151
  if (!cs)
@@ -4889,6 +5158,8 @@ app.get("/api/chat/poll", async (c) => {
4889
5158
  role: m.role,
4890
5159
  content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
4891
5160
  source: m.source || "pc",
5161
+ ...(m.toolsUsed ? { toolsUsed: m.toolsUsed } : {}),
5162
+ ...(m.agentsUsed ? { agentsUsed: m.agentsUsed } : {}),
4892
5163
  }));
4893
5164
  return c.json({ messages: newMsgs, total });
4894
5165
  });
@@ -4897,11 +5168,11 @@ app.post("/api/chat", async (c) => {
4897
5168
  const body = await c.req.json();
4898
5169
  const { sessionId, message, images } = body;
4899
5170
  if (!sessionId || !message) {
4900
- return c.json({ error: "sessionId and message required" }, 400);
5171
+ return badRequest("sessionId and message required");
4901
5172
  }
4902
5173
  const session = validateSession(sessionId);
4903
5174
  if (!session) {
4904
- return c.json({ error: "Invalid or expired session" }, 401);
5175
+ return unauthorized("Invalid or expired session");
4905
5176
  }
4906
5177
  const cs = await getOrCreateChatSession(sessionId, session.name);
4907
5178
  // Route to thread history if threadId is provided
@@ -5231,14 +5502,16 @@ app.post("/api/chat", async (c) => {
5231
5502
  visualEntries = await searchVisualMemories(chatMessage, maxImages);
5232
5503
  }
5233
5504
  if (visualEntries.length > 0) {
5234
- const hydrated = await hydrateVisualMemories(visualEntries, maxImages);
5235
- if (hydrated.length > 0) {
5236
- const blocks = [];
5237
- for (const h of hydrated) {
5238
- blocks.push({ type: "text", text: `[Visual memory from ${h.entry.createdAt}]: ${h.description}` });
5239
- blocks.push({ type: "image_url", image_url: { url: h.dataUri } });
5240
- }
5241
- ctx.messages.splice(1, 0, { role: "user", content: blocks });
5505
+ // Inject text descriptions only — the model already analyzed images on upload.
5506
+ // Sending raw base64 every turn wastes tokens and slows responses.
5507
+ const descriptions = visualEntries
5508
+ .map((e) => {
5509
+ const desc = e.meta?.description ?? e.content;
5510
+ return `[Visual memory from ${e.createdAt}]: ${desc}`;
5511
+ })
5512
+ .slice(0, maxImages);
5513
+ if (descriptions.length > 0) {
5514
+ ctx.messages.splice(1, 0, { role: "user", content: descriptions.join("\n") });
5242
5515
  }
5243
5516
  }
5244
5517
  }
@@ -5693,6 +5966,10 @@ app.post("/api/chat", async (c) => {
5693
5966
  resolve();
5694
5967
  };
5695
5968
  reqSignal?.addEventListener("abort", onAbort, { once: true });
5969
+ // Track tool usage for history persistence
5970
+ const toolsUsedInTurn = [];
5971
+ // Track spawned agents for history persistence
5972
+ const agentsSpawnedInTurn = [];
5696
5973
  // Token buffer for split-placeholder rehydration
5697
5974
  let tokenBuf2 = "";
5698
5975
  const streamStartMs2 = performance.now();
@@ -5706,10 +5983,28 @@ app.post("/api/chat", async (c) => {
5706
5983
  tokenBuf2 = "";
5707
5984
  stream.writeSSE({ data: JSON.stringify({ token: rehydrated }) }).catch(() => { });
5708
5985
  };
5709
- stream_fn({
5986
+ // Initialize tool registry for this session (kept for when tool layer is re-enabled)
5987
+ const toolHandlerCtx = {
5988
+ brainDir: BRAIN_DIR,
5989
+ encryptionKey: sessionKeys.get(sessionId) ?? undefined,
5990
+ getBrain: () => cs.brain,
5991
+ };
5992
+ const chatToolRegistry = new ToolRegistry();
5993
+ chatToolRegistry.registerAll(createToolHandlers(toolHandlerCtx));
5994
+ streamWithTools({
5995
+ streamFn: stream_fn,
5710
5996
  messages: redactedMessages,
5711
5997
  model: activeChatModel,
5712
5998
  signal: reqSignal,
5999
+ registry: chatToolRegistry,
6000
+ tier: activeTier,
6001
+ onToolCall: (call) => {
6002
+ stream.writeSSE({ data: JSON.stringify({ toolCall: { id: call.id, name: call.name, arguments: call.arguments } }) }).catch(() => { });
6003
+ },
6004
+ onToolResult: (name, result, isError) => {
6005
+ toolsUsedInTurn.push({ name, isError });
6006
+ stream.writeSSE({ data: JSON.stringify({ toolResult: { name, result: result.slice(0, 500), isError } }) }).catch(() => { });
6007
+ },
5713
6008
  onToken: (token) => {
5714
6009
  tokenBuf2 += token;
5715
6010
  // Hold if buffer ends with partial placeholder
@@ -5719,232 +6014,251 @@ app.post("/api/chat", async (c) => {
5719
6014
  flushBuf2();
5720
6015
  },
5721
6016
  onDone: async () => {
5722
- flushBuf2(); // flush remainder
5723
- reqSignal?.removeEventListener("abort", onAbort);
5724
- logLlmCall({
5725
- ts: new Date().toISOString(), mode: "stream",
5726
- provider: activeProvider, model: streamModel2,
5727
- durationMs: Math.round(performance.now() - streamStartMs2),
5728
- outputTokens: Math.ceil(fullResponse.length / 4), ok: true,
5729
- });
5730
- // Process action blocks BEFORE sending done — ensures SSE events reach client before stream closes.
5731
- // ALWAYS strip [AGENT_REQUEST] blocks from response, even if they can't be parsed.
5732
- // Capture raw block content first for logging/parsing.
5733
- const rawAgentBlocks = [...fullResponse.matchAll(/\[AGENT_REQUEST\]\s*([\s\S]*?)\s*\[\/AGENT_REQUEST\]/g)];
5734
- if (rawAgentBlocks.length > 0) {
5735
- fullResponse = fullResponse.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
5736
- agentLog.info(` Found ${rawAgentBlocks.length} AGENT_REQUEST block(s)`);
5737
- let spawnCount = 0;
5738
- for (const block of rawAgentBlocks) {
5739
- const rawContent = block[1].trim();
5740
- agentLog.info(` Block content: ${rawContent.slice(0, 300)}`);
5741
- // Try to extract JSON from the block — may be wrapped in backticks, code fences, or prose
5742
- let jsonStr = rawContent;
5743
- // Strip code fence wrappers: ```json ... ``` or ``` ... ```
5744
- jsonStr = jsonStr.replace(/^`{3,}(?:json)?\s*/i, "").replace(/\s*`{3,}$/i, "");
5745
- // Strip inline backticks
5746
- jsonStr = jsonStr.replace(/^`+|`+$/g, "");
5747
- // Try to find a JSON object in the content
5748
- const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
5749
- if (!jsonMatch) {
5750
- agentLog.error(` No JSON object found in block. Raw content: ${rawContent.slice(0, 300)}`);
5751
- logActivity({ source: "agent", summary: `AGENT_REQUEST has no JSON`, detail: rawContent.slice(0, 300) });
5752
- continue;
5753
- }
5754
- try {
5755
- const req = JSON.parse(jsonMatch[0]);
5756
- if (req.prompt) {
5757
- let finalPrompt = req.prompt;
5758
- const label = req.label || finalPrompt.slice(0, 60);
5759
- // Guard: detect vague prompts and prepend grounding instructions
5760
- const hasFilePath = /(?:src\/|brain\/|public\/|\.ts|\.js|\.md|\.json|\.yaml|\.yml)/.test(finalPrompt);
5761
- const isVague = /\b(?:comprehensive|robust|production-ready|enterprise|scalable|world-class)\b/i.test(finalPrompt)
5762
- && !hasFilePath;
5763
- const isWishList = (finalPrompt.match(/^\d+\.\s/gm) || []).length >= 5 && !hasFilePath;
5764
- if (isVague || isWishList) {
5765
- agentLog.warn(` Vague prompt detected, adding grounding preamble: ${label}`);
5766
- finalPrompt = [
5767
- `IMPORTANT: The original request below is vague. Do NOT try to build everything listed.`,
5768
- `Instead: 1) Read the existing codebase (start with src/ and package.json) to understand what exists.`,
5769
- `2) Pick ONE small, concrete piece you can actually implement that connects to existing code.`,
5770
- `3) Build that one thing well, with tests if a test framework exists.`,
5771
- `4) If nothing concrete can be built without more requirements, just create a brief spec document at brain/knowledge/notes/ describing what decisions are needed and exit.`,
5772
- ``,
5773
- `Original request:`,
5774
- finalPrompt,
5775
- ].join("\n");
5776
- }
5777
- spawnCount++;
5778
- agentLog.info(` Spawning: ${label}${(isVague || isWishList) ? " (grounded)" : ""}`);
5779
- // Await task submission so we can send the real task ID to the client
5780
- try {
5781
- const task = await submitTask({
5782
- label,
5783
- prompt: finalPrompt,
5784
- origin: "ai",
5785
- sessionId,
5786
- boardTaskId: req.taskId,
5787
- });
5788
- stream.writeSSE({ data: JSON.stringify({ agentSpawned: { label, taskId: task.id } }) }).catch(() => { });
5789
- logActivity({ source: "agent", summary: `AI-triggered agent: ${task.label}`, detail: `Task ${task.id}, PID ${task.pid}`, actionLabel: "PROMPTED", reason: "user chat triggered agent" });
6017
+ try {
6018
+ flushBuf2(); // flush remainder
6019
+ reqSignal?.removeEventListener("abort", onAbort);
6020
+ logLlmCall({
6021
+ ts: new Date().toISOString(), mode: "stream",
6022
+ provider: activeProvider, model: streamModel2,
6023
+ durationMs: Math.round(performance.now() - streamStartMs2),
6024
+ outputTokens: Math.ceil(fullResponse.length / 4), ok: true,
6025
+ });
6026
+ // Process action blocks BEFORE sending done ensures SSE events reach client before stream closes.
6027
+ // ALWAYS strip [AGENT_REQUEST] blocks from response, even if they can't be parsed.
6028
+ // Capture raw block content first for logging/parsing.
6029
+ const rawAgentBlocks = [...fullResponse.matchAll(/\[AGENT_REQUEST\]\s*([\s\S]*?)\s*\[\/AGENT_REQUEST\]/g)];
6030
+ if (rawAgentBlocks.length > 0) {
6031
+ fullResponse = fullResponse.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
6032
+ agentLog.info(` Found ${rawAgentBlocks.length} AGENT_REQUEST block(s)`);
6033
+ let spawnCount = 0;
6034
+ for (const block of rawAgentBlocks) {
6035
+ const rawContent = block[1].trim();
6036
+ agentLog.info(` Block content: ${rawContent.slice(0, 300)}`);
6037
+ // Try to extract JSON from the block — may be wrapped in backticks, code fences, or prose
6038
+ let jsonStr = rawContent;
6039
+ // Strip code fence wrappers: ```json ... ``` or ``` ... ```
6040
+ jsonStr = jsonStr.replace(/^`{3,}(?:json)?\s*/i, "").replace(/\s*`{3,}$/i, "");
6041
+ // Strip inline backticks
6042
+ jsonStr = jsonStr.replace(/^`+|`+$/g, "");
6043
+ // Try to find a JSON object in the content
6044
+ const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
6045
+ if (!jsonMatch) {
6046
+ agentLog.error(` No JSON object found in block. Raw content: ${rawContent.slice(0, 300)}`);
6047
+ logActivity({ source: "agent", summary: `AGENT_REQUEST has no JSON`, detail: rawContent.slice(0, 300) });
6048
+ continue;
6049
+ }
6050
+ try {
6051
+ const req = JSON.parse(jsonMatch[0]);
6052
+ if (req.prompt) {
6053
+ let finalPrompt = req.prompt;
6054
+ const label = req.label || finalPrompt.slice(0, 60);
6055
+ // Guard: detect vague prompts and prepend grounding instructions
6056
+ const hasFilePath = /(?:src\/|brain\/|public\/|\.ts|\.js|\.md|\.json|\.yaml|\.yml)/.test(finalPrompt);
6057
+ const isVague = /\b(?:comprehensive|robust|production-ready|enterprise|scalable|world-class)\b/i.test(finalPrompt)
6058
+ && !hasFilePath;
6059
+ const isWishList = (finalPrompt.match(/^\d+\.\s/gm) || []).length >= 5 && !hasFilePath;
6060
+ if (isVague || isWishList) {
6061
+ agentLog.warn(` Vague prompt detected, adding grounding preamble: ${label}`);
6062
+ finalPrompt = [
6063
+ `IMPORTANT: The original request below is vague. Do NOT try to build everything listed.`,
6064
+ `Instead: 1) Read the existing codebase (start with src/ and package.json) to understand what exists.`,
6065
+ `2) Pick ONE small, concrete piece you can actually implement that connects to existing code.`,
6066
+ `3) Build that one thing well, with tests if a test framework exists.`,
6067
+ `4) If nothing concrete can be built without more requirements, just create a brief spec document at brain/knowledge/notes/ describing what decisions are needed and exit.`,
6068
+ ``,
6069
+ `Original request:`,
6070
+ finalPrompt,
6071
+ ].join("\n");
6072
+ }
6073
+ spawnCount++;
6074
+ agentLog.info(` Spawning: ${label}${(isVague || isWishList) ? " (grounded)" : ""}`);
6075
+ // Await task submission so we can send the real task ID to the client
6076
+ try {
6077
+ const task = await submitTask({
6078
+ label,
6079
+ prompt: finalPrompt,
6080
+ origin: "ai",
6081
+ sessionId,
6082
+ boardTaskId: req.taskId,
6083
+ });
6084
+ stream.writeSSE({ data: JSON.stringify({ agentSpawned: { label, taskId: task.id } }) }).catch(() => { });
6085
+ agentsSpawnedInTurn.push({ label, taskId: task.id });
6086
+ logActivity({ source: "agent", summary: `AI-triggered agent: ${task.label}`, detail: `Task ${task.id}, PID ${task.pid}`, actionLabel: "PROMPTED", reason: "user chat triggered agent" });
6087
+ }
6088
+ catch (err) {
6089
+ agentLog.error(`Spawn failed for "${label}": ${err.message}`);
6090
+ logActivity({ source: "agent", summary: `AI agent spawn failed: ${err.message}`, actionLabel: "PROMPTED", reason: "user chat triggered agent spawn failed" });
6091
+ stream.writeSSE({ data: JSON.stringify({ agentError: { label, error: err.message } }) }).catch(() => { });
6092
+ }
5790
6093
  }
5791
- catch (err) {
5792
- agentLog.error(`Spawn failed for "${label}": ${err.message}`);
5793
- logActivity({ source: "agent", summary: `AI agent spawn failed: ${err.message}`, actionLabel: "PROMPTED", reason: "user chat triggered agent spawn failed" });
5794
- stream.writeSSE({ data: JSON.stringify({ agentError: { label, error: err.message } }) }).catch(() => { });
6094
+ else {
6095
+ agentLog.warn(`Parsed JSON but missing "prompt" field: ${jsonMatch[0].slice(0, 200)}`);
5795
6096
  }
5796
6097
  }
5797
- else {
5798
- agentLog.warn(`Parsed JSON but missing "prompt" field: ${jsonMatch[0].slice(0, 200)}`);
6098
+ catch (err) {
6099
+ const snippet = jsonMatch[0].slice(0, 300).replace(/\n/g, " ");
6100
+ agentLog.error(` JSON parse failed: ${snippet}`);
6101
+ logActivity({ source: "agent", summary: `AGENT_REQUEST parse error`, detail: snippet });
5799
6102
  }
5800
6103
  }
5801
- catch (err) {
5802
- const snippet = jsonMatch[0].slice(0, 300).replace(/\n/g, " ");
5803
- agentLog.error(` JSON parse failed: ${snippet}`);
5804
- logActivity({ source: "agent", summary: `AGENT_REQUEST parse error`, detail: snippet });
6104
+ if (spawnCount === 0) {
6105
+ agentLog.warn(` ${rawAgentBlocks.length} block(s) found but 0 spawned — check server logs for block content`);
5805
6106
  }
5806
6107
  }
5807
- if (spawnCount === 0) {
5808
- agentLog.warn(` ${rawAgentBlocks.length} block(s) found but 0 spawned check server logs for block content`);
5809
- }
5810
- }
5811
- // Check if AI requested board VIEW (read-only card rendering)
5812
- // Parsed here (not by registry) because BOARD_VIEW uses a different tag than BOARD_ACTION,
5813
- // but execution is routed through the board capability's "view" action.
5814
- const boardViewRe = /\[BOARD_VIEW\]\s*(\{[\s\S]*?\})\s*(?:\[\/BOARD_VIEW\])?/g;
5815
- const boardViewBlocks = [...fullResponse.matchAll(boardViewRe)];
5816
- const boardViewPromises = [];
5817
- const boardViewResults = [];
5818
- if (boardViewBlocks.length > 0) {
5819
- // Strip blocks from visible response
5820
- fullResponse = fullResponse.replace(/\s*\[BOARD_VIEW\][\s\S]*?\[\/BOARD_VIEW\]\s*/g, "").trim();
5821
- fullResponse = fullResponse.replace(/\s*\[BOARD_VIEW\]\s*\{[\s\S]*?\}\s*/g, "").trim();
5822
- for (const block of boardViewBlocks) {
5823
- try {
5824
- const req = JSON.parse(block[1]);
5825
- boardViewPromises.push((async () => {
5826
- try {
5827
- const result = await boardCapability.execute({ action: "view", ...req }, { origin: "chat" });
5828
- const issues = result.data;
5829
- const viewLabel = req.stateType || req.filter || "board";
5830
- if (result.ok && issues && issues.length > 0) {
5831
- boardViewResults.push({ query: viewLabel, issues });
5832
- stream.writeSSE({
5833
- data: JSON.stringify({ boardItems: { issues: issuesToCardPayload(issues) } }),
5834
- }).catch(() => { });
6108
+ // Check if AI requested board VIEW (read-only card rendering)
6109
+ // Parsed here (not by registry) because BOARD_VIEW uses a different tag than BOARD_ACTION,
6110
+ // but execution is routed through the board capability's "view" action.
6111
+ const boardViewRe = /\[BOARD_VIEW\]\s*(\{[\s\S]*?\})\s*(?:\[\/BOARD_VIEW\])?/g;
6112
+ const boardViewBlocks = [...fullResponse.matchAll(boardViewRe)];
6113
+ const boardViewPromises = [];
6114
+ const boardViewResults = [];
6115
+ if (boardViewBlocks.length > 0) {
6116
+ // Strip blocks from visible response
6117
+ fullResponse = fullResponse.replace(/\s*\[BOARD_VIEW\][\s\S]*?\[\/BOARD_VIEW\]\s*/g, "").trim();
6118
+ fullResponse = fullResponse.replace(/\s*\[BOARD_VIEW\]\s*\{[\s\S]*?\}\s*/g, "").trim();
6119
+ for (const block of boardViewBlocks) {
6120
+ try {
6121
+ const req = JSON.parse(block[1]);
6122
+ boardViewPromises.push((async () => {
6123
+ try {
6124
+ const result = await boardCapability.execute({ action: "view", ...req }, { origin: "chat" });
6125
+ const issues = result.data;
6126
+ const viewLabel = req.stateType || req.filter || "board";
6127
+ if (result.ok && issues && issues.length > 0) {
6128
+ boardViewResults.push({ query: viewLabel, issues });
6129
+ stream.writeSSE({
6130
+ data: JSON.stringify({ boardItems: { issues: issuesToCardPayload(issues) } }),
6131
+ }).catch(() => { });
6132
+ }
6133
+ else if (result.ok) {
6134
+ boardViewResults.push({ query: viewLabel, issues: [] });
6135
+ // Empty result — send a system message so the agent/user know
6136
+ stream.writeSSE({
6137
+ data: JSON.stringify({ boardItems: { issues: [], empty: true } }),
6138
+ }).catch(() => { });
6139
+ }
5835
6140
  }
5836
- else if (result.ok) {
5837
- boardViewResults.push({ query: viewLabel, issues: [] });
5838
- // Empty result — send a system message so the agent/user know
5839
- stream.writeSSE({
5840
- data: JSON.stringify({ boardItems: { issues: [], empty: true } }),
5841
- }).catch(() => { });
6141
+ catch {
6142
+ log.warn("BOARD_VIEW fetch error");
5842
6143
  }
5843
- }
5844
- catch {
5845
- log.warn("BOARD_VIEW fetch error");
5846
- }
5847
- })());
5848
- }
5849
- catch {
5850
- log.warn("BOARD_VIEW parse error");
6144
+ })());
6145
+ }
6146
+ catch {
6147
+ log.warn("BOARD_VIEW parse error");
6148
+ }
5851
6149
  }
5852
6150
  }
5853
- }
5854
- // Process capability action blocks (board, calendar, email, docs) via registry
5855
- {
5856
- const capReg = getCapabilityRegistry();
5857
- if (capReg) {
5858
- const blocks = capReg.parseActionBlocks(fullResponse);
5859
- if (blocks.length > 0) {
5860
- fullResponse = capReg.stripActionBlocks(fullResponse);
5861
- // Fire-and-forget: execute action blocks
5862
- (async () => {
5863
- for (const block of blocks) {
5864
- if (!block.payload)
5865
- continue;
5866
- const def = capReg.get(block.capabilityId);
5867
- if (!def || def.pattern !== "action")
5868
- continue;
5869
- try {
5870
- await def.execute(block.payload, { origin: "chat" });
6151
+ // Process capability action blocks (board, calendar, email, docs) via registry
6152
+ {
6153
+ const capReg = getCapabilityRegistry();
6154
+ if (capReg) {
6155
+ const blocks = capReg.parseActionBlocks(fullResponse);
6156
+ if (blocks.length > 0) {
6157
+ fullResponse = capReg.stripActionBlocks(fullResponse);
6158
+ // Fire-and-forget: execute action blocks
6159
+ (async () => {
6160
+ for (const block of blocks) {
6161
+ if (!block.payload)
6162
+ continue;
6163
+ const def = capReg.get(block.capabilityId);
6164
+ if (!def || def.pattern !== "action")
6165
+ continue;
6166
+ try {
6167
+ await def.execute(block.payload, { origin: "chat" });
6168
+ }
6169
+ catch { }
5871
6170
  }
5872
- catch { }
5873
- }
5874
- })();
6171
+ })();
6172
+ }
5875
6173
  }
5876
6174
  }
5877
- }
5878
- // Save assistant response to history (with AGENT_REQUEST + BOARD_ACTION + action blocks stripped)
5879
- cs.history.push({ role: "assistant", content: fullResponse });
5880
- persistSession();
5881
- // Fire-and-forget: extract learnable facts from conversation
5882
- cs.turnCount++;
5883
- const recentMessages = cs.history.slice(-4);
5884
- extractAndLearn({
5885
- brain: cs.brain,
5886
- recentMessages,
5887
- userMessage: message,
5888
- provider: resolveProvider(),
5889
- model: resolveUtilityModel(),
5890
- lastExtractionTurn: cs.lastExtractionTurn,
5891
- currentTurn: cs.turnCount,
5892
- }).then((result) => {
5893
- if (result.extracted > 0) {
5894
- logActivity({ source: "learn", summary: `Extracted ${result.extracted} fact(s)`, actionLabel: "PROMPTED", reason: "user conversation triggered learning" });
5895
- cs.lastExtractionTurn = cs.turnCount;
6175
+ // Save assistant response to history (with AGENT_REQUEST + BOARD_ACTION + action blocks stripped)
6176
+ const historyEntry = { role: "assistant", content: fullResponse };
6177
+ if (toolsUsedInTurn.length > 0)
6178
+ historyEntry.toolsUsed = toolsUsedInTurn;
6179
+ if (agentsSpawnedInTurn.length > 0) {
6180
+ historyEntry.agentsUsed = agentsSpawnedInTurn.map((a) => ({
6181
+ label: a.label,
6182
+ status: "completed", // placeholder — frontend polls for real status
6183
+ taskId: a.taskId,
6184
+ }));
5896
6185
  }
5897
- if (result.error) {
5898
- logActivity({ source: "learn", summary: `Extraction error: ${result.error}` });
5899
- }
5900
- }).catch(() => { });
5901
- // Fire-and-forget: generate avatar video (TTS → MuseTalk → MP4)
5902
- if ((_avatarSidecar?.isAvatarAvailable() ?? false) && (_ttsClient?.isTtsAvailable() ?? false) && fullResponse) {
5903
- const trimmedText = fullResponse.slice(0, 2000);
5904
- _ttsClient.synthesize(trimmedText).then(async (wavBuffer) => {
5905
- if (!wavBuffer)
5906
- return;
5907
- const cached = await _avatarClient.getCachedVideo(wavBuffer);
5908
- if (cached) {
5909
- pushPendingVideo(cached);
5910
- return;
6186
+ cs.history.push(historyEntry);
6187
+ persistSession();
6188
+ // Fire-and-forget: extract learnable facts from conversation
6189
+ cs.turnCount++;
6190
+ const recentMessages = cs.history.slice(-4);
6191
+ extractAndLearn({
6192
+ brain: cs.brain,
6193
+ recentMessages,
6194
+ userMessage: message,
6195
+ provider: resolveProvider(),
6196
+ model: resolveUtilityModel(),
6197
+ lastExtractionTurn: cs.lastExtractionTurn,
6198
+ currentTurn: cs.turnCount,
6199
+ }).then((result) => {
6200
+ if (result.extracted > 0) {
6201
+ logActivity({ source: "learn", summary: `Extracted ${result.extracted} fact(s)`, actionLabel: "PROMPTED", reason: "user conversation triggered learning" });
6202
+ cs.lastExtractionTurn = cs.turnCount;
5911
6203
  }
5912
- const mp4 = await _avatarClient.generateVideo(wavBuffer);
5913
- if (!mp4)
5914
- return;
5915
- const filename = await _avatarClient.cacheVideo(mp4, wavBuffer);
5916
- pushPendingVideo(filename);
5917
- logActivity({ source: "avatar", summary: "Generated avatar video", actionLabel: "PROMPTED", reason: "user conversation triggered avatar" });
5918
- }).catch((err) => {
5919
- const msg = err instanceof Error ? err.message : String(err);
5920
- logActivity({ source: "avatar", summary: `Avatar generation failed: ${msg}` });
5921
- });
5922
- }
5923
- // Wait for board view fetches so boardItems SSE events reach the client
5924
- // BEFORE resolve() closes the stream. Previously resolve() fired immediately,
5925
- // racing with the async boardViewPromises — cards never reached the client.
5926
- if (boardViewPromises.length > 0) {
5927
- Promise.all(boardViewPromises).finally(() => {
5928
- // Store board view results in history so next turn has context
5929
- if (boardViewResults.length > 0) {
5930
- const lines = boardViewResults.map((r) => {
5931
- if (r.issues.length === 0) {
5932
- return `Displayed 0 ${r.query} items to user.`;
5933
- }
5934
- const itemLines = r.issues.map((i) => `- ${i.identifier}: ${i.title} [${i.state || "unknown"}]`);
5935
- return `Displayed ${r.issues.length} ${r.query} item(s) to user:\n${itemLines.join("\n")}`;
5936
- });
5937
- cs.history.push({
5938
- role: "system",
5939
- content: `[BOARD_VIEW_RESULT]\n${lines.join("\n")}`,
5940
- });
5941
- persistSession();
6204
+ if (result.error) {
6205
+ logActivity({ source: "learn", summary: `Extraction error: ${result.error}` });
5942
6206
  }
6207
+ }).catch(() => { });
6208
+ // Fire-and-forget: generate avatar video (TTS → MuseTalk → MP4)
6209
+ if ((_avatarSidecar?.isAvatarAvailable() ?? false) && (_ttsClient?.isTtsAvailable() ?? false) && fullResponse) {
6210
+ const trimmedText = fullResponse.slice(0, 2000);
6211
+ _ttsClient.synthesize(trimmedText).then(async (wavBuffer) => {
6212
+ if (!wavBuffer)
6213
+ return;
6214
+ const cached = await _avatarClient.getCachedVideo(wavBuffer);
6215
+ if (cached) {
6216
+ pushPendingVideo(cached);
6217
+ return;
6218
+ }
6219
+ const mp4 = await _avatarClient.generateVideo(wavBuffer);
6220
+ if (!mp4)
6221
+ return;
6222
+ const filename = await _avatarClient.cacheVideo(mp4, wavBuffer);
6223
+ pushPendingVideo(filename);
6224
+ logActivity({ source: "avatar", summary: "Generated avatar video", actionLabel: "PROMPTED", reason: "user conversation triggered avatar" });
6225
+ }).catch((err) => {
6226
+ const msg = err instanceof Error ? err.message : String(err);
6227
+ logActivity({ source: "avatar", summary: `Avatar generation failed: ${msg}` });
6228
+ });
6229
+ }
6230
+ // Wait for board view fetches so boardItems SSE events reach the client
6231
+ // BEFORE resolve() closes the stream. Previously resolve() fired immediately,
6232
+ // racing with the async boardViewPromises — cards never reached the client.
6233
+ if (boardViewPromises.length > 0) {
6234
+ Promise.all(boardViewPromises).finally(() => {
6235
+ // Store board view results in history so next turn has context
6236
+ if (boardViewResults.length > 0) {
6237
+ const lines = boardViewResults.map((r) => {
6238
+ if (r.issues.length === 0) {
6239
+ return `Displayed 0 ${r.query} items to user.`;
6240
+ }
6241
+ const itemLines = r.issues.map((i) => `- ${i.identifier}: ${i.title} [${i.state || "unknown"}]`);
6242
+ return `Displayed ${r.issues.length} ${r.query} item(s) to user:\n${itemLines.join("\n")}`;
6243
+ });
6244
+ cs.history.push({
6245
+ role: "system",
6246
+ content: `[BOARD_VIEW_RESULT]\n${lines.join("\n")}`,
6247
+ });
6248
+ persistSession();
6249
+ }
6250
+ stream.writeSSE({ data: JSON.stringify({ done: true }) }).catch(() => { });
6251
+ resolve();
6252
+ });
6253
+ }
6254
+ else {
5943
6255
  stream.writeSSE({ data: JSON.stringify({ done: true }) }).catch(() => { });
5944
6256
  resolve();
5945
- });
6257
+ }
5946
6258
  }
5947
- else {
6259
+ catch (doneErr) {
6260
+ log.error("onDone handler crashed", { error: doneErr?.message ?? String(doneErr), stack: doneErr?.stack?.slice(0, 300) });
6261
+ stream.writeSSE({ data: JSON.stringify({ error: "Internal error processing response", errorDetail: doneErr?.message }) }).catch(() => { });
5948
6262
  stream.writeSSE({ data: JSON.stringify({ done: true }) }).catch(() => { });
5949
6263
  resolve();
5950
6264
  }
@@ -5983,7 +6297,7 @@ app.post("/api/chat", async (c) => {
5983
6297
  }
5984
6298
  resolve(); // Still resolve so stream closes
5985
6299
  },
5986
- });
6300
+ }).catch(reject);
5987
6301
  });
5988
6302
  });
5989
6303
  });
@@ -5991,7 +6305,7 @@ app.post("/api/chat", async (c) => {
5991
6305
  app.post("/api/freeze", async (c) => {
5992
6306
  const body = await c.req.json().catch(() => null);
5993
6307
  if (!body?.signal)
5994
- return c.json({ error: "Missing freeze signal" }, 400);
6308
+ return badRequest("Missing freeze signal");
5995
6309
  const { freeze, isFrozen } = await import("./tier/freeze.js");
5996
6310
  if (isFrozen())
5997
6311
  return c.json({ error: "Already frozen" }, 409);
@@ -6011,7 +6325,7 @@ app.get("/api/freeze/status", async (c) => {
6011
6325
  app.post("/api/import/folder", async (c) => {
6012
6326
  const body = await c.req.json();
6013
6327
  if (!body.paths || !Array.isArray(body.paths) || body.paths.length === 0) {
6014
- return c.json({ error: "paths[] required" }, 400);
6328
+ return badRequest("paths[] required");
6015
6329
  }
6016
6330
  const { resolve } = await import("node:path");
6017
6331
  const { importToBrain } = await import("./files/import.js");
@@ -6064,7 +6378,7 @@ app.post("/api/import/files", async (c) => {
6064
6378
  const formData = await c.req.formData();
6065
6379
  const files = formData.getAll("files");
6066
6380
  if (files.length === 0)
6067
- return c.json({ error: "No files provided" }, 400);
6381
+ return badRequest("No files provided");
6068
6382
  const { mkdir: mkdirFs, writeFile: writeFs } = await import("node:fs/promises");
6069
6383
  const { join: joinPath } = await import("node:path");
6070
6384
  const ingestDir = joinPath(process.cwd(), "ingest");