@vellumai/assistant 0.8.3 → 0.8.4

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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -53,7 +53,8 @@ mock.module("../page-store.js", () => ({
53
53
  },
54
54
  }));
55
55
 
56
- const { getPageIndex, invalidatePageIndex } = await import("../page-index.js");
56
+ const { getPageIndex, invalidatePageIndex, partitionPageIndex } =
57
+ await import("../page-index.js");
57
58
  const { writePage } = await import("../page-store.js");
58
59
  const { invalidateEdgeIndex } = await import("../edge-index.js");
59
60
 
@@ -358,3 +359,366 @@ describe("rendered prompt block", () => {
358
359
  expect(idx.rendered).toBe("[1] alice — A page\n");
359
360
  });
360
361
  });
362
+
363
+ // ---------------------------------------------------------------------------
364
+ // partitionPageIndex — stable batch assignment
365
+ // ---------------------------------------------------------------------------
366
+
367
+ describe("partitionPageIndex", () => {
368
+ async function buildIndex(slugs: string[]) {
369
+ for (const slug of slugs) {
370
+ await writePage(workspaceDir, makePage(slug, { summary: `${slug} sum` }));
371
+ }
372
+ return getPageIndex(workspaceDir);
373
+ }
374
+
375
+ test("batchSize=null returns the same index reference (no work, KV cache safe)", async () => {
376
+ const idx = await buildIndex(["alice", "bob", "carol"]);
377
+ const batches = partitionPageIndex(idx, null);
378
+ expect(batches).toHaveLength(1);
379
+ expect(batches[0]).toBe(idx);
380
+ });
381
+
382
+ test("batchSize >= entries.length is a single batch identical to the input", async () => {
383
+ const idx = await buildIndex(["alice", "bob", "carol"]);
384
+ const batches = partitionPageIndex(idx, 10);
385
+ expect(batches).toHaveLength(1);
386
+ expect(batches[0]).toBe(idx);
387
+ });
388
+
389
+ test("splits N=5 with batchSize=2 into 3 batches preserving all slugs", async () => {
390
+ const idx = await buildIndex(["a", "b", "c", "d", "e"]);
391
+ const batches = partitionPageIndex(idx, 2);
392
+ expect(batches.length).toBeGreaterThanOrEqual(2);
393
+ const allSlugs = batches.flatMap((b) => b.entries.map((e) => e.slug));
394
+ expect(new Set(allSlugs)).toEqual(new Set(["a", "b", "c", "d", "e"]));
395
+ // Every slug appears in exactly one batch.
396
+ expect(allSlugs.length).toBe(5);
397
+ });
398
+
399
+ test("re-renders each batch with local 1-based IDs", async () => {
400
+ const idx = await buildIndex(["a", "b", "c", "d", "e"]);
401
+ const batches = partitionPageIndex(idx, 2);
402
+ for (const batch of batches) {
403
+ expect(batch.entries.map((e) => e.id)).toEqual(
404
+ batch.entries.map((_, i) => i + 1),
405
+ );
406
+ for (const entry of batch.entries) {
407
+ expect(batch.byId.get(entry.id)).toBe(entry);
408
+ expect(batch.bySlug.get(entry.slug)).toBe(entry);
409
+ expect(batch.rendered).toContain(`[${entry.id}] ${entry.slug}`);
410
+ }
411
+ }
412
+ });
413
+
414
+ test("KV-cache stability: adding ONE slug only changes the batch it lands in", async () => {
415
+ // 4 slugs → 2 batches at batchSize=2. Adding a 5th slug should keep
416
+ // the slug→batch mapping of the original 4 intact except possibly
417
+ // shifting which 2 of the 3 resulting batches contain them. The
418
+ // critical invariant: for each ORIGINAL slug, the rendered string of
419
+ // its containing batch must be byte-identical before and after (so
420
+ // Anthropic's KV cache hits) — but only when the batch count is
421
+ // unchanged. When N crosses a ceiling boundary, batch_count grows
422
+ // and we accept the one-time reshuffle (rare in practice).
423
+
424
+ // Pick a slug set that stays at batch_count=3 both before (6 slugs,
425
+ // ceil(6/2)=3) and after (7 slugs, ceil(7/2)=4) → batch count
426
+ // changes. Instead, hold batch_count constant by going from 5 to 6
427
+ // slugs at batchSize=3: ceil(5/3)=2, ceil(6/3)=2.
428
+ const before = await buildIndex(["a", "b", "c", "d", "e"]);
429
+ const batchesBefore = partitionPageIndex(before, 3);
430
+ expect(batchesBefore).toHaveLength(2);
431
+
432
+ const renderedBySlug = new Map<string, string>();
433
+ for (const batch of batchesBefore) {
434
+ for (const entry of batch.entries) {
435
+ renderedBySlug.set(entry.slug, batch.rendered);
436
+ }
437
+ }
438
+
439
+ // Drop the cached global index, write one more page, rebuild.
440
+ invalidatePageIndex();
441
+ invalidateEdgeIndex();
442
+ await writePage(workspaceDir, makePage("f", { summary: "f sum" }));
443
+ const after = await getPageIndex(workspaceDir);
444
+ const batchesAfter = partitionPageIndex(after, 3);
445
+ expect(batchesAfter).toHaveLength(2);
446
+
447
+ // Locate each original slug's NEW batch and compare against its OLD
448
+ // rendered string. We expect exactly one of the two batches'
449
+ // rendered strings to be byte-identical to the pre-add version (the
450
+ // batch that didn't gain `f`); the other batch's rendering changed
451
+ // because `f` was added to it.
452
+ const renderedAfterBySlug = new Map<string, string>();
453
+ for (const batch of batchesAfter) {
454
+ for (const entry of batch.entries) {
455
+ renderedAfterBySlug.set(entry.slug, batch.rendered);
456
+ }
457
+ }
458
+ let unchangedSlugs = 0;
459
+ for (const slug of ["a", "b", "c", "d", "e"]) {
460
+ if (renderedBySlug.get(slug) === renderedAfterBySlug.get(slug)) {
461
+ unchangedSlugs += 1;
462
+ }
463
+ }
464
+ // At least some slugs must have their batch's rendered string
465
+ // preserved — index-modulo chunking would change ALL batches when
466
+ // a slug is added at any position. With hash-bucketing only the
467
+ // bucket `f` landed in changes.
468
+ expect(unchangedSlugs).toBeGreaterThan(0);
469
+ });
470
+
471
+ test("edges to pages in other batches drop; edges within a batch remap to local IDs", async () => {
472
+ // Force `alice → bob` edge. Choose batchSize=1 so alice and bob land
473
+ // in separate batches → the edge should drop.
474
+ await writePage(
475
+ workspaceDir,
476
+ makePage("alice", { summary: "A", edges: ["bob"] }),
477
+ );
478
+ await writePage(workspaceDir, makePage("bob", { summary: "B" }));
479
+ const idx = await getPageIndex(workspaceDir);
480
+ const batches = partitionPageIndex(idx, 1);
481
+
482
+ for (const batch of batches) {
483
+ for (const entry of batch.entries) {
484
+ // Edges must point to IDs that exist in THIS batch's byId map.
485
+ for (const edgeId of entry.edges) {
486
+ expect(batch.byId.has(edgeId)).toBe(true);
487
+ }
488
+ }
489
+ }
490
+ // alice's edge to bob must have dropped (bob is in a different batch).
491
+ const aliceBatch = batches.find((b) => b.bySlug.has("alice"))!;
492
+ expect(aliceBatch.bySlug.get("alice")!.edges).toEqual([]);
493
+ });
494
+ });
495
+
496
+ // ---------------------------------------------------------------------------
497
+ // splitTier1 — recently modified pool extraction
498
+ // ---------------------------------------------------------------------------
499
+
500
+ const { splitTier1, splitTier2 } = await import("../page-index.js");
501
+ const { utimes } = await import("node:fs/promises");
502
+ const { join: joinPath } = await import("node:path");
503
+
504
+ async function setMtime(
505
+ workspaceDir: string,
506
+ slug: string,
507
+ epochMs: number,
508
+ ): Promise<void> {
509
+ const seconds = epochMs / 1000;
510
+ await utimes(
511
+ joinPath(workspaceDir, "memory", "concepts", `${slug}.md`),
512
+ seconds,
513
+ seconds,
514
+ );
515
+ }
516
+
517
+ describe("splitTier1", () => {
518
+ async function buildIndex(slugs: string[]) {
519
+ for (const slug of slugs) {
520
+ await writePage(workspaceDir, makePage(slug, { summary: `${slug} sum` }));
521
+ }
522
+ return getPageIndex(workspaceDir);
523
+ }
524
+
525
+ test("tier1Size=null is a no-op — same index reference, no carve-out", async () => {
526
+ const idx = await buildIndex(["a", "b", "c"]);
527
+ const { tier1, rest } = splitTier1(idx, null);
528
+ expect(tier1).toBeNull();
529
+ expect(rest).toBe(idx);
530
+ });
531
+
532
+ test("returns no-op shape on an empty workspace", async () => {
533
+ const idx = await getPageIndex(workspaceDir);
534
+ const { tier1, rest } = splitTier1(idx, 100);
535
+ expect(tier1).toBeNull();
536
+ expect(rest).toBe(idx);
537
+ });
538
+
539
+ test("top-N by mtime desc become tier 1; the remainder is the rest", async () => {
540
+ await buildIndex(["a", "b", "c", "d", "e"]);
541
+ await setMtime(workspaceDir, "a", 1_000_000);
542
+ await setMtime(workspaceDir, "b", 5_000_000);
543
+ await setMtime(workspaceDir, "c", 2_000_000);
544
+ await setMtime(workspaceDir, "d", 4_000_000);
545
+ await setMtime(workspaceDir, "e", 3_000_000);
546
+ invalidatePageIndex();
547
+ const idx = await getPageIndex(workspaceDir);
548
+
549
+ const { tier1, rest } = splitTier1(idx, 2);
550
+ expect(tier1).not.toBeNull();
551
+ expect(tier1!.entries.map((e) => e.slug)).toEqual(["b", "d"]);
552
+ expect(rest.entries.map((e) => e.slug).sort()).toEqual(["a", "c", "e"]);
553
+ });
554
+
555
+ test("tier1 carries batch-local 1-based IDs and re-rendered prompt block", async () => {
556
+ await buildIndex(["a", "b"]);
557
+ const idx = await getPageIndex(workspaceDir);
558
+ const { tier1 } = splitTier1(idx, 5);
559
+ expect(tier1!.entries.map((e) => e.id)).toEqual([1, 2]);
560
+ expect(tier1!.rendered).toContain("[1] ");
561
+ expect(tier1!.rendered).toContain("[2] ");
562
+ });
563
+
564
+ test("rest preserves slug-ASCII order so downstream hash bucketing is stable", async () => {
565
+ await buildIndex(["zulu", "alpha", "mike", "bravo", "kilo"]);
566
+ // Push zulu's mtime ahead of every other page — wall-clock mtimes from
567
+ // writePage are all in the same second, so a future-stamped zulu is the
568
+ // unambiguous "most recent."
569
+ await setMtime(workspaceDir, "zulu", Date.now() + 60_000);
570
+ invalidatePageIndex();
571
+ const idx = await getPageIndex(workspaceDir);
572
+
573
+ const { rest } = splitTier1(idx, 1);
574
+ expect(rest.entries.map((e) => e.slug)).toEqual([
575
+ "alpha",
576
+ "bravo",
577
+ "kilo",
578
+ "mike",
579
+ ]);
580
+ });
581
+
582
+ test("synthetic entries (mtime=0) sort below real pages — tier 1 prefers concept pages", async () => {
583
+ skillState.entries = [
584
+ { id: "echo", content: "echo skill" },
585
+ { id: "foxtrot", content: "fox skill" },
586
+ ];
587
+ await buildIndex(["alpha", "bravo"]);
588
+ invalidatePageIndex();
589
+ const idx = await getPageIndex(workspaceDir);
590
+
591
+ const { tier1 } = splitTier1(idx, 2);
592
+ // Both real concept pages have mtime > 0; skill entries have mtime=0 →
593
+ // tier 1's top-2 must be the concept pages, regardless of their relative
594
+ // mtime ordering.
595
+ const tier1Slugs = new Set(tier1!.entries.map((e) => e.slug));
596
+ expect(tier1Slugs.has("alpha")).toBe(true);
597
+ expect(tier1Slugs.has("bravo")).toBe(true);
598
+ expect(tier1Slugs.has("skills/echo")).toBe(false);
599
+ expect(tier1Slugs.has("skills/foxtrot")).toBe(false);
600
+ });
601
+
602
+ test("tier1Size larger than total entries returns all in tier 1 and empty rest", async () => {
603
+ await buildIndex(["a", "b"]);
604
+ const idx = await getPageIndex(workspaceDir);
605
+ const { tier1, rest } = splitTier1(idx, 100);
606
+ expect(tier1!.entries.length).toBe(2);
607
+ expect(rest.entries.length).toBe(0);
608
+ });
609
+
610
+ test("mtime ties break by slug ASCII for determinism", async () => {
611
+ await buildIndex(["alpha", "bravo", "charlie"]);
612
+ // Force identical mtimes — file-system creation timestamps would otherwise
613
+ // be near-identical but not exactly equal, masking the tiebreaker path.
614
+ await setMtime(workspaceDir, "alpha", 5_000_000);
615
+ await setMtime(workspaceDir, "bravo", 5_000_000);
616
+ await setMtime(workspaceDir, "charlie", 5_000_000);
617
+ invalidatePageIndex();
618
+ const idx = await getPageIndex(workspaceDir);
619
+
620
+ const { tier1 } = splitTier1(idx, 2);
621
+ expect(tier1!.entries.map((e) => e.slug)).toEqual(["alpha", "bravo"]);
622
+ });
623
+ });
624
+
625
+ // ---------------------------------------------------------------------------
626
+ // splitTier2 — top-M-by-EMA pool extraction
627
+ // ---------------------------------------------------------------------------
628
+
629
+ describe("splitTier2", () => {
630
+ async function buildIndex(slugs: string[]) {
631
+ for (const slug of slugs) {
632
+ await writePage(workspaceDir, makePage(slug, { summary: `${slug} sum` }));
633
+ }
634
+ return getPageIndex(workspaceDir);
635
+ }
636
+
637
+ test("tier2Size=null is a no-op — same index reference, no carve-out", async () => {
638
+ const idx = await buildIndex(["a", "b", "c"]);
639
+ const { tier2, rest } = splitTier2(idx, null, new Map());
640
+ expect(tier2).toBeNull();
641
+ expect(rest).toBe(idx);
642
+ });
643
+
644
+ test("returns no-op when no pages have a positive score", async () => {
645
+ const idx = await buildIndex(["a", "b", "c"]);
646
+ // Empty scores map → every page has score 0 → none eligible.
647
+ const { tier2, rest } = splitTier2(idx, 2, new Map());
648
+ expect(tier2).toBeNull();
649
+ expect(rest).toBe(idx);
650
+ });
651
+
652
+ test("top-M by score desc become tier 2; lower-score pages stay in rest", async () => {
653
+ await buildIndex(["a", "b", "c", "d", "e"]);
654
+ const idx = await getPageIndex(workspaceDir);
655
+ const scores = new Map([
656
+ ["a", 1.0],
657
+ ["b", 5.0],
658
+ ["c", 2.0],
659
+ ["d", 4.0],
660
+ ["e", 3.0],
661
+ ]);
662
+
663
+ const { tier2, rest } = splitTier2(idx, 2, scores);
664
+ expect(tier2!.entries.map((e) => e.slug)).toEqual(["b", "d"]);
665
+ expect(rest.entries.map((e) => e.slug).sort()).toEqual(["a", "c", "e"]);
666
+ });
667
+
668
+ test("score=0 pages are ineligible even when tier2Size is large", async () => {
669
+ await buildIndex(["a", "b", "c", "d", "e"]);
670
+ const idx = await getPageIndex(workspaceDir);
671
+ // Only 2 pages have positive scores; tier2Size=10 should NOT pull in
672
+ // zero-score pages to fill the pool.
673
+ const scores = new Map([
674
+ ["b", 5.0],
675
+ ["d", 4.0],
676
+ ]);
677
+ const { tier2, rest } = splitTier2(idx, 10, scores);
678
+ expect(tier2!.entries.map((e) => e.slug)).toEqual(["b", "d"]);
679
+ expect(rest.entries.map((e) => e.slug).sort()).toEqual(["a", "c", "e"]);
680
+ });
681
+
682
+ test("tied scores break by slug ASCII for determinism", async () => {
683
+ await buildIndex(["alpha", "bravo", "charlie", "delta"]);
684
+ const idx = await getPageIndex(workspaceDir);
685
+ const scores = new Map([
686
+ ["alpha", 2.0],
687
+ ["bravo", 2.0],
688
+ ["charlie", 2.0],
689
+ ["delta", 1.0],
690
+ ]);
691
+
692
+ const { tier2 } = splitTier2(idx, 2, scores);
693
+ expect(tier2!.entries.map((e) => e.slug)).toEqual(["alpha", "bravo"]);
694
+ });
695
+
696
+ test("tier 2 entries carry batch-local 1-based IDs", async () => {
697
+ await buildIndex(["a", "b", "c"]);
698
+ const idx = await getPageIndex(workspaceDir);
699
+ const { tier2 } = splitTier2(
700
+ idx,
701
+ 2,
702
+ new Map([
703
+ ["a", 1.0],
704
+ ["b", 2.0],
705
+ ]),
706
+ );
707
+ expect(tier2!.entries.map((e) => e.id)).toEqual([1, 2]);
708
+ expect(tier2!.rendered).toContain("[1] ");
709
+ expect(tier2!.rendered).toContain("[2] ");
710
+ });
711
+
712
+ test("tier2Size larger than eligible count fills with available; rest gets remainder", async () => {
713
+ await buildIndex(["a", "b", "c"]);
714
+ const idx = await getPageIndex(workspaceDir);
715
+ const scores = new Map([
716
+ ["a", 1.0],
717
+ ["b", 2.0],
718
+ ["c", 3.0],
719
+ ]);
720
+ const { tier2, rest } = splitTier2(idx, 100, scores);
721
+ expect(tier2!.entries.length).toBe(3);
722
+ expect(rest.entries.length).toBe(0);
723
+ });
724
+ });