@vellumai/assistant 0.6.0 → 0.6.1

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 (285) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. package/src/util/clipboard.ts +0 -34
@@ -38,6 +38,10 @@ import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
38
38
  import { HostCuProxy } from "../../daemon/host-cu-proxy.js";
39
39
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
40
40
  import type { ServerMessage } from "../../daemon/message-protocol.js";
41
+ import type {
42
+ MacosTransportMetadata,
43
+ NonMacosTransportMetadata,
44
+ } from "../../daemon/message-types/conversations.js";
41
45
  import type { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
42
46
  import * as attachmentsStore from "../../memory/attachments-store.js";
43
47
  import {
@@ -410,8 +414,15 @@ export function handleListMessages(
410
414
  rawMessages = getMessages(resolvedConversationId);
411
415
  }
412
416
 
417
+ // During streaming, tool_use (assistant) and tool_result (user) events are
418
+ // assembled client-side into a single assistant ChatMessage. On reload, they
419
+ // are separate DB rows. Merge tool_result blocks from user messages into the
420
+ // preceding assistant message so renderHistoryContent can pair them via its
421
+ // pendingToolUses map — otherwise they render as "Unknown" tool calls.
422
+ const mergedMessages = mergeToolResultsIntoAssistantMessages(rawMessages);
423
+
413
424
  // Parse content blocks and extract text + tool calls
414
- const parsed = rawMessages.map((msg) => {
425
+ const parsed = mergedMessages.map((msg) => {
415
426
  let content: unknown;
416
427
  try {
417
428
  content = JSON.parse(msg.content);
@@ -424,10 +435,33 @@ export function handleListMessages(
424
435
  // was queued or its persistence was delayed (long assistant generation),
425
436
  // sentAt captures the actual event time. Falls back to createdAt.
426
437
  let sentAt: number | undefined;
438
+ let subagentNotification:
439
+ | {
440
+ subagentId: string;
441
+ label: string;
442
+ status: string;
443
+ error?: string;
444
+ conversationId?: string;
445
+ }
446
+ | undefined;
427
447
  if (msg.metadata) {
428
448
  try {
429
449
  const meta = JSON.parse(msg.metadata);
430
450
  if (typeof meta.sentAt === "number") sentAt = meta.sentAt;
451
+ if (meta.subagentNotification) {
452
+ const n = meta.subagentNotification;
453
+ if (typeof n.subagentId === "string" && typeof n.label === "string") {
454
+ subagentNotification = {
455
+ subagentId: n.subagentId,
456
+ label: n.label,
457
+ status: typeof n.status === "string" ? n.status : "completed",
458
+ ...(typeof n.error === "string" ? { error: n.error } : {}),
459
+ ...(typeof n.conversationId === "string"
460
+ ? { conversationId: n.conversationId }
461
+ : {}),
462
+ };
463
+ }
464
+ }
431
465
  } catch {
432
466
  // Ignore malformed metadata
433
467
  }
@@ -475,6 +509,7 @@ export function handleListMessages(
475
509
  ? { thinkingSegments: rendered.thinkingSegments }
476
510
  : {}),
477
511
  id: msg.id,
512
+ subagentNotification,
478
513
  };
479
514
  }
480
515
 
@@ -492,6 +527,7 @@ export function handleListMessages(
492
527
  ? { thinkingSegments: rendered.thinkingSegments }
493
528
  : {}),
494
529
  id: msg.id,
530
+ subagentNotification,
495
531
  };
496
532
  });
497
533
 
@@ -580,6 +616,9 @@ export function handleListMessages(
580
616
  ? { thinkingSegments: m.thinkingSegments }
581
617
  : {}),
582
618
  ...(m.contentOrder.length > 0 ? { contentOrder: m.contentOrder } : {}),
619
+ ...(m.subagentNotification
620
+ ? { subagentNotification: m.subagentNotification }
621
+ : {}),
583
622
  };
584
623
  });
585
624
 
@@ -599,6 +638,157 @@ export function handleListMessages(
599
638
  return Response.json({ messages });
600
639
  }
601
640
 
641
+ // ── Tool-result merging ─────────────────────────────────────────────
642
+
643
+ function isToolResultType(type: string): boolean {
644
+ return type === "tool_result" || type === "web_search_tool_result";
645
+ }
646
+
647
+ function isSystemNoticeText(block: Record<string, unknown>): boolean {
648
+ if (block.type !== "text") return false;
649
+ const text = typeof block.text === "string" ? block.text : "";
650
+ return (
651
+ text.startsWith("<system_notice>") && text.endsWith("</system_notice>")
652
+ );
653
+ }
654
+
655
+ /**
656
+ * Merge tool_result blocks from user messages into the preceding assistant
657
+ * message's content array. This lets renderHistoryContent's pendingToolUses
658
+ * map pair tool_use and tool_result blocks, preventing "unknown" tool names.
659
+ *
660
+ * User messages that consist entirely of tool_result blocks (and optional
661
+ * system_notice text) are removed from the output. Mixed messages (tool_result
662
+ * + real user text) keep only the non-tool-result blocks.
663
+ */
664
+ function mergeToolResultsIntoAssistantMessages(
665
+ messages: MessageRow[],
666
+ ): MessageRow[] {
667
+ // Index of the most recent assistant message in the output array.
668
+ let lastAssistantIdx = -1;
669
+ // Parsed content caches — lazily populated per assistant message.
670
+ const parsedAssistantContent = new Map<number, unknown[]>();
671
+
672
+ const result: MessageRow[] = [];
673
+
674
+ for (const msg of messages) {
675
+ if (msg.role === "assistant") {
676
+ lastAssistantIdx = result.length;
677
+ result.push(msg);
678
+ continue;
679
+ }
680
+
681
+ // Only process user messages — other roles pass through.
682
+ if (msg.role !== "user") {
683
+ result.push(msg);
684
+ continue;
685
+ }
686
+
687
+ let blocks: unknown[];
688
+ try {
689
+ const parsed = JSON.parse(msg.content);
690
+ if (!Array.isArray(parsed)) {
691
+ result.push(msg);
692
+ continue;
693
+ }
694
+ blocks = parsed;
695
+ } catch {
696
+ result.push(msg);
697
+ continue;
698
+ }
699
+
700
+ // Separate tool-result blocks from real user content.
701
+ const toolResultBlocks: unknown[] = [];
702
+ const otherBlocks: unknown[] = [];
703
+ for (const block of blocks) {
704
+ if (
705
+ typeof block === "object" &&
706
+ block !== null &&
707
+ typeof (block as Record<string, unknown>).type === "string"
708
+ ) {
709
+ const rec = block as Record<string, unknown>;
710
+ if (isToolResultType(rec.type as string)) {
711
+ toolResultBlocks.push(block);
712
+ } else if (isSystemNoticeText(rec)) {
713
+ // System notices don't count as user content — drop them when
714
+ // the message is otherwise tool-result-only.
715
+ otherBlocks.push(block);
716
+ } else {
717
+ otherBlocks.push(block);
718
+ }
719
+ } else {
720
+ otherBlocks.push(block);
721
+ }
722
+ }
723
+
724
+ // No tool results → pass through unchanged.
725
+ if (toolResultBlocks.length === 0) {
726
+ result.push(msg);
727
+ continue;
728
+ }
729
+
730
+ // Append tool_result blocks to the preceding assistant message's content.
731
+ if (lastAssistantIdx >= 0) {
732
+ const assistant = result[lastAssistantIdx];
733
+ let assistantContent = parsedAssistantContent.get(lastAssistantIdx);
734
+ if (!assistantContent) {
735
+ try {
736
+ const parsed = JSON.parse(assistant.content);
737
+ assistantContent = Array.isArray(parsed) ? parsed : [parsed];
738
+ } catch {
739
+ assistantContent = [];
740
+ }
741
+ parsedAssistantContent.set(lastAssistantIdx, assistantContent);
742
+ }
743
+ assistantContent.push(...toolResultBlocks);
744
+ } else {
745
+ // No preceding assistant message (pagination boundary) — keep the
746
+ // original message as-is to avoid permanent data loss. The preceding
747
+ // assistant tool_use lives in the previous page; dropping the result
748
+ // here would be unrecoverable.
749
+ // Still strip system notices so internal prompt text isn't exposed.
750
+ const filteredBlocks = blocks.filter(
751
+ (b) =>
752
+ !(
753
+ typeof b === "object" &&
754
+ b !== null &&
755
+ isSystemNoticeText(b as Record<string, unknown>)
756
+ ),
757
+ );
758
+ result.push({
759
+ ...msg,
760
+ content:
761
+ filteredBlocks.length === blocks.length
762
+ ? msg.content
763
+ : JSON.stringify(filteredBlocks),
764
+ });
765
+ continue;
766
+ }
767
+
768
+ // If the user message had only tool_result (+ system_notice) blocks,
769
+ // suppress it entirely. Otherwise keep the non-tool-result content.
770
+ const realUserContent = otherBlocks.filter(
771
+ (b) =>
772
+ !(
773
+ typeof b === "object" &&
774
+ b !== null &&
775
+ isSystemNoticeText(b as Record<string, unknown>)
776
+ ),
777
+ );
778
+ if (realUserContent.length > 0) {
779
+ result.push({ ...msg, content: JSON.stringify(otherBlocks) });
780
+ }
781
+ // else: tool-result-only → suppressed (results already merged above)
782
+ }
783
+
784
+ // Write back any modified assistant message content.
785
+ for (const [idx, content] of parsedAssistantContent) {
786
+ result[idx] = { ...result[idx], content: JSON.stringify(content) };
787
+ }
788
+
789
+ return result;
790
+ }
791
+
602
792
  /**
603
793
  * Build an `onEvent` callback that publishes every outbound event to the
604
794
  * assistant event hub, maintaining ordered delivery through a serial chain.
@@ -658,13 +848,10 @@ function makeHubPublisher(
658
848
  guardianPrincipalId: trustContext?.guardianPrincipalId ?? undefined,
659
849
  toolName: msg.toolName,
660
850
  commandPreview:
661
- redactSecrets(
662
- summarizeToolInput(msg.toolName, inputRecord),
663
- ) || undefined,
851
+ redactSecrets(summarizeToolInput(msg.toolName, inputRecord)) ||
852
+ undefined,
664
853
  riskLevel: msg.riskLevel,
665
- activityText: activityRaw
666
- ? redactSecrets(activityRaw)
667
- : undefined,
854
+ activityText: activityRaw ? redactSecrets(activityRaw) : undefined,
668
855
  executionTarget: msg.executionTarget,
669
856
  status: "pending",
670
857
  requestCode: generateCanonicalRequestCode(),
@@ -759,6 +946,8 @@ export async function handleSendMessage(
759
946
  conversationType?: string;
760
947
  automated?: boolean;
761
948
  bypassSecretCheck?: boolean;
949
+ hostHomeDir?: string;
950
+ hostUsername?: string;
762
951
  };
763
952
 
764
953
  const { conversationKey, content, attachmentIds } = body;
@@ -791,9 +980,11 @@ export async function handleSendMessage(
791
980
  );
792
981
  }
793
982
 
794
- if (!conversationKey) {
795
- return httpError("BAD_REQUEST", "conversationKey is required", 400);
796
- }
983
+ // When conversationKey is omitted, derive a stable default from
984
+ // sourceChannel + sourceInterface so that repeated calls from the same
985
+ // channel/interface pair share a single conversation thread.
986
+ const resolvedConversationKey =
987
+ conversationKey ?? `default:${sourceChannel}:${sourceInterface}`;
797
988
 
798
989
  // Reject non-string content values (numbers, objects, etc.)
799
990
  if (content != null && typeof content !== "string") {
@@ -856,12 +1047,29 @@ export async function handleSendMessage(
856
1047
 
857
1048
  const conversationType =
858
1049
  body.conversationType === "private" ? ("private" as const) : undefined;
859
- const mapping = getOrCreateConversation(conversationKey, {
1050
+ const mapping = getOrCreateConversation(resolvedConversationKey, {
860
1051
  conversationType,
861
1052
  });
862
1053
  const smDeps = deps.sendMessageDeps;
1054
+
1055
+ // Build transport metadata from the request so the daemon can inject
1056
+ // host environment hints (home directory, username) into the LLM context.
1057
+ const transport =
1058
+ sourceInterface === "macos"
1059
+ ? ({
1060
+ channelId: sourceChannel,
1061
+ interfaceId: "macos" as const,
1062
+ hostHomeDir: body.hostHomeDir,
1063
+ hostUsername: body.hostUsername,
1064
+ } satisfies MacosTransportMetadata)
1065
+ : ({
1066
+ channelId: sourceChannel,
1067
+ interfaceId: sourceInterface,
1068
+ } satisfies NonMacosTransportMetadata);
1069
+
863
1070
  const conversation = await smDeps.getOrCreateConversation(
864
1071
  mapping.conversationId,
1072
+ { transport },
865
1073
  );
866
1074
 
867
1075
  // Resolve guardian context from the AuthContext's actorPrincipalId.
@@ -932,7 +1140,7 @@ export async function handleSendMessage(
932
1140
  // channels, headless) fall back to local execution.
933
1141
  // Set the proxy BEFORE updateClient so updateClient's call to
934
1142
  // hostBashProxy.updateSender targets the correct (new) proxy.
935
- if (sourceInterface === "macos" || sourceInterface === "ios") {
1143
+ if (sourceInterface === "macos") {
936
1144
  // Reuse the existing proxy if the conversation is actively processing a
937
1145
  // host bash request to avoid orphaning in-flight requests.
938
1146
  if (!conversation.isProcessing() || !conversation.hostBashProxy) {
@@ -969,9 +1177,7 @@ export async function handleSendMessage(
969
1177
  // When proxies are preserved during an active turn (non-desktop request while
970
1178
  // processing), skip updating proxy senders to avoid degrading them.
971
1179
  const preservingProxies =
972
- conversation.isProcessing() &&
973
- sourceInterface !== "macos" &&
974
- sourceInterface !== "ios";
1180
+ conversation.isProcessing() && sourceInterface !== "macos";
975
1181
  conversation.updateClient(onEvent, !isInteractive, {
976
1182
  skipProxySenderUpdate: preservingProxies,
977
1183
  });
@@ -1143,36 +1349,47 @@ export async function handleSendMessage(
1143
1349
  // Auto-deny pending confirmations only after enqueue succeeds, so we
1144
1350
  // don't cancel approval-gated workflows when the replacement message
1145
1351
  // is itself rejected by the queue budget.
1146
- if (conversation.hasAnyPendingConfirmation()) {
1147
- // Emit authoritative denial state for each pending request.
1148
- // sendToClient (wired to the SSE hub) delivers these to the client.
1149
- for (const interaction of pendingInteractions.getByConversation(
1150
- mapping.conversationId,
1151
- )) {
1152
- if (
1153
- interaction.conversation === conversation &&
1154
- interaction.kind === "confirmation"
1155
- ) {
1156
- conversation.emitConfirmationStateChanged({
1157
- conversationId: mapping.conversationId,
1158
- requestId: interaction.requestId,
1159
- state: "denied" as const,
1160
- source: "auto_deny" as const,
1161
- });
1162
- // Sync canonical guardian request status so stale "pending" DB
1163
- // records don't get matched by later guardian reply routing.
1164
- resolveCanonicalGuardianRequest(interaction.requestId, "pending", {
1165
- status: "denied",
1166
- });
1352
+ // Wrapped in try-catch: the message is already enqueued, so a failure
1353
+ // here must not turn the 202 response into a 500 — that would leave
1354
+ // the client showing "Failed to send" for a message the daemon will
1355
+ // process from the queue.
1356
+ try {
1357
+ if (conversation.hasAnyPendingConfirmation()) {
1358
+ // Emit authoritative denial state for each pending request.
1359
+ // sendToClient (wired to the SSE hub) delivers these to the client.
1360
+ for (const interaction of pendingInteractions.getByConversation(
1361
+ mapping.conversationId,
1362
+ )) {
1363
+ if (
1364
+ interaction.conversation === conversation &&
1365
+ interaction.kind === "confirmation"
1366
+ ) {
1367
+ conversation.emitConfirmationStateChanged({
1368
+ conversationId: mapping.conversationId,
1369
+ requestId: interaction.requestId,
1370
+ state: "denied" as const,
1371
+ source: "auto_deny" as const,
1372
+ });
1373
+ // Sync canonical guardian request status so stale "pending" DB
1374
+ // records don't get matched by later guardian reply routing.
1375
+ resolveCanonicalGuardianRequest(interaction.requestId, "pending", {
1376
+ status: "denied",
1377
+ });
1378
+ }
1167
1379
  }
1380
+ conversation.denyAllPendingConfirmations();
1381
+ pendingInteractions.removeByConversation(conversation);
1168
1382
  }
1169
- conversation.denyAllPendingConfirmations();
1170
- pendingInteractions.removeByConversation(conversation);
1171
- }
1172
1383
 
1173
- // Expire any orphaned canonical requests that survived without a
1174
- // matching in-memory pending interaction (e.g. prompter timeouts).
1175
- expireOrphanedCanonicalRequests(mapping.conversationId);
1384
+ // Expire any orphaned canonical requests that survived without a
1385
+ // matching in-memory pending interaction (e.g. prompter timeouts).
1386
+ expireOrphanedCanonicalRequests(mapping.conversationId);
1387
+ } catch (err) {
1388
+ log.warn(
1389
+ { err, conversationId: mapping.conversationId },
1390
+ "Post-enqueue auto-deny failed — queued message unaffected",
1391
+ );
1392
+ }
1176
1393
 
1177
1394
  return Response.json(
1178
1395
  { accepted: true, queued: true, conversationId: mapping.conversationId },
@@ -1397,7 +1614,10 @@ export async function handleSendMessage(
1397
1614
  conversationId,
1398
1615
  });
1399
1616
  conversation.processing = false;
1400
- silentlyWithLog(conversation.drainQueue(), "compact-command queue drain");
1617
+ silentlyWithLog(
1618
+ conversation.drainQueue(),
1619
+ "compact-command queue drain",
1620
+ );
1401
1621
  }, 0);
1402
1622
 
1403
1623
  cleanupDeferred = true;
@@ -1713,7 +1933,7 @@ export function conversationRouteDefinitions(deps: {
1713
1933
  "Send a user message to a conversation and trigger the assistant response.",
1714
1934
  tags: ["messages"],
1715
1935
  requestBody: z.object({
1716
- conversationKey: z.string(),
1936
+ conversationKey: z.string().optional(),
1717
1937
  content: z.string().describe("Message text content"),
1718
1938
  attachments: z
1719
1939
  .array(z.unknown())
@@ -47,16 +47,10 @@ function handleUpdateConfig(
47
47
  if (typeof body.enabled === "boolean") heartbeat.enabled = body.enabled;
48
48
  if (typeof body.intervalMs === "number")
49
49
  heartbeat.intervalMs = body.intervalMs;
50
- if ("activeHoursStart" in body) {
51
- heartbeat.activeHoursStart =
52
- typeof body.activeHoursStart === "number"
53
- ? body.activeHoursStart
54
- : undefined;
55
- }
56
- if ("activeHoursEnd" in body) {
57
- heartbeat.activeHoursEnd =
58
- typeof body.activeHoursEnd === "number" ? body.activeHoursEnd : undefined;
59
- }
50
+ if (typeof body.activeHoursStart === "number")
51
+ heartbeat.activeHoursStart = body.activeHoursStart;
52
+ if (typeof body.activeHoursEnd === "number")
53
+ heartbeat.activeHoursEnd = body.activeHoursEnd;
60
54
 
61
55
  try {
62
56
  saveConfig({ ...config, heartbeat });
@@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
10
10
  import { z } from "zod";
11
11
 
12
12
  import { parseIdentityFields } from "../../daemon/handlers/identity.js";
13
+ import { getProfilerRuntimeStatus } from "../../daemon/profiler-run-store.js";
13
14
  import { getMaxMigrationVersion } from "../../memory/migrations/registry.js";
14
15
  import {
15
16
  getWorkspaceDir,
@@ -144,6 +145,13 @@ export function handleHealth(): Response {
144
145
  }
145
146
 
146
147
  export function handleDetailedHealth(): Response {
148
+ let profiler: ReturnType<typeof getProfilerRuntimeStatus> | undefined;
149
+ try {
150
+ profiler = getProfilerRuntimeStatus();
151
+ } catch {
152
+ // Profiler status is non-critical — omit on error
153
+ }
154
+
147
155
  return Response.json({
148
156
  status: "healthy",
149
157
  timestamp: new Date().toISOString(),
@@ -156,6 +164,7 @@ export function handleDetailedHealth(): Response {
156
164
  lastWorkspaceMigrationId:
157
165
  getLastWorkspaceMigrationId(WORKSPACE_MIGRATIONS),
158
166
  },
167
+ ...(profiler ? { profiler } : {}),
159
168
  });
160
169
  }
161
170
 
@@ -239,6 +248,48 @@ export function handleGetIdentityIntro(): Response {
239
248
  return Response.json({ text: cached.text });
240
249
  }
241
250
 
251
+ // ---------------------------------------------------------------------------
252
+ // Zod schemas for profiler health metadata
253
+ // ---------------------------------------------------------------------------
254
+
255
+ const profilerBudgetSchema = z.object({
256
+ maxBytes: z.number(),
257
+ remainingBytes: z.number(),
258
+ minFreeMb: z.number(),
259
+ freeMb: z.number(),
260
+ overBudget: z.boolean(),
261
+ });
262
+
263
+ const profilerLastCompletedRunSchema = z.object({
264
+ runId: z.string(),
265
+ totalBytes: z.number(),
266
+ artifactCount: z.number(),
267
+ hasSummaries: z.boolean(),
268
+ completedAt: z.string(),
269
+ });
270
+
271
+ const profilerStatusSchema = z.object({
272
+ enabled: z.boolean(),
273
+ mode: z.string().nullable(),
274
+ runId: z.string().nullable(),
275
+ runDir: z.string().nullable(),
276
+ totalBytes: z.number(),
277
+ artifactCount: z.number(),
278
+ budget: profilerBudgetSchema.nullable(),
279
+ lastCompletedRun: profilerLastCompletedRunSchema.nullable(),
280
+ });
281
+
282
+ const detailedHealthSchema = z.object({
283
+ status: z.string(),
284
+ timestamp: z.string(),
285
+ version: z.string(),
286
+ disk: z.object({}).passthrough(),
287
+ memory: z.object({}).passthrough(),
288
+ cpu: z.object({}).passthrough(),
289
+ migrations: z.object({}).passthrough(),
290
+ profiler: profilerStatusSchema.optional(),
291
+ });
292
+
242
293
  // ---------------------------------------------------------------------------
243
294
  // Route definitions
244
295
  // ---------------------------------------------------------------------------
@@ -253,15 +304,7 @@ export function identityRouteDefinitions(): RouteDefinition[] {
253
304
  description:
254
305
  "Returns runtime health including version, disk, memory, CPU, and migration status.",
255
306
  tags: ["system"],
256
- responseBody: z.object({
257
- status: z.string(),
258
- timestamp: z.string(),
259
- version: z.string(),
260
- disk: z.object({}).passthrough(),
261
- memory: z.object({}).passthrough(),
262
- cpu: z.object({}).passthrough(),
263
- migrations: z.object({}).passthrough(),
264
- }),
307
+ responseBody: detailedHealthSchema,
265
308
  },
266
309
  {
267
310
  endpoint: "healthz",
@@ -272,15 +315,7 @@ export function identityRouteDefinitions(): RouteDefinition[] {
272
315
  description:
273
316
  "Alias for /v1/health. Returns runtime health including version, disk, memory, CPU, and migration status.",
274
317
  tags: ["system"],
275
- responseBody: z.object({
276
- status: z.string(),
277
- timestamp: z.string(),
278
- version: z.string(),
279
- disk: z.object({}).passthrough(),
280
- memory: z.object({}).passthrough(),
281
- cpu: z.object({}).passthrough(),
282
- migrations: z.object({}).passthrough(),
283
- }),
318
+ responseBody: detailedHealthSchema,
284
319
  },
285
320
  {
286
321
  endpoint: "identity",
@@ -570,16 +570,8 @@ function anthropicMessageSections(
570
570
  const role = asString(message.role) ?? "unknown";
571
571
  const content = message.content;
572
572
  const sections: LlmContextSection[] = [];
573
- const text = collectAnthropicMessageText(content);
574
- if (text) {
575
- sections.push({
576
- kind: "message",
577
- label,
578
- role,
579
- text,
580
- });
581
- }
582
573
 
574
+ // Collect reasoning sections first so they appear before the message text.
583
575
  for (const block of asRecordArray(content) ?? []) {
584
576
  const type = asString(block.type);
585
577
  if (type === "thinking" || type === "redacted_thinking") {
@@ -589,9 +581,21 @@ function anthropicMessageSections(
589
581
  role,
590
582
  text: collectAnthropicReasoningText(block),
591
583
  });
592
- continue;
593
584
  }
585
+ }
594
586
 
587
+ const text = collectAnthropicMessageText(content);
588
+ if (text) {
589
+ sections.push({
590
+ kind: "message",
591
+ label,
592
+ role,
593
+ text,
594
+ });
595
+ }
596
+
597
+ for (const block of asRecordArray(content) ?? []) {
598
+ const type = asString(block.type);
595
599
  if (isAnthropicToolUseType(type)) {
596
600
  sections.push({
597
601
  kind: "tool_use",