@vellumai/assistant 0.8.1 → 0.8.3

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 (630) hide show
  1. package/ARCHITECTURE.md +13 -19
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +17 -0
  5. package/docker-init-apt-root.sh +167 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +642 -5
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  21. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  23. package/src/__tests__/anthropic-provider.test.ts +45 -0
  24. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  25. package/src/__tests__/app-executors.test.ts +220 -4
  26. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  27. package/src/__tests__/bundled-asset.test.ts +6 -6
  28. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  29. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  30. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  31. package/src/__tests__/clawhub.test.ts +75 -16
  32. package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
  33. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  34. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  35. package/src/__tests__/config-schema.test.ts +21 -0
  36. package/src/__tests__/config-set-route.test.ts +80 -0
  37. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  38. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  39. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  40. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  41. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  42. package/src/__tests__/context-token-estimator.test.ts +31 -65
  43. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  44. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  45. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  46. package/src/__tests__/conversation-agent-loop.test.ts +59 -1
  47. package/src/__tests__/conversation-error.test.ts +42 -3
  48. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  49. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  50. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  51. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  52. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  53. package/src/__tests__/conversation-pairing.test.ts +54 -0
  54. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  55. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  56. package/src/__tests__/conversation-queue.test.ts +4 -1
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
  58. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  59. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  60. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  61. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  64. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  65. package/src/__tests__/date-context.test.ts +45 -0
  66. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  67. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  68. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  69. package/src/__tests__/dm-backfill.test.ts +121 -10
  70. package/src/__tests__/document-tool-security.test.ts +258 -0
  71. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  72. package/src/__tests__/edit-propagation.test.ts +33 -0
  73. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  74. package/src/__tests__/external-plugin-loader.test.ts +151 -55
  75. package/src/__tests__/filing-service.test.ts +140 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  77. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  78. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  79. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  80. package/src/__tests__/heartbeat-service.test.ts +24 -164
  81. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  82. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  83. package/src/__tests__/helpers/wait-for.ts +21 -0
  84. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  85. package/src/__tests__/history-repair.test.ts +73 -0
  86. package/src/__tests__/host-app-control-proxy.test.ts +507 -10
  87. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  88. package/src/__tests__/image-credentials.test.ts +1 -1
  89. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  90. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  91. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  92. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  93. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  94. package/src/__tests__/injector-background-turn.test.ts +153 -0
  95. package/src/__tests__/injector-chain.test.ts +15 -8
  96. package/src/__tests__/install-skill-routing.test.ts +155 -37
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
  98. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  99. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  100. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  101. package/src/__tests__/llm-catalog-parity.test.ts +58 -13
  102. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  103. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  104. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
  105. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  106. package/src/__tests__/llm-resolver.test.ts +255 -2
  107. package/src/__tests__/llm-usage-store.test.ts +114 -0
  108. package/src/__tests__/managed-profile-guard.test.ts +41 -29
  109. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  110. package/src/__tests__/managed-store.test.ts +84 -192
  111. package/src/__tests__/media-generate-image.test.ts +1 -1
  112. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  113. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  114. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  115. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  116. package/src/__tests__/notification-deep-link.test.ts +15 -0
  117. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  118. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  119. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  120. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  121. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  122. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  123. package/src/__tests__/openai-provider.test.ts +242 -3
  124. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  125. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  126. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  127. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  128. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  129. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
  130. package/src/__tests__/platform.test.ts +2 -0
  131. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  132. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  133. package/src/__tests__/plugin-external-api.test.ts +68 -0
  134. package/src/__tests__/plugin-registry.test.ts +0 -77
  135. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  136. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  137. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  138. package/src/__tests__/plugin-types.test.ts +3 -13
  139. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  140. package/src/__tests__/process-message-display-content.test.ts +421 -0
  141. package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  143. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
  144. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  145. package/src/__tests__/schedule-routes.test.ts +50 -3
  146. package/src/__tests__/schedule-store.test.ts +94 -0
  147. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  148. package/src/__tests__/schema-transforms.test.ts +20 -0
  149. package/src/__tests__/search-skills-unified.test.ts +0 -5
  150. package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
  151. package/src/__tests__/server-history-render.test.ts +43 -0
  152. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  153. package/src/__tests__/skill-load-tool.test.ts +27 -89
  154. package/src/__tests__/skill-memory.test.ts +23 -3
  155. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  156. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  157. package/src/__tests__/skills-install-extract.test.ts +49 -38
  158. package/src/__tests__/skills-install-staging.test.ts +159 -0
  159. package/src/__tests__/skills-uninstall.test.ts +9 -41
  160. package/src/__tests__/skills.test.ts +51 -58
  161. package/src/__tests__/slack-channel-config.test.ts +9 -0
  162. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  163. package/src/__tests__/system-prompt.test.ts +670 -63
  164. package/src/__tests__/terminal-tools.test.ts +28 -1
  165. package/src/__tests__/thread-backfill.test.ts +557 -27
  166. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  167. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  168. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  169. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  170. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  171. package/src/__tests__/tool-executor.test.ts +16 -4
  172. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  173. package/src/__tests__/turn-events-store.test.ts +256 -0
  174. package/src/__tests__/twilio-routes.test.ts +4 -0
  175. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  176. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  177. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  178. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  179. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  180. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  181. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  182. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  183. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  184. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  185. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  186. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  187. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  188. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  189. package/src/a2a/__tests__/task-store.test.ts +246 -0
  190. package/src/a2a/agent-card.ts +58 -0
  191. package/src/a2a/feature-gate.ts +8 -0
  192. package/src/a2a/protocol-constants.ts +21 -0
  193. package/src/a2a/protocol-errors.ts +50 -0
  194. package/src/a2a/protocol-types.ts +162 -0
  195. package/src/a2a/task-store.ts +168 -0
  196. package/src/acp/resolve-agent.ts +1 -1
  197. package/src/agent/image-optimize.ts +13 -5
  198. package/src/agent/loop.ts +167 -18
  199. package/src/calls/voice-session-bridge.ts +61 -42
  200. package/src/channels/config.ts +9 -0
  201. package/src/channels/types.ts +122 -0
  202. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  203. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  204. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  205. package/src/cli/commands/__tests__/schedules.test.ts +960 -0
  206. package/src/cli/commands/changelog.ts +106 -42
  207. package/src/cli/commands/conversations.ts +102 -17
  208. package/src/cli/commands/default-action.ts +10 -53
  209. package/src/cli/commands/notifications.ts +388 -346
  210. package/src/cli/commands/plugins.ts +252 -0
  211. package/src/cli/commands/schedules.ts +683 -0
  212. package/src/cli/commands/telemetry.ts +40 -0
  213. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  214. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  215. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  216. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  217. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  218. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  219. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  220. package/src/cli/lib/cli-colors.ts +12 -0
  221. package/src/cli/lib/confirm-prompt.ts +79 -0
  222. package/src/cli/lib/install-from-github.ts +303 -0
  223. package/src/cli/lib/list-installed-plugins.ts +137 -0
  224. package/src/cli/lib/search-plugins.ts +163 -0
  225. package/src/cli/lib/uninstall-plugin.ts +82 -0
  226. package/src/cli/lib/unknown-command.ts +111 -0
  227. package/src/cli/program.ts +52 -2
  228. package/src/config/assistant-feature-flags.ts +24 -54
  229. package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
  230. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  231. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  232. package/src/config/bundled-skills/document/SKILL.md +23 -3
  233. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  234. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  235. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  236. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  237. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  238. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  239. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  240. package/src/config/bundled-tool-registry.ts +6 -0
  241. package/src/config/call-site-defaults.ts +105 -0
  242. package/src/config/feature-flag-registry.json +41 -9
  243. package/src/config/llm-resolver.ts +52 -1
  244. package/src/config/loader.ts +64 -38
  245. package/src/config/schema.ts +9 -10
  246. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  247. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  248. package/src/config/schemas/channels.ts +17 -0
  249. package/src/config/schemas/compaction.ts +28 -0
  250. package/src/config/schemas/conversations.ts +10 -0
  251. package/src/config/schemas/heartbeat.ts +23 -0
  252. package/src/config/schemas/llm-request-logs.ts +31 -7
  253. package/src/config/schemas/llm.ts +1 -0
  254. package/src/config/schemas/memory-retrieval.ts +18 -0
  255. package/src/config/schemas/memory-retrospective.ts +1 -1
  256. package/src/config/schemas/memory-v2.ts +4 -4
  257. package/src/config/schemas/memory.ts +3 -1
  258. package/src/config/schemas/tools.ts +14 -0
  259. package/src/config/seed-inference-profiles.ts +99 -29
  260. package/src/config/skills.ts +3 -96
  261. package/src/context/compactor.ts +1107 -0
  262. package/src/context/token-estimator.ts +34 -36
  263. package/src/context/window-manager.ts +197 -1520
  264. package/src/credential-execution/managed-catalog.ts +37 -0
  265. package/src/credential-health/credential-health-service.ts +280 -19
  266. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
  267. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  268. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  269. package/src/daemon/approval-generators.ts +8 -6
  270. package/src/daemon/config-watcher.ts +94 -31
  271. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  272. package/src/daemon/conversation-agent-loop.ts +198 -11
  273. package/src/daemon/conversation-error.ts +171 -37
  274. package/src/daemon/conversation-lifecycle.ts +53 -40
  275. package/src/daemon/conversation-messaging.ts +25 -6
  276. package/src/daemon/conversation-process.ts +49 -12
  277. package/src/daemon/conversation-runtime-assembly.ts +25 -1
  278. package/src/daemon/conversation-slash.ts +12 -5
  279. package/src/daemon/conversation-store.ts +11 -4
  280. package/src/daemon/conversation-tool-setup.ts +39 -7
  281. package/src/daemon/conversation.ts +33 -8
  282. package/src/daemon/date-context.ts +40 -0
  283. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  284. package/src/daemon/first-greeting.ts +22 -2
  285. package/src/daemon/guardian-action-generators.ts +1 -125
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  287. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  288. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  289. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  290. package/src/daemon/handlers/config-a2a.ts +289 -0
  291. package/src/daemon/handlers/config-model.ts +6 -5
  292. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  293. package/src/daemon/handlers/conversations.ts +1 -0
  294. package/src/daemon/handlers/shared.ts +14 -5
  295. package/src/daemon/handlers/skills.ts +111 -108
  296. package/src/daemon/history-repair.ts +28 -1
  297. package/src/daemon/host-app-control-proxy.ts +153 -27
  298. package/src/daemon/host-proxy-preactivation.ts +85 -18
  299. package/src/daemon/lifecycle.ts +89 -91
  300. package/src/daemon/meet-host-supervisor.ts +5 -4
  301. package/src/daemon/memory-v2-startup.ts +85 -0
  302. package/src/daemon/message-protocol.ts +1 -0
  303. package/src/daemon/message-types/conversations.ts +25 -0
  304. package/src/daemon/message-types/messages.ts +61 -0
  305. package/src/daemon/message-types/notifications.ts +21 -0
  306. package/src/daemon/message-types/subagents.ts +1 -0
  307. package/src/daemon/message-types/sync.ts +1 -0
  308. package/src/daemon/pkb-reminder-builder.test.ts +11 -54
  309. package/src/daemon/pkb-reminder-builder.ts +5 -20
  310. package/src/daemon/plugin-source-watcher.ts +146 -0
  311. package/src/daemon/process-message.ts +24 -3
  312. package/src/daemon/server.ts +11 -2
  313. package/src/daemon/skill-memory-refresh.ts +33 -0
  314. package/src/daemon/wake-target-adapter.ts +2 -0
  315. package/src/documents/document-store.ts +221 -3
  316. package/src/embedded/plugin-api.ts +40 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  318. package/src/export/transcript-formatter.ts +54 -20
  319. package/src/filing/filing-service.ts +39 -0
  320. package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
  321. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  322. package/src/heartbeat/heartbeat-service.ts +73 -189
  323. package/src/home/__tests__/feed-types.test.ts +80 -0
  324. package/src/home/feed-types.ts +36 -2
  325. package/src/home/post-connect-feed.ts +1 -0
  326. package/src/index.ts +18 -1
  327. package/src/ipc/cli-client.ts +147 -45
  328. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  329. package/src/mcp/client.ts +20 -4
  330. package/src/media/image-credentials.ts +3 -3
  331. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  332. package/src/memory/__tests__/conversation-queries.test.ts +483 -0
  333. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  334. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  335. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  336. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  337. package/src/memory/__tests__/message-content.test.ts +35 -0
  338. package/src/memory/bookmark-crud.ts +42 -10
  339. package/src/memory/context-search/sources/conversations.ts +62 -2
  340. package/src/memory/context-search/sources/workspace.ts +4 -0
  341. package/src/memory/conversation-crud.ts +63 -19
  342. package/src/memory/conversation-queries.ts +197 -11
  343. package/src/memory/conversation-title-service.ts +26 -4
  344. package/src/memory/db-init.ts +12 -0
  345. package/src/memory/delivery-crud.ts +152 -5
  346. package/src/memory/embedding-backend.ts +4 -4
  347. package/src/memory/external-conversation-store.ts +66 -5
  348. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
  349. package/src/memory/graph/conversation-graph-memory.ts +49 -21
  350. package/src/memory/graph/tools.ts +9 -40
  351. package/src/memory/indexer.ts +34 -29
  352. package/src/memory/invite-store.ts +53 -0
  353. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  354. package/src/memory/jobs/embed-concept-page.ts +20 -11
  355. package/src/memory/jobs-worker.ts +6 -1
  356. package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
  357. package/src/memory/llm-request-log-source.ts +19 -52
  358. package/src/memory/llm-request-log-store.ts +92 -1
  359. package/src/memory/llm-usage-store.ts +125 -5
  360. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  361. package/src/memory/memory-retrospective-job.ts +33 -6
  362. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  363. package/src/memory/message-content.ts +1 -1
  364. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  365. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  366. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  367. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  368. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  369. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  370. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  371. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  372. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  373. package/src/memory/migrations/index.ts +9 -0
  374. package/src/memory/migrations/registry.ts +16 -0
  375. package/src/memory/onboarding-events-store.ts +106 -0
  376. package/src/memory/schema/a2a.ts +15 -0
  377. package/src/memory/schema/bookmarks.ts +0 -2
  378. package/src/memory/schema/calls.ts +1 -0
  379. package/src/memory/schema/index.ts +1 -0
  380. package/src/memory/schema/inference.ts +3 -3
  381. package/src/memory/schema/infrastructure.ts +13 -0
  382. package/src/memory/turn-events-store.ts +127 -2
  383. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  384. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  385. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  386. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  387. package/src/memory/v2/__tests__/injection.test.ts +288 -11
  388. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  389. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  390. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  391. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  392. package/src/memory/v2/__tests__/router.test.ts +15 -0
  393. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  394. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  395. package/src/memory/v2/activation-store.ts +14 -16
  396. package/src/memory/v2/cli-command-content.ts +19 -0
  397. package/src/memory/v2/cli-command-store.ts +304 -0
  398. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  399. package/src/memory/v2/injection.ts +81 -26
  400. package/src/memory/v2/migration.ts +49 -19
  401. package/src/memory/v2/page-index.ts +63 -8
  402. package/src/memory/v2/prompts/router.ts +11 -8
  403. package/src/memory/v2/prompts/sweep.ts +2 -2
  404. package/src/memory/v2/qdrant.ts +135 -7
  405. package/src/memory/v2/router.ts +9 -8
  406. package/src/memory/v2/skill-store.ts +120 -35
  407. package/src/memory/v2/static-context.ts +4 -4
  408. package/src/memory/v2/types.ts +23 -0
  409. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  410. package/src/messaging/providers/a2a/deliver.ts +156 -0
  411. package/src/messaging/providers/gmail/client.ts +9 -2
  412. package/src/messaging/providers/index.ts +11 -2
  413. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  414. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  415. package/src/messaging/providers/slack/adapter.ts +43 -5
  416. package/src/messaging/providers/slack/client.ts +27 -0
  417. package/src/messaging/providers/slack/deep-link.ts +65 -0
  418. package/src/messaging/providers/slack/download.ts +104 -0
  419. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  420. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  421. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  422. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  423. package/src/messaging/providers/slack/types.ts +20 -1
  424. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  425. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  426. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  427. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  428. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  429. package/src/notifications/adapters/macos.ts +12 -2
  430. package/src/notifications/broadcaster.ts +29 -4
  431. package/src/notifications/conversation-pairing.ts +2 -1
  432. package/src/notifications/copy-composer.ts +17 -64
  433. package/src/notifications/decision-engine.ts +113 -45
  434. package/src/notifications/deterministic-checks.ts +96 -0
  435. package/src/notifications/emit-signal.ts +21 -1
  436. package/src/notifications/home-feed-side-effect.ts +138 -5
  437. package/src/notifications/signal.ts +3 -5
  438. package/src/notifications/types.ts +8 -0
  439. package/src/oauth/connection-resolver.ts +8 -4
  440. package/src/oauth/platform-connection.test.ts +43 -3
  441. package/src/oauth/platform-connection.ts +19 -6
  442. package/src/oauth/seed-providers.ts +10 -1
  443. package/src/permissions/checker.ts +2 -0
  444. package/src/permissions/ipc-risk-types.ts +1 -0
  445. package/src/permissions/question-prompter.test.ts +416 -0
  446. package/src/permissions/question-prompter.ts +294 -0
  447. package/src/platform/client.test.ts +1 -1
  448. package/src/platform/client.ts +1 -1
  449. package/src/plugin-api/constants.ts +26 -0
  450. package/src/plugin-api/index.ts +34 -1
  451. package/src/plugin-api/types.ts +104 -22
  452. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  453. package/src/plugins/defaults/compaction.ts +0 -4
  454. package/src/plugins/defaults/empty-response.ts +0 -2
  455. package/src/plugins/defaults/history-repair.ts +0 -2
  456. package/src/plugins/defaults/injectors.ts +74 -22
  457. package/src/plugins/defaults/llm-call.ts +0 -2
  458. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  459. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  460. package/src/plugins/defaults/persistence.ts +0 -2
  461. package/src/plugins/defaults/title-generate.ts +0 -5
  462. package/src/plugins/defaults/token-estimate.ts +0 -2
  463. package/src/plugins/defaults/tool-error.ts +0 -7
  464. package/src/plugins/defaults/tool-execute.ts +0 -2
  465. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  466. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  467. package/src/plugins/external-api.ts +104 -0
  468. package/src/plugins/external-plugin-loader.ts +187 -42
  469. package/src/plugins/feature-gate.ts +22 -0
  470. package/src/plugins/pipeline.ts +37 -0
  471. package/src/plugins/registry.ts +48 -80
  472. package/src/plugins/types.ts +40 -26
  473. package/src/plugins/user-loader.ts +21 -2
  474. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  475. package/src/proactive-artifact/job.test.ts +37 -5
  476. package/src/prompts/__tests__/system-prompt.test.ts +10 -43
  477. package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
  478. package/src/prompts/normalize-onboarding.ts +27 -0
  479. package/src/prompts/sections.ts +302 -0
  480. package/src/prompts/system-prompt.ts +63 -174
  481. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  482. package/src/prompts/templates/system-sections.ts +164 -0
  483. package/src/providers/__tests__/inference.test.ts +24 -7
  484. package/src/providers/anthropic/client.ts +28 -28
  485. package/src/providers/call-site-routing.ts +24 -6
  486. package/src/providers/connection-resolution.ts +68 -11
  487. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  488. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  489. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  490. package/src/providers/inference/adapter-factory.ts +32 -6
  491. package/src/providers/inference/auth.ts +12 -0
  492. package/src/providers/inference/backfill.ts +14 -1
  493. package/src/providers/inference/connections.ts +159 -34
  494. package/src/providers/inference/resolve-auth.ts +14 -4
  495. package/src/providers/model-catalog.ts +249 -12
  496. package/src/providers/model-intents.ts +3 -3
  497. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  498. package/src/providers/openai/chat-completions-provider.ts +169 -8
  499. package/src/providers/openrouter/client.ts +49 -4
  500. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
  501. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  502. package/src/providers/provider-availability.ts +17 -2
  503. package/src/providers/provider-catalog-visibility.ts +38 -0
  504. package/src/providers/provider-send-message.ts +27 -12
  505. package/src/providers/registry.ts +52 -15
  506. package/src/providers/retry.ts +47 -1
  507. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  508. package/src/runtime/agent-wake.ts +103 -15
  509. package/src/runtime/auth/route-policy.ts +21 -1
  510. package/src/runtime/btw-sidechain.ts +2 -0
  511. package/src/runtime/http-server.ts +7 -16
  512. package/src/runtime/http-types.ts +19 -47
  513. package/src/runtime/migrations/origin-mode.ts +1 -1
  514. package/src/runtime/pending-interactions.ts +1 -0
  515. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  516. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  517. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  518. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
  519. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  520. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  521. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  522. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  523. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  524. package/src/runtime/routes/acp-routes.ts +5 -3
  525. package/src/runtime/routes/auth-routes.ts +1 -1
  526. package/src/runtime/routes/bookmark-routes.ts +5 -3
  527. package/src/runtime/routes/btw-routes.ts +5 -1
  528. package/src/runtime/routes/channel-availability-routes.ts +126 -0
  529. package/src/runtime/routes/consolidation-routes.ts +100 -0
  530. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  531. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  532. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  533. package/src/runtime/routes/conversation-query-routes.ts +99 -35
  534. package/src/runtime/routes/conversation-routes.ts +97 -11
  535. package/src/runtime/routes/documents-routes.ts +25 -86
  536. package/src/runtime/routes/group-routes.ts +5 -0
  537. package/src/runtime/routes/inbound-conversation.ts +28 -8
  538. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  539. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  540. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  541. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  542. package/src/runtime/routes/index.ts +8 -0
  543. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  544. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  545. package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
  546. package/src/runtime/routes/integrations/a2a.ts +235 -0
  547. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  548. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  549. package/src/runtime/routes/integrations/twilio.ts +6 -13
  550. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  551. package/src/runtime/routes/notification-routes.ts +1 -1
  552. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  553. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  554. package/src/runtime/routes/question-routes.ts +259 -0
  555. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  556. package/src/runtime/routes/schedule-routes.ts +4 -7
  557. package/src/runtime/routes/subagents-routes.ts +98 -18
  558. package/src/runtime/routes/telemetry-routes.ts +27 -0
  559. package/src/runtime/routes/tts-routes.ts +27 -2
  560. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  561. package/src/runtime/routes/workspace-routes.ts +28 -0
  562. package/src/runtime/services/conversation-serializer.ts +39 -7
  563. package/src/runtime/sync/resource-sync-events.ts +93 -1
  564. package/src/schedule/schedule-store.ts +27 -2
  565. package/src/schedule/scheduler.ts +9 -1
  566. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  567. package/src/security/untrusted-content.ts +93 -8
  568. package/src/skills/catalog-files.ts +1 -1
  569. package/src/skills/catalog-install.ts +233 -116
  570. package/src/skills/clawhub.ts +70 -13
  571. package/src/skills/managed-store.ts +4 -119
  572. package/src/skills/skillssh-registry.ts +27 -48
  573. package/src/subagent/manager.ts +17 -7
  574. package/src/telemetry/types.ts +113 -1
  575. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  576. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  577. package/src/tools/apps/executors.ts +58 -7
  578. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  579. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  580. package/src/tools/browser/browser-execution.ts +15 -11
  581. package/src/tools/computer-use/definitions.ts +3 -3
  582. package/src/tools/credentials/vault.ts +1 -1
  583. package/src/tools/document/document-tool.ts +124 -1
  584. package/src/tools/filesystem/edit.ts +1 -1
  585. package/src/tools/filesystem/list.ts +1 -1
  586. package/src/tools/filesystem/read.ts +1 -1
  587. package/src/tools/filesystem/write.ts +5 -2
  588. package/src/tools/host-filesystem/transfer.ts +1 -1
  589. package/src/tools/host-terminal/host-shell.ts +1 -1
  590. package/src/tools/memory/register.ts +1 -9
  591. package/src/tools/permission-checker.ts +1 -1
  592. package/src/tools/registry.ts +17 -7
  593. package/src/tools/schedule/create.ts +2 -2
  594. package/src/tools/schema-transforms.ts +7 -2
  595. package/src/tools/side-effects.ts +1 -0
  596. package/src/tools/skills/delete-managed.ts +4 -4
  597. package/src/tools/skills/execute.ts +1 -1
  598. package/src/tools/skills/scaffold-managed.ts +3 -2
  599. package/src/tools/subagent/notify-parent.ts +1 -1
  600. package/src/tools/system/request-permission.ts +2 -2
  601. package/src/tools/terminal/safe-env.ts +60 -1
  602. package/src/tools/tool-manifest.ts +2 -0
  603. package/src/tools/types.ts +107 -21
  604. package/src/tools/ui-surface/definitions.ts +6 -5
  605. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  606. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  607. package/src/types/onboarding-context.ts +2 -0
  608. package/src/util/errors.ts +17 -0
  609. package/src/util/platform.ts +10 -0
  610. package/src/watcher/__tests__/engine.test.ts +22 -0
  611. package/src/watcher/engine.ts +6 -2
  612. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  613. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  614. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  615. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  616. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  617. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  618. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  619. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  620. package/src/workspace/migrations/registry.ts +10 -0
  621. package/src/workspace/migrations/runner.ts +39 -9
  622. package/src/workspace/migrations/types.ts +4 -0
  623. package/examples/plugins/echo/bun.lock +0 -25
  624. package/src/__tests__/context-window-manager.test.ts +0 -2481
  625. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  626. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  627. package/src/context/prompts/compact.md +0 -26
  628. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  629. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  630. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -65,10 +65,42 @@ mock.module("../conversation-crud.js", () => ({
65
65
  },
66
66
  }));
67
67
 
68
+ let transcriptFormatterCalls: Array<{
69
+ messageIds: string[];
70
+ timeZone?: string;
71
+ assistantName?: string | null;
72
+ userName?: string | null;
73
+ }> = [];
74
+
68
75
  mock.module("../../export/transcript-formatter.js", () => ({
69
76
  formatMessageSliceForTranscript: (
70
77
  messages: Array<{ id: string; createdAt: number }>,
71
- ) => messages.map((m) => `[msg ${m.id}]`).join("\n"),
78
+ options: {
79
+ timeZone?: string;
80
+ assistantName?: string | null;
81
+ userName?: string | null;
82
+ } = {},
83
+ ) => {
84
+ transcriptFormatterCalls.push({
85
+ messageIds: messages.map((m) => m.id),
86
+ timeZone: options.timeZone,
87
+ assistantName: options.assistantName,
88
+ userName: options.userName,
89
+ });
90
+ return messages.map((m) => `[msg ${m.id}]`).join("\n");
91
+ },
92
+ }));
93
+
94
+ let mockAssistantName: string | null = "Bob";
95
+ let mockUserName: string | null = "Alice";
96
+
97
+ mock.module("../../daemon/identity-helpers.js", () => ({
98
+ getAssistantName: () => mockAssistantName,
99
+ resolveUserName: (_workspaceDir: string) => mockUserName,
100
+ }));
101
+
102
+ mock.module("../../util/platform.js", () => ({
103
+ getWorkspaceDir: () => "/tmp/test-workspace",
72
104
  }));
73
105
 
74
106
  mock.module("../conversation-bootstrap.js", () => ({
@@ -102,9 +134,19 @@ mock.module("../jobs-store.js", () => ({
102
134
  import type { MemoryJob } from "../jobs-store.js";
103
135
  import { memoryRetrospectiveJob } from "../memory-retrospective-job.js";
104
136
 
105
- const stubConfig = {
106
- memory: { v2: { enabled: true } },
107
- } as unknown as Parameters<typeof memoryRetrospectiveJob>[1];
137
+ function makeConfig(
138
+ overrides: { userTimezone?: string; detectedTimezone?: string } = {},
139
+ ): Parameters<typeof memoryRetrospectiveJob>[1] {
140
+ return {
141
+ memory: { v2: { enabled: true } },
142
+ ui: {
143
+ userTimezone: overrides.userTimezone,
144
+ detectedTimezone: overrides.detectedTimezone,
145
+ },
146
+ } as unknown as Parameters<typeof memoryRetrospectiveJob>[1];
147
+ }
148
+
149
+ const stubConfig = makeConfig();
108
150
 
109
151
  function makeJob(conversationId = "src-conv-1"): MemoryJob<{
110
152
  conversationId?: string;
@@ -155,6 +197,9 @@ describe("memoryRetrospectiveJob", () => {
155
197
  bootstrappedConversationId = "bg-conv-new";
156
198
  bootstrapCalls = [];
157
199
  deletedConversationIds = [];
200
+ transcriptFormatterCalls = [];
201
+ mockAssistantName = "Bob";
202
+ mockUserName = "Alice";
158
203
  });
159
204
 
160
205
  test("first-run happy path: no state row, no prior retrospective, both pointer fields set on success", async () => {
@@ -274,6 +319,44 @@ describe("memoryRetrospectiveJob", () => {
274
319
  expect(hint).toContain("- a real save");
275
320
  });
276
321
 
322
+ test("transcript is formatted in the configured user timezone and the prompt discloses it", async () => {
323
+ const config = makeConfig({ userTimezone: "America/Los_Angeles" });
324
+ await memoryRetrospectiveJob(makeJob(), config);
325
+
326
+ expect(transcriptFormatterCalls).toHaveLength(1);
327
+ expect(transcriptFormatterCalls[0]!.timeZone).toBe("America/Los_Angeles");
328
+
329
+ const hint = wakeCalls[0]!.hint;
330
+ expect(hint).toContain("Timestamps are in America/Los_Angeles.");
331
+ });
332
+
333
+ test("detected timezone is used when no manual override is set", async () => {
334
+ const config = makeConfig({ detectedTimezone: "Europe/Berlin" });
335
+ await memoryRetrospectiveJob(makeJob(), config);
336
+
337
+ expect(transcriptFormatterCalls[0]!.timeZone).toBe("Europe/Berlin");
338
+
339
+ const hint = wakeCalls[0]!.hint;
340
+ expect(hint).toContain("Timestamps are in Europe/Berlin.");
341
+ });
342
+
343
+ test("resolved assistant and user display names are passed to the transcript formatter", async () => {
344
+ await memoryRetrospectiveJob(makeJob(), stubConfig);
345
+
346
+ expect(transcriptFormatterCalls).toHaveLength(1);
347
+ expect(transcriptFormatterCalls[0]!.assistantName).toBe("Bob");
348
+ expect(transcriptFormatterCalls[0]!.userName).toBe("Alice");
349
+ });
350
+
351
+ test("formatter receives null names when identity files are missing — formatter handles fallback", async () => {
352
+ mockAssistantName = null;
353
+ mockUserName = null;
354
+ await memoryRetrospectiveJob(makeJob(), stubConfig);
355
+
356
+ expect(transcriptFormatterCalls[0]!.assistantName).toBeNull();
357
+ expect(transcriptFormatterCalls[0]!.userName).toBeNull();
358
+ });
359
+
277
360
  test("non-remember tool_use blocks in the prior retro are ignored", async () => {
278
361
  priorRetroId = "prior-retro-conv-1";
279
362
  priorRetroMessages = [
@@ -15,6 +15,8 @@ type ConvRow = {
15
15
  id: string;
16
16
  source: string | null;
17
17
  last_message_at: number | null;
18
+ fork_parent_conversation_id: string | null;
19
+ created_at: number;
18
20
  };
19
21
  type JobRow = {
20
22
  type: string;
@@ -58,11 +60,26 @@ mock.module("../db-connection.js", () => ({
58
60
  return { conversationId: convId };
59
61
  });
60
62
  }
61
- // Otherwise, this is the conversation query.
62
- // The test harness applies its OWN filter logic below since the
63
- // production code uses drizzle's combinator. We expose all rows
64
- // tagged with the right source/last_message_at, and the test
65
- // post-filters them to mirror the production query.
63
+ // The "all retros" query (used to compute most-recent-per-source
64
+ // preservation) requests id + forkParentConversationId + createdAt
65
+ // with only the source + isNotNull(forkParent) predicate.
66
+ if (
67
+ colKeys.includes("forkParentConversationId") &&
68
+ colKeys.includes("createdAt")
69
+ ) {
70
+ return mockConversations
71
+ .filter((c) => c.source === "memory-retrospective")
72
+ .filter((c) => c.fork_parent_conversation_id !== null)
73
+ .map((c) => ({
74
+ id: c.id,
75
+ forkParentConversationId: c.fork_parent_conversation_id,
76
+ createdAt: c.created_at,
77
+ }));
78
+ }
79
+ // Otherwise, this is the orphan-candidate query. The production
80
+ // predicate compares `forkParentConversationId` (the source ID
81
+ // encoded on the background conversation row) against the set
82
+ // of source IDs extracted from active jobs.
66
83
  return mockConversations
67
84
  .filter((c) => c.source === "memory-retrospective")
68
85
  .filter(
@@ -70,7 +87,11 @@ mock.module("../db-connection.js", () => ({
70
87
  c.last_message_at !== null &&
71
88
  c.last_message_at < injectedNowMinusOrphanAgeMs,
72
89
  )
73
- .filter((c) => !activeJobConvIds.has(c.id))
90
+ .filter(
91
+ (c) =>
92
+ c.fork_parent_conversation_id === null ||
93
+ !activeJobSourceConvIds.has(c.fork_parent_conversation_id),
94
+ )
74
95
  .map((c) => ({ id: c.id }));
75
96
  },
76
97
  }),
@@ -79,7 +100,7 @@ mock.module("../db-connection.js", () => ({
79
100
  }),
80
101
  }));
81
102
 
82
- let activeJobConvIds = new Set<string>();
103
+ let activeJobSourceConvIds = new Set<string>();
83
104
  let injectedNowMinusOrphanAgeMs = 0;
84
105
 
85
106
  mock.module("../conversation-crud.js", () => ({
@@ -94,7 +115,7 @@ import { sweepOrphanMemoryRetrospectiveConversations } from "../memory-retrospec
94
115
  const ORPHAN_AGE_MS = 60 * 60 * 1000;
95
116
 
96
117
  function rebuildActiveJobSet(): void {
97
- activeJobConvIds = new Set();
118
+ activeJobSourceConvIds = new Set();
98
119
  for (const j of mockJobs) {
99
120
  if (
100
121
  j.type !== "memory_retrospective" ||
@@ -105,7 +126,7 @@ function rebuildActiveJobSet(): void {
105
126
  try {
106
127
  const parsed = JSON.parse(j.payload) as { conversationId?: unknown };
107
128
  if (typeof parsed.conversationId === "string") {
108
- activeJobConvIds.add(parsed.conversationId);
129
+ activeJobSourceConvIds.add(parsed.conversationId);
109
130
  }
110
131
  } catch {
111
132
  // ignore
@@ -118,7 +139,7 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
118
139
  mockConversations = [];
119
140
  mockJobs = [];
120
141
  deletedIds = [];
121
- activeJobConvIds = new Set();
142
+ activeJobSourceConvIds = new Set();
122
143
  injectedNowMinusOrphanAgeMs = 0;
123
144
  });
124
145
 
@@ -127,7 +148,7 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
127
148
  mockJobs = [];
128
149
  });
129
150
 
130
- test("sweeps an old memory-retrospective conversation with no active job", () => {
151
+ test("sweeps an old memory-retrospective orphan that has been superseded by a newer retro for the same source", () => {
131
152
  const now = Date.now();
132
153
  injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
133
154
  mockConversations = [
@@ -135,6 +156,16 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
135
156
  id: "old-orphan",
136
157
  source: "memory-retrospective",
137
158
  last_message_at: now - 2 * ORPHAN_AGE_MS,
159
+ fork_parent_conversation_id: "source-A",
160
+ created_at: now - 3 * ORPHAN_AGE_MS,
161
+ },
162
+ // Newer successful retro for the same source — this one is preserved.
163
+ {
164
+ id: "newer-retro",
165
+ source: "memory-retrospective",
166
+ last_message_at: now - 90 * 60 * 1000,
167
+ fork_parent_conversation_id: "source-A",
168
+ created_at: now - 2 * ORPHAN_AGE_MS,
138
169
  },
139
170
  ];
140
171
  rebuildActiveJobSet();
@@ -153,6 +184,8 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
153
184
  id: "fresh-bg",
154
185
  source: "memory-retrospective",
155
186
  last_message_at: now - 60_000,
187
+ fork_parent_conversation_id: "source-A",
188
+ created_at: now - 60_000,
156
189
  },
157
190
  ];
158
191
  rebuildActiveJobSet();
@@ -171,6 +204,8 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
171
204
  id: "auto-analysis-old",
172
205
  source: "auto-analysis",
173
206
  last_message_at: now - 2 * ORPHAN_AGE_MS,
207
+ fork_parent_conversation_id: "source-A",
208
+ created_at: now - 2 * ORPHAN_AGE_MS,
174
209
  },
175
210
  ];
176
211
  rebuildActiveJobSet();
@@ -180,21 +215,91 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
180
215
  expect(result.swept).toBe(0);
181
216
  });
182
217
 
183
- test("does NOT sweep an orphan whose source conversation has an active job", () => {
218
+ // Regression test for the previously-broken active-job guard. Before the
219
+ // fix, the predicate compared `conversations.id` (the BACKGROUND-conv id)
220
+ // to source-conv ids extracted from job payloads — two different identifier
221
+ // spaces — so the guard never matched and in-flight retros were swept.
222
+ test("does NOT sweep a background conversation whose SOURCE has an active job (different identifier spaces)", () => {
184
223
  const now = Date.now();
185
224
  injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
225
+ // The background conv has its own id, distinct from the source it forks
226
+ // from. The active job's payload references the SOURCE, not the
227
+ // background.
186
228
  mockConversations = [
187
229
  {
188
- id: "orphan-but-protected",
230
+ id: "background-conv-id",
189
231
  source: "memory-retrospective",
190
232
  last_message_at: now - 2 * ORPHAN_AGE_MS,
233
+ fork_parent_conversation_id: "source-conv-id",
234
+ created_at: now - 2 * ORPHAN_AGE_MS,
191
235
  },
192
236
  ];
193
237
  mockJobs = [
194
238
  {
195
239
  type: "memory_retrospective",
196
240
  status: "pending",
197
- payload: JSON.stringify({ conversationId: "orphan-but-protected" }),
241
+ payload: JSON.stringify({ conversationId: "source-conv-id" }),
242
+ },
243
+ ];
244
+ rebuildActiveJobSet();
245
+
246
+ const result = sweepOrphanMemoryRetrospectiveConversations(now);
247
+
248
+ expect(result.swept).toBe(0);
249
+ expect(deletedIds).toEqual([]);
250
+ });
251
+
252
+ test("sweeps a superseded background conversation whose source has NO active job, even when another unrelated job is pending", () => {
253
+ const now = Date.now();
254
+ injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
255
+ mockConversations = [
256
+ {
257
+ id: "background-A",
258
+ source: "memory-retrospective",
259
+ last_message_at: now - 2 * ORPHAN_AGE_MS,
260
+ fork_parent_conversation_id: "source-A",
261
+ created_at: now - 3 * ORPHAN_AGE_MS,
262
+ },
263
+ {
264
+ id: "newer-A",
265
+ source: "memory-retrospective",
266
+ last_message_at: now - 90 * 60 * 1000,
267
+ fork_parent_conversation_id: "source-A",
268
+ created_at: now - 2 * ORPHAN_AGE_MS,
269
+ },
270
+ ];
271
+ // Active job references a DIFFERENT source — neither retro above is
272
+ // protected by the active-job guard.
273
+ mockJobs = [
274
+ {
275
+ type: "memory_retrospective",
276
+ status: "pending",
277
+ payload: JSON.stringify({ conversationId: "source-B" }),
278
+ },
279
+ ];
280
+ rebuildActiveJobSet();
281
+
282
+ const result = sweepOrphanMemoryRetrospectiveConversations(now);
283
+
284
+ expect(result.swept).toBe(1);
285
+ expect(deletedIds).toEqual(["background-A"]);
286
+ });
287
+
288
+ // Regression test for Devin's concern on PR #30331: the sweep used to
289
+ // delete every memory-retrospective conversation older than 1h, including
290
+ // the most-recent successful one per source. That broke
291
+ // `findMostRecentRetrospectiveFor` on the next run — the next retro had
292
+ // no dedup context and could re-save facts the prior pass already captured.
293
+ test("PRESERVES the most-recent retro per source even when older than the orphan cutoff", () => {
294
+ const now = Date.now();
295
+ injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
296
+ mockConversations = [
297
+ {
298
+ id: "successful-retro",
299
+ source: "memory-retrospective",
300
+ last_message_at: now - 2 * ORPHAN_AGE_MS,
301
+ fork_parent_conversation_id: "source-A",
302
+ created_at: now - 2 * ORPHAN_AGE_MS,
198
303
  },
199
304
  ];
200
305
  rebuildActiveJobSet();
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { stringifyMessageContent } from "../message-content.js";
4
+
5
+ describe("stringifyMessageContent", () => {
6
+ test("returns trimmed raw text for legacy plain-string rows", () => {
7
+ expect(stringifyMessageContent(" hello world ")).toBe("hello world");
8
+ });
9
+
10
+ test("returns trimmed inner string when JSON parses to a string", () => {
11
+ expect(stringifyMessageContent(JSON.stringify(" inner "))).toBe("inner");
12
+ });
13
+
14
+ test("concatenates text blocks from a ContentBlock[] payload", () => {
15
+ const raw = JSON.stringify([
16
+ { type: "text", text: "alpha" },
17
+ { type: "tool_use", id: "x", name: "noop", input: {} },
18
+ { type: "text", text: "beta" },
19
+ ]);
20
+ expect(stringifyMessageContent(raw)).toBe("alpha\nbeta");
21
+ });
22
+
23
+ test("falls back to raw trimmed text when JSON parses to a non-array object", () => {
24
+ const raw = ' {"type":"text","text":"hi"} ';
25
+ expect(stringifyMessageContent(raw)).toBe('{"type":"text","text":"hi"}');
26
+ });
27
+
28
+ test("falls back to raw trimmed text when JSON parses to a number", () => {
29
+ expect(stringifyMessageContent(" 42 ")).toBe("42");
30
+ });
31
+
32
+ test("returns trimmed raw text when JSON parsing fails", () => {
33
+ expect(stringifyMessageContent(" not json ")).toBe("not json");
34
+ });
35
+ });
@@ -106,22 +106,51 @@ export function listBookmarks(db: DrizzleDb): BookmarkSummary[] {
106
106
  }
107
107
 
108
108
  /**
109
- * Create a bookmark for the given message and return its JOIN-shaped
110
- * {@link BookmarkSummary}. Idempotent on the unique `message_id` index
111
- * if a bookmark already exists for `messageId`, the existing summary is
112
- * returned and no new row is inserted.
109
+ * Discriminated result returned by {@link createBookmark}. `inserted`
110
+ * distinguishes a brand-new row from an idempotent return of an existing
111
+ * one, so callers can suppress side effects (e.g. `bookmark.created` SSE
112
+ * publishes) on duplicate POSTs.
113
+ */
114
+ export type CreateBookmarkResult =
115
+ | { inserted: true; bookmark: BookmarkSummary }
116
+ | { inserted: false; bookmark: BookmarkSummary };
117
+
118
+ /**
119
+ * Create a bookmark for the given message and return a discriminated
120
+ * result indicating whether a new row was actually inserted. Idempotent
121
+ * on the unique `message_id` index — if a bookmark already exists for
122
+ * `messageId`, the existing summary is returned with `inserted: false`.
123
+ *
124
+ * `conversationId` is derived from the message row rather than trusted from
125
+ * the caller, so a buggy or malicious caller cannot persist a bookmark
126
+ * whose `conversationId` disagrees with the message's actual conversation.
113
127
  */
114
128
  export function createBookmark(
115
129
  db: DrizzleDb,
116
- params: { messageId: string; conversationId: string },
117
- ): BookmarkSummary {
118
- const { messageId, conversationId } = params;
130
+ params: { messageId: string },
131
+ ): CreateBookmarkResult {
132
+ const { messageId } = params;
133
+ const message = db
134
+ .select({ conversationId: messages.conversationId })
135
+ .from(messages)
136
+ .where(eq(messages.id, messageId))
137
+ .get();
138
+ if (!message) {
139
+ throw new Error(`Message ${messageId} not found`);
140
+ }
141
+ const conversationId = message.conversationId;
142
+
119
143
  const existing = db
120
144
  .select({ id: messageBookmarks.id })
121
145
  .from(messageBookmarks)
122
146
  .where(eq(messageBookmarks.messageId, messageId))
123
147
  .get();
124
- if (existing) return readBookmarkSummaryOrThrow(db, existing.id);
148
+ if (existing) {
149
+ return {
150
+ inserted: false,
151
+ bookmark: readBookmarkSummaryOrThrow(db, existing.id),
152
+ };
153
+ }
125
154
 
126
155
  const id = uuid();
127
156
  try {
@@ -137,9 +166,12 @@ export function createBookmark(
137
166
  .where(eq(messageBookmarks.messageId, messageId))
138
167
  .get();
139
168
  if (!winner) throw err;
140
- return readBookmarkSummaryOrThrow(db, winner.id);
169
+ return {
170
+ inserted: false,
171
+ bookmark: readBookmarkSummaryOrThrow(db, winner.id),
172
+ };
141
173
  }
142
- return readBookmarkSummaryOrThrow(db, id);
174
+ return { inserted: true, bookmark: readBookmarkSummaryOrThrow(db, id) };
143
175
  }
144
176
 
145
177
  function readBookmarkSummaryOrThrow(
@@ -1,7 +1,12 @@
1
+ import { readSlackMetadata } from "../../../messaging/providers/slack/message-metadata.js";
2
+ import {
3
+ parseExternalContentEnvelope,
4
+ wrapUntrustedContent,
5
+ } from "../../../security/untrusted-content.js";
1
6
  import { AUTO_ANALYSIS_SOURCE } from "../../auto-analysis-guard.js";
2
7
  import {
3
- buildExcerpt,
4
8
  buildFtsMatchQuery,
9
+ buildRecallEvidenceExcerpt,
5
10
  } from "../../conversation-queries.js";
6
11
  import { rawAll } from "../../raw-query.js";
7
12
  import type { RecallSearchContext, RecallSearchResult } from "../types.js";
@@ -15,6 +20,7 @@ interface ConversationEvidenceRow {
15
20
  role: string;
16
21
  content: string;
17
22
  created_at: number;
23
+ metadata: string | null;
18
24
  title: string | null;
19
25
  }
20
26
 
@@ -118,7 +124,7 @@ export async function searchConversationSource(
118
124
  source: "conversations",
119
125
  title: row.title?.trim() || "Untitled conversation",
120
126
  locator: `${row.conversation_id}#${row.message_id}`,
121
- excerpt: buildExcerpt(row.content, trimmedQuery),
127
+ excerpt: buildRecallExcerpt(row, trimmedQuery),
122
128
  timestampMs: row.created_at,
123
129
  score,
124
130
  metadata: {
@@ -142,6 +148,7 @@ function searchWithFts(
142
148
  m.role,
143
149
  m.content,
144
150
  m.created_at,
151
+ m.metadata,
145
152
  c.title
146
153
  FROM messages_fts
147
154
  JOIN messages m ON m.id = messages_fts.message_id
@@ -175,6 +182,7 @@ function searchWithLike(
175
182
  m.role,
176
183
  m.content,
177
184
  m.created_at,
185
+ m.metadata,
178
186
  c.title
179
187
  FROM messages m
180
188
  JOIN conversations c ON c.id = m.conversation_id
@@ -194,6 +202,58 @@ function searchWithLike(
194
202
  );
195
203
  }
196
204
 
205
+ function buildRecallExcerpt(
206
+ row: ConversationEvidenceRow,
207
+ query: string,
208
+ ): string {
209
+ const excerpt = buildRecallEvidenceExcerpt(row.content, query);
210
+ const slackMeta = parseSlackRecallMetadata(row.metadata);
211
+ if (
212
+ row.role !== "user" ||
213
+ !slackMeta ||
214
+ slackMeta.provenanceTrustClass === "guardian" ||
215
+ excerpt.length === 0 ||
216
+ parseExternalContentEnvelope(excerpt)
217
+ ) {
218
+ return excerpt;
219
+ }
220
+
221
+ return wrapUntrustedContent(excerpt, {
222
+ source: "slack",
223
+ ...(slackMeta.displayName ? { sourceDetail: slackMeta.displayName } : {}),
224
+ });
225
+ }
226
+
227
+ function parseSlackRecallMetadata(rawMetadata: string | null): {
228
+ displayName?: string;
229
+ provenanceTrustClass?: string;
230
+ } | null {
231
+ if (!rawMetadata) return null;
232
+
233
+ let parsed: unknown;
234
+ try {
235
+ parsed = JSON.parse(rawMetadata);
236
+ } catch {
237
+ return null;
238
+ }
239
+
240
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
241
+ return null;
242
+ }
243
+
244
+ const metadata = parsed as Record<string, unknown>;
245
+ if (typeof metadata.slackMeta !== "string") return null;
246
+ const slackMeta = readSlackMetadata(metadata.slackMeta);
247
+ if (!slackMeta) return null;
248
+
249
+ return {
250
+ ...(slackMeta.displayName ? { displayName: slackMeta.displayName } : {}),
251
+ ...(typeof metadata.provenanceTrustClass === "string"
252
+ ? { provenanceTrustClass: metadata.provenanceTrustClass }
253
+ : {}),
254
+ };
255
+ }
256
+
197
257
  function buildRecallFtsMatchQueries(query: string): string[] {
198
258
  const queries: string[] = [];
199
259
  const exact = buildFtsMatchQuery(query);
@@ -68,6 +68,9 @@ const SECRET_SEGMENT_NAMES = new Set([
68
68
  const SECRET_TOKEN_PATTERN =
69
69
  /(?:^|[-_.])(?:keys?|secrets?|tokens?)(?:[-_.]|$)/i;
70
70
 
71
+ const SECRET_TOKEN_CAMEL_CASE_PATTERN =
72
+ /(?<=[a-z0-9])(?:Keys?|Secrets?|Tokens?)(?=[A-Z]|[-_.]|$)/;
73
+
71
74
  const QUERY_STOP_WORDS = new Set([
72
75
  "a",
73
76
  "about",
@@ -1185,6 +1188,7 @@ function shouldSkipSegmentName(name: string): boolean {
1185
1188
  GENERATED_OR_DEPENDENCY_DIR_NAMES.has(lowerName) ||
1186
1189
  lowerName.startsWith(".env") ||
1187
1190
  SECRET_TOKEN_PATTERN.test(lowerName) ||
1191
+ SECRET_TOKEN_CAMEL_CASE_PATTERN.test(name) ||
1188
1192
  lowerName.startsWith("credentials") ||
1189
1193
  SECRET_SEGMENT_NAMES.has(lowerName)
1190
1194
  );