@vellumai/assistant 0.8.2 → 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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { getLogger } from "../../util/logger.js";
20
- import { listPages, readPage } from "./page-store.js";
20
+ import { getPageMtimeMs, listPages, readPage } from "./page-store.js";
21
21
 
22
22
  // Dynamic import for `./skill-store.js` happens inside `getPageIndex` so that
23
23
  // modules that only need `invalidatePageIndex` (page-store.ts,
@@ -55,6 +55,12 @@ export interface PageIndexEntry {
55
55
  summary: string;
56
56
  /** Numeric IDs of outgoing edges, in sorted order. */
57
57
  edges: number[];
58
+ /**
59
+ * File mtime in epoch ms; 0 for synthetic entries (skills, CLI commands)
60
+ * that have no on-disk source file. Used by `splitTier1` to rank pages
61
+ * by recency.
62
+ */
63
+ modifiedAt: number;
58
64
  }
59
65
 
60
66
  /**
@@ -80,11 +86,12 @@ let cache: CachedIndex | null = null;
80
86
  /**
81
87
  * Return a `PageIndex` for `workspaceDir`. Cached module-locally; the cache
82
88
  * is invalidated by `invalidatePageIndex` (called by daemon-side hooks when
83
- * concept pages or skill entries change).
89
+ * concept pages, skill entries, or CLI-command entries change).
84
90
  *
85
91
  * Cold builds list every concept page in parallel, drop pages whose read
86
- * rejects, append seeded skill entries from `listSkillEntries()`, sort by
87
- * slug for deterministic IDs, then resolve outgoing edges to numeric IDs.
92
+ * rejects, append seeded skill entries from `listSkillEntries()` and CLI
93
+ * command entries from `listCliCommandEntries()`, sort by slug for
94
+ * deterministic IDs, then resolve outgoing edges to numeric IDs.
88
95
  */
89
96
  export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
90
97
  if (cache && cache.workspaceDir === workspaceDir) {
@@ -93,34 +100,50 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
93
100
 
94
101
  const slugs = await listPages(workspaceDir);
95
102
 
96
- // Read pages in parallel; pages whose read rejects are dropped with a warn
97
- // so a single broken page never blocks the rest of the index.
103
+ // Read pages and stat their mtimes in parallel. Pages whose read rejects
104
+ // are dropped with a warn so a single broken page never blocks the rest
105
+ // of the index. mtime is stat'd alongside readPage so tier-1 sorting has
106
+ // recency without a second pass over the filesystem.
98
107
  const settled = await Promise.allSettled(
99
- slugs.map((slug) => readPage(workspaceDir, slug)),
108
+ slugs.map(async (slug) => {
109
+ const [page, mtimeMs] = await Promise.all([
110
+ readPage(workspaceDir, slug),
111
+ getPageMtimeMs(workspaceDir, slug),
112
+ ]);
113
+ return { page, mtimeMs };
114
+ }),
100
115
  );
101
116
 
102
- // Intermediate shape used while we still need the raw outgoing slugs to
103
- // resolve into numeric IDs after sorting.
104
117
  interface DraftEntry {
105
118
  slug: string;
106
119
  summary: string;
107
120
  outgoingSlugs: string[];
121
+ modifiedAt: number;
108
122
  }
109
123
 
110
- const { listSkillEntries, SKILL_SLUG_PREFIX } =
111
- await import("./skill-store.js");
124
+ const [
125
+ { listSkillEntries, SKILL_SLUG_PREFIX },
126
+ { listCliCommandEntries, CLI_COMMAND_SLUG_PREFIX },
127
+ ] = await Promise.all([
128
+ import("./skill-store.js"),
129
+ import("./cli-command-store.js"),
130
+ ]);
112
131
 
113
- // Build the skill-slug set first so we can drop colliding concept pages.
114
- // Collision policy: **skill entries win**. Skill rows are seeded from the
115
- // curated catalog and the router needs them to be reachable under their
116
- // canonical slugs; a hand-authored page sitting under `skills/<id>` is
117
- // either a stale leftover from a prior write or a user mistake. `bySlug`
118
- // is last-writer-wins, so without explicit dedupe one side would silently
119
- // shadow the other depending on iteration order.
132
+ // Build the synthetic-slug sets first so we can drop colliding concept
133
+ // pages. Collision policy: **synthetic entries win**. Skill and CLI rows
134
+ // are seeded from authoritative in-process catalogs; a hand-authored page
135
+ // sitting under `skills/<id>` or `cli-commands/<name>` is either a stale
136
+ // leftover from a prior write or a user mistake. `bySlug` is last-writer-
137
+ // wins, so without explicit dedupe one side would silently shadow the
138
+ // other depending on iteration order.
120
139
  const skillEntries = listSkillEntries();
121
140
  const skillSlugs = new Set(
122
141
  skillEntries.map((entry) => `${SKILL_SLUG_PREFIX}${entry.id}`),
123
142
  );
143
+ const cliCommandEntries = listCliCommandEntries();
144
+ const cliCommandSlugs = new Set(
145
+ cliCommandEntries.map((entry) => `${CLI_COMMAND_SLUG_PREFIX}${entry.id}`),
146
+ );
124
147
 
125
148
  const drafts: DraftEntry[] = [];
126
149
  for (let i = 0; i < settled.length; i++) {
@@ -133,7 +156,7 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
133
156
  );
134
157
  continue;
135
158
  }
136
- const page = result.value;
159
+ const { page, mtimeMs } = result.value;
137
160
  if (!page) continue;
138
161
  if (skillSlugs.has(slug)) {
139
162
  log.warn(
@@ -142,11 +165,19 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
142
165
  );
143
166
  continue;
144
167
  }
168
+ if (cliCommandSlugs.has(slug)) {
169
+ log.warn(
170
+ { slug },
171
+ "Dropping concept page from index — slug collides with a seeded CLI-command entry; CLI command wins",
172
+ );
173
+ continue;
174
+ }
145
175
  const summarySource = page.frontmatter.summary?.trim() || page.body.trim();
146
176
  drafts.push({
147
177
  slug,
148
178
  summary: normalizeSummary(summarySource),
149
179
  outgoingSlugs: page.frontmatter.edges,
180
+ modifiedAt: mtimeMs,
150
181
  });
151
182
  }
152
183
 
@@ -155,6 +186,16 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
155
186
  slug: `${SKILL_SLUG_PREFIX}${entry.id}`,
156
187
  summary: normalizeSummary(entry.content),
157
188
  outgoingSlugs: [],
189
+ modifiedAt: 0,
190
+ });
191
+ }
192
+
193
+ for (const entry of cliCommandEntries) {
194
+ drafts.push({
195
+ slug: `${CLI_COMMAND_SLUG_PREFIX}${entry.id}`,
196
+ summary: normalizeSummary(entry.description),
197
+ outgoingSlugs: [],
198
+ modifiedAt: 0,
158
199
  });
159
200
  }
160
201
 
@@ -169,6 +210,7 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
169
210
  slug: draft.slug,
170
211
  summary: draft.summary,
171
212
  edges: [],
213
+ modifiedAt: draft.modifiedAt,
172
214
  };
173
215
  bySlug.set(entry.slug, entry);
174
216
  byId.set(entry.id, entry);
@@ -219,3 +261,188 @@ function renderIndex(entries: readonly PageIndexEntry[]): string {
219
261
  });
220
262
  return lines.length > 0 ? `${lines.join("\n")}\n` : "";
221
263
  }
264
+
265
+ // FNV-1a 32-bit. Stable across runtimes — never change the constants or
266
+ // future releases will silently reshuffle batches and torch every batch's
267
+ // KV cache simultaneously.
268
+ function fnv1aHash(input: string): number {
269
+ let h = 0x811c9dc5;
270
+ for (let i = 0; i < input.length; i++) {
271
+ h ^= input.charCodeAt(i);
272
+ h = Math.imul(h, 0x01000193);
273
+ }
274
+ return h >>> 0;
275
+ }
276
+
277
+ /**
278
+ * Split a global `PageIndex` into batches of approximately `batchSize`
279
+ * entries for parallel routing. Each batch is a self-contained `PageIndex`
280
+ * with batch-local 1-based IDs and a re-rendered prompt block.
281
+ *
282
+ * `batchSize === null` or `entries.length <= batchSize` short-circuits to
283
+ * `[pageIndex]` (the same object) so single-batch callers send a request
284
+ * bit-identical to the pre-batching code path and reuse v3's KV cache
285
+ * untouched.
286
+ *
287
+ * Assignment uses FNV-1a on the slug: adding or removing one page only
288
+ * invalidates the KV cache of the one batch it lands in, instead of
289
+ * cascading through every batch the way index-modulo chunking would.
290
+ *
291
+ * Edges are re-resolved to batch-local IDs — edges pointing to pages in
292
+ * other batches drop silently (the model can't reference them anyway).
293
+ */
294
+ export function partitionPageIndex(
295
+ pageIndex: PageIndex,
296
+ batchSize: number | null,
297
+ ): PageIndex[] {
298
+ if (batchSize === null || pageIndex.entries.length <= batchSize) {
299
+ return [pageIndex];
300
+ }
301
+ const batchCount = Math.ceil(pageIndex.entries.length / batchSize);
302
+ const buckets: PageIndexEntry[][] = Array.from(
303
+ { length: batchCount },
304
+ () => [],
305
+ );
306
+ for (const entry of pageIndex.entries) {
307
+ buckets[fnv1aHash(entry.slug) % batchCount].push(entry);
308
+ }
309
+ return buckets
310
+ .filter((b) => b.length > 0)
311
+ .map((entries) => buildLocalPageIndex(entries, pageIndex));
312
+ }
313
+
314
+ /**
315
+ * Build a self-contained `PageIndex` from a subset of another index's
316
+ * entries. Local entries get fresh 1-based IDs in input order, edges are
317
+ * remapped through the source index's `byId` to local IDs (cross-batch
318
+ * edges drop silently), and the prompt block is re-rendered.
319
+ */
320
+ function buildLocalPageIndex(
321
+ entries: readonly PageIndexEntry[],
322
+ source: PageIndex,
323
+ ): PageIndex {
324
+ const localBySlug = new Map<string, PageIndexEntry>();
325
+ const localById = new Map<number, PageIndexEntry>();
326
+ const localEntries: PageIndexEntry[] = entries.map((src, i) => {
327
+ const local: PageIndexEntry = {
328
+ id: i + 1,
329
+ slug: src.slug,
330
+ summary: src.summary,
331
+ edges: [],
332
+ modifiedAt: src.modifiedAt,
333
+ };
334
+ localBySlug.set(local.slug, local);
335
+ localById.set(local.id, local);
336
+ return local;
337
+ });
338
+ for (let i = 0; i < localEntries.length; i++) {
339
+ const localEdges: number[] = [];
340
+ for (const globalEdgeId of entries[i].edges) {
341
+ const target = source.byId.get(globalEdgeId);
342
+ if (!target) continue;
343
+ const localTarget = localBySlug.get(target.slug);
344
+ if (localTarget) localEdges.push(localTarget.id);
345
+ }
346
+ localEdges.sort((a, b) => a - b);
347
+ localEntries[i].edges = localEdges;
348
+ }
349
+ return {
350
+ entries: localEntries,
351
+ bySlug: localBySlug,
352
+ byId: localById,
353
+ rendered: renderIndex(localEntries),
354
+ };
355
+ }
356
+
357
+ /**
358
+ * Carve the top-N most recently modified pages into their own batch (tier
359
+ * 1 in the v4 router architecture) and return the leftover as a second
360
+ * `PageIndex` for downstream partitioning.
361
+ *
362
+ * `tier1Size === null` is a no-op — `{ tier1: null, rest: pageIndex }`
363
+ * with the original index reference preserved so the single-batch path
364
+ * stays bit-identical to v3 and the KV cache survives.
365
+ *
366
+ * Tier 1 entries are sorted by `modifiedAt` descending; ties break by
367
+ * slug ASCII so the order is deterministic when several pages share a
368
+ * mtime (e.g. fresh workspaces). Synthetic entries (mtime=0) sort to the
369
+ * bottom and only enter tier 1 when there aren't enough real pages to
370
+ * fill the pool. The rest is sorted by slug ASCII so downstream
371
+ * hash-bucketing produces stable batches across mtime churn.
372
+ */
373
+ export function splitTier1(
374
+ pageIndex: PageIndex,
375
+ tier1Size: number | null,
376
+ ): { tier1: PageIndex | null; rest: PageIndex } {
377
+ if (tier1Size === null || pageIndex.entries.length === 0) {
378
+ return { tier1: null, rest: pageIndex };
379
+ }
380
+ const sortedByRecency = [...pageIndex.entries].sort((a, b) => {
381
+ if (a.modifiedAt !== b.modifiedAt) return b.modifiedAt - a.modifiedAt;
382
+ return a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0;
383
+ });
384
+ const tier1Entries = sortedByRecency.slice(0, tier1Size);
385
+ const tier1Slugs = new Set(tier1Entries.map((e) => e.slug));
386
+ const restEntries = pageIndex.entries.filter((e) => !tier1Slugs.has(e.slug));
387
+
388
+ const tier1 = buildLocalPageIndex(tier1Entries, pageIndex);
389
+ if (restEntries.length === 0) {
390
+ return { tier1, rest: emptyPageIndex() };
391
+ }
392
+ return { tier1, rest: buildLocalPageIndex(restEntries, pageIndex) };
393
+ }
394
+
395
+ /**
396
+ * Carve the top-M highest-EMA pages into their own batch (tier 2 in the
397
+ * v4 router architecture). Caller computes `scores` via
398
+ * `computeInjectionScores`; this function stays pure so unit tests don't
399
+ * need a database.
400
+ *
401
+ * `tier2Size === null` is a no-op. Pages with `score <= 0` (no events in
402
+ * the read window) are ineligible regardless of `tier2Size` — a stale
403
+ * page with zero score belongs in tier 3, not in the "useful" pool.
404
+ * Ordering is score desc, slug-ASCII tiebreak.
405
+ *
406
+ * Expected call shape: orchestrator passes the *post-tier-1* `PageIndex`,
407
+ * so we never re-promote a tier-1 page to tier 2.
408
+ */
409
+ export function splitTier2(
410
+ pageIndex: PageIndex,
411
+ tier2Size: number | null,
412
+ scores: ReadonlyMap<string, number>,
413
+ ): { tier2: PageIndex | null; rest: PageIndex } {
414
+ if (tier2Size === null || pageIndex.entries.length === 0) {
415
+ return { tier2: null, rest: pageIndex };
416
+ }
417
+ const eligible = pageIndex.entries
418
+ .map((entry) => ({ entry, score: scores.get(entry.slug) ?? 0 }))
419
+ .filter((x) => x.score > 0)
420
+ .sort((a, b) => {
421
+ if (a.score !== b.score) return b.score - a.score;
422
+ return a.entry.slug < b.entry.slug
423
+ ? -1
424
+ : a.entry.slug > b.entry.slug
425
+ ? 1
426
+ : 0;
427
+ });
428
+ const tier2Entries = eligible.slice(0, tier2Size).map((x) => x.entry);
429
+ if (tier2Entries.length === 0) {
430
+ return { tier2: null, rest: pageIndex };
431
+ }
432
+ const tier2Slugs = new Set(tier2Entries.map((e) => e.slug));
433
+ const restEntries = pageIndex.entries.filter((e) => !tier2Slugs.has(e.slug));
434
+ const tier2 = buildLocalPageIndex(tier2Entries, pageIndex);
435
+ if (restEntries.length === 0) {
436
+ return { tier2, rest: emptyPageIndex() };
437
+ }
438
+ return { tier2, rest: buildLocalPageIndex(restEntries, pageIndex) };
439
+ }
440
+
441
+ function emptyPageIndex(): PageIndex {
442
+ return {
443
+ entries: [],
444
+ bySlug: new Map(),
445
+ byId: new Map(),
446
+ rendered: "",
447
+ };
448
+ }
@@ -260,6 +260,24 @@ export async function readPage(
260
260
  return { slug, frontmatter, body };
261
261
  }
262
262
 
263
+ /**
264
+ * File mtime for a concept page, in epoch ms. Returns 0 when the file is
265
+ * missing or unreadable — callers treat 0 as "no mtime" so tier-1 sorting
266
+ * can rank synthetic entries (skills, CLI commands) below real pages.
267
+ */
268
+ export async function getPageMtimeMs(
269
+ workspaceDir: string,
270
+ slug: string,
271
+ ): Promise<number> {
272
+ validateSlug(slug);
273
+ try {
274
+ const s = await stat(getPagePath(workspaceDir, slug));
275
+ return s.mtimeMs;
276
+ } catch {
277
+ return 0;
278
+ }
279
+ }
280
+
263
281
  /**
264
282
  * Write a concept page atomically (temp file + rename). A crash between the
265
283
  * temp write and the rename leaves the prior file intact; a crash after the