@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
@@ -28,3 +28,11 @@ If your user finds proactive check-ins unwanted, they can disable it by setting
28
28
  The default checklist focuses on your user relationship, not generic tasks like weather or news. You can customize it by editing HEARTBEAT.md in your workspace.
29
29
  <!-- /vellum-update-release:heartbeat-default -->
30
30
 
31
+ <!-- vellum-update-release:corrupted-attachment-cleanup -->
32
+ ## Corrupted image attachments cleaned up
33
+
34
+ Some Slack image attachments were stored incorrectly due to a missing OAuth scope — the files contained error pages instead of actual image data. This caused conversations with those images to fail with "The AI provider rejected the request" on every subsequent message.
35
+
36
+ This has been fixed automatically: the corrupted attachments were removed from affected conversations during this update, and the OAuth scope issue has been resolved so new image uploads work correctly. If your user mentions missing images from earlier conversations, this is why — the images were never successfully received in the first place.
37
+ <!-- /vellum-update-release:corrupted-attachment-cleanup -->
38
+
@@ -206,143 +206,6 @@ function hasOrderedToolResultPrefix(
206
206
  * regular content — they are self-paired within the assistant message and must
207
207
  * not be separated by the cross-message pairing logic.
208
208
  */
209
-
210
- /**
211
- * Expand collapsed multi-turn assistant messages. During agentic tool use, the
212
- * daemon stores multiple thinking→tool_use→tool_result cycles in a single
213
- * assistant message. The Anthropic API rejects thinking blocks between
214
- * tool_use blocks ("tool_use without tool_result immediately after") and
215
- * requires thinking blocks in the latest assistant message to remain exactly
216
- * as generated.
217
- *
218
- * This function splits collapsed messages at each "thinking/redacted_thinking
219
- * after tool_use" boundary, recreating the original multi-turn structure.
220
- * It also distributes tool_result blocks from the following user message to
221
- * match each segment's tool_use blocks, creating proper assistant→user pairs.
222
- */
223
- function expandCollapsedAssistantTurns(
224
- messages: Anthropic.MessageParam[],
225
- ): Anthropic.MessageParam[] {
226
- const result: Anthropic.MessageParam[] = [];
227
-
228
- for (let mi = 0; mi < messages.length; mi++) {
229
- const msg = messages[mi];
230
- if (msg.role !== "assistant") {
231
- result.push(msg);
232
- continue;
233
- }
234
-
235
- const content = Array.isArray(msg.content) ? msg.content : [];
236
-
237
- // Check if this message has thinking blocks between tool_use blocks
238
- let hasThinkingAfterToolUse = false;
239
- let seenToolUse = false;
240
- for (const block of content) {
241
- if (isToolUseBlock(block)) {
242
- seenToolUse = true;
243
- } else if (seenToolUse) {
244
- const type = (block as { type: string }).type;
245
- if (type === "thinking" || type === "redacted_thinking") {
246
- hasThinkingAfterToolUse = true;
247
- break;
248
- }
249
- }
250
- }
251
-
252
- if (!hasThinkingAfterToolUse) {
253
- result.push(msg);
254
- continue;
255
- }
256
-
257
- // Split at each "thinking after tool_use" boundary into separate segments
258
- const segments: Anthropic.ContentBlockParam[][] = [];
259
- let current: Anthropic.ContentBlockParam[] = [];
260
- let segmentHasToolUse = false;
261
-
262
- for (const block of content) {
263
- const type = (block as { type: string }).type;
264
- const isThinking = type === "thinking" || type === "redacted_thinking";
265
-
266
- if (isThinking && segmentHasToolUse) {
267
- segments.push(current);
268
- current = [block];
269
- segmentHasToolUse = false;
270
- } else {
271
- current.push(block);
272
- if (isToolUseBlock(block)) {
273
- segmentHasToolUse = true;
274
- }
275
- }
276
- }
277
- if (current.length > 0) {
278
- segments.push(current);
279
- }
280
-
281
- // Build a map of tool_results from the following user message (if any)
282
- const nextMsg = messages[mi + 1];
283
- const nextIsUser = nextMsg && nextMsg.role === "user";
284
- const nextContent =
285
- nextIsUser && Array.isArray(nextMsg.content) ? nextMsg.content : [];
286
- const toolResultMap = new Map<string, Anthropic.ContentBlockParam>();
287
- const nonToolResultContent: Anthropic.ContentBlockParam[] = [];
288
- for (const block of nextContent) {
289
- if (isToolResultBlock(block)) {
290
- toolResultMap.set(block.tool_use_id, block);
291
- } else {
292
- nonToolResultContent.push(block);
293
- }
294
- }
295
-
296
- // Emit each segment as assistant→user pairs, distributing tool_results
297
- for (let si = 0; si < segments.length; si++) {
298
- const segment = segments[si];
299
- const segToolUseIds = getOrderedToolUseIds(segment);
300
- const isLastSegment = si === segments.length - 1;
301
-
302
- result.push({ role: "assistant" as const, content: segment });
303
-
304
- if (segToolUseIds.length > 0 && !isLastSegment) {
305
- // Intermediate segment: pair with matching tool_results
306
- const segResults = segToolUseIds.map(
307
- (id) => toolResultMap.get(id) ?? buildSyntheticToolResult(id),
308
- );
309
- // Remove matched results from the map
310
- for (const id of segToolUseIds) toolResultMap.delete(id);
311
- result.push({ role: "user" as const, content: segResults });
312
- }
313
- }
314
-
315
- // For the last segment, let ensureToolPairing handle pairing with the
316
- // (now reduced) user message. Rebuild the user message without the
317
- // tool_results that were already distributed to intermediate segments.
318
- if (nextIsUser) {
319
- const remainingResults = Array.from(toolResultMap.values());
320
- const rebuiltUserContent = [...remainingResults, ...nonToolResultContent];
321
- // Replace the original user message with the rebuilt one. When all
322
- // tool_results were distributed to intermediate segments (empty rebuilt
323
- // content), skip the synthetic placeholder if the next message is already
324
- // a user turn — ensureToolPairing will pair the last assistant segment
325
- // with that next user message naturally.
326
- if (rebuiltUserContent.length > 0) {
327
- result.push({ role: "user" as const, content: rebuiltUserContent });
328
- } else {
329
- const nextAfterUser = messages[mi + 2];
330
- if (!nextAfterUser || nextAfterUser.role !== "user") {
331
- result.push({
332
- role: "user" as const,
333
- content: [
334
- { type: "text" as const, text: SYNTHETIC_CONTINUATION_TEXT },
335
- ],
336
- });
337
- }
338
- }
339
- mi++; // skip the original user message
340
- }
341
- }
342
-
343
- return result;
344
- }
345
-
346
209
  function splitAssistantForToolPairing(content: Anthropic.ContentBlockParam[]): {
347
210
  pairedContent: Anthropic.ContentBlockParam[];
348
211
  carryoverContent: Anthropic.ContentBlockParam[];
@@ -716,6 +579,7 @@ export class AnthropicProvider implements Provider {
716
579
  options?: SendMessageOptions,
717
580
  ): Promise<ProviderResponse> {
718
581
  const { config, onEvent, signal } = options ?? {};
582
+ const cacheTtl: "5m" | "1h" = ((config as Record<string, unknown> | undefined)?.cacheTtl as "5m" | "1h") ?? "1h";
719
583
  let sentMessages: Anthropic.MessageParam[] | undefined;
720
584
  try {
721
585
  const formatted = messages
@@ -819,58 +683,13 @@ export class AnthropicProvider implements Provider {
819
683
  }
820
684
  }
821
685
 
822
- // Strip thinking/redacted_thinking blocks from historical assistant
823
- // messages. These blocks carry cryptographic signatures tied to their
824
- // original API response. Consolidated messages (from multi-step tool use)
825
- // combine thinking blocks from different responses, making signature
826
- // validation fail with "thinking blocks cannot be modified". Stripping is
827
- // safe: the API allows it for all historical messages, and new responses
828
- // generate fresh thinking blocks.
829
- //
830
- // The latest assistant turn is preserved: the API requires the most recent
831
- // assistant message's thinking blocks to be passed back unmodified when
832
- // sending tool results during in-progress tool-use loops.
833
- let lastAssistantIdx = -1;
834
- for (let i = formatted.length - 1; i >= 0; i--) {
835
- if (formatted[i].role === "assistant") {
836
- lastAssistantIdx = i;
837
- break;
838
- }
839
- }
840
- for (let i = 0; i < formatted.length; i++) {
841
- const msg = formatted[i];
842
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
843
- if (i === lastAssistantIdx) continue;
844
- const stripped = msg.content.filter(
845
- (b) =>
846
- (b as { type: string }).type !== "thinking" &&
847
- (b as { type: string }).type !== "redacted_thinking",
848
- );
849
- if (stripped.length < msg.content.length) {
850
- // Ensure the message isn't empty after stripping
851
- msg.content =
852
- stripped.length > 0
853
- ? stripped
854
- : [
855
- {
856
- type: "text" as const,
857
- text: PLACEHOLDER_BLOCKS_OMITTED,
858
- },
859
- ];
860
- }
861
- }
686
+ // Thinking blocks are stripped at rest by DB migration 209 so
687
+ // historical messages are clean when loaded. Within a turn,
688
+ // assistant messages have original thinking with valid signatures
689
+ // the API accepts them. No provider-side stripping needed.
862
690
 
863
- // Expand collapsed multi-turn assistant messages. When agentic tool use
864
- // produces multiple thinking→tool_use cycles in a single stored message,
865
- // the API rejects thinking blocks between tool_use blocks. Split such
866
- // messages at each "thinking after tool_use" boundary to recreate the
867
- // original multi-turn structure. With thinking blocks stripped above, the
868
- // expansion is typically a no-op, but is kept as a safety net for edge
869
- // cases where stripping is incomplete.
870
- const expanded = expandCollapsedAssistantTurns(formatted);
871
-
872
- sentMessages = ensureToolPairing(repairOrphanedServerToolUse(expanded));
873
- const { effort, speed, output_config, ...restConfig } = (config ??
691
+ sentMessages = ensureToolPairing(repairOrphanedServerToolUse(formatted));
692
+ const { effort, speed, output_config, cacheTtl: _cacheTtl, ...restConfig } = (config ??
874
693
  {}) as Record<string, unknown> & {
875
694
  effort?: Anthropic.OutputConfig["effort"];
876
695
  speed?: "standard" | "fast";
@@ -914,12 +733,12 @@ export class AnthropicProvider implements Provider {
914
733
  {
915
734
  type: "text" as const,
916
735
  text: staticBlock,
917
- cache_control: { type: "ephemeral" as const, ttl: "1h" as const },
736
+ cache_control: { type: "ephemeral" as const, ttl: cacheTtl },
918
737
  },
919
738
  {
920
739
  type: "text" as const,
921
740
  text: dynamicBlock,
922
- cache_control: { type: "ephemeral" as const, ttl: "1h" as const },
741
+ cache_control: { type: "ephemeral" as const, ttl: cacheTtl },
923
742
  },
924
743
  ];
925
744
  } else {
@@ -927,7 +746,7 @@ export class AnthropicProvider implements Provider {
927
746
  {
928
747
  type: "text" as const,
929
748
  text: systemPrompt,
930
- cache_control: { type: "ephemeral" as const, ttl: "1h" as const },
749
+ cache_control: { type: "ephemeral" as const, ttl: cacheTtl },
931
750
  },
932
751
  ];
933
752
  }
@@ -944,7 +763,12 @@ export class AnthropicProvider implements Provider {
944
763
  description: t.description,
945
764
  input_schema: t.input_schema as Anthropic.Tool["input_schema"],
946
765
  ...(i === otherTools.length - 1
947
- ? { cache_control: { type: "ephemeral" as const, ttl: "1h" as const } }
766
+ ? {
767
+ cache_control: {
768
+ type: "ephemeral" as const,
769
+ ttl: cacheTtl,
770
+ },
771
+ }
948
772
  : {}),
949
773
  }));
950
774
  const webSearchTool: Anthropic.WebSearchTool20250305 = {
@@ -959,41 +783,98 @@ export class AnthropicProvider implements Provider {
959
783
  description: t.description,
960
784
  input_schema: t.input_schema as Anthropic.Tool["input_schema"],
961
785
  ...(i === tools.length - 1
962
- ? { cache_control: { type: "ephemeral" as const, ttl: "1h" as const } }
786
+ ? {
787
+ cache_control: {
788
+ type: "ephemeral" as const,
789
+ ttl: cacheTtl,
790
+ },
791
+ }
963
792
  : {}),
964
793
  }));
965
794
  }
966
795
  }
967
796
 
968
- // Place a cache breakpoint on the second-to-last user turn so the
969
- // conversation prefix is cached between agent-loop iterations.
970
- //
971
- // Why second-to-last, not last? The last user message is always new
972
- // (either the initial message with fresh temporal context, or a tool
973
- // result appended by the agent loop) so its breakpoint never produces
974
- // a cache hit. The second-to-last user turn is stable between
975
- // iterations and caching up to it saves re-processing the full
976
- // conversation prefix.
977
- //
978
- // We use only 1 user-turn breakpoint to stay within the Anthropic
979
- // API limit of 4 cache_control blocks total:
980
- // system-static (1) + system-dynamic (2) + last-tool (3) + user (4)
981
- const userIndices: number[] = [];
982
- for (let i = 0; i < params.messages.length; i++) {
983
- if (params.messages[i].role === "user") userIndices.push(i);
797
+ // Manual cache breakpoint on the turn-starting user message.
798
+ // This is the stable anchor for the current turn — everything up to
799
+ // and including it won't change during tool-use iterations, so a long
800
+ // TTL is appropriate. Walk backwards to find the last user message
801
+ // with a real text block (skipping tool_result-only messages and
802
+ // synthetic continuation placeholders injected by ensureToolPairing).
803
+ let turnStartIdx = -1;
804
+ for (let i = sentMessages.length - 1; i >= 0; i--) {
805
+ const msg = sentMessages[i];
806
+ if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
807
+ const hasText = msg.content.some(
808
+ (b) =>
809
+ typeof b !== "string" &&
810
+ b.type === "text" &&
811
+ b.text !== SYNTHETIC_CONTINUATION_TEXT,
812
+ );
813
+ if (!hasText) continue;
814
+ const lastBlock = msg.content[msg.content.length - 1];
815
+ if (typeof lastBlock !== "string") {
816
+ (lastBlock as unknown as Record<string, unknown>).cache_control = {
817
+ type: "ephemeral",
818
+ ttl: cacheTtl,
819
+ };
820
+ }
821
+ turnStartIdx = i;
822
+ break;
984
823
  }
985
- // slice(-2, -1) gives the second-to-last; empty array if < 2 user turns.
986
- for (const idx of userIndices.slice(-2, -1)) {
987
- const content = params.messages[idx].content;
988
- if (Array.isArray(content) && content.length > 0) {
989
- (
990
- content[content.length - 1] as unknown as {
991
- cache_control?: { type: string; ttl?: string };
824
+
825
+ // Advancing tail: place a short-lived 5m cache breakpoint on the last
826
+ // block of the last message when it falls after the turn-starting user
827
+ // message (i.e. tool-use loop content). This caches the growing tail
828
+ // cheaply without conflicting with the 1h breakpoints above.
829
+ // Skip thinking/redacted_thinking blocks Anthropic doesn't allow
830
+ // cache_control on those types.
831
+ let tailBreakpointApplied = false;
832
+ if (turnStartIdx >= 0 && turnStartIdx < sentMessages.length - 1) {
833
+ const lastMsg = sentMessages[sentMessages.length - 1];
834
+ if (Array.isArray(lastMsg.content) && lastMsg.content.length > 0) {
835
+ const NON_CACHEABLE_TYPES = new Set(["thinking", "redacted_thinking"]);
836
+ let tailBlock: (typeof lastMsg.content)[number] | undefined;
837
+ for (let j = lastMsg.content.length - 1; j >= 0; j--) {
838
+ const block = lastMsg.content[j];
839
+ if (
840
+ typeof block !== "string" &&
841
+ !NON_CACHEABLE_TYPES.has((block as { type: string }).type)
842
+ ) {
843
+ tailBlock = block;
844
+ break;
992
845
  }
993
- ).cache_control = { type: "ephemeral", ttl: "1h" };
846
+ }
847
+ if (tailBlock && typeof tailBlock !== "string") {
848
+ (tailBlock as unknown as Record<string, unknown>).cache_control = {
849
+ type: "ephemeral",
850
+ ttl: "5m",
851
+ };
852
+ tailBreakpointApplied = true;
853
+ }
994
854
  }
995
855
  }
996
856
 
857
+ // Enforce Anthropic API maximum of 4 cache_control blocks.
858
+ // When the system prompt boundary splits into 2 cached blocks AND
859
+ // tools + turn-start + advancing-tail breakpoints are all present,
860
+ // we'd have 5. Drop the static system block's breakpoint — it's
861
+ // small (<1K tokens) so the re-read cost is negligible, while the
862
+ // dynamic block (workspace context) rarely changes mid-session and
863
+ // benefits more from caching.
864
+ const hasTailBreakpoint = tailBreakpointApplied;
865
+ const hasToolCacheBreakpoint =
866
+ params.tools?.some(
867
+ (t) => "cache_control" in t && t.cache_control != null,
868
+ ) ?? false;
869
+ if (
870
+ hasTailBreakpoint &&
871
+ Array.isArray(params.system) &&
872
+ params.system.length === 2 &&
873
+ hasToolCacheBreakpoint
874
+ ) {
875
+ delete (params.system[0] as unknown as Record<string, unknown>).cache_control;
876
+ }
877
+
997
878
  const { signal: timeoutSignal, cleanup: cleanupTimeout } =
998
879
  createStreamTimeout(this.streamTimeoutMs, signal);
999
880
 
@@ -1010,8 +891,7 @@ export class AnthropicProvider implements Provider {
1010
891
  }
1011
892
 
1012
893
  // Fast mode: use the beta endpoint with speed: "fast" for Opus 4.6
1013
- const useFastMode =
1014
- speed === "fast" && effectiveModel.includes("opus");
894
+ const useFastMode = speed === "fast" && effectiveModel.includes("opus");
1015
895
 
1016
896
  // Collect required betas: extended cache TTL for 1h system prompt caching,
1017
897
  // 1M context window, and fast-mode when applicable.
@@ -1198,6 +1078,14 @@ export class AnthropicProvider implements Provider {
1198
1078
  "Anthropic 400: tool_use/tool_result pairing error — dumping message structure",
1199
1079
  );
1200
1080
  }
1081
+ log.error(
1082
+ {
1083
+ status: error.status,
1084
+ message: error.message,
1085
+ headers: Object.fromEntries(error.headers?.entries() ?? []),
1086
+ },
1087
+ `Anthropic API error (${error.status})`,
1088
+ );
1201
1089
  const retryAfterMs = extractRetryAfterMs(error.headers);
1202
1090
  throw new ProviderError(
1203
1091
  `Anthropic API error (${error.status}): ${error.message}`,
@@ -130,6 +130,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
130
130
  { endpoint: "conversations", scopes: ["chat.read"] },
131
131
  { endpoint: "conversations:POST", scopes: ["chat.write"] },
132
132
  { endpoint: "conversations/fork", scopes: ["chat.write"] },
133
+ { endpoint: "conversations/analyze", scopes: ["chat.write"] },
133
134
  { endpoint: "conversations/switch", scopes: ["chat.write"] },
134
135
  { endpoint: "conversations/name", scopes: ["chat.write"] },
135
136
  { endpoint: "conversations/cancel", scopes: ["chat.write"] },
@@ -374,6 +375,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
374
375
  // Message content
375
376
  { endpoint: "messages/content", scopes: ["chat.read"] },
376
377
  { endpoint: "messages/llm-context", scopes: ["chat.read"] },
378
+ { endpoint: "llm-request-logs/payload", scopes: ["chat.read"] },
377
379
  { endpoint: "messages/tts", scopes: ["chat.read"] },
378
380
 
379
381
  // Queued message deletion
@@ -479,6 +481,10 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
479
481
  // Tools
480
482
  { endpoint: "tools", scopes: ["settings.read"] },
481
483
  { endpoint: "tools/simulate-permission", scopes: ["settings.read"] },
484
+
485
+ // Permission mode
486
+ { endpoint: "permission-mode:GET", scopes: ["settings.read"] },
487
+ { endpoint: "permission-mode", scopes: ["settings.write"] },
482
488
  ];
483
489
 
484
490
  for (const { endpoint, scopes } of ACTOR_ENDPOINTS) {
@@ -531,3 +537,20 @@ registerPolicy("admin/rollback-migrations", {
531
537
  requiredScopes: ["internal.write"],
532
538
  allowedPrincipalTypes: ["svc_gateway"],
533
539
  });
540
+
541
+ // Profiler management: gateway-only control-plane endpoints
542
+ registerPolicy("profiler/runs", {
543
+ requiredScopes: ["internal.write"],
544
+ allowedPrincipalTypes: ["svc_gateway"],
545
+ });
546
+
547
+ registerPolicy("profiler/runs/export", {
548
+ requiredScopes: ["internal.write"],
549
+ allowedPrincipalTypes: ["svc_gateway"],
550
+ });
551
+
552
+ // User-defined routes under /x/*
553
+ registerPolicy("x", {
554
+ requiredScopes: ["settings.read"],
555
+ allowedPrincipalTypes: ["actor", "svc_gateway", "svc_daemon", "local"],
556
+ });
@@ -52,6 +52,7 @@ import { resolveConversationId } from "../memory/conversation-key-store.js";
52
52
  import {
53
53
  countConversations,
54
54
  listConversations,
55
+ listPinnedConversations,
55
56
  } from "../memory/conversation-queries.js";
56
57
  import type { ExternalConversationBinding } from "../memory/external-conversation-store.js";
57
58
  import * as externalConversationStore from "../memory/external-conversation-store.js";
@@ -126,6 +127,7 @@ import {
126
127
  contactCatchAllRouteDefinitions,
127
128
  contactRouteDefinitions,
128
129
  } from "./routes/contact-routes.js";
130
+ import { conversationAnalysisRouteDefinitions } from "./routes/conversation-analysis-routes.js";
129
131
  import { conversationAttentionRouteDefinitions } from "./routes/conversation-attention-routes.js";
130
132
  import {
131
133
  type ConversationManagementDeps,
@@ -171,6 +173,7 @@ import {
171
173
  handlePairingStatus,
172
174
  pairingRouteDefinitions,
173
175
  } from "./routes/pairing-routes.js";
176
+ import { profilerRouteDefinitions } from "./routes/profiler-routes.js";
174
177
  import { recordingRouteDefinitions } from "./routes/recording-routes.js";
175
178
  import { scheduleRouteDefinitions } from "./routes/schedule-routes.js";
176
179
  import { secretRouteDefinitions } from "./routes/secret-routes.js";
@@ -185,6 +188,7 @@ import { trustRulesRouteDefinitions } from "./routes/trust-rules-routes.js";
185
188
  import { ttsRouteDefinitions } from "./routes/tts-routes.js";
186
189
  import { upgradeBroadcastRouteDefinitions } from "./routes/upgrade-broadcast-routes.js";
187
190
  import { usageRouteDefinitions } from "./routes/usage-routes.js";
191
+ import { userRouteDefinitions } from "./routes/user-routes.js";
188
192
  import { watchRouteDefinitions } from "./routes/watch-routes.js";
189
193
  import { workItemRouteDefinitions } from "./routes/work-items-routes.js";
190
194
  import { workspaceCommitRouteDefinitions } from "./routes/workspace-commit-routes.js";
@@ -826,6 +830,7 @@ export class RuntimeHttpServer {
826
830
  title: conversation.title ?? "Untitled",
827
831
  createdAt: conversation.createdAt,
828
832
  updatedAt: conversation.updatedAt,
833
+ lastMessageAt: conversation.lastMessageAt,
829
834
  conversationType: conversation.conversationType ?? "standard",
830
835
  source: conversation.source ?? "user",
831
836
  ...(conversation.scheduleJobId
@@ -961,6 +966,7 @@ export class RuntimeHttpServer {
961
966
  ...notificationRouteDefinitions(),
962
967
  ...diagnosticsRouteDefinitions(),
963
968
  ...logExportRouteDefinitions(),
969
+ ...profilerRouteDefinitions(),
964
970
  ...documentRouteDefinitions(),
965
971
  ...workItemRouteDefinitions(
966
972
  this.sendMessageDeps
@@ -1025,8 +1031,18 @@ export class RuntimeHttpServer {
1025
1031
  const offset = Number(url.searchParams.get("offset") ?? 0);
1026
1032
  const backgroundOnly =
1027
1033
  url.searchParams.get("conversationType") === "background";
1028
- const rows = listConversations(limit, backgroundOnly, offset);
1034
+ let rows = listConversations(limit, backgroundOnly, offset);
1029
1035
  const totalCount = countConversations(backgroundOnly);
1036
+ // On the first page, ensure all pinned conversations are included
1037
+ // even if they fall outside the paginated window.
1038
+ if (offset === 0 && !backgroundOnly) {
1039
+ const pinned = listPinnedConversations();
1040
+ const seen = new Set(rows.map((c) => c.id));
1041
+ const missing = pinned.filter((c) => !seen.has(c.id));
1042
+ if (missing.length > 0) {
1043
+ rows = [...rows, ...missing];
1044
+ }
1045
+ }
1030
1046
  const conversationIds = rows.map((c) => c.id);
1031
1047
  const displayMeta = getDisplayMetaForConversations(conversationIds);
1032
1048
  const bindings =
@@ -1036,6 +1052,7 @@ export class RuntimeHttpServer {
1036
1052
  const attentionStates =
1037
1053
  getAttentionStateByConversationIds(conversationIds);
1038
1054
  const parentCache = new Map<string, ConversationRow | null>();
1055
+ const nextOffset = offset + limit;
1039
1056
  const response: Record<string, unknown> = {
1040
1057
  conversations: rows.map((conversation) =>
1041
1058
  this.serializeConversationSummary({
@@ -1046,7 +1063,8 @@ export class RuntimeHttpServer {
1046
1063
  parentCache,
1047
1064
  }),
1048
1065
  ),
1049
- hasMore: offset + rows.length < totalCount,
1066
+ nextOffset,
1067
+ hasMore: nextOffset < totalCount,
1050
1068
  };
1051
1069
  // Include groups array on first page only
1052
1070
  if (offset === 0) {
@@ -1067,6 +1085,14 @@ export class RuntimeHttpServer {
1067
1085
  ? conversationManagementRouteDefinitions(conversationManagementDeps)
1068
1086
  : []),
1069
1087
 
1088
+ ...(this.sendMessageDeps
1089
+ ? conversationAnalysisRouteDefinitions({
1090
+ sendMessageDeps: this.sendMessageDeps,
1091
+ buildConversationDetailResponse: (id) =>
1092
+ this.buildConversationDetailResponse(id),
1093
+ })
1094
+ : []),
1095
+
1070
1096
  ...groupRouteDefinitions(),
1071
1097
 
1072
1098
  {
@@ -1305,6 +1331,10 @@ export class RuntimeHttpServer {
1305
1331
  ...traceEventRouteDefinitions(),
1306
1332
  ...migrationRouteDefinitions(),
1307
1333
 
1334
+ // User-defined routes under /x/* — must be LAST so built-in routes
1335
+ // always take priority.
1336
+ ...userRouteDefinitions(),
1337
+
1308
1338
  // Internal OAuth callback (gateway -> runtime)
1309
1339
  {
1310
1340
  endpoint: "internal/oauth/callback",
@@ -5,6 +5,7 @@ import type { ChannelId, InterfaceId } from "../channels/types.js";
5
5
  import type { CesClient } from "../credential-execution/client.js";
6
6
  import type { Conversation } from "../daemon/conversation.js";
7
7
  import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
8
+ import type { ConversationCreateOptions } from "../daemon/handlers/shared.js";
8
9
  import type { SkillOperationContext } from "../daemon/handlers/skills.js";
9
10
  import type { ServerMessage } from "../daemon/message-protocol.js";
10
11
  import type {
@@ -150,7 +151,10 @@ export type MessageProcessor = (
150
151
  * Hub publishing wires outbound events to the SSE stream.
151
152
  */
152
153
  export interface SendMessageDeps {
153
- getOrCreateConversation: (conversationId: string) => Promise<Conversation>;
154
+ getOrCreateConversation: (
155
+ conversationId: string,
156
+ options?: ConversationCreateOptions,
157
+ ) => Promise<Conversation>;
154
158
  assistantEventHub: AssistantEventHub;
155
159
  resolveAttachments: (attachmentIds: string[]) => Array<{
156
160
  id: string;
@@ -274,4 +278,11 @@ export interface RuntimeMessagePayload {
274
278
  textSegments?: string[];
275
279
  thinkingSegments?: string[];
276
280
  contentOrder?: string[];
281
+ subagentNotification?: {
282
+ subagentId: string;
283
+ label: string;
284
+ status: string;
285
+ error?: string;
286
+ conversationId?: string;
287
+ };
277
288
  }