@vellumai/assistant 0.11.4 → 0.11.5-staging.1

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 (358) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/conversation-attachments.test.ts +0 -1
  38. package/src/__tests__/conversation-error.test.ts +17 -0
  39. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  40. package/src/__tests__/conversation-pairing.test.ts +186 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  42. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  43. package/src/__tests__/credential-routes.test.ts +22 -3
  44. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  45. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  46. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  47. package/src/__tests__/dm-backfill.test.ts +63 -0
  48. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  49. package/src/__tests__/edit-propagation.test.ts +107 -4
  50. package/src/__tests__/events-client-registration.test.ts +28 -0
  51. package/src/__tests__/file-write-tool.test.ts +4 -2
  52. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  53. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  54. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  55. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  56. package/src/__tests__/host-shell-tool.test.ts +23 -4
  57. package/src/__tests__/image-conversion.test.ts +143 -1
  58. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  59. package/src/__tests__/input-repairs.test.ts +207 -0
  60. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  61. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  62. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  63. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  64. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  65. package/src/__tests__/mtime-cache.test.ts +2 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  67. package/src/__tests__/openai-provider.test.ts +22 -0
  68. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  69. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  70. package/src/__tests__/platform.test.ts +16 -1
  71. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  72. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  73. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  74. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  75. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  76. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  77. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  78. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  79. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  80. package/src/__tests__/reaction-persistence.test.ts +200 -7
  81. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  82. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  83. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  84. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  85. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  86. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  87. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  88. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  89. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  90. package/src/__tests__/tool-executor.test.ts +63 -38
  91. package/src/__tests__/tool-policy.test.ts +97 -1
  92. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  93. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  94. package/src/__tests__/validate-input.test.ts +243 -7
  95. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  97. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  98. package/src/acp/acp-claude-oauth.ts +38 -21
  99. package/src/acp/prepare-agent-env.ts +146 -55
  100. package/src/agent/loop-tool-dedup.test.ts +161 -0
  101. package/src/agent/loop.ts +68 -22
  102. package/src/api/constants/app-tools.ts +34 -0
  103. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  104. package/src/api/index.ts +19 -0
  105. package/src/api/responses/resource-pressure-status.ts +25 -0
  106. package/src/approvals/guardian-channel-delivery.ts +70 -6
  107. package/src/approvals/guardian-request-resolvers.ts +32 -66
  108. package/src/channels/__tests__/message-audience.test.ts +49 -0
  109. package/src/channels/__tests__/types.test.ts +17 -3
  110. package/src/channels/message-audience.ts +40 -0
  111. package/src/channels/types.ts +16 -12
  112. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  113. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  114. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  115. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  116. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  117. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  118. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  119. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  120. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  121. package/src/cli/commands/conversations.help.ts +34 -0
  122. package/src/cli/commands/conversations.ts +125 -0
  123. package/src/cli/commands/inference-providers.ts +9 -4
  124. package/src/cli/commands/inference.help.ts +9 -4
  125. package/src/cli/commands/keys.help.ts +13 -1
  126. package/src/cli/commands/keys.ts +26 -15
  127. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  128. package/src/cli/commands/memory/index.help.ts +41 -27
  129. package/src/cli/commands/memory/index.ts +2 -0
  130. package/src/cli/commands/memory/memory-v2.ts +58 -54
  131. package/src/cli/commands/memory/memory-validate.ts +18 -0
  132. package/src/cli/commands/monitoring.ts +1 -0
  133. package/src/cli/commands/plugins.help.ts +28 -31
  134. package/src/cli/commands/plugins.ts +10 -0
  135. package/src/cli/commands/skills.help.ts +13 -16
  136. package/src/cli/commands/trust.ts +3 -14
  137. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  138. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  139. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  140. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  141. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  142. package/src/cli/lib/bundled-marketplace.json +13 -0
  143. package/src/cli/lib/daemon-credential-client.ts +25 -3
  144. package/src/cli/lib/install-from-github.ts +35 -24
  145. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  146. package/src/cli/lib/plugin-surfaces.ts +27 -0
  147. package/src/config/feature-flag-registry.json +35 -19
  148. package/src/config/webhook-routing.ts +8 -0
  149. package/src/context/compactor.ts +31 -2
  150. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  151. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  152. package/src/daemon/conversation-error.ts +13 -0
  153. package/src/daemon/conversation-messaging.ts +22 -2
  154. package/src/daemon/conversation-process.ts +11 -0
  155. package/src/daemon/conversation-store.ts +10 -0
  156. package/src/daemon/conversation-surfaces.ts +21 -8
  157. package/src/daemon/conversation.ts +12 -10
  158. package/src/daemon/lifecycle.ts +6 -2
  159. package/src/daemon/message-types/conversations.ts +5 -7
  160. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  161. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  162. package/src/daemon/resource-pressure-guard.ts +387 -0
  163. package/src/daemon/shutdown-handlers.ts +2 -0
  164. package/src/daemon/startup-error.ts +16 -0
  165. package/src/daemon/trust-context.ts +14 -10
  166. package/src/daemon/unsendable-image-notice.ts +76 -0
  167. package/src/hooks/registry.ts +65 -51
  168. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  169. package/src/ipc/gateway-client.test.ts +1 -1
  170. package/src/ipc/gateway-client.ts +14 -21
  171. package/src/ipc/socket-cleanup.ts +2 -11
  172. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  173. package/src/messaging/provider-message-metadata.ts +128 -0
  174. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  175. package/src/messaging/providers/channel-transport.ts +90 -13
  176. package/src/messaging/providers/discord/send.ts +16 -0
  177. package/src/messaging/providers/discord/transport.ts +11 -1
  178. package/src/messaging/providers/index.ts +109 -16
  179. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  180. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  181. package/src/messaging/providers/slack/send.test.ts +6 -9
  182. package/src/messaging/providers/slack/send.ts +40 -28
  183. package/src/messaging/providers/slack/transport.ts +22 -26
  184. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  185. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  186. package/src/messaging/read-provider-metadata.test.ts +157 -0
  187. package/src/messaging/read-provider-metadata.ts +51 -0
  188. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  189. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  190. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  191. package/src/notifications/adapters/macos.ts +55 -0
  192. package/src/notifications/adapters/slack.ts +10 -5
  193. package/src/notifications/assistant-reply-producer.ts +37 -4
  194. package/src/notifications/conversation-pairing.ts +111 -21
  195. package/src/notifications/edit-notification.ts +38 -8
  196. package/src/notifications/emit-signal.ts +12 -14
  197. package/src/notifications/home-feed-side-effect.ts +218 -15
  198. package/src/notifications/types.ts +5 -0
  199. package/src/permissions/AGENTS.md +16 -0
  200. package/src/permissions/checker.test.ts +75 -122
  201. package/src/permissions/checker.ts +44 -560
  202. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  203. package/src/permissions/confirmation-guardian-request.ts +5 -1
  204. package/src/persistence/conversation-crud.ts +23 -20
  205. package/src/persistence/conversation-queries.ts +14 -1
  206. package/src/persistence/conversation-types.ts +22 -0
  207. package/src/persistence/delivery-crud.ts +122 -15
  208. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  209. package/src/plugin-api/conversation-turn.ts +27 -0
  210. package/src/plugin-api/credential-scope.test.ts +15 -0
  211. package/src/plugin-api/credential-scope.ts +14 -0
  212. package/src/plugin-api/index.ts +19 -1
  213. package/src/plugin-api/resolve-credential.ts +15 -10
  214. package/src/plugin-api/store-credential.ts +148 -0
  215. package/src/plugin-api/system-card.ts +36 -0
  216. package/src/plugin-api/vision-support.test.ts +82 -0
  217. package/src/plugin-api/vision-support.ts +14 -2
  218. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  219. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  220. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  221. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  222. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  223. package/src/plugins/defaults/main.ts +15 -0
  224. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  225. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  226. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  227. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  228. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  229. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  230. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  231. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  232. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  233. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  234. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  238. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  239. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  240. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  241. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  242. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  243. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  244. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  245. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  246. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  247. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  248. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  249. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  250. package/src/plugins/mtime-cache.ts +1 -1
  251. package/src/plugins/pipeline.ts +14 -7
  252. package/src/plugins/plugin-execution-context.ts +35 -11
  253. package/src/plugins/plugin-tree-walk.ts +63 -4
  254. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  255. package/src/providers/anthropic/client.ts +332 -241
  256. package/src/providers/connection-resolution.ts +2 -1
  257. package/src/providers/content-blocks.ts +22 -0
  258. package/src/providers/gemini/client.ts +5 -2
  259. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  260. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  261. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  262. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  263. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  264. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  265. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  266. package/src/providers/inference/adapter-factory.ts +33 -2
  267. package/src/providers/inference/auth.ts +13 -2
  268. package/src/providers/inference/credential-usage.ts +37 -0
  269. package/src/providers/inference/missing-credential-guard.ts +110 -0
  270. package/src/providers/inference/resolve-auth.ts +7 -7
  271. package/src/providers/media-resolve.ts +176 -15
  272. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  273. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  274. package/src/providers/openai/chat-completions-provider.ts +322 -42
  275. package/src/routes/route-host-protocol.ts +7 -0
  276. package/src/routes/worker.ts +31 -10
  277. package/src/runtime/AGENTS.md +35 -0
  278. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  279. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  280. package/src/runtime/assistant-event-hub.ts +38 -0
  281. package/src/runtime/channel-reply-delivery.ts +54 -31
  282. package/src/runtime/channel-retry-sweep.ts +6 -4
  283. package/src/runtime/effective-capabilities.test.ts +19 -0
  284. package/src/runtime/effective-capabilities.ts +25 -0
  285. package/src/runtime/guardian-reply-router.ts +6 -2
  286. package/src/runtime/http-errors.ts +1 -0
  287. package/src/runtime/http-server.ts +20 -5
  288. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  289. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  290. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  291. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  292. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  293. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  294. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  295. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  296. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  297. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  298. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  299. package/src/runtime/routes/approval-routes.ts +37 -4
  300. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  301. package/src/runtime/routes/canned-message-complete.ts +68 -29
  302. package/src/runtime/routes/client-routes.ts +113 -0
  303. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  304. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  305. package/src/runtime/routes/conversation-routes.ts +36 -22
  306. package/src/runtime/routes/credential-in-use.ts +85 -0
  307. package/src/runtime/routes/credential-routes.ts +55 -85
  308. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  309. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  310. package/src/runtime/routes/identity-routes.ts +5 -121
  311. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  312. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  313. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  314. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  315. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  316. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  317. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  318. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  319. package/src/runtime/routes/index.ts +2 -0
  320. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  321. package/src/runtime/routes/monitoring-routes.ts +9 -1
  322. package/src/runtime/routes/plugins-routes.ts +17 -5
  323. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  324. package/src/runtime/routes/secret-routes.ts +39 -8
  325. package/src/runtime/routes/settings-routes.ts +6 -6
  326. package/src/runtime/routes/surface-action-routes.ts +20 -19
  327. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  328. package/src/runtime/routes/user-route-resolution.ts +18 -1
  329. package/src/runtime/slack-reply-session.test.ts +23 -13
  330. package/src/runtime/slack-reply-session.ts +38 -55
  331. package/src/runtime/web-presence.ts +90 -0
  332. package/src/skills/validate-input.ts +177 -40
  333. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  334. package/src/subagent/consult-context.ts +7 -23
  335. package/src/tools/credentials/broker.ts +24 -79
  336. package/src/tools/credentials/ref-parse.ts +35 -0
  337. package/src/tools/credentials/resolve.ts +4 -10
  338. package/src/tools/credentials/store.ts +168 -0
  339. package/src/tools/credentials/tool-policy.ts +67 -0
  340. package/src/tools/executor.ts +53 -15
  341. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  342. package/src/tools/network/url-safety.ts +9 -16
  343. package/src/tools/network/web-search.ts +21 -3
  344. package/src/tools/permission-checker.ts +43 -52
  345. package/src/tools/schema-transforms.ts +40 -5
  346. package/src/tools/shared/input-repairs.ts +160 -0
  347. package/src/tools/skills/skill-tool-factory.ts +18 -4
  348. package/src/tools/subagent/spawn.ts +0 -1
  349. package/src/tools/tool-approval-handler.ts +16 -10
  350. package/src/tools/tool-types.ts +26 -13
  351. package/src/tools/types.ts +6 -6
  352. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  353. package/src/util/cgroup-memory.ts +3 -0
  354. package/src/util/container-cpu-sampler.ts +250 -0
  355. package/src/util/image-conversion.ts +178 -14
  356. package/src/util/platform.ts +93 -10
  357. package/src/permissions/ipc-risk-types.ts +0 -143
  358. package/src/permissions/risk-types.ts +0 -76
@@ -17,6 +17,14 @@
17
17
  * {@link captionImagesInMessages} (which also persists the original and
18
18
  * caches captions across turns), sweeping top-level blocks and images
19
19
  * nested in `tool_result` blocks alike.
20
+ * 4. Posts a transcript system card when an image the user attached to this
21
+ * turn reached step 3 with no vision profile configured, since that image
22
+ * is information the model never receives in any form.
23
+ *
24
+ * The card covers only images attached to the message this turn submitted.
25
+ * The sweep also walks earlier turns (persisted rows keep their raw images, so
26
+ * every turn re-encounters them) and tool-returned media, and describing those
27
+ * again would post a card per turn for images the user already heard about.
20
28
  *
21
29
  * The companion `post-tool-use` hook applies the same substitution to images a
22
30
  * tool returns (e.g. a browser screenshot), and `post-compact` re-sweeps the
@@ -25,6 +33,7 @@
25
33
 
26
34
  import {
27
35
  type HookFunction,
36
+ persistSystemCard,
28
37
  type UserPromptSubmitContext,
29
38
  } from "@vellumai/plugin-api";
30
39
 
@@ -34,6 +43,29 @@ import {
34
43
  } from "../src/caption-blocks.js";
35
44
  import { findVisionProfile } from "../src/vision-caption.js";
36
45
 
46
+ /**
47
+ * How many images the user attached to the message this turn submitted, read
48
+ * off the pre-hook history snapshot: its tail is the submitted row as the user
49
+ * sent it, before any earlier hook's injected blocks (memory context, runtime
50
+ * context) reach the working history. Counting there keeps the tally to the
51
+ * user's own attachments and needs no object identity, which injection breaks
52
+ * by rebuilding the tail message around its added blocks.
53
+ */
54
+ function submittedImageCount(ctx: UserPromptSubmitContext): number {
55
+ const submitted = ctx.originalMessages[ctx.originalMessages.length - 1];
56
+ if (submitted == null || submitted.role !== "user") {
57
+ return 0;
58
+ }
59
+ return submitted.content.filter((block) => block.type === "image").length;
60
+ }
61
+
62
+ /** Transcript copy for images this turn could not send to the model. */
63
+ function droppedImageCardText(count: number): string {
64
+ return count === 1
65
+ ? "The image you attached was not sent to the model: no vision-capable model is configured to describe it. Configure one to use images in this conversation."
66
+ : `The ${count} images you attached were not sent to the model: no vision-capable model is configured to describe them. Configure one to use images in this conversation.`;
67
+ }
68
+
37
69
  const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
38
70
  // If the turn's model already supports vision, nothing to do.
39
71
  if (!needsImageFallback(ctx.modelProfileKey)) {
@@ -43,6 +75,12 @@ const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
43
75
  // Find a vision-capable profile for captioning.
44
76
  const visionProfileKey = findVisionProfile();
45
77
 
78
+ // Images the user attached to this turn, tallied before the sweep replaces
79
+ // them: with no vision profile every one of them is dropped, so this is the
80
+ // count the card reports.
81
+ const droppedFromSubmission =
82
+ visionProfileKey == null ? submittedImageCount(ctx) : 0;
83
+
46
84
  // Scan all messages for image blocks and replace them with captions.
47
85
  const imageCount = await captionImagesInMessages(
48
86
  ctx.latestMessages,
@@ -57,6 +95,26 @@ const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
57
95
  "Replaced image blocks with text captions for text-only model",
58
96
  );
59
97
  }
98
+
99
+ if (droppedFromSubmission > 0) {
100
+ try {
101
+ await persistSystemCard({
102
+ conversationId: ctx.conversationId,
103
+ text: droppedImageCardText(droppedFromSubmission),
104
+ metadata: {
105
+ plugin: "image-fallback",
106
+ droppedImageCount: droppedFromSubmission,
107
+ },
108
+ });
109
+ } catch (err) {
110
+ // The turn proceeds with the placeholder text either way; the notice is
111
+ // the only thing lost.
112
+ ctx.logger.warn(
113
+ { plugin: "image-fallback", err },
114
+ "Failed to post dropped-image notice",
115
+ );
116
+ }
117
+ }
60
118
  };
61
119
 
62
120
  export default userPromptSubmit;
@@ -11,7 +11,10 @@
11
11
  * original image to a known location, caption it via a vision-capable
12
12
  * profile, and swap in a `[Image …]` text block — and the message-level deep
13
13
  * sweep ({@link captionImagesInMessages}) that reaches images nested inside
14
- * `tool_result` blocks as well as top-level ones.
14
+ * `tool_result` blocks as well as top-level ones. The message-level sweeps
15
+ * close by merging text-only user content into a single text block
16
+ * ({@link flattenTextOnlyBlocks}), the shape providers serialize as a plain
17
+ * string.
15
18
  *
16
19
  * The substitution mutates the blocks in place, but the hook pipeline hands
17
20
  * each hook a deep clone of its context, so the caption reaches only the
@@ -62,8 +65,7 @@ export function needsImageFallback(modelProfileKey: string): boolean {
62
65
 
63
66
  /**
64
67
  * Replace every `image` block in `blocks` (in place) with a text caption so a
65
- * text-only model can still reason about the image's content. Returns the
66
- * number of image blocks replaced.
68
+ * text-only model can still reason about the image's content.
67
69
  *
68
70
  * @param blocks Content-block array to scan and mutate in place.
69
71
  * @param conversationId Conversation the blocks belong to, recorded on the
@@ -80,7 +82,7 @@ export async function captionImageBlocks(
80
82
  visionProfileKey: string | null,
81
83
  logger: PluginLogger,
82
84
  ): Promise<number> {
83
- let imageCount = 0;
85
+ let replaced = 0;
84
86
 
85
87
  for (let i = 0; i < blocks.length; i++) {
86
88
  const block = blocks[i];
@@ -88,7 +90,7 @@ export async function captionImageBlocks(
88
90
  continue;
89
91
  }
90
92
 
91
- imageCount++;
93
+ replaced++;
92
94
  const image = block as ImageContent;
93
95
 
94
96
  // Persist the original to a known, content-hash-deduped location so it
@@ -114,7 +116,7 @@ export async function captionImageBlocks(
114
116
  : `[Image: auto-description failed (text-only model)]`,
115
117
  };
116
118
  } else {
117
- // No vision profile configured at all — fail-open placeholder.
119
+ // No vision profile configured at all: fail-open placeholder.
118
120
  blocks[i] = {
119
121
  type: "text",
120
122
  text: `[Image: no vision-capable model configured to describe it]`,
@@ -122,12 +124,12 @@ export async function captionImageBlocks(
122
124
  }
123
125
  }
124
126
 
125
- return imageCount;
127
+ return replaced;
126
128
  }
127
129
 
128
130
  /**
129
131
  * Replace image blocks nested in a message's `tool_result` blocks' rich
130
- * `contentBlocks` (in place) with text captions. Returns the number replaced.
132
+ * `contentBlocks` (in place) with text captions.
131
133
  */
132
134
  async function captionToolResultMedia(
133
135
  message: Message,
@@ -135,10 +137,10 @@ async function captionToolResultMedia(
135
137
  visionProfileKey: string | null,
136
138
  logger: PluginLogger,
137
139
  ): Promise<number> {
138
- let imageCount = 0;
140
+ let replaced = 0;
139
141
  for (const block of message.content) {
140
142
  if (block.type === "tool_result" && block.contentBlocks != null) {
141
- imageCount += await captionImageBlocks(
143
+ replaced += await captionImageBlocks(
142
144
  block.contentBlocks,
143
145
  conversationId,
144
146
  visionProfileKey,
@@ -146,7 +148,42 @@ async function captionToolResultMedia(
146
148
  );
147
149
  }
148
150
  }
149
- return imageCount;
151
+ return replaced;
152
+ }
153
+
154
+ /**
155
+ * Merge a user message's text blocks (in place) into a single text block,
156
+ * joined by a blank line, for every message whose content is more than one
157
+ * block and entirely text. Returns how many messages were merged.
158
+ *
159
+ * A text-only turn's user content is text after image substitution, and a
160
+ * single text block is the shape providers serialize as a plain string:
161
+ * OpenAI-compatible endpoints that accept only `messages[].content` as a
162
+ * string (rejecting an array of content parts with
163
+ * `body/messages/N/content must be string`) can then take the turn, and the
164
+ * request costs fewer tokens than the equivalent array of parts. The blank
165
+ * line keeps the boundaries between the blocks a user message carries (the
166
+ * turn's runtime-injected context blocks plus the user's own text) legible.
167
+ *
168
+ * Content holding any non-text block (audio, tool results, an image no
169
+ * fallback replaced) keeps its array shape, since merging would drop it.
170
+ */
171
+ export function flattenTextOnlyBlocks(messages: Message[]): number {
172
+ let flattened = 0;
173
+ for (const message of messages) {
174
+ if (message.role !== "user" || message.content.length <= 1) {
175
+ continue;
176
+ }
177
+ if (!message.content.every((block) => block.type === "text")) {
178
+ continue;
179
+ }
180
+ const text = message.content
181
+ .map((block) => (block.type === "text" ? block.text : ""))
182
+ .join("\n\n");
183
+ message.content = [{ type: "text", text }];
184
+ flattened++;
185
+ }
186
+ return flattened;
150
187
  }
151
188
 
152
189
  /**
@@ -154,8 +191,9 @@ async function captionToolResultMedia(
154
191
  * a text caption via {@link captionImageBlocks}. Covers both top-level image
155
192
  * blocks (user-attached images, the compactor's retained-image message) and
156
193
  * images nested in a `tool_result` block's rich `contentBlocks` (tool results
157
- * restored from persistence carry their raw images there). Returns the number
158
- * of image blocks replaced.
194
+ * restored from persistence carry their raw images there).
195
+ *
196
+ * Text-only user content is then merged via {@link flattenTextOnlyBlocks}.
159
197
  */
160
198
  export async function captionImagesInMessages(
161
199
  messages: Message[],
@@ -163,22 +201,23 @@ export async function captionImagesInMessages(
163
201
  visionProfileKey: string | null,
164
202
  logger: PluginLogger,
165
203
  ): Promise<number> {
166
- let imageCount = 0;
204
+ let replaced = 0;
167
205
  for (const message of messages) {
168
- imageCount += await captionImageBlocks(
206
+ replaced += await captionImageBlocks(
169
207
  message.content,
170
208
  conversationId,
171
209
  visionProfileKey,
172
210
  logger,
173
211
  );
174
- imageCount += await captionToolResultMedia(
212
+ replaced += await captionToolResultMedia(
175
213
  message,
176
214
  conversationId,
177
215
  visionProfileKey,
178
216
  logger,
179
217
  );
180
218
  }
181
- return imageCount;
219
+ flattenTextOnlyBlocks(messages);
220
+ return replaced;
182
221
  }
183
222
 
184
223
  /**
@@ -188,8 +227,9 @@ export async function captionImagesInMessages(
188
227
  * message ({@link lastToolResultUserMessageIndex}, the one the sanitizer keeps
189
228
  * intact). Older tool_result media is left raw so the sanitizer replaces it
190
229
  * with its compact removed-media marker on the retry rather than a full
191
- * caption — captioning it would waste vision calls and balloon context.
192
- * Returns the number of image blocks replaced.
230
+ * caption: captioning it would waste vision calls and balloon context.
231
+ *
232
+ * Text-only user content is then merged via {@link flattenTextOnlyBlocks}.
193
233
  */
194
234
  export async function captionOutboundImagesInMessages(
195
235
  messages: Message[],
@@ -198,16 +238,16 @@ export async function captionOutboundImagesInMessages(
198
238
  logger: PluginLogger,
199
239
  ): Promise<number> {
200
240
  const currentTurnIdx = lastToolResultUserMessageIndex(messages);
201
- let imageCount = 0;
241
+ let replaced = 0;
202
242
  for (let i = 0; i < messages.length; i++) {
203
- imageCount += await captionImageBlocks(
243
+ replaced += await captionImageBlocks(
204
244
  messages[i].content,
205
245
  conversationId,
206
246
  visionProfileKey,
207
247
  logger,
208
248
  );
209
249
  if (i === currentTurnIdx) {
210
- imageCount += await captionToolResultMedia(
250
+ replaced += await captionToolResultMedia(
211
251
  messages[i],
212
252
  conversationId,
213
253
  visionProfileKey,
@@ -215,5 +255,6 @@ export async function captionOutboundImagesInMessages(
215
255
  );
216
256
  }
217
257
  }
218
- return imageCount;
258
+ flattenTextOnlyBlocks(messages);
259
+ return replaced;
219
260
  }
@@ -13,6 +13,14 @@
13
13
  * classification matters because retrying with the same image is futile — the
14
14
  * recovery path must resize, relabel, or note it instead.
15
15
  *
16
+ * OpenAI-compatible providers reject the same class of input in their own
17
+ * wording ("The image data you provided does not represent a valid image.
18
+ * Please check your input and try again with one of the supported image
19
+ * formats: [image/jpeg, image/png, image/gif, image/webp]"), so the
20
+ * unsupported-format patterns carry that shape. Without them an OpenAI-family
21
+ * 400 falls through to the generic provider-error copy and takes the whole
22
+ * turn down with no hint that an attachment is what broke it.
23
+ *
16
24
  * Exported as the single source of truth: the image-recovery hook reads these
17
25
  * to recognize the rejections it can recover, and `daemon/conversation-error`
18
26
  * imports them so the user-facing classification stays in lockstep with what
@@ -32,6 +40,11 @@ export const IMAGE_MEDIA_TYPE_MISMATCH_PATTERNS: readonly RegExp[] = [
32
40
  /image does not match the provided media type/i,
33
41
  ];
34
42
 
43
+ export const IMAGE_UNSUPPORTED_FORMAT_PATTERNS: readonly RegExp[] = [
44
+ /does not represent a valid image/i,
45
+ /supported image formats?:/i,
46
+ ];
47
+
35
48
  /** Whether an error message indicates an image-input dimension/payload failure. */
36
49
  export function isImageDimensionsTooLargeError(message: string): boolean {
37
50
  return IMAGE_DIMENSIONS_TOO_LARGE_PATTERNS.some((p) => p.test(message));
@@ -53,11 +66,21 @@ export function isImageMediaTypeMismatchError(message: string): boolean {
53
66
  return IMAGE_MEDIA_TYPE_MISMATCH_PATTERNS.some((p) => p.test(message));
54
67
  }
55
68
 
69
+ /**
70
+ * Whether an error message indicates the image's bytes are not in a format the
71
+ * provider accepts at all, rather than merely mislabeled: an unsupported
72
+ * codec (HEIC, AVIF, BMP), or a truncated or corrupt payload.
73
+ */
74
+ export function isImageUnsupportedFormatError(message: string): boolean {
75
+ return IMAGE_UNSUPPORTED_FORMAT_PATTERNS.some((p) => p.test(message));
76
+ }
77
+
56
78
  /** Any image-input rejection the recovery hook knows how to act on. */
57
79
  export function isRecoverableImageError(message: string): boolean {
58
80
  return (
59
81
  isImageDimensionsTooLargeError(message) ||
60
82
  isImageUnprocessableError(message) ||
61
- isImageMediaTypeMismatchError(message)
83
+ isImageMediaTypeMismatchError(message) ||
84
+ isImageUnsupportedFormatError(message)
62
85
  );
63
86
  }
@@ -29,6 +29,7 @@ const DEFAULTS_DIR = resolveBundledDir(import.meta.dir, ".", "default-plugins");
29
29
  const DEFAULT_PLUGIN_NAMESPACE_PREFIX = "default-";
30
30
 
31
31
  let cachedNames: readonly string[] | null = null;
32
+ let cachedNameSet: ReadonlySet<string> | null = null;
32
33
  let cachedDirToManifest: ReadonlyMap<string, string> | null = null;
33
34
 
34
35
  /**
@@ -58,10 +59,24 @@ export function getAllDefaultPluginNames(): readonly string[] {
58
59
  }
59
60
  }
60
61
  cachedNames = names.sort();
62
+ cachedNameSet = new Set(cachedNames);
61
63
  }
62
64
  return cachedNames;
63
65
  }
64
66
 
67
+ /**
68
+ * Whether `name` is one of the first-party default plugins that ship in
69
+ * `plugins/defaults/` (e.g. `default-memory`). Used by hook collection so
70
+ * those plugins' hooks are not dropped by a per-chat allowlist that only
71
+ * names user-installed plugins.
72
+ */
73
+ export function isFirstPartyDefaultPlugin(name: string): boolean {
74
+ if (cachedNameSet === null) {
75
+ getAllDefaultPluginNames();
76
+ }
77
+ return cachedNameSet !== null && cachedNameSet.has(name);
78
+ }
79
+
65
80
  /**
66
81
  * Manifest name (e.g. `default-platform-hosted`) for a default plugin's
67
82
  * *directory* name (e.g. `platform-hosted`), or `null` when `<dirName>` is not
@@ -38,11 +38,16 @@ them. Everything not listed as a tier directory is spine.
38
38
 
39
39
  | Directory | What lives there | Gate predicate |
40
40
  | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
41
- | `substrate/` | The shared concept-page substrate (v2 + v3): `page-store`, `page-index`, `edge-index`, `qdrant`, `sim`, `sparse-bm25`, `spread`, `skill-store`, `cli-command-store`, `skill-content`, `cli-command-content`, `injected-block-slugs`, `static-context`, `boot-maintenance`, `frontmatter-sweep`, `consolidation-lock`, `consolidation-job`, `ingest`, `sweep-job`, `reembed-job`, `tuning`, `types`, `constants`, `prompts/`. | `usesConceptPageMemory()` |
41
+ | `substrate/` | The shared concept-page substrate (v2 + v3): `page-store`, `page-index`, `edge-index`, `qdrant`, `sim`, `sparse-bm25`, `spread`, `skill-store`, `cli-command-store`, `skill-content`, `cli-command-content`, `injected-block-slugs`, `static-context`, `boot-maintenance`, `frontmatter-sweep`, `consolidation-lock`, `consolidation-job`, `ingest`, `sweep-job`, `reembed-job`, `tuning`, `types`, `page-links`, `prompts/`. | `usesConceptPageMemory()` |
42
42
  | `v1/` | The legacy PKB/graph engine: `pkb/`, `graph/` (retriever, triggers, scoring, serendipity, extraction, extraction-job, decay, consolidation, pattern-scan, narrative, bootstrap, injection, and the hybrid node-search half of `graph-search`), `jobs/embed-pkb-file.ts`, `job-handlers/{backfill,embedding,index-maintenance}.ts`, `pkb-schedule.ts`, `filing-jobs.ts`, `semantic-search.ts`, `identity-context.ts`, `README.md`. | `isMemoryV1Active()` |
43
43
  | `v2/` | The v2 activation/router injection engine: `activation`, `activation-store`, `activation-log-store`, `concept-frequency`, `injection`, `injection-events`, `router`, `reranker`, `rerank-local`, `migration`, `now-text`, `backfill-jobs` (migrate + activation-recompute), `harness/`, `prompts/router`. | `isV2InjectionEngineActive()` |
44
44
  | `v3/`, `v3-eval/` | The v3 lanes, orchestrator, injectors, maintain job, and the eval siblings. | `isMemoryV3Live()` |
45
45
 
46
+ `substrate/page-links` is the one owner of `links:`-entry and `[[wikilink]]`
47
+ parsing; every reader in any tier imports it (the same single-matcher rule
48
+ `buffer-format` follows for buffer entries, below). Do not add a second
49
+ wikilink matcher or `links:` splitter anywhere.
50
+
46
51
  Everything else under the plugin root is **spine**:
47
52
 
48
53
  - `graph/` — the **all-tier** legacy-graph store plus the tier dispatcher:
@@ -708,8 +713,11 @@ not deleted**. Today the test-only hits are:
708
713
  client-coordinated change.
709
714
  - `cli/commands/memory/memory-v2.ts` — follows its routes rather than being
710
715
  deleted outright. `memory v2 reembed` (which posts the surviving `reembed`
711
- backfill op), `memory v2 reembed-skills`, and `memory v2 validate` keep
712
- working and move with the substrate routes; `memory v2 activation` (the
716
+ backfill op) and `memory v2 reembed-skills` keep working and move with
717
+ the substrate routes; `memory v2 validate` and the top-level
718
+ `memory validate` (`cli/commands/memory/memory-validate.ts`) share
719
+ `runMemoryValidate`, which lives in `memory-v2.ts` and moves with the
720
+ survivors; `memory v2 activation` (the
713
721
  `activation-recompute` op), `memory v2 ema`, `memory v2 simulate`, and
714
722
  `memory v2 compare` go with the engine. There is no `migrate` subcommand —
715
723
  the route is that job's only enqueue path.
@@ -151,7 +151,6 @@ const HOST_TIER_IMPORT_ALLOWLIST: readonly HostTierImportExemption[] = [
151
151
  { path: "src/daemon/embedding-reconcile.ts", tiers: ["substrate", "v3"] },
152
152
  { path: "src/daemon/skill-memory-refresh.ts", tiers: ["substrate"] },
153
153
  { path: "src/daemon/tool-side-effects.ts", tiers: ["substrate"] },
154
- { path: "src/daemon/trust-context.ts", tiers: ["substrate"] },
155
154
  { path: "src/persistence/steps.ts", tiers: ["v1"] },
156
155
  { path: "src/runtime/routes/consolidation-routes.ts", tiers: ["substrate"] },
157
156
  {
@@ -22,6 +22,11 @@ import {
22
22
  formatRememberEntry,
23
23
  matchBufferEntryStart,
24
24
  } from "../buffer-format.js";
25
+ import {
26
+ extractWikilinkTargets,
27
+ WIKILINK_REGEX,
28
+ wikilinkTarget,
29
+ } from "../substrate/page-links.js";
25
30
  import type { MemoryGraphEdge, MemoryGraphNode } from "./types.js";
26
31
 
27
32
  /** Node id prefix marking a pending buffer entry (id = prefix + text hash). */
@@ -43,9 +48,6 @@ const PENDING_LABEL_MAX_CHARS = 60;
43
48
  * timestamped entry seen yet still counts as an entry of its own. */
44
49
  const PLAIN_BULLET_REGEX = /^-\s+(.+)$/;
45
50
 
46
- /** Matches `[[slug]]` / `[[slug|label]]` wikilinks inside an entry. */
47
- const WIKILINK_REGEX = /\[\[([^[\]|]+)(?:\|[^\]]*)?\]\]/g;
48
-
49
51
  export interface PendingBufferEntry {
50
52
  /** Stable node id (`buffer:` + content hash, deduped within one parse). */
51
53
  id: string;
@@ -116,13 +118,7 @@ export function parseBufferEntries(content: string): PendingBufferEntry[] {
116
118
  flush();
117
119
 
118
120
  for (const text of texts) {
119
- const slugs: string[] = [];
120
- for (const link of text.matchAll(WIKILINK_REGEX)) {
121
- const slug = link[1]!.trim();
122
- if (slug && !slugs.includes(slug)) {
123
- slugs.push(slug);
124
- }
125
- }
121
+ const slugs = [...new Set(extractWikilinkTargets(text))];
126
122
 
127
123
  const hash = hashText(text);
128
124
  const count = (seen.get(hash) ?? 0) + 1;
@@ -144,8 +140,9 @@ export function parseBufferEntries(content: string): PendingBufferEntry[] {
144
140
  function pendingLabel(text: string): string {
145
141
  const firstLine = text.split("\n", 1)[0] ?? text;
146
142
  const plain = firstLine
147
- .replace(WIKILINK_REGEX, (_match, slug: string) => {
148
- return slug.trim().split("/").pop() ?? slug;
143
+ .replace(WIKILINK_REGEX, (_match, inner: string) => {
144
+ const slug = wikilinkTarget(inner);
145
+ return slug.split("/").pop() || slug;
149
146
  })
150
147
  .trim();
151
148
  if (plain.length <= PENDING_LABEL_MAX_CHARS) {
@@ -223,7 +223,7 @@ If your review window contains a PROCEDURE you actually carried out — a sequen
223
223
 
224
224
  When you do capture a procedure:
225
225
 
226
- 1. Deduplicate against existing skills first. Call \`find_similar_skills\` with a short description of the procedure's goal. Each hit carries a \`source\` (bundled, managed, plugin, workspace, or extra), and a managed hit also carries \`author\` (\`"assistant"\` if you authored it, \`"user"\` if a person did, omitted if untagged). You may only overwrite or refine a skill YOU authored — a hit with \`source: "managed"\` AND \`author: "assistant"\`. ANY other hit means the procedure is ALREADY COVERED: a non-managed source (bundled, plugin, workspace, or extra), OR a managed skill that is NOT \`author: "assistant"\` (a person wrote it, or it is untagged). For an ALREADY COVERED hit do not \`overwrite\` it, do not shadow it by creating a skill with its \`skill_id\`, and do not create a near-duplicate — skip it. Only when a returned skill is one of your own (\`source: "managed"\`, \`author: "assistant"\`) and is the SAME procedure, UPDATE it: call \`scaffold_managed_skill\` with that \`skill_id\` and \`overwrite: true\`, rewriting the body from what you actually observed in the trace. Only CREATE a new skill (fresh \`skill_id\`) when no existing skill of any source covers the procedure. Bias strongly toward reusing or refining your own skills over spawning near-duplicates.
226
+ 1. Deduplicate against existing skills first. Call \`find_similar_skills\` with the procedure's goal as its \`goal\` argument. Each hit carries a \`source\` (bundled, managed, plugin, workspace, or extra), and a managed hit also carries \`author\` (\`"assistant"\` if you authored it, \`"user"\` if a person did, omitted if untagged). You may only overwrite or refine a skill YOU authored — a hit with \`source: "managed"\` AND \`author: "assistant"\`. ANY other hit means the procedure is ALREADY COVERED: a non-managed source (bundled, plugin, workspace, or extra), OR a managed skill that is NOT \`author: "assistant"\` (a person wrote it, or it is untagged). For an ALREADY COVERED hit do not \`overwrite\` it, do not shadow it by creating a skill with its \`skill_id\`, and do not create a near-duplicate — skip it. Only when a returned skill is one of your own (\`source: "managed"\`, \`author: "assistant"\`) and is the SAME procedure, UPDATE it: call \`scaffold_managed_skill\` with that \`skill_id\` and \`overwrite: true\`, rewriting \`body_markdown\` from what you actually observed in the trace. Only CREATE a new skill (fresh \`skill_id\`) when no existing skill of any source covers the procedure. Bias strongly toward reusing or refining your own skills over spawning near-duplicates.
227
227
 
228
228
  2. Capture procedure-scoped knowledge alongside the body. Failure modes, gotchas, and cached values you observed in the trace (error signatures and how you recovered, preconditions, IDs/paths/endpoints that held steady) belong in companion files passed via \`scaffold_managed_skill\`'s \`files\` input (for example \`references/failure-modes.md\`), and the SKILL.md body should reference them so a future load surfaces them.
229
229
 
@@ -22,12 +22,9 @@ import type { RouteDefinition } from "../../../../runtime/routes/types.js";
22
22
  import type { RouteHandlerArgs } from "../../../../runtime/routes/types.js";
23
23
  import { getLogger } from "../logging.js";
24
24
  import { getWorkspaceDir } from "../paths.js";
25
- import {
26
- getEdgeIndex,
27
- totalEdgeCount,
28
- validateEdgeTargets,
29
- } from "../substrate/edge-index.js";
25
+ import { getEdgeIndex, totalEdgeCount } from "../substrate/edge-index.js";
30
26
  import { getPageIndex } from "../substrate/page-index.js";
27
+ import type { DanglingLink } from "../substrate/page-links.js";
31
28
  import {
32
29
  getConceptsDir,
33
30
  listPages,
@@ -111,14 +108,17 @@ async function handleBackfill({
111
108
 
112
109
  const MemoryV2ValidateParams = z.object({}).strict();
113
110
 
114
- type MissingEdgeEndpoint = { from: string; to: string };
115
111
  type OversizedPage = { slug: string; chars: number };
116
112
  type ParseFailure = { slug: string; error: string };
117
113
 
118
114
  export type MemoryV2ValidateResult = {
119
115
  pageCount: number;
120
116
  edgeCount: number;
121
- missingEdgeEndpoints: MissingEdgeEndpoint[];
117
+ /**
118
+ * `links:`, `[[wikilink]]`, and `edges:` references whose target has no
119
+ * page (`PageIndex.danglingLinks`).
120
+ */
121
+ danglingLinks: DanglingLink[];
122
122
  oversizedPages: OversizedPage[];
123
123
  parseFailures: ParseFailure[];
124
124
  };
@@ -162,13 +162,15 @@ async function handleValidate({
162
162
  }
163
163
  }
164
164
 
165
- const edgeIndex = await getEdgeIndex(workspaceDir);
166
- const { missing } = validateEdgeTargets(edgeIndex, knownSlugs);
165
+ const [edgeIndex, pageIndex] = await Promise.all([
166
+ getEdgeIndex(workspaceDir),
167
+ getPageIndex(workspaceDir),
168
+ ]);
167
169
 
168
170
  return {
169
171
  pageCount: knownSlugs.size,
170
172
  edgeCount: totalEdgeCount(edgeIndex),
171
- missingEdgeEndpoints: missing,
173
+ danglingLinks: pageIndex.danglingLinks,
172
174
  oversizedPages,
173
175
  parseFailures,
174
176
  };
@@ -615,6 +615,32 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
615
615
  }
616
616
  });
617
617
 
618
+ test("threads page-index dangling links into the prompt's repair step", async () => {
619
+ const conceptsDir = join(tmpWorkspace, "memory", "concepts");
620
+ const sourcePath = join(conceptsDir, "atl-1290.md");
621
+ // A `links:` entry naming a page that does not exist (the LUM-3316 shape).
622
+ writeFileSync(
623
+ sourcePath,
624
+ "---\nlinks:\n - atl-1291\n---\n# ATL-1290\n\nSee [[atl-1291]].\n",
625
+ );
626
+ invalidatePageIndex();
627
+ try {
628
+ const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
629
+ expect(result.kind).toBe("invoked");
630
+ const prompt = runnerLastArgs?.prompt as string;
631
+ expect(prompt).toContain("resolve dangling links");
632
+ expect(prompt).toContain(
633
+ "- `memory/concepts/atl-1290.md` → `atl-1291` (frontmatter `links:` entry)",
634
+ );
635
+ expect(prompt).toContain(
636
+ "- `memory/concepts/atl-1290.md` → `atl-1291` (inline `[[wikilink]]` in the body)",
637
+ );
638
+ } finally {
639
+ rmSync(sourcePath, { force: true });
640
+ invalidatePageIndex();
641
+ }
642
+ });
643
+
618
644
  test("dispatches with skipPromptIndexing so the kickoff prompt is not indexed", async () => {
619
645
  // The prompt is a static instruction manual — indexing it would write
620
646
  // near-identical memory segments, embeddings, and a lexical entry on
@@ -880,6 +906,94 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
880
906
  expect(enqueuedJobs).toHaveLength(0);
881
907
  });
882
908
 
909
+ test("a run that leaves dangling links reports the count and still enqueues follow-ups", async () => {
910
+ const conceptsDir = join(tmpWorkspace, "memory", "concepts");
911
+ runnerImpl = async () => {
912
+ // The agent files the buffer into a page whose links: names a page it
913
+ // never spawned. Writing through the file tools would invalidate the
914
+ // index via the tool hook; the stub does so by hand.
915
+ writeFileSync(
916
+ join(conceptsDir, "atl-1290.md"),
917
+ "---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
918
+ );
919
+ writeFileSync(bufferPath(), "");
920
+ invalidatePageIndex();
921
+ return { conversationId: "conv-1", ok: true };
922
+ };
923
+ try {
924
+ const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
925
+
926
+ expect(result.kind).toBe("invoked");
927
+ if (result.kind !== "invoked") {
928
+ throw new Error("unreachable");
929
+ }
930
+ expect(result.noProgress).toBe(false);
931
+ expect(result.danglingLinks).toBe(1);
932
+ // Surfaced, not gating: the page that was written still gets indexed.
933
+ expect(result.followUpJobIds).toEqual(["job-1"]);
934
+ expect(enqueuedJobs.map((j) => j.type)).toEqual(["memory_v2_reembed"]);
935
+ } finally {
936
+ rmSync(join(conceptsDir, "atl-1290.md"), { force: true });
937
+ invalidatePageIndex();
938
+ }
939
+ });
940
+
941
+ test("a run that spawns the missing target clears the pre-run dangling link", async () => {
942
+ const conceptsDir = join(tmpWorkspace, "memory", "concepts");
943
+ writeFileSync(
944
+ join(conceptsDir, "atl-1290.md"),
945
+ "---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
946
+ );
947
+ invalidatePageIndex();
948
+ runnerImpl = async () => {
949
+ writeFileSync(join(conceptsDir, "atl-1291.md"), "# ATL-1291\n");
950
+ writeFileSync(bufferPath(), "");
951
+ invalidatePageIndex();
952
+ return { conversationId: "conv-1", ok: true };
953
+ };
954
+ try {
955
+ const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
956
+
957
+ expect(result.kind).toBe("invoked");
958
+ if (result.kind !== "invoked") {
959
+ throw new Error("unreachable");
960
+ }
961
+ // The pre-run index fed the repair step...
962
+ expect(runnerLastArgs?.prompt as string).toContain(
963
+ "resolve dangling links",
964
+ );
965
+ // ...and the post-run count reflects the repair.
966
+ expect(result.danglingLinks).toBe(0);
967
+ } finally {
968
+ rmSync(join(conceptsDir, "atl-1290.md"), { force: true });
969
+ rmSync(join(conceptsDir, "atl-1291.md"), { force: true });
970
+ invalidatePageIndex();
971
+ }
972
+ });
973
+
974
+ test("a no-progress run still reports its dangling-link count", async () => {
975
+ const conceptsDir = join(tmpWorkspace, "memory", "concepts");
976
+ writeFileSync(
977
+ join(conceptsDir, "atl-1290.md"),
978
+ "---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
979
+ );
980
+ invalidatePageIndex();
981
+ runnerImpl = async () => ({ conversationId: "conv-1", ok: true });
982
+ try {
983
+ const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
984
+
985
+ expect(result.kind).toBe("invoked");
986
+ if (result.kind !== "invoked") {
987
+ throw new Error("unreachable");
988
+ }
989
+ expect(result.noProgress).toBe(true);
990
+ expect(result.danglingLinks).toBe(1);
991
+ } finally {
992
+ rmSync(join(conceptsDir, "atl-1290.md"), { force: true });
993
+ invalidatePageIndex();
994
+ }
995
+ });
996
+
883
997
  test("a pending reembed suppresses its duplicate; v3 maintain still enqueues", async () => {
884
998
  v3FlagOn = true;
885
999
  pendingJobTypes.add("memory_v2_reembed");