@vellumai/assistant 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (630) hide show
  1. package/ARCHITECTURE.md +13 -19
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +17 -0
  5. package/docker-init-apt-root.sh +167 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +642 -5
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  21. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  23. package/src/__tests__/anthropic-provider.test.ts +45 -0
  24. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  25. package/src/__tests__/app-executors.test.ts +220 -4
  26. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  27. package/src/__tests__/bundled-asset.test.ts +6 -6
  28. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  29. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  30. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  31. package/src/__tests__/clawhub.test.ts +75 -16
  32. package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
  33. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  34. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  35. package/src/__tests__/config-schema.test.ts +21 -0
  36. package/src/__tests__/config-set-route.test.ts +80 -0
  37. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  38. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  39. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  40. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  41. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  42. package/src/__tests__/context-token-estimator.test.ts +31 -65
  43. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  44. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  45. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  46. package/src/__tests__/conversation-agent-loop.test.ts +59 -1
  47. package/src/__tests__/conversation-error.test.ts +42 -3
  48. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  49. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  50. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  51. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  52. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  53. package/src/__tests__/conversation-pairing.test.ts +54 -0
  54. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  55. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  56. package/src/__tests__/conversation-queue.test.ts +4 -1
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
  58. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  59. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  60. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  61. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  64. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  65. package/src/__tests__/date-context.test.ts +45 -0
  66. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  67. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  68. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  69. package/src/__tests__/dm-backfill.test.ts +121 -10
  70. package/src/__tests__/document-tool-security.test.ts +258 -0
  71. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  72. package/src/__tests__/edit-propagation.test.ts +33 -0
  73. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  74. package/src/__tests__/external-plugin-loader.test.ts +151 -55
  75. package/src/__tests__/filing-service.test.ts +140 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  77. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  78. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  79. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  80. package/src/__tests__/heartbeat-service.test.ts +24 -164
  81. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  82. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  83. package/src/__tests__/helpers/wait-for.ts +21 -0
  84. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  85. package/src/__tests__/history-repair.test.ts +73 -0
  86. package/src/__tests__/host-app-control-proxy.test.ts +507 -10
  87. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  88. package/src/__tests__/image-credentials.test.ts +1 -1
  89. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  90. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  91. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  92. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  93. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  94. package/src/__tests__/injector-background-turn.test.ts +153 -0
  95. package/src/__tests__/injector-chain.test.ts +15 -8
  96. package/src/__tests__/install-skill-routing.test.ts +155 -37
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
  98. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  99. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  100. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  101. package/src/__tests__/llm-catalog-parity.test.ts +58 -13
  102. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  103. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  104. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
  105. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  106. package/src/__tests__/llm-resolver.test.ts +255 -2
  107. package/src/__tests__/llm-usage-store.test.ts +114 -0
  108. package/src/__tests__/managed-profile-guard.test.ts +41 -29
  109. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  110. package/src/__tests__/managed-store.test.ts +84 -192
  111. package/src/__tests__/media-generate-image.test.ts +1 -1
  112. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  113. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  114. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  115. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  116. package/src/__tests__/notification-deep-link.test.ts +15 -0
  117. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  118. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  119. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  120. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  121. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  122. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  123. package/src/__tests__/openai-provider.test.ts +242 -3
  124. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  125. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  126. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  127. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  128. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  129. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
  130. package/src/__tests__/platform.test.ts +2 -0
  131. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  132. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  133. package/src/__tests__/plugin-external-api.test.ts +68 -0
  134. package/src/__tests__/plugin-registry.test.ts +0 -77
  135. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  136. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  137. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  138. package/src/__tests__/plugin-types.test.ts +3 -13
  139. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  140. package/src/__tests__/process-message-display-content.test.ts +421 -0
  141. package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  143. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
  144. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  145. package/src/__tests__/schedule-routes.test.ts +50 -3
  146. package/src/__tests__/schedule-store.test.ts +94 -0
  147. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  148. package/src/__tests__/schema-transforms.test.ts +20 -0
  149. package/src/__tests__/search-skills-unified.test.ts +0 -5
  150. package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
  151. package/src/__tests__/server-history-render.test.ts +43 -0
  152. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  153. package/src/__tests__/skill-load-tool.test.ts +27 -89
  154. package/src/__tests__/skill-memory.test.ts +23 -3
  155. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  156. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  157. package/src/__tests__/skills-install-extract.test.ts +49 -38
  158. package/src/__tests__/skills-install-staging.test.ts +159 -0
  159. package/src/__tests__/skills-uninstall.test.ts +9 -41
  160. package/src/__tests__/skills.test.ts +51 -58
  161. package/src/__tests__/slack-channel-config.test.ts +9 -0
  162. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  163. package/src/__tests__/system-prompt.test.ts +670 -63
  164. package/src/__tests__/terminal-tools.test.ts +28 -1
  165. package/src/__tests__/thread-backfill.test.ts +557 -27
  166. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  167. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  168. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  169. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  170. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  171. package/src/__tests__/tool-executor.test.ts +16 -4
  172. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  173. package/src/__tests__/turn-events-store.test.ts +256 -0
  174. package/src/__tests__/twilio-routes.test.ts +4 -0
  175. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  176. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  177. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  178. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  179. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  180. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  181. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  182. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  183. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  184. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  185. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  186. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  187. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  188. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  189. package/src/a2a/__tests__/task-store.test.ts +246 -0
  190. package/src/a2a/agent-card.ts +58 -0
  191. package/src/a2a/feature-gate.ts +8 -0
  192. package/src/a2a/protocol-constants.ts +21 -0
  193. package/src/a2a/protocol-errors.ts +50 -0
  194. package/src/a2a/protocol-types.ts +162 -0
  195. package/src/a2a/task-store.ts +168 -0
  196. package/src/acp/resolve-agent.ts +1 -1
  197. package/src/agent/image-optimize.ts +13 -5
  198. package/src/agent/loop.ts +167 -18
  199. package/src/calls/voice-session-bridge.ts +61 -42
  200. package/src/channels/config.ts +9 -0
  201. package/src/channels/types.ts +122 -0
  202. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  203. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  204. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  205. package/src/cli/commands/__tests__/schedules.test.ts +960 -0
  206. package/src/cli/commands/changelog.ts +106 -42
  207. package/src/cli/commands/conversations.ts +102 -17
  208. package/src/cli/commands/default-action.ts +10 -53
  209. package/src/cli/commands/notifications.ts +388 -346
  210. package/src/cli/commands/plugins.ts +252 -0
  211. package/src/cli/commands/schedules.ts +683 -0
  212. package/src/cli/commands/telemetry.ts +40 -0
  213. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  214. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  215. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  216. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  217. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  218. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  219. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  220. package/src/cli/lib/cli-colors.ts +12 -0
  221. package/src/cli/lib/confirm-prompt.ts +79 -0
  222. package/src/cli/lib/install-from-github.ts +303 -0
  223. package/src/cli/lib/list-installed-plugins.ts +137 -0
  224. package/src/cli/lib/search-plugins.ts +163 -0
  225. package/src/cli/lib/uninstall-plugin.ts +82 -0
  226. package/src/cli/lib/unknown-command.ts +111 -0
  227. package/src/cli/program.ts +52 -2
  228. package/src/config/assistant-feature-flags.ts +24 -54
  229. package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
  230. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  231. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  232. package/src/config/bundled-skills/document/SKILL.md +23 -3
  233. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  234. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  235. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  236. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  237. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  238. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  239. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  240. package/src/config/bundled-tool-registry.ts +6 -0
  241. package/src/config/call-site-defaults.ts +105 -0
  242. package/src/config/feature-flag-registry.json +41 -9
  243. package/src/config/llm-resolver.ts +52 -1
  244. package/src/config/loader.ts +64 -38
  245. package/src/config/schema.ts +9 -10
  246. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  247. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  248. package/src/config/schemas/channels.ts +17 -0
  249. package/src/config/schemas/compaction.ts +28 -0
  250. package/src/config/schemas/conversations.ts +10 -0
  251. package/src/config/schemas/heartbeat.ts +23 -0
  252. package/src/config/schemas/llm-request-logs.ts +31 -7
  253. package/src/config/schemas/llm.ts +1 -0
  254. package/src/config/schemas/memory-retrieval.ts +18 -0
  255. package/src/config/schemas/memory-retrospective.ts +1 -1
  256. package/src/config/schemas/memory-v2.ts +4 -4
  257. package/src/config/schemas/memory.ts +3 -1
  258. package/src/config/schemas/tools.ts +14 -0
  259. package/src/config/seed-inference-profiles.ts +99 -29
  260. package/src/config/skills.ts +3 -96
  261. package/src/context/compactor.ts +1107 -0
  262. package/src/context/token-estimator.ts +34 -36
  263. package/src/context/window-manager.ts +197 -1520
  264. package/src/credential-execution/managed-catalog.ts +37 -0
  265. package/src/credential-health/credential-health-service.ts +280 -19
  266. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
  267. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  268. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  269. package/src/daemon/approval-generators.ts +8 -6
  270. package/src/daemon/config-watcher.ts +94 -31
  271. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  272. package/src/daemon/conversation-agent-loop.ts +198 -11
  273. package/src/daemon/conversation-error.ts +171 -37
  274. package/src/daemon/conversation-lifecycle.ts +53 -40
  275. package/src/daemon/conversation-messaging.ts +25 -6
  276. package/src/daemon/conversation-process.ts +49 -12
  277. package/src/daemon/conversation-runtime-assembly.ts +25 -1
  278. package/src/daemon/conversation-slash.ts +12 -5
  279. package/src/daemon/conversation-store.ts +11 -4
  280. package/src/daemon/conversation-tool-setup.ts +39 -7
  281. package/src/daemon/conversation.ts +33 -8
  282. package/src/daemon/date-context.ts +40 -0
  283. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  284. package/src/daemon/first-greeting.ts +22 -2
  285. package/src/daemon/guardian-action-generators.ts +1 -125
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  287. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  288. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  289. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  290. package/src/daemon/handlers/config-a2a.ts +289 -0
  291. package/src/daemon/handlers/config-model.ts +6 -5
  292. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  293. package/src/daemon/handlers/conversations.ts +1 -0
  294. package/src/daemon/handlers/shared.ts +14 -5
  295. package/src/daemon/handlers/skills.ts +111 -108
  296. package/src/daemon/history-repair.ts +28 -1
  297. package/src/daemon/host-app-control-proxy.ts +153 -27
  298. package/src/daemon/host-proxy-preactivation.ts +85 -18
  299. package/src/daemon/lifecycle.ts +89 -91
  300. package/src/daemon/meet-host-supervisor.ts +5 -4
  301. package/src/daemon/memory-v2-startup.ts +85 -0
  302. package/src/daemon/message-protocol.ts +1 -0
  303. package/src/daemon/message-types/conversations.ts +25 -0
  304. package/src/daemon/message-types/messages.ts +61 -0
  305. package/src/daemon/message-types/notifications.ts +21 -0
  306. package/src/daemon/message-types/subagents.ts +1 -0
  307. package/src/daemon/message-types/sync.ts +1 -0
  308. package/src/daemon/pkb-reminder-builder.test.ts +11 -54
  309. package/src/daemon/pkb-reminder-builder.ts +5 -20
  310. package/src/daemon/plugin-source-watcher.ts +146 -0
  311. package/src/daemon/process-message.ts +24 -3
  312. package/src/daemon/server.ts +11 -2
  313. package/src/daemon/skill-memory-refresh.ts +33 -0
  314. package/src/daemon/wake-target-adapter.ts +2 -0
  315. package/src/documents/document-store.ts +221 -3
  316. package/src/embedded/plugin-api.ts +40 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  318. package/src/export/transcript-formatter.ts +54 -20
  319. package/src/filing/filing-service.ts +39 -0
  320. package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
  321. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  322. package/src/heartbeat/heartbeat-service.ts +73 -189
  323. package/src/home/__tests__/feed-types.test.ts +80 -0
  324. package/src/home/feed-types.ts +36 -2
  325. package/src/home/post-connect-feed.ts +1 -0
  326. package/src/index.ts +18 -1
  327. package/src/ipc/cli-client.ts +147 -45
  328. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  329. package/src/mcp/client.ts +20 -4
  330. package/src/media/image-credentials.ts +3 -3
  331. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  332. package/src/memory/__tests__/conversation-queries.test.ts +483 -0
  333. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  334. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  335. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  336. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  337. package/src/memory/__tests__/message-content.test.ts +35 -0
  338. package/src/memory/bookmark-crud.ts +42 -10
  339. package/src/memory/context-search/sources/conversations.ts +62 -2
  340. package/src/memory/context-search/sources/workspace.ts +4 -0
  341. package/src/memory/conversation-crud.ts +63 -19
  342. package/src/memory/conversation-queries.ts +197 -11
  343. package/src/memory/conversation-title-service.ts +26 -4
  344. package/src/memory/db-init.ts +12 -0
  345. package/src/memory/delivery-crud.ts +152 -5
  346. package/src/memory/embedding-backend.ts +4 -4
  347. package/src/memory/external-conversation-store.ts +66 -5
  348. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
  349. package/src/memory/graph/conversation-graph-memory.ts +49 -21
  350. package/src/memory/graph/tools.ts +9 -40
  351. package/src/memory/indexer.ts +34 -29
  352. package/src/memory/invite-store.ts +53 -0
  353. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  354. package/src/memory/jobs/embed-concept-page.ts +20 -11
  355. package/src/memory/jobs-worker.ts +6 -1
  356. package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
  357. package/src/memory/llm-request-log-source.ts +19 -52
  358. package/src/memory/llm-request-log-store.ts +92 -1
  359. package/src/memory/llm-usage-store.ts +125 -5
  360. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  361. package/src/memory/memory-retrospective-job.ts +33 -6
  362. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  363. package/src/memory/message-content.ts +1 -1
  364. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  365. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  366. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  367. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  368. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  369. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  370. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  371. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  372. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  373. package/src/memory/migrations/index.ts +9 -0
  374. package/src/memory/migrations/registry.ts +16 -0
  375. package/src/memory/onboarding-events-store.ts +106 -0
  376. package/src/memory/schema/a2a.ts +15 -0
  377. package/src/memory/schema/bookmarks.ts +0 -2
  378. package/src/memory/schema/calls.ts +1 -0
  379. package/src/memory/schema/index.ts +1 -0
  380. package/src/memory/schema/inference.ts +3 -3
  381. package/src/memory/schema/infrastructure.ts +13 -0
  382. package/src/memory/turn-events-store.ts +127 -2
  383. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  384. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  385. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  386. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  387. package/src/memory/v2/__tests__/injection.test.ts +288 -11
  388. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  389. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  390. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  391. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  392. package/src/memory/v2/__tests__/router.test.ts +15 -0
  393. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  394. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  395. package/src/memory/v2/activation-store.ts +14 -16
  396. package/src/memory/v2/cli-command-content.ts +19 -0
  397. package/src/memory/v2/cli-command-store.ts +304 -0
  398. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  399. package/src/memory/v2/injection.ts +81 -26
  400. package/src/memory/v2/migration.ts +49 -19
  401. package/src/memory/v2/page-index.ts +63 -8
  402. package/src/memory/v2/prompts/router.ts +11 -8
  403. package/src/memory/v2/prompts/sweep.ts +2 -2
  404. package/src/memory/v2/qdrant.ts +135 -7
  405. package/src/memory/v2/router.ts +9 -8
  406. package/src/memory/v2/skill-store.ts +120 -35
  407. package/src/memory/v2/static-context.ts +4 -4
  408. package/src/memory/v2/types.ts +23 -0
  409. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  410. package/src/messaging/providers/a2a/deliver.ts +156 -0
  411. package/src/messaging/providers/gmail/client.ts +9 -2
  412. package/src/messaging/providers/index.ts +11 -2
  413. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  414. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  415. package/src/messaging/providers/slack/adapter.ts +43 -5
  416. package/src/messaging/providers/slack/client.ts +27 -0
  417. package/src/messaging/providers/slack/deep-link.ts +65 -0
  418. package/src/messaging/providers/slack/download.ts +104 -0
  419. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  420. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  421. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  422. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  423. package/src/messaging/providers/slack/types.ts +20 -1
  424. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  425. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  426. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  427. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  428. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  429. package/src/notifications/adapters/macos.ts +12 -2
  430. package/src/notifications/broadcaster.ts +29 -4
  431. package/src/notifications/conversation-pairing.ts +2 -1
  432. package/src/notifications/copy-composer.ts +17 -64
  433. package/src/notifications/decision-engine.ts +113 -45
  434. package/src/notifications/deterministic-checks.ts +96 -0
  435. package/src/notifications/emit-signal.ts +21 -1
  436. package/src/notifications/home-feed-side-effect.ts +138 -5
  437. package/src/notifications/signal.ts +3 -5
  438. package/src/notifications/types.ts +8 -0
  439. package/src/oauth/connection-resolver.ts +8 -4
  440. package/src/oauth/platform-connection.test.ts +43 -3
  441. package/src/oauth/platform-connection.ts +19 -6
  442. package/src/oauth/seed-providers.ts +10 -1
  443. package/src/permissions/checker.ts +2 -0
  444. package/src/permissions/ipc-risk-types.ts +1 -0
  445. package/src/permissions/question-prompter.test.ts +416 -0
  446. package/src/permissions/question-prompter.ts +294 -0
  447. package/src/platform/client.test.ts +1 -1
  448. package/src/platform/client.ts +1 -1
  449. package/src/plugin-api/constants.ts +26 -0
  450. package/src/plugin-api/index.ts +34 -1
  451. package/src/plugin-api/types.ts +104 -22
  452. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  453. package/src/plugins/defaults/compaction.ts +0 -4
  454. package/src/plugins/defaults/empty-response.ts +0 -2
  455. package/src/plugins/defaults/history-repair.ts +0 -2
  456. package/src/plugins/defaults/injectors.ts +74 -22
  457. package/src/plugins/defaults/llm-call.ts +0 -2
  458. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  459. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  460. package/src/plugins/defaults/persistence.ts +0 -2
  461. package/src/plugins/defaults/title-generate.ts +0 -5
  462. package/src/plugins/defaults/token-estimate.ts +0 -2
  463. package/src/plugins/defaults/tool-error.ts +0 -7
  464. package/src/plugins/defaults/tool-execute.ts +0 -2
  465. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  466. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  467. package/src/plugins/external-api.ts +104 -0
  468. package/src/plugins/external-plugin-loader.ts +187 -42
  469. package/src/plugins/feature-gate.ts +22 -0
  470. package/src/plugins/pipeline.ts +37 -0
  471. package/src/plugins/registry.ts +48 -80
  472. package/src/plugins/types.ts +40 -26
  473. package/src/plugins/user-loader.ts +21 -2
  474. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  475. package/src/proactive-artifact/job.test.ts +37 -5
  476. package/src/prompts/__tests__/system-prompt.test.ts +10 -43
  477. package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
  478. package/src/prompts/normalize-onboarding.ts +27 -0
  479. package/src/prompts/sections.ts +302 -0
  480. package/src/prompts/system-prompt.ts +63 -174
  481. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  482. package/src/prompts/templates/system-sections.ts +164 -0
  483. package/src/providers/__tests__/inference.test.ts +24 -7
  484. package/src/providers/anthropic/client.ts +28 -28
  485. package/src/providers/call-site-routing.ts +24 -6
  486. package/src/providers/connection-resolution.ts +68 -11
  487. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  488. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  489. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  490. package/src/providers/inference/adapter-factory.ts +32 -6
  491. package/src/providers/inference/auth.ts +12 -0
  492. package/src/providers/inference/backfill.ts +14 -1
  493. package/src/providers/inference/connections.ts +159 -34
  494. package/src/providers/inference/resolve-auth.ts +14 -4
  495. package/src/providers/model-catalog.ts +249 -12
  496. package/src/providers/model-intents.ts +3 -3
  497. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  498. package/src/providers/openai/chat-completions-provider.ts +169 -8
  499. package/src/providers/openrouter/client.ts +49 -4
  500. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
  501. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  502. package/src/providers/provider-availability.ts +17 -2
  503. package/src/providers/provider-catalog-visibility.ts +38 -0
  504. package/src/providers/provider-send-message.ts +27 -12
  505. package/src/providers/registry.ts +52 -15
  506. package/src/providers/retry.ts +47 -1
  507. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  508. package/src/runtime/agent-wake.ts +103 -15
  509. package/src/runtime/auth/route-policy.ts +21 -1
  510. package/src/runtime/btw-sidechain.ts +2 -0
  511. package/src/runtime/http-server.ts +7 -16
  512. package/src/runtime/http-types.ts +19 -47
  513. package/src/runtime/migrations/origin-mode.ts +1 -1
  514. package/src/runtime/pending-interactions.ts +1 -0
  515. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  516. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  517. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  518. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
  519. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  520. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  521. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  522. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  523. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  524. package/src/runtime/routes/acp-routes.ts +5 -3
  525. package/src/runtime/routes/auth-routes.ts +1 -1
  526. package/src/runtime/routes/bookmark-routes.ts +5 -3
  527. package/src/runtime/routes/btw-routes.ts +5 -1
  528. package/src/runtime/routes/channel-availability-routes.ts +126 -0
  529. package/src/runtime/routes/consolidation-routes.ts +100 -0
  530. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  531. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  532. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  533. package/src/runtime/routes/conversation-query-routes.ts +99 -35
  534. package/src/runtime/routes/conversation-routes.ts +97 -11
  535. package/src/runtime/routes/documents-routes.ts +25 -86
  536. package/src/runtime/routes/group-routes.ts +5 -0
  537. package/src/runtime/routes/inbound-conversation.ts +28 -8
  538. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  539. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  540. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  541. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  542. package/src/runtime/routes/index.ts +8 -0
  543. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  544. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  545. package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
  546. package/src/runtime/routes/integrations/a2a.ts +235 -0
  547. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  548. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  549. package/src/runtime/routes/integrations/twilio.ts +6 -13
  550. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  551. package/src/runtime/routes/notification-routes.ts +1 -1
  552. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  553. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  554. package/src/runtime/routes/question-routes.ts +259 -0
  555. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  556. package/src/runtime/routes/schedule-routes.ts +4 -7
  557. package/src/runtime/routes/subagents-routes.ts +98 -18
  558. package/src/runtime/routes/telemetry-routes.ts +27 -0
  559. package/src/runtime/routes/tts-routes.ts +27 -2
  560. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  561. package/src/runtime/routes/workspace-routes.ts +28 -0
  562. package/src/runtime/services/conversation-serializer.ts +39 -7
  563. package/src/runtime/sync/resource-sync-events.ts +93 -1
  564. package/src/schedule/schedule-store.ts +27 -2
  565. package/src/schedule/scheduler.ts +9 -1
  566. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  567. package/src/security/untrusted-content.ts +93 -8
  568. package/src/skills/catalog-files.ts +1 -1
  569. package/src/skills/catalog-install.ts +233 -116
  570. package/src/skills/clawhub.ts +70 -13
  571. package/src/skills/managed-store.ts +4 -119
  572. package/src/skills/skillssh-registry.ts +27 -48
  573. package/src/subagent/manager.ts +17 -7
  574. package/src/telemetry/types.ts +113 -1
  575. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  576. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  577. package/src/tools/apps/executors.ts +58 -7
  578. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  579. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  580. package/src/tools/browser/browser-execution.ts +15 -11
  581. package/src/tools/computer-use/definitions.ts +3 -3
  582. package/src/tools/credentials/vault.ts +1 -1
  583. package/src/tools/document/document-tool.ts +124 -1
  584. package/src/tools/filesystem/edit.ts +1 -1
  585. package/src/tools/filesystem/list.ts +1 -1
  586. package/src/tools/filesystem/read.ts +1 -1
  587. package/src/tools/filesystem/write.ts +5 -2
  588. package/src/tools/host-filesystem/transfer.ts +1 -1
  589. package/src/tools/host-terminal/host-shell.ts +1 -1
  590. package/src/tools/memory/register.ts +1 -9
  591. package/src/tools/permission-checker.ts +1 -1
  592. package/src/tools/registry.ts +17 -7
  593. package/src/tools/schedule/create.ts +2 -2
  594. package/src/tools/schema-transforms.ts +7 -2
  595. package/src/tools/side-effects.ts +1 -0
  596. package/src/tools/skills/delete-managed.ts +4 -4
  597. package/src/tools/skills/execute.ts +1 -1
  598. package/src/tools/skills/scaffold-managed.ts +3 -2
  599. package/src/tools/subagent/notify-parent.ts +1 -1
  600. package/src/tools/system/request-permission.ts +2 -2
  601. package/src/tools/terminal/safe-env.ts +60 -1
  602. package/src/tools/tool-manifest.ts +2 -0
  603. package/src/tools/types.ts +107 -21
  604. package/src/tools/ui-surface/definitions.ts +6 -5
  605. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  606. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  607. package/src/types/onboarding-context.ts +2 -0
  608. package/src/util/errors.ts +17 -0
  609. package/src/util/platform.ts +10 -0
  610. package/src/watcher/__tests__/engine.test.ts +22 -0
  611. package/src/watcher/engine.ts +6 -2
  612. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  613. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  614. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  615. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  616. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  617. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  618. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  619. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  620. package/src/workspace/migrations/registry.ts +10 -0
  621. package/src/workspace/migrations/runner.ts +39 -9
  622. package/src/workspace/migrations/types.ts +4 -0
  623. package/examples/plugins/echo/bun.lock +0 -25
  624. package/src/__tests__/context-window-manager.test.ts +0 -2481
  625. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  626. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  627. package/src/context/prompts/compact.md +0 -26
  628. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  629. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  630. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -0,0 +1,276 @@
1
+ import * as fs from "node:fs";
2
+ import {
3
+ basename,
4
+ dirname,
5
+ isAbsolute,
6
+ join,
7
+ normalize,
8
+ relative,
9
+ resolve,
10
+ } from "node:path";
11
+
12
+ import { getLogger } from "../../util/logger.js";
13
+ import type { WorkspaceMigration } from "./types.js";
14
+
15
+ const log = getLogger("workspace-migration-084-remove-legacy-skills-index");
16
+
17
+ function isNotFoundError(err: unknown): boolean {
18
+ return (
19
+ typeof err === "object" &&
20
+ err !== null &&
21
+ "code" in err &&
22
+ err.code === "ENOENT"
23
+ );
24
+ }
25
+
26
+ function isInsideDirectory(rootDir: string, candidatePath: string): boolean {
27
+ const rootRealPath = fs.realpathSync(rootDir);
28
+ const candidateRealPath = fs.realpathSync(candidatePath);
29
+ const relativePath = relative(rootRealPath, candidateRealPath);
30
+ return (
31
+ relativePath === "" ||
32
+ (!relativePath.startsWith("..") && !isAbsolute(relativePath))
33
+ );
34
+ }
35
+
36
+ function parseLegacySkillIndexEntry(line: string): string | null {
37
+ const match = line.match(/^\s*[-*]\s+(.+?)\s*$/);
38
+ if (!match) return null;
39
+
40
+ let entry = match[1].trim();
41
+ const markdownLink = entry.match(/^\[.+?\]\((.+?)\)$/);
42
+ if (markdownLink) {
43
+ entry = markdownLink[1].trim();
44
+ } else {
45
+ entry = entry.split(/\s+/)[0]?.trim() ?? "";
46
+ }
47
+
48
+ entry = entry.replace(/^`|`$/g, "");
49
+ if (!entry || entry.includes("\0") || isAbsolute(entry)) return null;
50
+
51
+ const normalized = normalize(entry);
52
+ if (
53
+ normalized === "." ||
54
+ normalized.startsWith("..") ||
55
+ isAbsolute(normalized)
56
+ ) {
57
+ return null;
58
+ }
59
+
60
+ if (basename(normalized).toLowerCase() === "skill.md") {
61
+ const skillDir = dirname(normalized);
62
+ return skillDir === "." ? null : skillDir;
63
+ }
64
+
65
+ return normalized;
66
+ }
67
+
68
+ function parseLegacySkillIndexEntries(contents: string): string[] {
69
+ const entries = new Set<string>();
70
+ for (const line of contents.split(/\r?\n/)) {
71
+ const entry = parseLegacySkillIndexEntry(line);
72
+ if (entry) entries.add(entry);
73
+ }
74
+ return [...entries];
75
+ }
76
+
77
+ function skillFileContentsMatch(
78
+ sourceDir: string,
79
+ destinationDir: string,
80
+ ): boolean {
81
+ try {
82
+ return (
83
+ fs.readFileSync(join(sourceDir, "SKILL.md"), "utf-8") ===
84
+ fs.readFileSync(join(destinationDir, "SKILL.md"), "utf-8")
85
+ );
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+
91
+ function topLevelPreservationName(relativeSkillDir: string): string {
92
+ return relativeSkillDir
93
+ .split(/[\\/]+/)
94
+ .filter(Boolean)
95
+ .join("__");
96
+ }
97
+
98
+ function getPreservationDestinationDir(
99
+ skillsDir: string,
100
+ sourceDir: string,
101
+ relativeSkillDir: string,
102
+ ): string | null {
103
+ const skillName = basename(relativeSkillDir);
104
+ if (!skillName || skillName === relativeSkillDir) return null;
105
+
106
+ const alternateName = topLevelPreservationName(relativeSkillDir);
107
+ const candidateNames = [
108
+ skillName,
109
+ ...(alternateName && alternateName !== skillName ? [alternateName] : []),
110
+ ];
111
+
112
+ for (const candidateName of candidateNames) {
113
+ const destinationDir = join(skillsDir, candidateName);
114
+ if (!fs.existsSync(destinationDir)) return destinationDir;
115
+ if (skillFileContentsMatch(sourceDir, destinationDir)) {
116
+ log.info(
117
+ { destinationDir, sourceDir },
118
+ "Nested indexed skill already preserved at top-level skills directory",
119
+ );
120
+ return null;
121
+ }
122
+ }
123
+
124
+ const baseName =
125
+ alternateName && alternateName !== skillName
126
+ ? alternateName
127
+ : `legacy__${skillName}`;
128
+ for (let suffix = 2; ; suffix += 1) {
129
+ const destinationDir = join(skillsDir, `${baseName}-${suffix}`);
130
+ if (!fs.existsSync(destinationDir)) return destinationDir;
131
+ if (skillFileContentsMatch(sourceDir, destinationDir)) {
132
+ log.info(
133
+ { destinationDir, sourceDir },
134
+ "Nested indexed skill already preserved at top-level skills directory",
135
+ );
136
+ return null;
137
+ }
138
+ }
139
+ }
140
+
141
+ function preserveNestedIndexedSkill(
142
+ tempRootDir: string,
143
+ skillsDir: string,
144
+ relativeSkillDir: string,
145
+ ): void {
146
+ const sourceDir = resolve(skillsDir, relativeSkillDir);
147
+ let destinationDir: string | null = null;
148
+
149
+ try {
150
+ if (!fs.existsSync(sourceDir)) return;
151
+ if (!isInsideDirectory(skillsDir, sourceDir)) {
152
+ log.warn(
153
+ { relativeSkillDir, sourceDir },
154
+ "Skipping nested indexed skill that resolves outside skills root",
155
+ );
156
+ return;
157
+ }
158
+
159
+ const sourceStat = fs.lstatSync(sourceDir);
160
+ if (!sourceStat.isDirectory()) return;
161
+
162
+ const skillFilePath = join(sourceDir, "SKILL.md");
163
+ if (!fs.existsSync(skillFilePath)) return;
164
+ if (!isInsideDirectory(sourceDir, skillFilePath)) {
165
+ log.warn(
166
+ { skillFilePath, sourceDir },
167
+ "Skipping nested indexed skill with SKILL.md outside skill directory",
168
+ );
169
+ return;
170
+ }
171
+
172
+ const skillFileStat = fs.lstatSync(skillFilePath);
173
+ if (!skillFileStat.isFile()) return;
174
+
175
+ destinationDir = getPreservationDestinationDir(
176
+ skillsDir,
177
+ sourceDir,
178
+ relativeSkillDir,
179
+ );
180
+ if (!destinationDir) return;
181
+
182
+ fs.mkdirSync(tempRootDir, { recursive: true });
183
+ const tempDir = join(tempRootDir, basename(destinationDir));
184
+ if (fs.existsSync(tempDir)) {
185
+ fs.rmSync(tempDir, { recursive: true, force: true });
186
+ }
187
+
188
+ fs.cpSync(sourceDir, tempDir, {
189
+ dereference: false,
190
+ errorOnExist: true,
191
+ force: false,
192
+ recursive: true,
193
+ });
194
+
195
+ if (fs.existsSync(destinationDir)) {
196
+ fs.rmSync(tempDir, { recursive: true, force: true });
197
+ if (skillFileContentsMatch(sourceDir, destinationDir)) return;
198
+ log.warn(
199
+ { destinationDir, sourceDir },
200
+ "Skipping nested indexed skill preservation because destination appeared during copy",
201
+ );
202
+ return;
203
+ }
204
+
205
+ fs.renameSync(tempDir, destinationDir);
206
+ log.info(
207
+ { destinationDir, sourceDir },
208
+ "Preserved nested indexed skill at top-level skills directory",
209
+ );
210
+ } catch (err) {
211
+ if (isNotFoundError(err)) return;
212
+ log.warn(
213
+ { err, relativeSkillDir, sourceDir, destinationDir },
214
+ "Failed to preserve nested indexed skill",
215
+ );
216
+ throw err;
217
+ }
218
+ }
219
+
220
+ function preserveNestedIndexedSkills(
221
+ workspaceDir: string,
222
+ skillsDir: string,
223
+ indexPath: string,
224
+ ): void {
225
+ const contents = fs.readFileSync(indexPath, "utf-8");
226
+ const tempRootDir = join(
227
+ workspaceDir,
228
+ ".workspace-migration-084-remove-legacy-skills-index",
229
+ );
230
+ for (const relativeSkillDir of parseLegacySkillIndexEntries(contents)) {
231
+ preserveNestedIndexedSkill(tempRootDir, skillsDir, relativeSkillDir);
232
+ }
233
+ if (fs.existsSync(tempRootDir)) {
234
+ fs.rmSync(tempRootDir, { recursive: true, force: true });
235
+ }
236
+ }
237
+
238
+ export const removeLegacySkillsIndexMigration: WorkspaceMigration = {
239
+ id: "084-remove-legacy-skills-index",
240
+ description: "Remove legacy workspace skills/SKILLS.md index file",
241
+ retryFailedCheckpoint: true,
242
+
243
+ run(workspaceDir: string): void {
244
+ const skillsDir = join(workspaceDir, "skills");
245
+ const indexPath = join(skillsDir, "SKILLS.md");
246
+
247
+ try {
248
+ const stat = fs.lstatSync(indexPath);
249
+ if (!stat.isFile() && !stat.isSymbolicLink()) {
250
+ log.warn(
251
+ { path: indexPath },
252
+ "Legacy SKILLS.md path is not a file; leaving it in place",
253
+ );
254
+ return;
255
+ }
256
+
257
+ if (stat.isFile()) {
258
+ preserveNestedIndexedSkills(workspaceDir, skillsDir, indexPath);
259
+ }
260
+
261
+ fs.unlinkSync(indexPath);
262
+ log.info({ path: indexPath }, "Removed legacy skills index file");
263
+ } catch (err) {
264
+ if (isNotFoundError(err)) return;
265
+ log.warn(
266
+ { err, path: indexPath },
267
+ "Failed to remove legacy skills index file",
268
+ );
269
+ throw err;
270
+ }
271
+ },
272
+
273
+ down(_workspaceDir: string): void {
274
+ // Forward-only: SKILLS.md is no longer a supported skill catalog format.
275
+ },
276
+ };
@@ -0,0 +1,137 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { Database } from "bun:sqlite";
5
+
6
+ import type { WorkspaceMigration } from "./types.js";
7
+
8
+ /**
9
+ * Follow-up to `075-memory-v2-bm25-b-default-reembed`. Migration 075 shipped
10
+ * in v0.8.1 with no gating beyond "db exists", so workspaces with no v2
11
+ * pages still recorded a checkpoint entry and a queued reembed job. We
12
+ * cannot edit 075 to add gating retroactively — the runner skips any
13
+ * already-checkpointed id — so this migration re-runs the enqueue with the
14
+ * gating we want now.
15
+ *
16
+ * Two gates:
17
+ *
18
+ * 1. `hasConceptPages` — only enqueue if `memory/concepts/` actually has a
19
+ * `.md` page. Workspaces that never wrote a v2 page have nothing to
20
+ * reembed.
21
+ * 2. `isMemoryV2Disabled` — skip when `memory.v2.enabled` is explicitly
22
+ * `false`. The worker does not currently gate `memory_v2_reembed`
23
+ * dispatch on the config flag, so enqueueing for a workspace that
24
+ * intentionally disabled v2 would immediately re-embed pages and hit
25
+ * the embedding backend against the user's intent.
26
+ *
27
+ * When either gate fails, we also DELETE any pending `memory_v2_reembed`
28
+ * job that 075 may have enqueued in the same startup sweep. For workspaces
29
+ * that never checkpointed 075 (upgrades from pre-v0.8.1, first-boot sweeps),
30
+ * 075 runs first and unconditionally enqueues a job; without the explicit
31
+ * cancellation here, 075's job would survive 085's gate and execute against
32
+ * the user's intent.
33
+ */
34
+ export const memoryV2Bm25BReembedDisabledV2PagesMigration: WorkspaceMigration =
35
+ {
36
+ id: "085-memory-v2-bm25-b-reembed-disabled-v2-pages",
37
+ description:
38
+ "Re-enqueue memory_v2_reembed for workspaces with v2 pages, gated on v2 not being explicitly disabled",
39
+
40
+ run(workspaceDir: string): void {
41
+ const dbPath = join(workspaceDir, "data", "db", "assistant.db");
42
+ if (!existsSync(dbPath)) return;
43
+
44
+ let db: Database;
45
+ try {
46
+ db = new Database(dbPath);
47
+ } catch {
48
+ return;
49
+ }
50
+
51
+ try {
52
+ const tableRow = db
53
+ .query(
54
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='memory_jobs'`,
55
+ )
56
+ .get();
57
+ if (!tableRow) return;
58
+
59
+ // If either gate fails, cancel any pending reembed job that 075 may
60
+ // have enqueued in this same startup sweep. Leave 'running' jobs
61
+ // alone — the worker is already mid-flight and canceling would orphan
62
+ // its state. Only 'pending' jobs are safe to delete here.
63
+ if (
64
+ isMemoryV2Disabled(workspaceDir) ||
65
+ !hasConceptPages(workspaceDir)
66
+ ) {
67
+ db.query(
68
+ `DELETE FROM memory_jobs WHERE type='memory_v2_reembed' AND status='pending'`,
69
+ ).run();
70
+ return;
71
+ }
72
+
73
+ const existing = db
74
+ .query(
75
+ `SELECT id FROM memory_jobs WHERE type='memory_v2_reembed' AND status IN ('pending','running') LIMIT 1`,
76
+ )
77
+ .get();
78
+ if (existing) return;
79
+
80
+ const now = Date.now();
81
+ db.query(
82
+ `INSERT INTO memory_jobs
83
+ (id, type, payload, status, attempts, deferrals, run_after, last_error, created_at, updated_at)
84
+ VALUES (?, 'memory_v2_reembed', '{}', 'pending', 0, 0, ?, NULL, ?, ?)`,
85
+ ).run(randomUUID(), now, now, now);
86
+ } finally {
87
+ db.close();
88
+ }
89
+ },
90
+
91
+ down(_workspaceDir: string): void {
92
+ // Forward-only: the reembed is a one-shot data refresh.
93
+ },
94
+ };
95
+
96
+ /**
97
+ * Returns true only when `memory.v2.enabled` is explicitly set to `false`
98
+ * in the workspace `config.json`. Missing/unparseable config falls through
99
+ * to the schema default (enabled), matching `MemoryV2ConfigSchema`.
100
+ */
101
+ function isMemoryV2Disabled(workspaceDir: string): boolean {
102
+ const configPath = join(workspaceDir, "config.json");
103
+ if (!existsSync(configPath)) return false;
104
+ try {
105
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
106
+ const memory = (raw as { memory?: { v2?: { enabled?: unknown } } })?.memory;
107
+ return memory?.v2?.enabled === false;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Returns true when `memory/concepts/` contains any `.md` file. Walks the
115
+ * tree iteratively so we bail on the first hit — pages can be nested in
116
+ * subdirectories (e.g. `memory/concepts/people/alice.md`).
117
+ */
118
+ function hasConceptPages(workspaceDir: string): boolean {
119
+ const stack = [join(workspaceDir, "memory", "concepts")];
120
+ while (stack.length > 0) {
121
+ const dir = stack.pop()!;
122
+ let entries;
123
+ try {
124
+ entries = readdirSync(dir, { withFileTypes: true });
125
+ } catch {
126
+ continue;
127
+ }
128
+ for (const entry of entries) {
129
+ if (entry.isDirectory()) {
130
+ stack.push(join(dir, entry.name));
131
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
132
+ return true;
133
+ }
134
+ }
135
+ }
136
+ return false;
137
+ }
@@ -0,0 +1,198 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ /**
7
+ * Revert mis-rewrites by migration 057 where a non-Gemini fragment was
8
+ * incorrectly treated as Gemini.
9
+ *
10
+ * Migration 057's `inferProvider` helper hardcodes `model === "gemini-3-flash"`
11
+ * as Gemini even when no `provider` is set, but `resolveCallSiteConfig` only
12
+ * infers providers via the catalog and `gemini-3-flash` is not a catalog
13
+ * entry. The runtime resolver therefore leaves such fragments inheriting from
14
+ * lower layers, so a workspace like `llm.default = {provider: "ollama"}` with
15
+ * `llm.callSites.recall = {model: "gemini-3-flash"}` (a user-named local
16
+ * model) resolves as Ollama at runtime — but 057 rewrites that recall model
17
+ * to `gemini-3-flash-preview`, flipping the catalog-based provider inference
18
+ * to Gemini and corrupting the user's intent.
19
+ *
20
+ * For each call-site or profile fragment that currently looks like a 057
21
+ * rewrite output (model in the replacement set, no explicit `provider`), this
22
+ * migration recomputes the effective provider via proper catalog-based
23
+ * inference using only the other layers. If that effective provider is
24
+ * explicitly non-Gemini, the fragment's model is reverted to
25
+ * `gemini-3-flash`, restoring the user's pre-057 state.
26
+ */
27
+ export const revertStaleGeminiMisRewritesMigration: WorkspaceMigration = {
28
+ id: "086-revert-stale-gemini-mis-rewrites",
29
+ description:
30
+ "Revert 057 mis-rewrites of gemini-3-flash in non-Gemini fragment contexts",
31
+ run(workspaceDir: string): void {
32
+ const configPath = join(workspaceDir, "config.json");
33
+ if (!existsSync(configPath)) return;
34
+
35
+ let config: Record<string, unknown>;
36
+ try {
37
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
38
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
39
+ config = raw as Record<string, unknown>;
40
+ } catch {
41
+ return;
42
+ }
43
+
44
+ const llm = readObject(config.llm);
45
+ if (llm === null) return;
46
+
47
+ const defaultBlock = readObject(llm.default);
48
+ const defaultProvider = inferLayerProvider(defaultBlock);
49
+ const profiles = readObject(llm.profiles);
50
+ const activeProfileName =
51
+ typeof llm.activeProfile === "string" ? llm.activeProfile : undefined;
52
+
53
+ let changed = false;
54
+
55
+ // Pass 1: revert profile candidates first so that pass 2's call-site
56
+ // evaluation sees the reverted profile state. A call-site that references
57
+ // a profile candidate would otherwise infer `siteProfileProvider = gemini`
58
+ // from the profile's pre-revert rewritten model, masking the call-site's
59
+ // own need to revert.
60
+ if (profiles !== null) {
61
+ for (const rawProfile of Object.values(profiles)) {
62
+ const profile = readObject(rawProfile);
63
+ if (profile === null) continue;
64
+ if (!isRevertCandidate(profile)) continue;
65
+ if (isExplicitlyNonGemini(defaultProvider)) {
66
+ profile.model = STALE_MODEL;
67
+ changed = true;
68
+ }
69
+ }
70
+ }
71
+
72
+ // Compute the active-profile provider after pass 1 so it reflects any
73
+ // reversion of the active profile itself.
74
+ const activeProfileBlock =
75
+ profiles !== null && activeProfileName !== undefined
76
+ ? readObject(profiles[activeProfileName])
77
+ : null;
78
+ const activeProfileProvider = inferLayerProvider(activeProfileBlock);
79
+
80
+ // Pass 2: call-site candidates. `inferLayerProvider` is re-read from
81
+ // `profiles` per call site, so it observes pass 1's reversions.
82
+ const callSites = readObject(llm.callSites);
83
+ if (callSites !== null) {
84
+ for (const [site, rawConfig] of Object.entries(callSites)) {
85
+ const callSiteConfig = readObject(rawConfig);
86
+ if (callSiteConfig === null) continue;
87
+ if (!isRevertCandidate(callSiteConfig)) continue;
88
+ const siteProfileName =
89
+ typeof callSiteConfig.profile === "string"
90
+ ? callSiteConfig.profile
91
+ : undefined;
92
+ const siteProfileBlock =
93
+ profiles !== null && siteProfileName !== undefined
94
+ ? readObject(profiles[siteProfileName])
95
+ : null;
96
+ const siteProfileProvider = inferLayerProvider(siteProfileBlock);
97
+
98
+ const effective = effectiveProviderExcludingSite({
99
+ callSite: site,
100
+ siteProfileProvider,
101
+ activeProfileProvider,
102
+ defaultProvider,
103
+ });
104
+ if (isExplicitlyNonGemini(effective)) {
105
+ callSiteConfig.model = STALE_MODEL;
106
+ changed = true;
107
+ }
108
+ }
109
+ }
110
+
111
+ if (!changed) return;
112
+
113
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
114
+ },
115
+ down(_workspaceDir: string): void {
116
+ // Forward-only: re-applying the broken rewrite would reintroduce the bug.
117
+ },
118
+ };
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Helpers — self-contained per workspace migrations AGENTS.md
122
+ // ---------------------------------------------------------------------------
123
+
124
+ const STALE_MODEL = "gemini-3-flash";
125
+
126
+ // Models 057 writes when rewriting. Any fragment now holding one of these
127
+ // values without an explicit provider is a potential mis-rewrite candidate.
128
+ const REPLACEMENT_MODELS = new Set<string>([
129
+ "gemini-3-flash-preview",
130
+ "gemini-3.1-flash-lite-preview",
131
+ ]);
132
+
133
+ // Subset of the Gemini provider catalog used for per-layer catalog inference.
134
+ // Mirrors `getCatalogProviderForModel` for Gemini entries only — for other
135
+ // providers we treat a bare model as "provider unknown" and fall through to
136
+ // lower layers, which is safe because the revert only fires when at least one
137
+ // layer below carries an explicit non-Gemini provider.
138
+ const GEMINI_CATALOG_MODELS = new Set<string>([
139
+ "gemini-3.1-pro-preview",
140
+ "gemini-3.1-pro-preview-customtools",
141
+ "gemini-3-flash-preview",
142
+ "gemini-3.1-flash-lite-preview",
143
+ "gemini-2.5-flash",
144
+ "gemini-2.5-flash-lite",
145
+ "gemini-2.5-pro",
146
+ ]);
147
+
148
+ function isRevertCandidate(block: Record<string, unknown>): boolean {
149
+ if (typeof block.provider === "string") return false;
150
+ return typeof block.model === "string" && REPLACEMENT_MODELS.has(block.model);
151
+ }
152
+
153
+ function inferLayerProvider(
154
+ block: Record<string, unknown> | null,
155
+ ): string | undefined {
156
+ if (block === null) return undefined;
157
+ if (typeof block.provider === "string") return block.provider;
158
+ if (
159
+ typeof block.model === "string" &&
160
+ GEMINI_CATALOG_MODELS.has(block.model)
161
+ ) {
162
+ return "gemini";
163
+ }
164
+ return undefined;
165
+ }
166
+
167
+ function isExplicitlyNonGemini(provider: string | undefined): boolean {
168
+ return provider !== undefined && provider !== "gemini";
169
+ }
170
+
171
+ // Mirrors `resolveCallSiteConfig`'s layered provider resolution, omitting the
172
+ // candidate call-site fragment itself (its provider is undefined and its
173
+ // model is the replacement value, so including it would always say "gemini"
174
+ // via catalog inference — defeating the revert check).
175
+ function effectiveProviderExcludingSite(args: {
176
+ callSite: string;
177
+ siteProfileProvider: string | undefined;
178
+ activeProfileProvider: string | undefined;
179
+ defaultProvider: string | undefined;
180
+ }): string | undefined {
181
+ const {
182
+ callSite,
183
+ siteProfileProvider,
184
+ activeProfileProvider,
185
+ defaultProvider,
186
+ } = args;
187
+ if (callSite === "mainAgent") {
188
+ return activeProfileProvider ?? siteProfileProvider ?? defaultProvider;
189
+ }
190
+ return siteProfileProvider ?? activeProfileProvider ?? defaultProvider;
191
+ }
192
+
193
+ function readObject(value: unknown): Record<string, unknown> | null {
194
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
195
+ return null;
196
+ }
197
+ return value as Record<string, unknown>;
198
+ }
@@ -0,0 +1,91 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ // Upgrade callSites.memoryRouter from the 077-seeded
7
+ // {model: "claude-sonnet-4-6", contextWindow: {maxInputTokens: 1_000_000}}
8
+ // shape to {profile: "balanced"} so the router rides the workspace's active
9
+ // inference profile (with thinking enabled, higher effort, etc.) instead of a
10
+ // bare model pin.
11
+ //
12
+ // Two skip conditions guard against runtime regressions:
13
+ //
14
+ // 1. BYOK / non-Anthropic workspaces. `balanced` resolves to the managed
15
+ // Anthropic connection (see seedInferenceProfiles), which off-platform
16
+ // installs explicitly disable. Forcing `balanced` there would make
17
+ // getConfiguredProvider("memoryRouter") return null and silently
18
+ // disable memory injection. Detect this by inspecting llm.default.provider
19
+ // — same heuristic migration 077 used to gate its seed.
20
+ //
21
+ // 2. User-customized memoryRouter config. If the existing entry isn't the
22
+ // exact 077-seeded shape (and isn't already {profile: "balanced"}), the
23
+ // user — or a platform overlay — chose those values deliberately. Match
24
+ // 077's pattern of preserving any prior config.
25
+ export const memoryRouterBalancedProfileMigration: WorkspaceMigration = {
26
+ id: "087-memory-router-balanced-profile",
27
+ description:
28
+ "Set callSites.memoryRouter to { profile: 'balanced' }, dropping the seeded model and contextWindow override",
29
+ run(workspaceDir: string): void {
30
+ if (process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH) return;
31
+
32
+ const configPath = join(workspaceDir, "config.json");
33
+ const configExisted = existsSync(configPath);
34
+
35
+ let config: Record<string, unknown> = {};
36
+ if (configExisted) {
37
+ try {
38
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
39
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
40
+ config = raw as Record<string, unknown>;
41
+ } catch {
42
+ return;
43
+ }
44
+ }
45
+
46
+ const llm = readObject(config.llm) ?? {};
47
+
48
+ const explicitProvider = readString(readObject(llm.default)?.provider);
49
+ if (explicitProvider !== undefined && explicitProvider !== "anthropic") {
50
+ return;
51
+ }
52
+
53
+ const callSites = readObject(llm.callSites) ?? {};
54
+ const existing = readObject(callSites.memoryRouter);
55
+
56
+ if (existing !== null && !isSeededBy077(existing)) {
57
+ return;
58
+ }
59
+
60
+ callSites.memoryRouter = { profile: "balanced" };
61
+ llm.callSites = callSites;
62
+ config.llm = llm;
63
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
64
+ },
65
+ down(_workspaceDir: string): void {
66
+ // Forward-only.
67
+ },
68
+ };
69
+
70
+ // True when the entry looks exactly like what migration 077 wrote: a model pin
71
+ // of claude-sonnet-4-6 plus the 1M-token context window, and nothing else.
72
+ function isSeededBy077(entry: Record<string, unknown>): boolean {
73
+ const keys = Object.keys(entry);
74
+ if (keys.length !== 2) return false;
75
+ if (entry.model !== "claude-sonnet-4-6") return false;
76
+ const contextWindow = readObject(entry.contextWindow);
77
+ if (contextWindow === null) return false;
78
+ const cwKeys = Object.keys(contextWindow);
79
+ return cwKeys.length === 1 && contextWindow.maxInputTokens === 1_000_000;
80
+ }
81
+
82
+ function readObject(value: unknown): Record<string, unknown> | null {
83
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
84
+ return null;
85
+ }
86
+ return value as Record<string, unknown>;
87
+ }
88
+
89
+ function readString(value: unknown): string | undefined {
90
+ return typeof value === "string" && value.length > 0 ? value : undefined;
91
+ }