@vellumai/assistant 0.8.1 → 0.8.2

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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -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 +325 -3
  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-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -17,6 +17,11 @@ const log = getLogger("filing-service");
17
17
 
18
18
  const FILING_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
19
19
 
20
+ // When compaction skips because a filing run holds the serialization lock,
21
+ // retry on this near-term cadence so phase-aligned 24h timers don't starve
22
+ // compaction across consecutive ticks.
23
+ const COMPACTION_CONTENDED_RETRY_MS = 10 * 60 * 1000; // 10 minutes
24
+
20
25
  const FILING_PROMPT_TEMPLATE = `You are running a periodic knowledge base filing job. This is a background maintenance task focused on the buffer.
21
26
 
22
27
  Read \`pkb/buffer.md\`. For each item in the buffer:
@@ -66,6 +71,7 @@ This is your knowledge base — keep it sharp.`;
66
71
 
67
72
  export interface FilingDeps {
68
73
  getCurrentHour?: () => number;
74
+ compactionContendedRetryMs?: number;
69
75
  }
70
76
 
71
77
  export class FilingService {
@@ -79,8 +85,10 @@ export class FilingService {
79
85
  private readonly deps: FilingDeps;
80
86
  private timer: ReturnType<typeof setInterval> | null = null;
81
87
  private compactionTimer: ReturnType<typeof setInterval> | null = null;
88
+ private compactionRetryTimer: ReturnType<typeof setTimeout> | null = null;
82
89
  private activeRun: Promise<void> | null = null;
83
90
  private activeCompactionRun: Promise<void> | null = null;
91
+ private stopped = false;
84
92
  private _lastRunAt: number | null = null;
85
93
  private _nextRunAt: number | null = null;
86
94
  private _lastCompactionAt: number | null = null;
@@ -108,6 +116,7 @@ export class FilingService {
108
116
  }
109
117
 
110
118
  start(): void {
119
+ this.stopped = false;
111
120
  const fullConfig = getConfig();
112
121
  if (fullConfig.memory.v2.enabled) {
113
122
  log.info("Filing service disabled — memory v2 is active");
@@ -157,12 +166,14 @@ export class FilingService {
157
166
  clearInterval(this.compactionTimer);
158
167
  this.compactionTimer = null;
159
168
  }
169
+ this.clearCompactionRetry();
160
170
  this._nextRunAt = null;
161
171
  this._nextCompactionAt = null;
162
172
  this.start();
163
173
  }
164
174
 
165
175
  async stop(): Promise<void> {
176
+ this.stopped = true;
166
177
  if (this.timer) {
167
178
  clearInterval(this.timer);
168
179
  this.timer = null;
@@ -171,6 +182,7 @@ export class FilingService {
171
182
  clearInterval(this.compactionTimer);
172
183
  this.compactionTimer = null;
173
184
  }
185
+ this.clearCompactionRetry();
174
186
  this._nextRunAt = null;
175
187
  this._nextCompactionAt = null;
176
188
  const inflight: Promise<void>[] = [];
@@ -269,9 +281,13 @@ export class FilingService {
269
281
  log.debug(
270
282
  "Filing run in progress, skipping compaction to avoid concurrent PKB writes",
271
283
  );
284
+ this.scheduleCompactionRetry(
285
+ this.deps.compactionContendedRetryMs ?? COMPACTION_CONTENDED_RETRY_MS,
286
+ );
272
287
  return false;
273
288
  }
274
289
 
290
+ this.clearCompactionRetry();
275
291
  const run = this.executeCompactionRun();
276
292
  this.activeCompactionRun = run;
277
293
  try {
@@ -301,6 +317,29 @@ export class FilingService {
301
317
  this._nextCompactionAt = Date.now() + intervalMs;
302
318
  }
303
319
 
320
+ private scheduleCompactionRetry(delayMs: number): void {
321
+ this.clearCompactionRetry();
322
+ if (this.stopped) return;
323
+ this.compactionRetryTimer = setTimeout(() => {
324
+ this.compactionRetryTimer = null;
325
+ if (this.stopped) return;
326
+ this.runCompactionOnce().catch((err) => {
327
+ log.error({ err }, "Compaction retry failed");
328
+ });
329
+ }, delayMs);
330
+ // unref so the pending retry doesn't keep the daemon process alive on
331
+ // shutdown paths that don't call stop().
332
+ this.compactionRetryTimer.unref?.();
333
+ this._nextCompactionAt = Date.now() + delayMs;
334
+ }
335
+
336
+ private clearCompactionRetry(): void {
337
+ if (this.compactionRetryTimer) {
338
+ clearTimeout(this.compactionRetryTimer);
339
+ this.compactionRetryTimer = null;
340
+ }
341
+ }
342
+
304
343
  private shouldSkipForDiskPressure(source: "filing" | "compaction"): boolean {
305
344
  const diskPressureGate = checkDiskPressureBackgroundGate("background-work");
306
345
  if (diskPressureGate.action === "allow") return false;
@@ -47,12 +47,22 @@ mock.module("../../util/platform.js", () => ({
47
47
 
48
48
  // Stub config so heartbeat is enabled. Must export every symbol from
49
49
  // the real module because Bun's mock.module replaces the entire module.
50
- const stubConfig = {
50
+ // Tests that need to flex maxConsecutiveRuns mutate this in-place.
51
+ const stubConfig: {
52
+ heartbeat: {
53
+ enabled: boolean;
54
+ intervalMs: number;
55
+ activeHoursStart: number | null;
56
+ activeHoursEnd: number | null;
57
+ maxConsecutiveRuns: number | null;
58
+ };
59
+ } = {
51
60
  heartbeat: {
52
61
  enabled: true,
53
62
  intervalMs: 60_000,
54
63
  activeHoursStart: null,
55
64
  activeHoursEnd: null,
65
+ maxConsecutiveRuns: null,
56
66
  },
57
67
  };
58
68
  mock.module("../../config/loader.js", () => ({
@@ -91,7 +101,6 @@ mock.module("../../prompts/system-prompt.js", () => ({
91
101
  SYSTEM_PROMPT_CACHE_BOUNDARY: "<<CACHE_BOUNDARY>>",
92
102
  buildCoreIdentityContext: () => "",
93
103
  buildSystemPrompt: () => "",
94
- buildCliReferenceSection: () => "",
95
104
  ensurePromptFiles: () => {},
96
105
  stripCommentLines: (s: string) => s,
97
106
  }));
@@ -165,7 +174,6 @@ mock.module("../../runtime/pre-first-message-gate.js", () => ({
165
174
  hasReceivedUserMessage: () => preFirstMessageGateOpen,
166
175
  }));
167
176
 
168
-
169
177
  const { HeartbeatService } = await import("../heartbeat-service.js");
170
178
 
171
179
  let origWorkspaceDir: string | undefined;
@@ -178,6 +186,7 @@ beforeEach(() => {
178
186
  runBackgroundJobCalls.length = 0;
179
187
  skipHeartbeatRunCalls.length = 0;
180
188
  preFirstMessageGateOpen = true;
189
+ stubConfig.heartbeat.maxConsecutiveRuns = null;
181
190
  runBackgroundJobImpl = async () => ({
182
191
  conversationId: STUB_CONVERSATION_ID,
183
192
  ok: true,
@@ -351,9 +360,85 @@ describe("HeartbeatService", () => {
351
360
 
352
361
  expect(runBackgroundJobCalls).toHaveLength(1);
353
362
  expect(
354
- skipHeartbeatRunCalls.some(
355
- (c) => c.reason === "pre_first_user_message",
356
- ),
363
+ skipHeartbeatRunCalls.some((c) => c.reason === "pre_first_user_message"),
357
364
  ).toBe(false);
358
365
  });
366
+
367
+ describe("max consecutive runs cap", () => {
368
+ test("skips with reason 'max_consecutive_runs' after the cap is hit", async () => {
369
+ stubConfig.heartbeat.maxConsecutiveRuns = 2;
370
+ const service = new HeartbeatService({ alerter: () => {} });
371
+
372
+ expect(await service.runOnce({ force: false })).toBe(true);
373
+ expect(await service.runOnce({ force: false })).toBe(true);
374
+ expect(await service.runOnce({ force: false })).toBe(false);
375
+
376
+ expect(runBackgroundJobCalls).toHaveLength(2);
377
+ expect(
378
+ skipHeartbeatRunCalls.some((c) => c.reason === "max_consecutive_runs"),
379
+ ).toBe(true);
380
+ });
381
+
382
+ test("resetTimer() clears the counter so auto runs resume", async () => {
383
+ stubConfig.heartbeat.maxConsecutiveRuns = 1;
384
+ const service = new HeartbeatService({ alerter: () => {} });
385
+ service.start();
386
+ try {
387
+ await service.runOnce({ force: false });
388
+ expect(await service.runOnce({ force: false })).toBe(false);
389
+
390
+ service.resetTimer();
391
+ expect(await service.runOnce({ force: false })).toBe(true);
392
+ expect(runBackgroundJobCalls).toHaveLength(2);
393
+ } finally {
394
+ await service.stop();
395
+ }
396
+ });
397
+
398
+ test("null disables the cap entirely", async () => {
399
+ stubConfig.heartbeat.maxConsecutiveRuns = null;
400
+ const service = new HeartbeatService({ alerter: () => {} });
401
+
402
+ for (let i = 0; i < 5; i++) {
403
+ expect(await service.runOnce({ force: false })).toBe(true);
404
+ }
405
+ expect(runBackgroundJobCalls).toHaveLength(5);
406
+ expect(
407
+ skipHeartbeatRunCalls.some((c) => c.reason === "max_consecutive_runs"),
408
+ ).toBe(false);
409
+ });
410
+
411
+ test("force runs bypass the cap and do not increment the counter", async () => {
412
+ stubConfig.heartbeat.maxConsecutiveRuns = 2;
413
+ const service = new HeartbeatService({ alerter: () => {} });
414
+
415
+ // Five force runs would push us well past the cap if force counted.
416
+ for (let i = 0; i < 5; i++) {
417
+ expect(await service.runOnce({ force: true })).toBe(true);
418
+ }
419
+ // Two auto runs should still proceed because the counter is at zero.
420
+ expect(await service.runOnce({ force: false })).toBe(true);
421
+ expect(await service.runOnce({ force: false })).toBe(true);
422
+ // The third auto run trips the cap.
423
+ expect(await service.runOnce({ force: false })).toBe(false);
424
+ expect(
425
+ skipHeartbeatRunCalls.some((c) => c.reason === "max_consecutive_runs"),
426
+ ).toBe(true);
427
+ });
428
+
429
+ test("reconfigure() resets the counter", async () => {
430
+ stubConfig.heartbeat.maxConsecutiveRuns = 1;
431
+ const service = new HeartbeatService({ alerter: () => {} });
432
+ service.start();
433
+ try {
434
+ await service.runOnce({ force: false });
435
+ expect(await service.runOnce({ force: false })).toBe(false);
436
+
437
+ service.reconfigure();
438
+ expect(await service.runOnce({ force: false })).toBe(true);
439
+ } finally {
440
+ await service.stop();
441
+ }
442
+ });
443
+ });
359
444
  });
@@ -23,7 +23,8 @@ export type HeartbeatSkipReason =
23
23
  | "disabled"
24
24
  | "outside_active_hours"
25
25
  | "overlap"
26
- | "pre_first_user_message";
26
+ | "pre_first_user_message"
27
+ | "max_consecutive_runs";
27
28
 
28
29
  export interface HeartbeatRunRecord {
29
30
  id: string;
@@ -198,6 +198,10 @@ export class HeartbeatService {
198
198
  private _startupMissedCount = 0;
199
199
  private _startupCrashedCount = 0;
200
200
  private _hasRunStartupRecovery = false;
201
+ // Counter of consecutive auto-heartbeats since the last guardian message.
202
+ // Reset by resetTimer (guardian message), reconfigure, and stop. Force runs
203
+ // bypass the cap and do not increment.
204
+ private _consecutiveRuns = 0;
201
205
 
202
206
  constructor(deps: HeartbeatDeps) {
203
207
  this.deps = deps;
@@ -262,6 +266,11 @@ export class HeartbeatService {
262
266
  isAsyncBackground: true,
263
267
  visibleInSourceNow: false,
264
268
  },
269
+ conversationMetadata: {
270
+ source: "heartbeat",
271
+ groupId: "system:background",
272
+ conversationType: "background",
273
+ },
265
274
  }).catch((err) => {
266
275
  log.warn(
267
276
  { err },
@@ -350,6 +359,7 @@ export class HeartbeatService {
350
359
 
351
360
  /** Restart the timer with the latest config (e.g. after settings change). */
352
361
  reconfigure(): void {
362
+ this._consecutiveRuns = 0;
353
363
  this.configEpoch++;
354
364
  if (this._pendingRunId) {
355
365
  supersedePendingRun(this._pendingRunId);
@@ -371,6 +381,9 @@ export class HeartbeatService {
371
381
  * after an active conversation.
372
382
  */
373
383
  resetTimer(): void {
384
+ // Counter resets even when the timer is null so a guardian message during
385
+ // a stopped window still clears the count.
386
+ this._consecutiveRuns = 0;
374
387
  if (!this.timer) return;
375
388
  if (this.cronMode) {
376
389
  clearTimeout(this.timer as ReturnType<typeof setTimeout>);
@@ -390,6 +403,7 @@ export class HeartbeatService {
390
403
  }
391
404
 
392
405
  async stop(): Promise<void> {
406
+ this._consecutiveRuns = 0;
393
407
  this.stopped = true;
394
408
  if (this.timer) {
395
409
  clearTimeout(this.timer as ReturnType<typeof setTimeout>);
@@ -500,6 +514,28 @@ export class HeartbeatService {
500
514
  }
501
515
  }
502
516
 
517
+ // Cap consecutive auto-runs without a guardian message so the assistant
518
+ // stops burning LLM tokens when the user is away. Force runs (manual
519
+ // operator action) bypass the cap and do not increment the counter.
520
+ if (
521
+ !force &&
522
+ config.maxConsecutiveRuns != null &&
523
+ this._consecutiveRuns >= config.maxConsecutiveRuns
524
+ ) {
525
+ log.debug(
526
+ {
527
+ consecutiveRuns: this._consecutiveRuns,
528
+ maxConsecutiveRuns: config.maxConsecutiveRuns,
529
+ },
530
+ "Max consecutive runs reached, skipping",
531
+ );
532
+ if (runId) skipHeartbeatRun(runId, "max_consecutive_runs");
533
+ if (!this.cronMode) {
534
+ this.scheduleNextRun(config.intervalMs);
535
+ }
536
+ return false;
537
+ }
538
+
503
539
  // Overlap prevention
504
540
  if (this.activeRun) {
505
541
  log.debug("Previous heartbeat run still active, skipping");
@@ -525,6 +561,9 @@ export class HeartbeatService {
525
561
  this.activeRun = null;
526
562
  }
527
563
  this._lastRunAt = Date.now();
564
+ if (!force) {
565
+ this._consecutiveRuns++;
566
+ }
528
567
  if (!this.cronMode) {
529
568
  this.scheduleNextRun(getConfig().heartbeat.intervalMs);
530
569
  }
@@ -648,6 +687,7 @@ export class HeartbeatService {
648
687
  conversationMetadata: {
649
688
  source: "heartbeat",
650
689
  groupId: "system:background",
690
+ conversationType: "background",
651
691
  },
652
692
  });
653
693
  } catch (err) {
@@ -714,6 +754,7 @@ export class HeartbeatService {
714
754
  conversationMetadata: {
715
755
  source: "heartbeat",
716
756
  groupId: "system:background",
757
+ conversationType: "background",
717
758
  },
718
759
  });
719
760
  }
@@ -82,6 +82,40 @@ describe("feedItemSchema — valid minimal items", () => {
82
82
  expect(parsed.expiresAt).toBe("2026-04-15T00:00:00.000Z");
83
83
  expect(parsed.detailPanel?.kind).toBe("emailDraft");
84
84
  });
85
+
86
+ test("category field passes through when present", () => {
87
+ for (const cat of [
88
+ "security",
89
+ "scheduling",
90
+ "background",
91
+ "email",
92
+ "system",
93
+ ] as const) {
94
+ const parsed = feedItemSchema.parse({
95
+ ...minimalNotification(),
96
+ category: cat,
97
+ });
98
+ expect(parsed.category).toBe(cat);
99
+ }
100
+ });
101
+
102
+ test("metadata field passes through when present", () => {
103
+ const parsed = feedItemSchema.parse({
104
+ ...minimalNotification(),
105
+ metadata: { subject: "Hello", count: 3, nested: { ok: true } },
106
+ });
107
+ expect(parsed.metadata).toEqual({
108
+ subject: "Hello",
109
+ count: 3,
110
+ nested: { ok: true },
111
+ });
112
+ });
113
+
114
+ test("items without category or metadata still parse (backward compat)", () => {
115
+ const parsed = feedItemSchema.parse(minimalNotification());
116
+ expect(parsed.category).toBeUndefined();
117
+ expect(parsed.metadata).toBeUndefined();
118
+ });
85
119
  });
86
120
 
87
121
  // ---------------------------------------------------------------------------
@@ -142,6 +176,12 @@ describe("feedItemSchema — enum validation", () => {
142
176
  }
143
177
  });
144
178
 
179
+ test("rejects unknown `category`", () => {
180
+ expect(() =>
181
+ feedItemSchema.parse({ ...minimalNotification(), category: "weather" }),
182
+ ).toThrow();
183
+ });
184
+
145
185
  test("rejects unknown `status`", () => {
146
186
  expect(() =>
147
187
  feedItemSchema.parse({ ...minimalNotification(), status: "archived" }),
@@ -38,6 +38,14 @@ export type FeedItemStatus = "new" | "seen" | "acted_on" | "dismissed";
38
38
  /** Visual urgency treatment — controls badge color independently of sort priority. */
39
39
  export type FeedItemUrgency = "low" | "medium" | "high" | "critical";
40
40
 
41
+ /** Broad category for grouping and filtering feed items. */
42
+ export type FeedItemCategory =
43
+ | "security"
44
+ | "scheduling"
45
+ | "background"
46
+ | "email"
47
+ | "system";
48
+
41
49
  /**
42
50
  * A single action button attached to a feed item.
43
51
  *
@@ -96,6 +104,10 @@ export interface FeedItem {
96
104
  conversationId?: string;
97
105
  /** Server-driven detail panel descriptor; when present, the client opens this panel kind. */
98
106
  detailPanel?: FeedItemDetailPanel;
107
+ /** Broad category for grouping and filtering feed items. */
108
+ category?: FeedItemCategory;
109
+ /** Arbitrary structured data the detail panel or other consumers can use. */
110
+ metadata?: Record<string, unknown>;
99
111
  /** Internal: ISO-8601 writer-record time, used for ordering + TTL. */
100
112
  createdAt: string;
101
113
  }
@@ -164,6 +176,14 @@ const feedItemDetailPanelSchema = z.object({
164
176
  kind: feedItemDetailPanelKindSchema,
165
177
  });
166
178
 
179
+ const feedItemCategorySchema = z.enum([
180
+ "security",
181
+ "scheduling",
182
+ "background",
183
+ "email",
184
+ "system",
185
+ ]);
186
+
167
187
  /**
168
188
  * Schema for a single `FeedItem`.
169
189
  *
@@ -187,6 +207,8 @@ export const feedItemSchema = z.object({
187
207
  urgency: feedItemUrgencySchema.optional(),
188
208
  conversationId: z.string().optional(),
189
209
  detailPanel: feedItemDetailPanelSchema.optional(),
210
+ category: feedItemCategorySchema.optional(),
211
+ metadata: z.record(z.string(), z.unknown()).optional(),
190
212
  createdAt: z.string(),
191
213
  });
192
214
 
@@ -44,6 +44,7 @@ export async function emitPostConnectNudge(service: string): Promise<void> {
44
44
  "I can triage your inbox, summarize new emails, or draft replies to important threads.",
45
45
  timestamp: now.toISOString(),
46
46
  status: "new",
47
+ category: "email",
47
48
  expiresAt,
48
49
  createdAt: now.toISOString(),
49
50
  actions: [
package/src/index.ts CHANGED
@@ -1,5 +1,22 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { red } from "./cli/lib/cli-colors.js";
4
+ import {
5
+ detectUnknownCommand,
6
+ formatUnknownCommandMessage,
7
+ } from "./cli/lib/unknown-command.js";
3
8
  import { buildCliProgram } from "./cli/program.js";
4
9
 
5
- (await buildCliProgram()).parse();
10
+ const program = await buildCliProgram();
11
+
12
+ // Commander processes `--help` before any action or hook fires, so
13
+ // `assistant <unknown> --help` would dump the root help instead of flagging
14
+ // the typo. Pre-scan argv so the unknown-command error wins over the help
15
+ // short-circuit. See cli/lib/unknown-command.ts.
16
+ const unknown = detectUnknownCommand(program, process.argv.slice(2));
17
+ if (unknown) {
18
+ process.stderr.write(`${red(formatUnknownCommandMessage(unknown))}\n`);
19
+ process.exit(1);
20
+ }
21
+
22
+ program.parse();
@@ -229,6 +229,63 @@ describe("LiveVoiceSession STT", () => {
229
229
  ]);
230
230
  });
231
231
 
232
+ test("retains transcriber handle when stop() throws so close() can clean up", async () => {
233
+ class ThrowingStopTranscriber extends MockStreamingTranscriber {
234
+ stopCalls = 0;
235
+ override stop(): void {
236
+ this.stopCalls += 1;
237
+ if (this.stopCalls === 1) {
238
+ throw new Error("stop failed");
239
+ }
240
+ }
241
+ }
242
+
243
+ const transcriber = new ThrowingStopTranscriber();
244
+ const { frames, session } = createSessionWithTranscriber(transcriber);
245
+
246
+ await session.start();
247
+ await session.handleClientFrame({ type: "ptt_release" });
248
+
249
+ expect(transcriber.stopCalls).toBe(1);
250
+ expect(
251
+ frames.some(
252
+ (frame) =>
253
+ frame.type === "error" &&
254
+ frame.message.includes(
255
+ "Live voice transcription could not be stopped",
256
+ ),
257
+ ),
258
+ ).toBe(true);
259
+
260
+ await session.close("websocket_close");
261
+
262
+ expect(transcriber.stopCalls).toBe(2);
263
+ });
264
+
265
+ test("retains transcriber handle when stop() throws so interrupt() can clean up", async () => {
266
+ class ThrowingStopTranscriber extends MockStreamingTranscriber {
267
+ stopCalls = 0;
268
+ override stop(): void {
269
+ this.stopCalls += 1;
270
+ if (this.stopCalls === 1) {
271
+ throw new Error("stop failed");
272
+ }
273
+ }
274
+ }
275
+
276
+ const transcriber = new ThrowingStopTranscriber();
277
+ const { session } = createSessionWithTranscriber(transcriber);
278
+
279
+ await session.start();
280
+ await session.handleClientFrame({ type: "ptt_release" });
281
+
282
+ expect(transcriber.stopCalls).toBe(1);
283
+
284
+ await session.handleClientFrame({ type: "interrupt" });
285
+
286
+ expect(transcriber.stopCalls).toBe(2);
287
+ });
288
+
232
289
  test("uses the production streaming transcriber resolver by default", () => {
233
290
  const source = readFileSync(
234
291
  new URL("../live-voice-session.ts", import.meta.url),
package/src/mcp/client.ts CHANGED
@@ -14,6 +14,24 @@ const log = getLogger("mcp-client");
14
14
 
15
15
  const CONNECT_TIMEOUT_MS = 30_000;
16
16
 
17
+ // MCP servers occasionally return tools with a missing or non-object
18
+ // `inputSchema` (spec violation, but seen in the wild). Coerce to a valid
19
+ // empty object schema so downstream code that assumes `input_schema: object`
20
+ // (e.g. `injectActivityField`) doesn't crash.
21
+ function normalizeInputSchema(
22
+ raw: unknown,
23
+ toolName: string,
24
+ ): Record<string, unknown> {
25
+ if (raw != null && typeof raw === "object" && !Array.isArray(raw)) {
26
+ return raw as Record<string, unknown>;
27
+ }
28
+ log.warn(
29
+ { toolName, received: typeof raw },
30
+ "MCP tool returned non-object inputSchema; defaulting to empty object schema",
31
+ );
32
+ return { type: "object", properties: {} };
33
+ }
34
+
17
35
  export interface McpToolInfo {
18
36
  name: string;
19
37
  description: string;
@@ -80,9 +98,7 @@ export class McpClient {
80
98
  `mcp:${this.serverId}:tokens`,
81
99
  );
82
100
  if (cachedTokens) {
83
- const callbackTransport = getIsPlatform()
84
- ? "gateway"
85
- : "loopback";
101
+ const callbackTransport = getIsPlatform() ? "gateway" : "loopback";
86
102
  this.oauthProvider = new McpOAuthProvider(
87
103
  this.serverId,
88
104
  transportConfig.url,
@@ -164,7 +180,7 @@ export class McpClient {
164
180
  return result.tools.map((tool) => ({
165
181
  name: tool.name,
166
182
  description: tool.description ?? "",
167
- inputSchema: tool.inputSchema as Record<string, unknown>,
183
+ inputSchema: normalizeInputSchema(tool.inputSchema, tool.name),
168
184
  }));
169
185
  }
170
186
 
@@ -8,8 +8,8 @@
8
8
  * credentials are unavailable.
9
9
  */
10
10
 
11
- import { MANAGED_PROVIDER_META } from "../providers/managed-proxy/constants.js";
12
- import { resolveManagedProxyContext } from "../providers/managed-proxy/context.js";
11
+ import { PLATFORM_PROVIDER_META } from "../providers/platform-proxy/constants.js";
12
+ import { resolveManagedProxyContext } from "../providers/platform-proxy/context.js";
13
13
  import { getProviderKeyAsync } from "../security/secure-keys.js";
14
14
  import type { ImageGenCredentials, ImageGenProvider } from "./types.js";
15
15
 
@@ -33,7 +33,7 @@ export async function resolveImageGenCredentials(opts: {
33
33
  // Resolve platform URL + assistant API key from a single snapshot so
34
34
  // baseUrl and assistantApiKey can't diverge if the credential is cleared
35
35
  // between lookups.
36
- const meta = MANAGED_PROVIDER_META[provider];
36
+ const meta = PLATFORM_PROVIDER_META[provider];
37
37
  const ctx = await resolveManagedProxyContext();
38
38
  if (
39
39
  !meta?.managed ||