@vellumai/assistant 0.11.3 → 0.11.4-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +139 -37
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +177 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  67. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  68. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  69. package/src/__tests__/visible-app-context.test.ts +16 -9
  70. package/src/__tests__/voice-config-update.test.ts +40 -0
  71. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  72. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  73. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  74. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  75. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  76. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  77. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  78. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  79. package/src/acp/__tests__/auth-required.test.ts +161 -0
  80. package/src/acp/acp-claude-oauth.ts +19 -2
  81. package/src/acp/agent-process.test.ts +100 -0
  82. package/src/acp/agent-process.ts +29 -26
  83. package/src/acp/auth-required.ts +102 -0
  84. package/src/acp/session-manager.test.ts +119 -0
  85. package/src/acp/session-manager.ts +68 -2
  86. package/src/api/events/acp-auth-required.ts +55 -0
  87. package/src/api/index.ts +7 -0
  88. package/src/api/surfaces.ts +7 -3
  89. package/src/apps/app-store.ts +3 -0
  90. package/src/bundler/package-resolver.ts +2 -30
  91. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  92. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  93. package/src/calls/call-controller.ts +19 -3
  94. package/src/calls/call-setup-flow.ts +0 -1
  95. package/src/calls/media-stream-stt-session.ts +15 -0
  96. package/src/calls/voice-session-bridge.ts +71 -16
  97. package/src/calls/voice-triage-escalate.ts +104 -2
  98. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  99. package/src/channels/config.ts +13 -0
  100. package/src/channels/plugin-channel-declarations.ts +108 -0
  101. package/src/channels/types.ts +30 -0
  102. package/src/cli/AGENTS.md +5 -2
  103. package/src/cli/commands/credentials.help.ts +2 -2
  104. package/src/cli/commands/inference-providers.ts +1 -1
  105. package/src/cli/commands/mcp.help.ts +13 -4
  106. package/src/cli/commands/mcp.ts +9 -0
  107. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  108. package/src/cli/commands/memory/index.help.ts +43 -1
  109. package/src/cli/commands/memory/memory-v3.ts +64 -0
  110. package/src/cli/commands/stt.help.ts +27 -2
  111. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  112. package/src/cli/lib/bundled-marketplace.json +13 -0
  113. package/src/cli/lib/upgrade-plugin.ts +42 -0
  114. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  115. package/src/config/__tests__/default-provider.test.ts +6 -1
  116. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  117. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  118. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  119. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  121. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  122. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  123. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  124. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  125. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  126. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  127. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  128. package/src/config/call-site-defaults.ts +4 -5
  129. package/src/config/default-profile-catalog.ts +83 -12
  130. package/src/config/default-profile-names.ts +4 -1
  131. package/src/config/default-provider-resolution.ts +4 -0
  132. package/src/config/llm-context-resolution.ts +11 -3
  133. package/src/config/llm-resolver.ts +28 -1
  134. package/src/config/profile-materialization.ts +70 -22
  135. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  136. package/src/config/schemas/call-site-catalog.ts +4 -4
  137. package/src/config/schemas/live-voice.ts +57 -23
  138. package/src/config/schemas/llm.ts +59 -32
  139. package/src/config/schemas/mcp.ts +23 -0
  140. package/src/config/schemas/plugin-updates.ts +6 -2
  141. package/src/config/schemas/stt.ts +1 -0
  142. package/src/context/outbound-sanitize.ts +96 -1
  143. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  144. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  145. package/src/daemon/conversation-agent-loop.ts +17 -6
  146. package/src/daemon/conversation-messaging.ts +5 -1
  147. package/src/daemon/conversation-notifiers.ts +9 -1
  148. package/src/daemon/conversation-process.ts +36 -6
  149. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  150. package/src/daemon/conversation-surfaces.ts +27 -5
  151. package/src/daemon/conversation-tool-setup.ts +1 -2
  152. package/src/daemon/conversation.ts +48 -0
  153. package/src/daemon/interactive-turn-sender.ts +59 -0
  154. package/src/daemon/mcp-reload-service.ts +36 -6
  155. package/src/daemon/process-message.ts +24 -24
  156. package/src/daemon/providers-setup.ts +6 -3
  157. package/src/daemon/trust-context-types.ts +29 -0
  158. package/src/daemon/wake-conversation-ops.ts +3 -2
  159. package/src/documents/document-store.ts +138 -5
  160. package/src/hooks/hook-loader.ts +3 -3
  161. package/src/hooks/registry.ts +50 -6
  162. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  163. package/src/inbound/oauth-callback-url.ts +61 -0
  164. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  165. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  166. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  167. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  168. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  169. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  170. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  171. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  172. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  173. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  174. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  175. package/src/live-voice/live-voice-archive.ts +2 -0
  176. package/src/live-voice/live-voice-metrics.ts +57 -32
  177. package/src/live-voice/live-voice-photo.ts +1 -2
  178. package/src/live-voice/live-voice-session.ts +535 -314
  179. package/src/live-voice/progress-narration.ts +277 -0
  180. package/src/live-voice/protocol.ts +21 -1
  181. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  182. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  183. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  184. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  185. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  186. package/src/mcp/client.ts +16 -11
  187. package/src/mcp/effective-config.ts +113 -0
  188. package/src/mcp/manager.ts +11 -6
  189. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  190. package/src/mcp/mcp-oauth-provider.ts +205 -240
  191. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  192. package/src/monitoring/plugin-auto-update.ts +128 -24
  193. package/src/notifications/signal.ts +1 -0
  194. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  195. package/src/permissions/confirmation-guardian-request.ts +2 -2
  196. package/src/permissions/question-guardian-request.test.ts +14 -6
  197. package/src/permissions/question-guardian-request.ts +1 -2
  198. package/src/persistence/attachments-store.ts +8 -1
  199. package/src/persistence/bookmark-crud.ts +3 -7
  200. package/src/persistence/conversation-attention-store.ts +16 -45
  201. package/src/persistence/conversation-crud.ts +33 -4
  202. package/src/persistence/conversation-lineage.ts +9 -0
  203. package/src/persistence/conversation-queries.ts +108 -41
  204. package/src/persistence/delivery-crud.ts +38 -29
  205. package/src/persistence/external-conversation-store.ts +32 -4
  206. package/src/persistence/llm-request-log-store.ts +4 -10
  207. package/src/persistence/llm-usage-store.ts +8 -3
  208. package/src/persistence/message-reads.test.ts +197 -0
  209. package/src/persistence/message-reads.ts +211 -0
  210. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  211. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  212. package/src/persistence/real-user-turn-filter.ts +27 -3
  213. package/src/persistence/steps.ts +9 -0
  214. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  215. package/src/plugin-api/conversation-turn.ts +168 -5
  216. package/src/plugin-api/index.ts +21 -5
  217. package/src/plugin-api/vision-support.test.ts +1 -1
  218. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  219. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  220. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  224. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  225. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  226. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  227. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  228. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  229. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  230. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  231. package/src/plugins/defaults/memory/indexer.ts +3 -1
  232. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  233. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  234. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  238. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  239. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  240. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  241. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  242. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  243. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  244. package/src/plugins/defaults/memory/worker.ts +6 -3
  245. package/src/plugins/external-plugin-loader.ts +47 -0
  246. package/src/plugins/mcp-servers.ts +361 -0
  247. package/src/plugins/mtime-cache.ts +23 -49
  248. package/src/plugins/worker-plugin-surface.ts +33 -0
  249. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  250. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  251. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  252. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  253. package/src/providers/call-site-routing.ts +30 -3
  254. package/src/providers/connection-resolution.ts +194 -11
  255. package/src/providers/inference/auth.ts +6 -0
  256. package/src/providers/inference/connection-availability.ts +24 -2
  257. package/src/providers/inference/connections.ts +2 -0
  258. package/src/providers/model-catalog.ts +3 -3
  259. package/src/providers/model-intents.ts +28 -8
  260. package/src/providers/openai/chat-completions-provider.ts +5 -6
  261. package/src/providers/openai/codex-models.ts +2 -1
  262. package/src/providers/provider-send-message.ts +32 -3
  263. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  264. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  265. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  266. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  267. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  268. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  269. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  270. package/src/providers/speech-to-text/resolve.ts +25 -2
  271. package/src/routes/worker.ts +17 -5
  272. package/src/runtime/access-request-helper.ts +9 -12
  273. package/src/runtime/agent-wake.ts +3 -3
  274. package/src/runtime/pre-first-message-gate.ts +4 -0
  275. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  276. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  277. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  278. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  279. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  280. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  281. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  282. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  283. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  284. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  285. package/src/runtime/routes/channel-route-shared.ts +0 -6
  286. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  287. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  288. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  289. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  290. package/src/runtime/routes/default-provider-routes.ts +15 -0
  291. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  292. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  294. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  295. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  296. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  297. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  298. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  299. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  300. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  301. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  302. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  303. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  304. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  305. package/src/runtime/routes/log-export-routes.ts +3 -0
  306. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  307. package/src/runtime/routes/plugins-routes.ts +21 -3
  308. package/src/runtime/routes/stt-routes.ts +31 -25
  309. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  310. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  311. package/src/runtime/routes/user-route-import.ts +108 -0
  312. package/src/schedule/worker.ts +6 -0
  313. package/src/security/oauth2.ts +6 -22
  314. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  315. package/src/stt/__tests__/types.test.ts +94 -0
  316. package/src/stt/daemon-batch-transcriber.ts +10 -0
  317. package/src/stt/stt-stream-session.ts +8 -4
  318. package/src/stt/types.ts +103 -0
  319. package/src/subagent/manager.ts +1 -3
  320. package/src/subagent/types.ts +7 -6
  321. package/src/tools/acp/spawn.test.ts +97 -0
  322. package/src/tools/acp/spawn.ts +32 -0
  323. package/src/tools/document/document-tool.ts +12 -3
  324. package/src/tools/registry.ts +2 -1
  325. package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
  326. package/src/tools/workflows/run-workflow.ts +1 -2
  327. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  328. package/src/tts/reasoning-tag-filter.ts +89 -0
  329. package/src/util/think-tag-stream.ts +95 -0
  330. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  331. package/src/workspace/custom-profile-ensure.ts +4 -24
  332. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  333. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  334. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  335. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  336. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  337. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  338. package/src/workspace/migrations/registry.ts +10 -0
  339. package/src/workspace/provider-commit-message-generator.ts +7 -5
  340. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  341. package/src/live-voice/front-decision.ts +0 -476
@@ -19,6 +19,7 @@ import { z } from "zod";
19
19
 
20
20
  import { getConfig } from "../../../../config/loader.js";
21
21
  import type { AssistantConfig } from "../../../../config/types.js";
22
+ import { getTelemetrySqlite } from "../../../../persistence/db-connection.js";
22
23
  import {
23
24
  ACTOR_PRINCIPALS,
24
25
  type RoutePolicy,
@@ -26,6 +27,7 @@ import {
26
27
  import type { RouteDefinition } from "../../../../runtime/routes/types.js";
27
28
  import { getLogger } from "../logging.js";
28
29
  import { backfillAllSections } from "../v3/maintain-job.js";
30
+ import { MEMORY_V3_INJECTION_GATE_CHECK_NAME } from "../v3/orchestrate.js";
29
31
  import { invalidateLanes } from "../v3/shadow-plugin.js";
30
32
 
31
33
  const log = getLogger("memory-v3-routes");
@@ -101,6 +103,211 @@ const WRITE_POLICY: RoutePolicy = {
101
103
  allowedPrincipalTypes: ACTOR_PRINCIPALS,
102
104
  };
103
105
 
106
+ // ---------------------------------------------------------------------------
107
+ // gate-stats
108
+ // ---------------------------------------------------------------------------
109
+
110
+ const DEFAULT_LOOKBACK_DAYS = 30;
111
+ const MAX_LOOKBACK_DAYS = 90;
112
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
113
+
114
+ /** Corpus size buckets for gate-stats aggregation (in concept pages). */
115
+ const CORPUS_BUCKETS = [
116
+ { label: "0–9", min: 0, max: 9 },
117
+ { label: "10–49", min: 10, max: 49 },
118
+ { label: "50–199", min: 50, max: 199 },
119
+ { label: "200+", min: 200, max: Infinity },
120
+ ] as const;
121
+
122
+ type BucketLabel = (typeof CORPUS_BUCKETS)[number]["label"];
123
+
124
+ interface BucketAcc {
125
+ total: number;
126
+ scored: number;
127
+ passed: number;
128
+ /** Passes that came from scored runs — the numerator for scoredPassRate. */
129
+ scoredPassed: number;
130
+ reasons: Record<string, number>;
131
+ }
132
+
133
+ function corpusBucketLabel(pageCount: number): BucketLabel {
134
+ for (const b of CORPUS_BUCKETS) {
135
+ if (pageCount <= b.max) {
136
+ return b.label;
137
+ }
138
+ }
139
+ return "200+";
140
+ }
141
+
142
+ interface GatePayloadDetail {
143
+ pass?: boolean;
144
+ reason?: string;
145
+ scored?: boolean;
146
+ real_concept_page_count?: number;
147
+ }
148
+
149
+ const GateStatsBucketSchema = z.object({
150
+ pageCountRange: z
151
+ .string()
152
+ .describe("Concept page count range (e.g. '10–49')"),
153
+ total: z
154
+ .number()
155
+ .describe("All gate runs in this bucket (scored + pass-open)"),
156
+ scored: z
157
+ .number()
158
+ .describe("Runs that weighed scores (dense lane available)"),
159
+ passed: z.number().describe("Runs that passed the gate"),
160
+ scoredPassRate: z
161
+ .number()
162
+ .nullable()
163
+ .describe(
164
+ "scoredPasses / scored (contested decisions only); null when no scored runs in bucket",
165
+ ),
166
+ reasons: z
167
+ .record(z.string(), z.number())
168
+ .describe("Gate reason code → run count (dense_pass, fail_no_signal, …)"),
169
+ });
170
+
171
+ export const GateStatsResponseSchema = z.object({
172
+ lookbackDays: z.number().describe("Lookback window applied"),
173
+ totalRuns: z.number().describe("Total gate runs found in the window"),
174
+ buckets: z
175
+ .array(GateStatsBucketSchema)
176
+ .describe("Stats by concept page count range, lean→large"),
177
+ unknownPageCount: z
178
+ .object({
179
+ total: z.number(),
180
+ passed: z.number(),
181
+ reasons: z.record(z.string(), z.number()),
182
+ })
183
+ .describe("Runs with no real_concept_page_count in telemetry detail"),
184
+ });
185
+
186
+ export type GateStatsResponse = z.infer<typeof GateStatsResponseSchema>;
187
+
188
+ /**
189
+ * Aggregate memory v3 injection gate runs from the telemetry outbox for the
190
+ * given lookback window. Groups runs by concept page count bucket and computes
191
+ * pass rates and reason distributions per bucket.
192
+ *
193
+ * Pass-open shortcuts (dense disabled/unavailable, gate threw) are included in
194
+ * `total` but not in `scored`, so `scoredPassRate` reflects only contested gate
195
+ * decisions — the signal that actually reveals whether the gate thresholds are
196
+ * calibrated correctly.
197
+ */
198
+ export function handleMemoryV3GateStats(
199
+ lookbackDays: number = DEFAULT_LOOKBACK_DAYS,
200
+ db: ReturnType<typeof getTelemetrySqlite> = getTelemetrySqlite(),
201
+ ): GateStatsResponse {
202
+ const days = Math.min(
203
+ Math.max(1, Math.floor(lookbackDays)),
204
+ MAX_LOOKBACK_DAYS,
205
+ );
206
+ const since = Date.now() - days * MS_PER_DAY;
207
+
208
+ const emptyBuckets = (): GateStatsResponse["buckets"] =>
209
+ CORPUS_BUCKETS.map((b) => ({
210
+ pageCountRange: b.label,
211
+ total: 0,
212
+ scored: 0,
213
+ passed: 0,
214
+ scoredPassRate: null,
215
+ reasons: {},
216
+ }));
217
+ if (!db) {
218
+ return {
219
+ lookbackDays: days,
220
+ totalRuns: 0,
221
+ buckets: emptyBuckets(),
222
+ unknownPageCount: { total: 0, passed: 0, reasons: {} },
223
+ };
224
+ }
225
+
226
+ const rows = db
227
+ .query(
228
+ /*sql*/ `
229
+ SELECT payload FROM telemetry_events
230
+ WHERE name = 'watchdog'
231
+ AND created_at >= ?
232
+ AND json_extract(payload, '$.check_name') = ?
233
+ `,
234
+ )
235
+ .all(since, MEMORY_V3_INJECTION_GATE_CHECK_NAME) as Array<{
236
+ payload: string;
237
+ }>;
238
+
239
+ const bucketMap = new Map<BucketLabel, BucketAcc>(
240
+ CORPUS_BUCKETS.map((b) => [
241
+ b.label,
242
+ { total: 0, scored: 0, passed: 0, scoredPassed: 0, reasons: {} },
243
+ ]),
244
+ );
245
+ const unknown: BucketAcc = {
246
+ total: 0,
247
+ scored: 0,
248
+ passed: 0,
249
+ scoredPassed: 0,
250
+ reasons: {},
251
+ };
252
+
253
+ for (const row of rows) {
254
+ let detail: GatePayloadDetail;
255
+ try {
256
+ const parsed = JSON.parse(row.payload) as { detail?: GatePayloadDetail };
257
+ detail = parsed.detail ?? {};
258
+ } catch {
259
+ continue;
260
+ }
261
+
262
+ const pageCount =
263
+ typeof detail.real_concept_page_count === "number"
264
+ ? detail.real_concept_page_count
265
+ : null;
266
+
267
+ const acc =
268
+ pageCount !== null
269
+ ? (bucketMap.get(corpusBucketLabel(pageCount)) ?? unknown)
270
+ : unknown;
271
+
272
+ acc.total += 1;
273
+ if (detail.scored === true) {
274
+ acc.scored += 1;
275
+ if (detail.pass === true) {
276
+ acc.scoredPassed += 1;
277
+ }
278
+ }
279
+ if (detail.pass === true) {
280
+ acc.passed += 1;
281
+ }
282
+ const reason =
283
+ typeof detail.reason === "string" ? detail.reason : "unknown";
284
+ acc.reasons[reason] = (acc.reasons[reason] ?? 0) + 1;
285
+ }
286
+
287
+ const buckets = CORPUS_BUCKETS.map((b) => {
288
+ const acc = bucketMap.get(b.label)!;
289
+ return {
290
+ pageCountRange: b.label,
291
+ total: acc.total,
292
+ scored: acc.scored,
293
+ passed: acc.passed,
294
+ scoredPassRate: acc.scored > 0 ? acc.scoredPassed / acc.scored : null,
295
+ reasons: acc.reasons,
296
+ };
297
+ });
298
+
299
+ return {
300
+ lookbackDays: days,
301
+ totalRuns: rows.length,
302
+ buckets,
303
+ unknownPageCount: {
304
+ total: unknown.total,
305
+ passed: unknown.passed,
306
+ reasons: unknown.reasons,
307
+ },
308
+ };
309
+ }
310
+
104
311
  export const ROUTES: RouteDefinition[] = [
105
312
  {
106
313
  operationId: "memory_v3_rebuild_index",
@@ -197,6 +197,7 @@ const { memoryV2ConsolidateJob, CONSOLIDATION_FAILURE_CHECKPOINT_KEY } =
197
197
  const { CUTOFF_PLACEHOLDER, CONSOLIDATION_PROMPT } =
198
198
  await import("../prompts/consolidation.js");
199
199
  const { invalidatePageIndex } = await import("../page-index.js");
200
+ const { formatRememberEntry } = await import("../../buffer-format.js");
200
201
 
201
202
  // The handler only reads `config.memory.enabled`, `config.memory.v2.enabled`,
202
203
  // `config.memory.v2.consolidation_prompt_path`, and
@@ -437,6 +438,38 @@ describe("memoryV2ConsolidateJob — chunked cutoff (consolidation_max_entries_p
437
438
  expect(outcome.cutoff).not.toBe("[meeting-notes]");
438
439
  });
439
440
 
441
+ test("a fact whose body holds a canonical entry line counts once and never supplies the cutoff", async () => {
442
+ // Written through the real writer, so this covers the round trip rather
443
+ // than a hand-built buffer: the body line carries the exact shape an entry
444
+ // opening has. Counting it would both inflate the per-run budget and let
445
+ // the cutoff be drawn from inside a fact, deferring entries against a
446
+ // timestamp the user never filed anything at.
447
+ writeFileSync(
448
+ bufferPath(),
449
+ formatRememberEntry(
450
+ "Alice's rollout plan:\n- [Apr 28, 9:00 AM] cutover",
451
+ new Date(2026, 3, 27, 9, 0),
452
+ ) +
453
+ formatRememberEntry(
454
+ "Bob takes his coffee black.",
455
+ new Date(2026, 3, 27, 9, 1),
456
+ ),
457
+ );
458
+
459
+ const outcome = await memoryV2ConsolidateJob(
460
+ makeJob(),
461
+ configWithMaxEntries(2),
462
+ );
463
+
464
+ expect(outcome.kind).toBe("invoked");
465
+ if (outcome.kind !== "invoked") {
466
+ throw new Error("unreachable");
467
+ }
468
+ // Two facts, cap 2 → no chunking. Three would chunk and defer one.
469
+ expect(outcome.deferredEntries).toBe(0);
470
+ expect(outcome.cutoff).not.toBe("Apr 28, 9:00 AM");
471
+ });
472
+
440
473
  test("multi-line entries under the cap → full-buffer cutoff even when raw line count exceeds it", async () => {
441
474
  writeFileSync(
442
475
  bufferPath(),
@@ -5,6 +5,7 @@
5
5
  * - Reads the four files in canonical order and joins them under headings.
6
6
  * - Skips empty / missing files.
7
7
  * - Returns null when every file is empty or missing.
8
+ * - Caps the Buffer section at `consolidation_max_buffer_lines`.
8
9
  */
9
10
 
10
11
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
@@ -33,12 +34,19 @@ mock.module("../../../../../util/logger.js", () => ({
33
34
 
34
35
  let configMemoryV2Enabled = true;
35
36
  let configMemoryEnabled = true;
37
+ // Mirrors the `memory.v2.consolidation_max_buffer_lines` schema default; the
38
+ // Buffer-cap tests move it.
39
+ let configMaxBufferLines: number | null = 100;
36
40
 
37
- // static-context reads its gates through the plugin's own config accessor.
41
+ // static-context reads its gates and the substrate tuning through the
42
+ // plugin's own config accessor.
38
43
  mock.module("../../config.js", () => ({
39
44
  getMemoryConfig: () => ({
40
45
  enabled: configMemoryEnabled,
41
- v2: { enabled: configMemoryV2Enabled },
46
+ v2: {
47
+ enabled: configMemoryV2Enabled,
48
+ consolidation_max_buffer_lines: configMaxBufferLines,
49
+ },
42
50
  }),
43
51
  }));
44
52
 
@@ -70,6 +78,7 @@ describe("readMemoryV2StaticContent", () => {
70
78
  mkdirSync(TEST_DIR, { recursive: true });
71
79
  configMemoryV2Enabled = true;
72
80
  configMemoryEnabled = true;
81
+ configMaxBufferLines = 100;
73
82
  });
74
83
 
75
84
  afterEach(() => {
@@ -165,6 +174,194 @@ describe("readMemoryV2StaticContent", () => {
165
174
  });
166
175
  });
167
176
 
177
+ describe("readMemoryV2StaticContent Buffer cap", () => {
178
+ /** `n` timestamped buffer entries, oldest first, in `remember()`'s shape. */
179
+ function bufferEntries(n: number): string {
180
+ return Array.from(
181
+ { length: n },
182
+ (_, i) => `- [Jan 1, 9:00 AM] entry-${i}`,
183
+ ).join("\n");
184
+ }
185
+
186
+ beforeEach(() => {
187
+ mkdirSync(TEST_DIR, { recursive: true });
188
+ configMemoryV2Enabled = true;
189
+ configMemoryEnabled = true;
190
+ configMaxBufferLines = 10;
191
+ });
192
+
193
+ afterEach(() => {
194
+ cleanupMemoryDir();
195
+ });
196
+
197
+ test("passes a buffer at or under the cap through byte-identically", () => {
198
+ const buffer = bufferEntries(10);
199
+ writeMemoryFile("buffer.md", buffer);
200
+
201
+ expect(readMemoryV2StaticContent()).toBe(`## Buffer\n\n${buffer}`);
202
+ });
203
+
204
+ test("keeps the newest entries and drops the oldest when over the cap", () => {
205
+ writeMemoryFile("buffer.md", bufferEntries(30));
206
+
207
+ const text = readMemoryV2StaticContent()!;
208
+ expect(text).toContain(
209
+ "(Older entries trimmed. Read memory/buffer.md for the full backlog.)",
210
+ );
211
+ // The cap counts non-empty lines, matching the scheduler's
212
+ // `countBufferLines`, so exactly the newest 10 entries survive.
213
+ const kept = text
214
+ .split("\n")
215
+ .filter((line) => line.startsWith("- ["))
216
+ .map((line) => line.slice(line.lastIndexOf(" ") + 1));
217
+ expect(kept).toEqual(
218
+ Array.from({ length: 10 }, (_, i) => `entry-${i + 20}`),
219
+ );
220
+ });
221
+
222
+ test("never opens mid-entry when the cap lands inside a multiline fact", () => {
223
+ // The 5-line fact straddles the cap: counting back 10 non-empty lines
224
+ // lands on one of its continuation lines. Injecting from there would show
225
+ // orphan bullets with no timestamp and no opening clause.
226
+ const multiline = [
227
+ "- [Jan 1, 9:00 AM] the straddling fact",
228
+ " - [ ] a checklist item inside the fact",
229
+ " continuation prose",
230
+ " - another bullet",
231
+ " closing line",
232
+ ].join("\n");
233
+ writeMemoryFile(
234
+ "buffer.md",
235
+ `${bufferEntries(20)}\n${multiline}\n${bufferEntries(8)}`,
236
+ );
237
+
238
+ const text = readMemoryV2StaticContent()!;
239
+ const body = text.slice(text.indexOf("buffer.md for the full backlog.)\n"));
240
+ const firstLine = body.split("\n")[1]!;
241
+ expect(firstLine).toMatch(/^- \[Jan 1, 9:00 AM\]/);
242
+ // The partial entry is dropped whole: none of its continuation lines
243
+ // survive on their own.
244
+ expect(text).not.toContain("continuation prose");
245
+ expect(text).not.toContain("the straddling fact");
246
+ });
247
+
248
+ test("keeps the newest entry attributable when its body alone exceeds the cap", () => {
249
+ // The newest entry has no successor to fall back to: counting back 10
250
+ // non-empty lines lands inside its body and there is no later timestamped
251
+ // entry to open on. Dropping it would leave the section with no facts at
252
+ // all, so the entry's opening line is kept and its head elided.
253
+ writeMemoryFile(
254
+ "buffer.md",
255
+ [
256
+ ...Array.from(
257
+ { length: 20 },
258
+ (_, i) => `- [Jan 1, 9:00 AM] entry-${i}`,
259
+ ),
260
+ "- [Jan 2, 9:00 AM] the oversized fact",
261
+ ...Array.from({ length: 14 }, (_, i) => ` body line ${i}`),
262
+ ].join("\n"),
263
+ );
264
+
265
+ const text = readMemoryV2StaticContent()!;
266
+ const body = text
267
+ .slice(text.indexOf("full backlog.)\n") + "full backlog.)\n".length)
268
+ .split("\n")
269
+ .filter((line) => line.trim().length > 0);
270
+
271
+ // Opens on the entry's own timestamped line, never on an orphan body line.
272
+ expect(body[0]).toBe("- [Jan 2, 9:00 AM] the oversized fact");
273
+ expect(body[1]).toBe(
274
+ "(This entry's body was trimmed. Read memory/buffer.md for the rest of it.)",
275
+ );
276
+ // The retained tail is the newest end of the body, not its head.
277
+ expect(body.at(-1)).toBe(" body line 13");
278
+ expect(body).toContain(" body line 4");
279
+ expect(body).not.toContain(" body line 3");
280
+ // Bounded at the cap plus exactly the opening line and the marker, so the
281
+ // oversized entry cannot reintroduce an unbounded injection.
282
+ expect(body).toHaveLength(12);
283
+ // The older entries are still dropped.
284
+ expect(text).not.toContain("entry-19");
285
+ });
286
+
287
+ test("does not claim a trim when the newest entry's body exactly fills the cap", () => {
288
+ // The cut lands on the line right after the opening, so nothing of the
289
+ // entry was elided and the marker would be a lie.
290
+ writeMemoryFile(
291
+ "buffer.md",
292
+ [
293
+ ...Array.from(
294
+ { length: 20 },
295
+ (_, i) => `- [Jan 1, 9:00 AM] entry-${i}`,
296
+ ),
297
+ "- [Jan 2, 9:00 AM] the exact-fit fact",
298
+ ...Array.from({ length: 10 }, (_, i) => ` body line ${i}`),
299
+ ].join("\n"),
300
+ );
301
+
302
+ const text = readMemoryV2StaticContent()!;
303
+ const body = text
304
+ .slice(text.indexOf("full backlog.)\n") + "full backlog.)\n".length)
305
+ .split("\n")
306
+ .filter((line) => line.trim().length > 0);
307
+
308
+ expect(body[0]).toBe("- [Jan 2, 9:00 AM] the exact-fit fact");
309
+ expect(text).not.toContain("This entry's body was trimmed");
310
+ // The entry survives whole: opening line plus its full 10-line body.
311
+ expect(body).toHaveLength(11);
312
+ expect(body.at(-1)).toBe(" body line 9");
313
+ expect(body).toContain(" body line 0");
314
+ });
315
+
316
+ test("keeps a multiline entry intact when it sits fully inside the cap", () => {
317
+ const multiline = [
318
+ "- [Jan 1, 9:00 AM] a fact with a body",
319
+ " second line of the same fact",
320
+ ].join("\n");
321
+ writeMemoryFile("buffer.md", `${bufferEntries(20)}\n${multiline}`);
322
+
323
+ const text = readMemoryV2StaticContent()!;
324
+ expect(text).toContain("a fact with a body");
325
+ expect(text).toContain("second line of the same fact");
326
+ });
327
+
328
+ test("falls back to the line cut for a buffer with no timestamped entries", () => {
329
+ // A hand-written buffer that never went through `remember()` has no entry
330
+ // structure to preserve, so the line-based cut stands rather than dropping
331
+ // everything.
332
+ const handWritten = Array.from(
333
+ { length: 30 },
334
+ (_, i) => `just a line ${i}`,
335
+ ).join("\n");
336
+ writeMemoryFile("buffer.md", handWritten);
337
+
338
+ const text = readMemoryV2StaticContent()!;
339
+ expect(text).toContain("just a line 29");
340
+ expect(text).not.toContain("just a line 0");
341
+ });
342
+
343
+ test("leaves the buffer unbounded when the size trigger is disabled", () => {
344
+ configMaxBufferLines = null;
345
+ const buffer = bufferEntries(30);
346
+ writeMemoryFile("buffer.md", buffer);
347
+
348
+ expect(readMemoryV2StaticContent()).toBe(`## Buffer\n\n${buffer}`);
349
+ });
350
+
351
+ test("caps only the Buffer section, never the curated views", () => {
352
+ const long = Array.from({ length: 30 }, (_, i) => `line-${i}`).join("\n");
353
+ writeMemoryFile("essentials.md", long);
354
+ writeMemoryFile("threads.md", long);
355
+ writeMemoryFile("recent.md", long);
356
+ writeMemoryFile("buffer.md", bufferEntries(30));
357
+
358
+ const lines = readMemoryV2StaticContent()!.split("\n");
359
+ expect(lines.filter((line) => line === "line-0")).toHaveLength(3);
360
+ expect(lines.filter((line) => line === "line-29")).toHaveLength(3);
361
+ expect(lines.some((line) => line.endsWith("entry-0"))).toBe(false);
362
+ });
363
+ });
364
+
168
365
  describe("shouldExposePersonalMemory", () => {
169
366
  test("allows guardian-trusted local conversations", () => {
170
367
  expect(
@@ -91,7 +91,10 @@ import {
91
91
  type MemoryJobType,
92
92
  } from "../../../../persistence/jobs-store.js";
93
93
  import { runBackgroundJob } from "../../../../runtime/background-job-runner.js";
94
- import { formatBufferTimestamp } from "../graph/tool-handlers.js";
94
+ import {
95
+ formatBufferTimestamp,
96
+ matchBufferEntryStart,
97
+ } from "../buffer-format.js";
95
98
  import { getLogger } from "../logging.js";
96
99
  import { getWorkspaceDir } from "../paths.js";
97
100
  import {
@@ -597,32 +600,36 @@ function readBufferContent(bufferPath: string): string {
597
600
 
598
601
  /**
599
602
  * Extract the bracketed timestamp from a `buffer.md` entry line
600
- * (`- [Mon D, h:mm AM/PM] …`, see `formatBufferEntry`). Returned verbatim so
601
- * it can serve directly as a consolidation cutoff — both sides of the agent's
602
- * "timestamp ≥ cutoff" comparison then share the exact `formatBufferTimestamp`
603
- * shape.
603
+ * (`- [Mon D, h:mm AM/PM] …`, see {@link formatRememberEntry}). Returned
604
+ * verbatim so it can serve directly as a consolidation cutoff: both sides of
605
+ * the agent's "timestamp >= cutoff" comparison then share the exact
606
+ * {@link formatBufferTimestamp} shape.
604
607
  *
605
- * The bracket contents must match that shape exactly: a remembered fact's
606
- * continuation lines can themselves start with `- [` (markdown checklists
607
- * `- [ ] …`, wikilink bullets `- [[…]]`), and counting those as entries
608
- * would inflate the per-run budget — or worse, hand the agent a garbage
609
- * cutoff like a blank string. Returns `null` for anything that isn't a real
610
- * timestamped entry start.
608
+ * Recognition is delegated to the shared matcher, so a remembered fact's
609
+ * continuation lines never register as entries. That matters here beyond
610
+ * tidiness: counting a fact's `- [ ] …` checklist lines or an indented
611
+ * entry-shaped body line as entries would inflate the per-run budget, or hand
612
+ * the agent a cutoff drawn from the middle of a fact.
611
613
  */
612
614
  function extractBufferEntryTimestamp(line: string): string | null {
613
- const match = /^\s*-\s*\[([A-Z][a-z]{2} \d{1,2}, \d{1,2}:\d{2} [AP]M)\]/.exec(
614
- line,
615
- );
616
- return match ? match[1] : null;
615
+ return matchBufferEntryStart(line)?.timestamp ?? null;
617
616
  }
618
617
 
619
618
  /**
620
619
  * Count non-empty lines in `memory/buffer.md`. Used by the scheduler to
621
620
  * implement the size-based consolidation trigger. Missing file → 0.
622
621
  *
623
- * Each entry is one line (`- [Mon D, h:mm AM/PM] …\n`), so non-empty-line
624
- * count == entry count for a well-formed buffer; blank lines and trailing
625
- * newlines don't inflate the count.
622
+ * Lines, deliberately, not entries. A multiline `remember()` fact is one
623
+ * entry spread over several lines, so the two counts diverge and each answers
624
+ * a different question. This trigger and the injected-Buffer cap that reuses
625
+ * it (`capBufferSection` in `static-context.ts`) both care about how much
626
+ * context the buffer costs, which scales with lines. The per-run budget
627
+ * `consolidation_max_entries_per_run` cares about how many facts the agent
628
+ * must file, so it counts entry-start lines instead.
629
+ *
630
+ * Do not "fix" this to count entries: a single 200-line fact is a context
631
+ * problem the trigger should fire on, even though it is one entry. Blank
632
+ * lines and trailing newlines don't inflate the count.
626
633
  */
627
634
  export function countBufferLines(bufferPath: string): number {
628
635
  return countNonEmptyLines(readBufferContent(bufferPath));
@@ -22,6 +22,13 @@ export interface SkillCapabilityInput {
22
22
  */
23
23
  export const ALWAYS_CANDIDATE_CARD_CHARS = 900;
24
24
 
25
+ /**
26
+ * Character budget for an ordinary skill's capability statement. A statement
27
+ * that overruns is hard-truncated, dropping its trailing activation hints and
28
+ * its entire avoid-when list from what the selector sees.
29
+ */
30
+ export const DEFAULT_CARD_CHARS = 500;
31
+
25
32
  /**
26
33
  * Render the prose-style capability statement embedded into the unified
27
34
  * `memory_v2_concept_pages` Qdrant collection (under the `skills/<id>` slug
@@ -32,7 +39,7 @@ export const ALWAYS_CANDIDATE_CARD_CHARS = 900;
32
39
  */
33
40
  export function buildSkillContent(
34
41
  input: SkillCapabilityInput,
35
- maxChars = 500,
42
+ maxChars: number = DEFAULT_CARD_CHARS,
36
43
  ): string {
37
44
  const list = maxChars > 500;
38
45
  let content = `The "${input.displayName}" skill (${input.id}) is available. ${input.description}.`;