@vellumai/assistant 0.8.12 → 0.9.0-staging.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 (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Route handlers for workflow-run management.
3
+ *
4
+ * All routes are served by both the HTTP server and the IPC server via the
5
+ * shared ROUTES array. They are read/abort surfaces over the workflow run
6
+ * manager and the saved-workflow library — a human (via the `vellum workflows`
7
+ * CLI or the app) can inspect runs and abort an in-flight one.
8
+ *
9
+ * Every handler is gated on the `workflows` feature flag: when it is off, the
10
+ * routes behave as if they do not exist (404), so disabling the flag fully
11
+ * hides the surface.
12
+ */
13
+
14
+ import { z } from "zod";
15
+
16
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
17
+ import { getConfig } from "../../config/loader.js";
18
+ import type { AssistantConfig } from "../../config/schema.js";
19
+ import { getAutoApproveThreshold } from "../../permissions/gateway-threshold-reader.js";
20
+ import { isFullAccessThreshold } from "../../permissions/threshold.js";
21
+ import { manifestGrantsSideEffects } from "../../workflows/capabilities.js";
22
+ import type { WorkflowRun } from "../../workflows/journal-store.js";
23
+ import { listWorkflows } from "../../workflows/library.js";
24
+ import {
25
+ getWorkflowRunManager,
26
+ WorkflowResumeNotPossibleError,
27
+ WorkflowRunCapError,
28
+ type WorkflowRunManager,
29
+ } from "../../workflows/run-manager.js";
30
+ import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
31
+ import {
32
+ ConflictError,
33
+ ForbiddenError,
34
+ NotFoundError,
35
+ TooManyRequestsError,
36
+ } from "./errors.js";
37
+ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Injectable dependencies (production defaults; tests override)
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export interface WorkflowRoutesDeps {
44
+ getManager: () => Pick<
45
+ WorkflowRunManager,
46
+ "list" | "status" | "abort" | "resume"
47
+ >;
48
+ listWorkflows: typeof listWorkflows;
49
+ getConfig: () => AssistantConfig;
50
+ isFlagEnabled: (config: AssistantConfig) => boolean;
51
+ getAutoApproveThreshold: typeof getAutoApproveThreshold;
52
+ }
53
+
54
+ function defaultDeps(): WorkflowRoutesDeps {
55
+ return {
56
+ getManager: getWorkflowRunManager,
57
+ listWorkflows,
58
+ getConfig,
59
+ isFlagEnabled: (config) =>
60
+ isAssistantFeatureFlagEnabled("workflows", config),
61
+ getAutoApproveThreshold,
62
+ };
63
+ }
64
+
65
+ let deps: WorkflowRoutesDeps = defaultDeps();
66
+
67
+ /** Test seam: override the route dependencies. Pass no arg to restore defaults. */
68
+ export function __setWorkflowRoutesDeps(
69
+ overrides?: Partial<WorkflowRoutesDeps>,
70
+ ): void {
71
+ deps = overrides ? { ...defaultDeps(), ...overrides } : defaultDeps();
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Response schemas
76
+ // ---------------------------------------------------------------------------
77
+
78
+ const VALID_RUN_STATUSES = [
79
+ "running",
80
+ "completed",
81
+ "failed",
82
+ "aborted",
83
+ "cap_exceeded",
84
+ "interrupted",
85
+ ] as const;
86
+
87
+ const workflowRunSchema = z.object({
88
+ id: z.string(),
89
+ name: z.string().nullable(),
90
+ scriptHash: z.string(),
91
+ status: z.enum(VALID_RUN_STATUSES),
92
+ conversationId: z.string().nullable(),
93
+ agentsSpawned: z.number(),
94
+ inputTokens: z.number(),
95
+ outputTokens: z.number(),
96
+ error: z.string().nullable(),
97
+ createdAt: z.number().nullable(),
98
+ updatedAt: z.number().nullable(),
99
+ finishedAt: z.number().nullable(),
100
+ });
101
+
102
+ const savedWorkflowSchema = z.object({
103
+ name: z.string(),
104
+ description: z.string(),
105
+ path: z.string(),
106
+ });
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Handlers (transport-agnostic)
110
+ // ---------------------------------------------------------------------------
111
+
112
+ /**
113
+ * Throw a 404 when the `workflows` flag is off, hiding the whole surface. The
114
+ * NotFoundError (rather than a 403) keeps a disabled-flag indistinguishable
115
+ * from a route that does not exist.
116
+ */
117
+ function assertFlagEnabled(): void {
118
+ if (!deps.isFlagEnabled(deps.getConfig())) {
119
+ throw new NotFoundError("Workflows are not enabled.");
120
+ }
121
+ }
122
+
123
+ /**
124
+ * The single compact-run wire contract, inferred from {@link workflowRunSchema}.
125
+ * The HTTP routes return this shape; other producers (e.g. the CLI's client-side
126
+ * mirror) should track it so projections can't silently drift.
127
+ */
128
+ export type WorkflowRunWire = z.infer<typeof workflowRunSchema>;
129
+
130
+ /** Project a stored run into the wire shape (drops bulky source/args/result). */
131
+ export function toWireRun(run: WorkflowRun): WorkflowRunWire {
132
+ return {
133
+ id: run.id,
134
+ name: run.name,
135
+ scriptHash: run.scriptHash,
136
+ status: run.status,
137
+ conversationId: run.conversationId,
138
+ agentsSpawned: run.agentsSpawned,
139
+ inputTokens: run.inputTokens,
140
+ outputTokens: run.outputTokens,
141
+ error: run.error,
142
+ createdAt: run.createdAt,
143
+ updatedAt: run.updatedAt,
144
+ finishedAt: run.finishedAt,
145
+ };
146
+ }
147
+
148
+ function parseLimit(raw: string | undefined): number | undefined {
149
+ if (raw === undefined) return undefined;
150
+ const n = Number(raw);
151
+ if (!Number.isFinite(n)) return undefined;
152
+ return Math.min(Math.max(Math.floor(n), 1), 200);
153
+ }
154
+
155
+ function parseStatus(
156
+ raw: string | undefined,
157
+ ): WorkflowRun["status"] | undefined {
158
+ if (raw === undefined) return undefined;
159
+ return (VALID_RUN_STATUSES as readonly string[]).includes(raw)
160
+ ? (raw as WorkflowRun["status"])
161
+ : undefined;
162
+ }
163
+
164
+ function handleListRuns(queryParams: Record<string, string>) {
165
+ assertFlagEnabled();
166
+ const limit = parseLimit(queryParams.limit);
167
+ const status = parseStatus(queryParams.status);
168
+ const runs = deps.getManager().list({
169
+ ...(limit !== undefined ? { limit } : {}),
170
+ ...(status !== undefined ? { status } : {}),
171
+ });
172
+ return { runs: runs.map(toWireRun) };
173
+ }
174
+
175
+ function handleGetRun(id: string) {
176
+ assertFlagEnabled();
177
+ const run = deps.getManager().status(id);
178
+ if (!run) {
179
+ throw new NotFoundError(`Workflow run ${id} not found`);
180
+ }
181
+ return toWireRun(run);
182
+ }
183
+
184
+ function handleAbortRun(id: string) {
185
+ assertFlagEnabled();
186
+ // status() is the source of truth for existence; abort() itself is a no-op
187
+ // for unknown/finished runs, so 404 on an unknown id is surfaced here.
188
+ const manager = deps.getManager();
189
+ if (!manager.status(id)) {
190
+ throw new NotFoundError(`Workflow run ${id} not found`);
191
+ }
192
+ manager.abort(id);
193
+ return { ok: true, runId: id };
194
+ }
195
+
196
+ async function handleResumeRun(id: string) {
197
+ assertFlagEnabled();
198
+ const manager = deps.getManager();
199
+ // status() is the source of truth for existence, so 404 an unknown id here
200
+ // (matching abort) rather than letting resume's "not_found" reach the client
201
+ // as a 409.
202
+ const run = manager.status(id);
203
+ if (!run) {
204
+ throw new NotFoundError(`Workflow run ${id} not found`);
205
+ }
206
+ // Consent gate. Resuming restarts the run's unfinished leaves, which execute
207
+ // the side-effecting tools/host functions the original manifest granted. The
208
+ // conversational `manage_workflows` resume path promotes such resumes to a
209
+ // fresh interactive approval (executor.ts → requireFreshApproval); this
210
+ // HTTP/IPC route (and the `vellum workflows resume` CLI on top of it) has no
211
+ // prompt channel. At full-access posture the user has opted into
212
+ // auto-approving even high-risk tools, so the resume proceeds without a
213
+ // prompt; otherwise it must not silently bypass consent — refuse and direct
214
+ // the caller to the assistant. A read-only run (no declared tools/host
215
+ // functions) resumes freely regardless of posture.
216
+ if (manifestGrantsSideEffects(run.capabilities)) {
217
+ const threshold = await deps.getAutoApproveThreshold(
218
+ run.conversationId ?? undefined,
219
+ "conversation",
220
+ );
221
+ if (!isFullAccessThreshold(threshold)) {
222
+ throw new ForbiddenError(
223
+ `Workflow run ${id} was granted side-effecting capabilities; resuming it ` +
224
+ `can restart steps that perform those side effects. Resume it through ` +
225
+ `the assistant (which will ask for approval) instead of this route.`,
226
+ );
227
+ }
228
+ }
229
+ try {
230
+ const { runId } = manager.resume(id);
231
+ return { ok: true, runId };
232
+ } catch (err) {
233
+ if (err instanceof WorkflowResumeNotPossibleError) {
234
+ if (err.reason === "not_found") {
235
+ throw new NotFoundError(`Workflow run ${id} not found`);
236
+ }
237
+ // not_interrupted / in_flight — the run exists but isn't resumable now.
238
+ throw new ConflictError(err.message);
239
+ }
240
+ if (err instanceof WorkflowRunCapError) {
241
+ throw new TooManyRequestsError(err.message);
242
+ }
243
+ throw err;
244
+ }
245
+ }
246
+
247
+ function handleListSavedWorkflows() {
248
+ assertFlagEnabled();
249
+ return { workflows: deps.listWorkflows() };
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Shared route definitions (HTTP + IPC)
254
+ // ---------------------------------------------------------------------------
255
+
256
+ export const ROUTES: RouteDefinition[] = [
257
+ {
258
+ operationId: "listWorkflowRuns",
259
+ endpoint: "workflows/runs",
260
+ method: "GET",
261
+ policy: {
262
+ requiredScopes: ["settings.read"],
263
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
264
+ },
265
+ summary: "List workflow runs",
266
+ description: "Return recent workflow runs, newest first.",
267
+ tags: ["workflows"],
268
+ queryParams: [
269
+ {
270
+ name: "limit",
271
+ schema: { type: "integer" },
272
+ description: "Max runs to return (default 50, max 200)",
273
+ },
274
+ {
275
+ name: "status",
276
+ schema: { type: "string" },
277
+ description:
278
+ "Filter by run status (running, completed, failed, aborted, cap_exceeded, interrupted).",
279
+ },
280
+ ],
281
+ responseBody: z.object({
282
+ runs: z.array(workflowRunSchema).describe("Workflow run objects"),
283
+ }),
284
+ handler: ({ queryParams }: RouteHandlerArgs) =>
285
+ handleListRuns(queryParams ?? {}),
286
+ },
287
+ {
288
+ operationId: "getWorkflowRun",
289
+ endpoint: "workflows/runs/:id",
290
+ method: "GET",
291
+ policy: {
292
+ requiredScopes: ["settings.read"],
293
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
294
+ },
295
+ summary: "Get workflow run",
296
+ description: "Return a single workflow run by ID.",
297
+ tags: ["workflows"],
298
+ responseBody: workflowRunSchema,
299
+ additionalResponses: { "404": { description: "Run not found" } },
300
+ handler: ({ pathParams }: RouteHandlerArgs) => handleGetRun(pathParams!.id),
301
+ },
302
+ {
303
+ operationId: "abortWorkflowRun",
304
+ endpoint: "workflows/runs/:id/abort",
305
+ method: "POST",
306
+ policy: {
307
+ requiredScopes: ["settings.write"],
308
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
309
+ },
310
+ summary: "Abort workflow run",
311
+ description: "Signal an in-flight workflow run to abort.",
312
+ tags: ["workflows"],
313
+ responseBody: z.object({
314
+ ok: z.boolean(),
315
+ runId: z.string(),
316
+ }),
317
+ additionalResponses: { "404": { description: "Run not found" } },
318
+ handler: ({ pathParams }: RouteHandlerArgs) =>
319
+ handleAbortRun(pathParams!.id),
320
+ },
321
+ {
322
+ operationId: "resumeWorkflowRun",
323
+ endpoint: "workflows/runs/:id/resume",
324
+ method: "POST",
325
+ policy: {
326
+ requiredScopes: ["settings.write"],
327
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
328
+ },
329
+ summary: "Resume workflow run",
330
+ description:
331
+ "Resume an interrupted workflow run (one orphaned by an assistant restart), replaying its journaled prefix and continuing from the first unfinished step.",
332
+ tags: ["workflows"],
333
+ responseBody: z.object({
334
+ ok: z.boolean(),
335
+ runId: z.string(),
336
+ }),
337
+ additionalResponses: {
338
+ "404": { description: "Run not found" },
339
+ "409": { description: "Run is not resumable (not interrupted)" },
340
+ "429": { description: "Concurrent-run cap reached" },
341
+ },
342
+ handler: ({ pathParams }: RouteHandlerArgs) =>
343
+ handleResumeRun(pathParams!.id),
344
+ },
345
+ {
346
+ operationId: "listSavedWorkflows",
347
+ endpoint: "workflows",
348
+ method: "GET",
349
+ policy: {
350
+ requiredScopes: ["settings.read"],
351
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
352
+ },
353
+ summary: "List saved workflows",
354
+ description: "Return the saved (named) workflows the assistant can run.",
355
+ tags: ["workflows"],
356
+ responseBody: z.object({
357
+ workflows: z
358
+ .array(savedWorkflowSchema)
359
+ .describe("Saved workflow entries"),
360
+ }),
361
+ handler: () => handleListSavedWorkflows(),
362
+ },
363
+ ];
@@ -5,7 +5,15 @@
5
5
  * directory listing, file metadata, write/mkdir/rename/delete, and raw
6
6
  * content serving with range support (HTTP-only).
7
7
  */
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import {
9
+ existsSync,
10
+ linkSync,
11
+ mkdirSync,
12
+ readdirSync,
13
+ readFileSync,
14
+ symlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
9
17
  import { join } from "node:path";
10
18
  import { beforeAll, describe, expect, test } from "bun:test";
11
19
 
@@ -676,6 +684,58 @@ describe("POST /v1/workspace/rename", () => {
676
684
  ).toThrow(ConflictError);
677
685
  });
678
686
 
687
+ // Hard links share an inode while being distinct entries, and POSIX
688
+ // rename() between two hard links is a silent no-op — must 409 instead
689
+ // of reporting a successful rename that never happened.
690
+ test("throws ConflictError when renaming a hard link over its sibling link", () => {
691
+ writeFileSync(join(testWorkspaceDir, "hard-a.txt"), "hard");
692
+ linkSync(
693
+ join(testWorkspaceDir, "hard-a.txt"),
694
+ join(testWorkspaceDir, "hard-b.txt"),
695
+ );
696
+
697
+ expect(() =>
698
+ handler({
699
+ body: { oldPath: "hard-a.txt", newPath: "hard-b.txt" },
700
+ }),
701
+ ).toThrow(ConflictError);
702
+ });
703
+
704
+ // statSync would follow both links to the shared target and treat them as
705
+ // the same entry, letting the rename clobber a real, separate symlink.
706
+ test("throws ConflictError when renaming a symlink over a sibling symlink to the same target", () => {
707
+ writeFileSync(join(testWorkspaceDir, "link-target.txt"), "target");
708
+ symlinkSync(
709
+ join(testWorkspaceDir, "link-target.txt"),
710
+ join(testWorkspaceDir, "link-a.txt"),
711
+ );
712
+ symlinkSync(
713
+ join(testWorkspaceDir, "link-target.txt"),
714
+ join(testWorkspaceDir, "link-b.txt"),
715
+ );
716
+
717
+ expect(() =>
718
+ handler({
719
+ body: { oldPath: "link-a.txt", newPath: "link-b.txt" },
720
+ }),
721
+ ).toThrow(ConflictError);
722
+ });
723
+
724
+ // On case-insensitive filesystems (macOS default) the destination "exists"
725
+ // because it resolves to the source itself — must rename, not conflict.
726
+ test("allows case-only rename of the same file", () => {
727
+ const srcPath = join(testWorkspaceDir, "CaseFile.txt");
728
+ writeFileSync(srcPath, "case test");
729
+
730
+ const result = handler({
731
+ body: { oldPath: "CaseFile.txt", newPath: "casefile.txt" },
732
+ }) as { oldPath: string; newPath: string };
733
+ expect(result.newPath).toBe("casefile.txt");
734
+ const names = readdirSync(testWorkspaceDir);
735
+ expect(names).toContain("casefile.txt");
736
+ expect(names).not.toContain("CaseFile.txt");
737
+ });
738
+
679
739
  test("rejects path traversal on oldPath", () => {
680
740
  expect(() =>
681
741
  handler({
@@ -6,9 +6,11 @@
6
6
  import {
7
7
  type Dirent,
8
8
  existsSync,
9
+ lstatSync,
9
10
  mkdirSync,
10
11
  readdirSync,
11
12
  readFileSync,
13
+ realpathSync,
12
14
  renameSync,
13
15
  rmSync,
14
16
  statSync,
@@ -448,6 +450,29 @@ function handleWorkspaceMkdir({ body, headers }: RouteHandlerArgs) {
448
450
  // POST /v1/workspace/rename — rename/move files and directories
449
451
  // ---------------------------------------------------------------------------
450
452
 
453
+ /**
454
+ * On case-insensitive filesystems (macOS/iOS defaults) a case-only rename's
455
+ * destination "exists" because it resolves to the source itself. Treat only
456
+ * true path aliases of one directory entry as the same file:
457
+ * - lstat, not stat, so two symlinks to one target stay distinct entries;
458
+ * - matching inodes alone are not enough — hard links share an inode while
459
+ * being distinct entries, and POSIX rename() between two hard links is a
460
+ * silent no-op. realpath canonicalizes case/normalization aliases of the
461
+ * same entry to one string, while distinct entries keep distinct names.
462
+ */
463
+ function isSamePathAlias(a: string, b: string): boolean {
464
+ try {
465
+ const statA = lstatSync(a);
466
+ const statB = lstatSync(b);
467
+ if (statA.dev !== statB.dev || statA.ino !== statB.ino) {
468
+ return false;
469
+ }
470
+ return realpathSync(a) === realpathSync(b);
471
+ } catch {
472
+ return false;
473
+ }
474
+ }
475
+
451
476
  function handleWorkspaceRename({ body, headers }: RouteHandlerArgs) {
452
477
  const oldPath = body?.oldPath as string | undefined;
453
478
  const newPath = body?.newPath as string | undefined;
@@ -474,7 +499,7 @@ function handleWorkspaceRename({ body, headers }: RouteHandlerArgs) {
474
499
  throw new NotFoundError("Source path not found");
475
500
  }
476
501
 
477
- if (existsSync(resolvedNew)) {
502
+ if (existsSync(resolvedNew) && !isSamePathAlias(resolvedOld, resolvedNew)) {
478
503
  throw new ConflictError("Destination already exists");
479
504
  }
480
505
 
@@ -365,6 +365,44 @@ describe("analyzeConversation", () => {
365
365
  expect(mockAddMessage).not.toHaveBeenCalled();
366
366
  });
367
367
 
368
+ test.each(["memory-retrospective", "memory-retrospective-fork"])(
369
+ "auto: rejects when the source conversation is a %s conversation",
370
+ async (source) => {
371
+ // Fork-kind retrospective conversations carry a full copy of the
372
+ // source conversation's history; auto-analyzing one would re-process
373
+ // the entire source conversation and double-write memory.
374
+ mockGetConversationSource.mockImplementation(() => source);
375
+
376
+ const result = await analyzeConversation("conv-1", {
377
+ trigger: "auto",
378
+ });
379
+
380
+ expect("error" in result).toBe(true);
381
+ if (!("error" in result)) throw new Error("expected error");
382
+ expect(result.error.kind).toBe("BAD_REQUEST");
383
+ expect(result.error.status).toBe(400);
384
+
385
+ // Nothing downstream of the guard should have fired.
386
+ expect(mockFindAnalysisConversationFor).not.toHaveBeenCalled();
387
+ expect(mockCreateConversation).not.toHaveBeenCalled();
388
+ expect(mockAddMessage).not.toHaveBeenCalled();
389
+ },
390
+ );
391
+
392
+ test("manual: does NOT reject a memory-retrospective conversation (guard is auto-only)", async () => {
393
+ mockGetConversationSource.mockImplementation(
394
+ () => "memory-retrospective-fork",
395
+ );
396
+
397
+ const result = await analyzeConversation("conv-1", {
398
+ trigger: "manual",
399
+ });
400
+
401
+ expect("error" in result).toBe(false);
402
+ if ("error" in result) throw new Error("expected success");
403
+ expect(result.analysisConversationId).toBe("analysis-new");
404
+ });
405
+
368
406
  test("auto: routes the agent loop through callSite: 'analyzeConversation'", async () => {
369
407
  await analyzeConversation("conv-1", { trigger: "auto" });
370
408
 
@@ -34,6 +34,7 @@ import {
34
34
  getMessages,
35
35
  } from "../../memory/conversation-crud.js";
36
36
  import { resolveConversationId } from "../../memory/conversation-key-store.js";
37
+ import { isMemoryRetrospectiveSource } from "../../memory/memory-retrospective-constants.js";
37
38
  import { getLogger } from "../../util/logger.js";
38
39
  import { assistantEventHub, broadcastMessage } from "../assistant-event-hub.js";
39
40
  import {
@@ -118,19 +119,31 @@ export async function analyzeConversation(
118
119
 
119
120
  // e. Defense-in-depth recursion guard for auto mode: refuse to
120
121
  // auto-analyze a conversation that is itself an auto-analysis
121
- // conversation. Prevents job-handler bugs from triggering runaway
122
- // self-analysis loops.
123
- if (
124
- opts.trigger === "auto" &&
125
- getConversationSource(resolvedId) === AUTO_ANALYSIS_SOURCE
126
- ) {
127
- return {
128
- error: {
129
- kind: "BAD_REQUEST",
130
- status: 400,
131
- message: "Cannot auto-analyze an auto-analysis conversation",
132
- },
133
- };
122
+ // conversation (prevents job-handler bugs from triggering runaway
123
+ // self-analysis loops) or a memory-retrospective conversation
124
+ // (fork-kind retrospectives carry a full copy of the source history,
125
+ // so auto-analyzing one would re-process the entire source
126
+ // conversation and double-write memory).
127
+ if (opts.trigger === "auto") {
128
+ const source = getConversationSource(resolvedId);
129
+ if (source === AUTO_ANALYSIS_SOURCE) {
130
+ return {
131
+ error: {
132
+ kind: "BAD_REQUEST",
133
+ status: 400,
134
+ message: "Cannot auto-analyze an auto-analysis conversation",
135
+ },
136
+ };
137
+ }
138
+ if (source !== null && isMemoryRetrospectiveSource(source)) {
139
+ return {
140
+ error: {
141
+ kind: "BAD_REQUEST",
142
+ status: 400,
143
+ message: "Cannot auto-analyze a memory-retrospective conversation",
144
+ },
145
+ };
146
+ }
134
147
  }
135
148
 
136
149
  // f. Build the analysis transcript
@@ -0,0 +1,28 @@
1
+ import { getConfigReadOnly } from "../config/loader.js";
2
+
3
+ /**
4
+ * Validate a schedule's inference-profile key against the configured
5
+ * `llm.profiles` catalog. Returns a user-facing error message when the key is
6
+ * empty or unknown, or `null` when valid.
7
+ *
8
+ * Only create/update paths validate — a profile deleted after a schedule was
9
+ * created degrades gracefully at run time (the resolver silently drops a
10
+ * missing `overrideProfile` reference and falls through to the defaults).
11
+ */
12
+ export function validateScheduleInferenceProfile(
13
+ profile: string,
14
+ ): string | null {
15
+ if (!profile.trim()) {
16
+ return "inferenceProfile must be a non-empty string";
17
+ }
18
+ const profiles = getConfigReadOnly().llm?.profiles ?? {};
19
+ if (!Object.prototype.hasOwnProperty.call(profiles, profile)) {
20
+ const available = Object.keys(profiles).sort();
21
+ const hint =
22
+ available.length > 0
23
+ ? ` Available profiles: ${available.join(", ")}.`
24
+ : " No profiles defined in llm.profiles.";
25
+ return `Inference profile "${profile}" is not defined in llm.profiles.${hint}`;
26
+ }
27
+ return null;
28
+ }