@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
@@ -26,6 +26,7 @@ import {
26
26
  CONSOLIDATION_PROMPT_V3,
27
27
  CORE_PAGES_CONSOLIDATION_SECTION,
28
28
  renderConsolidationPrompt,
29
+ renderDanglingLinksSection,
29
30
  renderParseFailuresSection,
30
31
  } from "../prompts/consolidation.js";
31
32
 
@@ -59,6 +60,13 @@ const GATED_SECTIONS: Array<{
59
60
  ]),
60
61
  marker: "repair unreadable pages",
61
62
  },
63
+ {
64
+ name: "dangling-link repair step (gate: index-reported dangling links)",
65
+ section: renderDanglingLinksSection([
66
+ { from: "example-page", to: "example-target", kind: "links" },
67
+ ]),
68
+ marker: "resolve dangling links",
69
+ },
62
70
  ];
63
71
 
64
72
  /** All `ConsolidationPromptOptions` gates forced off — the v2-only rendering. */
@@ -15,7 +15,6 @@ import {
15
15
  getReachable,
16
16
  invalidateEdgeIndex,
17
17
  totalEdgeCount,
18
- validateEdgeTargets,
19
18
  } from "../edge-index.js";
20
19
  import { deletePage, writePage } from "../page-store.js";
21
20
  import type { ConceptPage } from "../types.js";
@@ -228,35 +227,6 @@ describe("getReachable", () => {
228
227
  });
229
228
  });
230
229
 
231
- // ---------------------------------------------------------------------------
232
- // validateEdgeTargets
233
- // ---------------------------------------------------------------------------
234
-
235
- describe("validateEdgeTargets", () => {
236
- test("ok=true when every outgoing target has a known slug", async () => {
237
- await writePage(workspaceDir, makePage("alice", ["bob"]));
238
- await writePage(workspaceDir, makePage("bob"));
239
-
240
- const idx = await getEdgeIndex(workspaceDir);
241
- const result = validateEdgeTargets(idx, new Set(["alice", "bob"]));
242
- expect(result).toEqual({ ok: true, missing: [] });
243
- });
244
-
245
- test("flags outgoing targets that don't correspond to a known slug", async () => {
246
- await writePage(workspaceDir, makePage("alice", ["ghost", "bob"]));
247
- await writePage(workspaceDir, makePage("bob", ["phantom"]));
248
-
249
- const idx = await getEdgeIndex(workspaceDir);
250
- const result = validateEdgeTargets(idx, new Set(["alice", "bob"]));
251
-
252
- expect(result.ok).toBe(false);
253
- expect(result.missing).toEqual([
254
- { from: "alice", to: "ghost" },
255
- { from: "bob", to: "phantom" },
256
- ]);
257
- });
258
- });
259
-
260
230
  // ---------------------------------------------------------------------------
261
231
  // totalEdgeCount
262
232
  // ---------------------------------------------------------------------------
@@ -12,7 +12,8 @@
12
12
  * - Batch over the cap: RangeError.
13
13
  * - In-batch duplicate slug: later occurrence `invalid`.
14
14
  * - Non-blocking warnings: missing `source`, unparseable `origin_date`,
15
- * frontmatter `slug` differing from the storage slug.
15
+ * frontmatter `slug` differing from the storage slug, link targets
16
+ * neither on disk nor in the batch.
16
17
  * - Under-lock collision snapshot: a page committed between validation and
17
18
  * the write phase is skipped, and `listPages` runs after `tryAcquireLock`.
18
19
  * - Partial write failure: the reindex fan-out still runs for committed
@@ -363,6 +364,77 @@ describe("ingestPages", () => {
363
364
  expect(await listPages(workspace)).toEqual(["people/alice", "people/bob"]);
364
365
  });
365
366
 
367
+ test("link targets neither on disk nor in the batch warn without blocking the write", async () => {
368
+ // `people/bob` already exists on disk; `projects/garden` arrives in the
369
+ // same batch; `atl-1291` and `ghost` exist nowhere.
370
+ await ingestPages(workspace, [
371
+ { slug: "people/bob", content: validPage("Bob.") },
372
+ ]);
373
+ enqueuedJobs.length = 0;
374
+
375
+ const summary = await ingestPages(workspace, [
376
+ {
377
+ slug: "people/alice",
378
+ content:
379
+ "---\nsource: import:chatgpt\norigin_date: 2025-03-14\n" +
380
+ "links:\n - people/bob\n - projects/garden\n - atl-1291\n" +
381
+ "edges: [ghost]\n---\n" +
382
+ "Alice mentions [[projects/garden]] and [[people/bob]] and [[skills/deploy]].\n",
383
+ },
384
+ { slug: "projects/garden", content: validPage("Garden.") },
385
+ ]);
386
+
387
+ expect(summary.written).toBe(2);
388
+ expect(summary.invalid).toBe(0);
389
+ const [alice, garden] = summary.results;
390
+ expect(alice.action).toBe("written");
391
+ expect(alice.warnings).toHaveLength(1);
392
+ // Every unresolved target on one line, sorted; targets that resolve on
393
+ // disk, in the batch, or under a synthetic prefix stay silent.
394
+ expect(alice.warnings[0]).toContain(
395
+ "link targets not on disk or in this batch: atl-1291, ghost",
396
+ );
397
+ expect(alice.warnings[0]).not.toContain("people/bob");
398
+ expect(alice.warnings[0]).not.toContain("projects/garden");
399
+ expect(alice.warnings[0]).not.toContain("skills/deploy");
400
+ expect(garden.warnings).toEqual([]);
401
+ // Written regardless, and reindexed.
402
+ expect(await readPage(workspace, "people/alice")).not.toBeNull();
403
+ expect(enqueuedJobs.map((j) => j.type).sort()).toEqual([
404
+ "memory_v2_reembed",
405
+ "memory_v3_maintain",
406
+ ]);
407
+ });
408
+
409
+ test("dangling-link warnings are reported on dry-run and skipped for pages the batch will not write", async () => {
410
+ await ingestPages(workspace, [
411
+ { slug: "people/bob", content: validPage("Bob.") },
412
+ ]);
413
+
414
+ const summary = await ingestPages(
415
+ workspace,
416
+ [
417
+ {
418
+ slug: "people/alice",
419
+ content: validPage("Alice mentions [[nowhere]]."),
420
+ },
421
+ // Exists on disk and overwrite is off, so this content is never
422
+ // written; its dangling link is not this batch's business.
423
+ {
424
+ slug: "people/bob",
425
+ content: validPage("Bob mentions [[also-nowhere]]."),
426
+ },
427
+ ],
428
+ { dryRun: true },
429
+ );
430
+
431
+ expect(summary.results[0].action).toBe("written");
432
+ expect(summary.results[0].warnings.join(" ")).toContain("nowhere");
433
+ expect(summary.results[1].action).toBe("skipped_exists");
434
+ expect(summary.results[1].warnings).toEqual([]);
435
+ expect(await readPage(workspace, "people/alice")).toBeNull();
436
+ });
437
+
366
438
  test("a pending reindex job of the same type suppresses its duplicate enqueue", async () => {
367
439
  pendingJobTypes = new Set(["memory_v2_reembed"]);
368
440
  await ingestPages(workspace, [
@@ -56,6 +56,7 @@ const {
56
56
  } = await import("../page-index.js");
57
57
  const { writePage } = await import("../page-store.js");
58
58
  const { invalidateEdgeIndex } = await import("../edge-index.js");
59
+ const { LINK_SEPARATOR } = await import("../page-links.js");
59
60
 
60
61
  let workspaceDir: string;
61
62
 
@@ -272,6 +273,42 @@ describe("getPageIndex", () => {
272
273
  const alice = idx.bySlug.get("alice")!;
273
274
  const bob = idx.bySlug.get("bob")!;
274
275
  expect(alice.edges).toEqual([bob.id]);
276
+ // Dropped from the numeric edges, reported on the index.
277
+ expect(idx.danglingLinks).toEqual([
278
+ { from: "alice", to: "ghost", kind: "edges" },
279
+ ]);
280
+ });
281
+
282
+ test("danglingLinks covers links:, body wikilinks, and edges:, resolved against pages and synthetic rows", async () => {
283
+ skillState.entries = [{ id: "browser", content: "Drive a browser." }];
284
+ const alice = makePage("alice", {
285
+ summary: "A",
286
+ edges: ["bob"],
287
+ body: "Alice uses [[skills/browser]] and [[bob|Bob]] but also [[nobody#sec]].",
288
+ });
289
+ alice.frontmatter.links = [
290
+ `bob${LINK_SEPARATOR}peer`,
291
+ `atl-1291${LINK_SEPARATOR}ticket`,
292
+ ];
293
+ await writePage(workspaceDir, alice);
294
+ await writePage(workspaceDir, makePage("bob", { summary: "B" }));
295
+
296
+ const idx = await getPageIndex(workspaceDir);
297
+ expect(idx.danglingLinks).toEqual([
298
+ { from: "alice", to: "atl-1291", kind: "links" },
299
+ { from: "alice", to: "nobody", kind: "wikilink" },
300
+ ]);
301
+ });
302
+
303
+ test("danglingLinks is empty when every reference resolves", async () => {
304
+ await writePage(
305
+ workspaceDir,
306
+ makePage("alice", { summary: "A", edges: ["bob"], body: "See [[bob]]." }),
307
+ );
308
+ await writePage(workspaceDir, makePage("bob", { summary: "B" }));
309
+
310
+ const idx = await getPageIndex(workspaceDir);
311
+ expect(idx.danglingLinks).toEqual([]);
275
312
  });
276
313
 
277
314
  test("exposes frontmatter leaves in the index entry", async () => {
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Tests for `substrate/page-links.ts`: the single owner of `links:` entry and
3
+ * `[[wikilink]]` parsing, plus the dangling-link finder every write-side
4
+ * report uses.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+
9
+ import {
10
+ extractWikilinkTargets,
11
+ findDanglingLinks,
12
+ LINK_SEPARATOR,
13
+ parseLinkEntry,
14
+ WIKILINK_REGEX,
15
+ wikilinkTarget,
16
+ } from "../page-links.js";
17
+ import { isValidSlug } from "../page-store.js";
18
+
19
+ /** Build a `"<slug> <sep> <why>"` links entry without spelling the separator. */
20
+ const link = (target: string, why?: string): string =>
21
+ why === undefined ? target : `${target}${LINK_SEPARATOR}${why}`;
22
+
23
+ describe("LINK_SEPARATOR", () => {
24
+ test("is space, U+2014 EM DASH, space", () => {
25
+ expect(LINK_SEPARATOR).toHaveLength(3);
26
+ expect(LINK_SEPARATOR.codePointAt(1)).toBe(0x2014);
27
+ expect(LINK_SEPARATOR[0]).toBe(" ");
28
+ expect(LINK_SEPARATOR[2]).toBe(" ");
29
+ });
30
+ });
31
+
32
+ describe("parseLinkEntry", () => {
33
+ test("splits target and description on the first separator", () => {
34
+ expect(parseLinkEntry(link("alice", "the principal"))).toEqual({
35
+ target: "alice",
36
+ description: "the principal",
37
+ });
38
+ });
39
+
40
+ test("keeps later separators inside the description", () => {
41
+ const why = `why${LINK_SEPARATOR}with a second dash`;
42
+ expect(parseLinkEntry(link("alice", why))).toEqual({
43
+ target: "alice",
44
+ description: why,
45
+ });
46
+ });
47
+
48
+ test("a bare slug carries no description; whitespace is trimmed", () => {
49
+ expect(parseLinkEntry(" alice ")).toEqual({
50
+ target: "alice",
51
+ description: undefined,
52
+ });
53
+ });
54
+
55
+ test("an empty annotation after the separator reads as no description", () => {
56
+ expect(parseLinkEntry(`alice${LINK_SEPARATOR} `)).toEqual({
57
+ target: "alice",
58
+ description: undefined,
59
+ });
60
+ });
61
+ });
62
+
63
+ describe("wikilinkTarget / extractWikilinkTargets", () => {
64
+ test("strips |display and #anchor suffixes and trims", () => {
65
+ expect(wikilinkTarget("alice")).toBe("alice");
66
+ expect(wikilinkTarget("alice|Alice B.")).toBe("alice");
67
+ expect(wikilinkTarget("alice#role")).toBe("alice");
68
+ expect(wikilinkTarget(" tools/vs-code#setup|editor ")).toBe(
69
+ "tools/vs-code",
70
+ );
71
+ expect(wikilinkTarget("|label")).toBe("");
72
+ expect(wikilinkTarget("#anchor")).toBe("");
73
+ });
74
+
75
+ test("extracts every target in order, dropping empty ones, keeping duplicates", () => {
76
+ const body =
77
+ "See [[alice]] and [[bob|Bob]] then [[alice#role]] but not [[|x]] or [[#y]].";
78
+ expect(extractWikilinkTargets(body)).toEqual(["alice", "bob", "alice"]);
79
+ });
80
+
81
+ test("the regex is global and captures the raw inner text", () => {
82
+ const matches = [..."[[a|A]] [[b#c]]".matchAll(WIKILINK_REGEX)].map(
83
+ (m) => m[1],
84
+ );
85
+ expect(matches).toEqual(["a|A", "b#c"]);
86
+ });
87
+ });
88
+
89
+ describe("findDanglingLinks", () => {
90
+ const known = new Set(["alice", "bob", "hub", "skills/deploy"]);
91
+
92
+ test("reports links:, wikilink, and edges: targets absent from the known set", () => {
93
+ const result = findDanglingLinks(
94
+ [
95
+ {
96
+ slug: "hub",
97
+ frontmatter: {
98
+ links: [link("alice", "principal"), link("atl-1291", "ticket")],
99
+ edges: ["bob", "ghost-edge"],
100
+ },
101
+ body: "Lead mentions [[alice]] and [[ghost-wiki|Ghost]].",
102
+ },
103
+ ],
104
+ known,
105
+ isValidSlug,
106
+ );
107
+ expect(result).toEqual([
108
+ { from: "hub", to: "atl-1291", kind: "links" },
109
+ { from: "hub", to: "ghost-edge", kind: "edges" },
110
+ { from: "hub", to: "ghost-wiki", kind: "wikilink" },
111
+ ]);
112
+ });
113
+
114
+ test("resolves against synthetic slugs too when the caller counts them as known", () => {
115
+ expect(
116
+ findDanglingLinks(
117
+ [
118
+ {
119
+ slug: "alice",
120
+ frontmatter: { links: [link("skills/deploy")] },
121
+ body: "",
122
+ },
123
+ ],
124
+ known,
125
+ isValidSlug,
126
+ ),
127
+ ).toEqual([]);
128
+ });
129
+
130
+ test("ignores self-references and empty targets, dedupes per (from, to, kind)", () => {
131
+ const result = findDanglingLinks(
132
+ [
133
+ {
134
+ slug: "alice",
135
+ frontmatter: {
136
+ links: [
137
+ link("alice", "self"),
138
+ link("ghost"),
139
+ link("ghost", "twice"),
140
+ ],
141
+ },
142
+ body: "[[alice]] [[ghost]] [[ghost#sec]] [[|nothing]]",
143
+ },
144
+ ],
145
+ known,
146
+ isValidSlug,
147
+ );
148
+ expect(result).toEqual([
149
+ { from: "alice", to: "ghost", kind: "links" },
150
+ { from: "alice", to: "ghost", kind: "wikilink" },
151
+ ]);
152
+ });
153
+
154
+ test("output is sorted by (from, to, kind) regardless of page order", () => {
155
+ const pages = [
156
+ { slug: "zed", frontmatter: { links: [link("nope")] }, body: "[[nope]]" },
157
+ { slug: "amy", frontmatter: {}, body: "[[zzz]] [[aaa]]" },
158
+ ];
159
+ const forward = findDanglingLinks(pages, known, isValidSlug);
160
+ const reversed = findDanglingLinks(
161
+ [...pages].reverse(),
162
+ known,
163
+ isValidSlug,
164
+ );
165
+ expect(forward).toEqual(reversed);
166
+ expect(forward.map((d) => `${d.from}>${d.to}>${d.kind}`)).toEqual([
167
+ "amy>aaa>wikilink",
168
+ "amy>zzz>wikilink",
169
+ "zed>nope>links",
170
+ "zed>nope>wikilink",
171
+ ]);
172
+ });
173
+
174
+ test("targets that are not slug-shaped are not references at all", () => {
175
+ // Shell tests, code, and prose in double brackets must not be reported
176
+ // (and so never reach the repair step that would unwrap them).
177
+ expect(
178
+ findDanglingLinks(
179
+ [
180
+ {
181
+ slug: "procs/deploy",
182
+ frontmatter: { links: ["Not A Slug"] },
183
+ body: "Run `if [[ -f foo ]]; then` and [[ Some Prose ]] then [[real-missing]].",
184
+ },
185
+ ],
186
+ known,
187
+ isValidSlug,
188
+ ),
189
+ ).toEqual([{ from: "procs/deploy", to: "real-missing", kind: "wikilink" }]);
190
+ });
191
+
192
+ test("a fully resolved corpus yields an empty list", () => {
193
+ expect(
194
+ findDanglingLinks(
195
+ [
196
+ {
197
+ slug: "alice",
198
+ frontmatter: { links: [link("bob", "peer")] },
199
+ body: "[[hub]]",
200
+ },
201
+ { slug: "bob", frontmatter: { edges: ["alice"] }, body: "" },
202
+ ],
203
+ known,
204
+ isValidSlug,
205
+ ),
206
+ ).toEqual([]);
207
+ });
208
+ });
@@ -66,8 +66,10 @@ const {
66
66
  CORE_PAGES_CONSOLIDATION_SECTION,
67
67
  CORE_PAGES_PLACEHOLDER,
68
68
  CUTOFF_PLACEHOLDER,
69
+ DANGLING_LINKS_PLACEHOLDER,
69
70
  PARSE_FAILURES_PLACEHOLDER,
70
71
  renderConsolidationPrompt,
72
+ renderDanglingLinksSection,
71
73
  renderParseFailuresSection,
72
74
  resolveConsolidationPrompt,
73
75
  } = await import("../prompts/consolidation.js");
@@ -107,7 +109,15 @@ const bundledPrompt = (includeCorePagesSection = false): string =>
107
109
  ? (CORE_PAGES_CONSOLIDATION_SECTION as string)
108
110
  : "",
109
111
  )
110
- .replaceAll(PARSE_FAILURES_PLACEHOLDER, "");
112
+ .replaceAll(PARSE_FAILURES_PLACEHOLDER, "")
113
+ .replaceAll(DANGLING_LINKS_PLACEHOLDER, "");
114
+
115
+ /** Sample dangling links for the repair-section tests. */
116
+ const SAMPLE_DANGLING = [
117
+ { from: "atl-1290", to: "atl-1291", kind: "links" as const },
118
+ { from: "people/alice", to: "ghost-page", kind: "wikilink" as const },
119
+ { from: "arcs/day", to: "old-slug", kind: "edges" as const },
120
+ ];
111
121
 
112
122
  beforeEach(() => {
113
123
  warnCalls.length = 0;
@@ -316,6 +326,153 @@ describe("resolveConsolidationPrompt — parse-failures repair section", () => {
316
326
  });
317
327
  });
318
328
 
329
+ describe("resolveConsolidationPrompt: dangling-links repair section", () => {
330
+ test("no dangling links: no section and no placeholder residue, both article shapes", () => {
331
+ expect(renderDanglingLinksSection([])).toBe("");
332
+ for (const articleShape of ["v2", "v3"] as const) {
333
+ const result = renderConsolidationPrompt(CUTOFF, {
334
+ includeCorePagesSection: false,
335
+ articleShape,
336
+ });
337
+ expect(result).not.toContain("resolve dangling links");
338
+ expect(result).not.toContain(DANGLING_LINKS_PLACEHOLDER);
339
+ expect(result).toContain("# The work\n\n## 1. Read the buffer");
340
+ }
341
+ });
342
+
343
+ test("with dangling links: section renders once, before step 1, one line per reference with its kind", () => {
344
+ const result = renderConsolidationPrompt(CUTOFF, {
345
+ ...NO_CORE,
346
+ danglingLinks: SAMPLE_DANGLING,
347
+ });
348
+ expect(result.split("resolve dangling links").length - 1).toBe(1);
349
+ const sectionAt = result.indexOf("## 0. FIRST: resolve dangling links");
350
+ expect(sectionAt).toBeGreaterThan(result.indexOf("# The work"));
351
+ expect(sectionAt).toBeLessThan(result.indexOf("## 1. Read the buffer"));
352
+ expect(result).toContain(
353
+ "- `memory/concepts/atl-1290.md` → `atl-1291` (frontmatter `links:` entry)",
354
+ );
355
+ expect(result).toContain(
356
+ "- `memory/concepts/people/alice.md` → `ghost-page` (inline `[[wikilink]]` in the body)",
357
+ );
358
+ expect(result).toContain(
359
+ "- `memory/concepts/arcs/day.md` → `old-slug` (frontmatter `edges:` entry)",
360
+ );
361
+ // The three resolutions the agent must choose between.
362
+ expect(result).toContain("spawn it, a stub is fine, and keep the link");
363
+ expect(result).toContain("repoint the reference at the surviving slug");
364
+ expect(result).toContain("Prose stays prose");
365
+ expect(result).not.toContain("more, reported next pass");
366
+ });
367
+
368
+ test("renders after the parse-failures section when both are present", () => {
369
+ const result = renderConsolidationPrompt(CUTOFF, {
370
+ ...NO_CORE,
371
+ parseFailures: SAMPLE_FAILURES,
372
+ danglingLinks: SAMPLE_DANGLING,
373
+ });
374
+ expect(result.indexOf("repair unreadable pages")).toBeLessThan(
375
+ result.indexOf("resolve dangling links"),
376
+ );
377
+ expect(result.indexOf("resolve dangling links")).toBeLessThan(
378
+ result.indexOf("## 1. Read the buffer"),
379
+ );
380
+ });
381
+
382
+ test("caps the rendered list at 40 and counts the remainder", () => {
383
+ const many = Array.from({ length: 45 }, (_, i) => ({
384
+ from: `page-${String(i).padStart(2, "0")}`,
385
+ to: `missing-${i}`,
386
+ kind: "links" as const,
387
+ }));
388
+ const section = renderDanglingLinksSection(many);
389
+ const listLines = section
390
+ .split("\n")
391
+ .filter((line) => line.startsWith("- `memory/concepts/"));
392
+ expect(listLines).toHaveLength(40);
393
+ expect(section).toContain("`memory/concepts/page-39.md`");
394
+ expect(section).not.toContain("`memory/concepts/page-40.md`");
395
+ expect(section).toContain("...and 5 more, reported next pass");
396
+ });
397
+
398
+ test("sanitizes injection-shaped slugs and targets: newlines, backticks, oversize", () => {
399
+ // Both ends of a dangling link are page content: the source is a
400
+ // filename, the target is whatever the page wrote inside `[[...]]` or
401
+ // `links:`. Raw, either could break out of the list item.
402
+ const section = renderDanglingLinksSection([
403
+ {
404
+ from: "evil\n\n## Injected heading\n- `nested`",
405
+ to: "target`code`\n## also injected",
406
+ kind: "wikilink",
407
+ },
408
+ { from: "s".repeat(500), to: "t".repeat(500), kind: "links" },
409
+ ]);
410
+ const listLines = section
411
+ .split("\n")
412
+ .filter((line) => line.startsWith("- `memory/concepts/"));
413
+ expect(listLines).toHaveLength(2);
414
+ for (const line of listLines) {
415
+ // Source path, target, and the kind label's own inline-code span:
416
+ // six template backticks, no injected ones.
417
+ expect((line.match(/`/g) ?? []).length).toBe(6);
418
+ }
419
+ const headingLines = section
420
+ .split("\n")
421
+ .filter((line) => line.startsWith("## "));
422
+ expect(headingLines).toHaveLength(1);
423
+ expect(section).toContain("memory/concepts/evil ");
424
+ expect(section).toContain(`${"s".repeat(199)}…`);
425
+ expect(section).toContain(`${"t".repeat(199)}…`);
426
+ expect(section).not.toContain("s".repeat(200));
427
+ expect(section).not.toContain("t".repeat(200));
428
+ });
429
+
430
+ test("override containing the placeholder: substituted in place", () => {
431
+ const path = join(tmpWorkspace, "custom-prompt.md");
432
+ writeFileSync(path, "Top\n{{DANGLING_LINKS_SECTION}}Bottom\n");
433
+
434
+ const result = resolveConsolidationPrompt(path, CUTOFF, {
435
+ ...NO_CORE,
436
+ danglingLinks: SAMPLE_DANGLING,
437
+ });
438
+ expect(result).toContain("## 0. FIRST: resolve dangling links");
439
+ expect(result.indexOf("resolve dangling links")).toBeLessThan(
440
+ result.indexOf("Bottom"),
441
+ );
442
+ expect(result).not.toContain(DANGLING_LINKS_PLACEHOLDER);
443
+ });
444
+
445
+ test("override without either placeholder: both non-empty repair sections are appended, in order", () => {
446
+ const path = join(tmpWorkspace, "no-placeholder.md");
447
+ writeFileSync(path, "My custom prompt {{CUTOFF}}\n");
448
+
449
+ const result = resolveConsolidationPrompt(path, CUTOFF, {
450
+ ...NO_CORE,
451
+ parseFailures: SAMPLE_FAILURES,
452
+ danglingLinks: SAMPLE_DANGLING,
453
+ });
454
+ expect(result.startsWith(`My custom prompt ${CUTOFF}`)).toBe(true);
455
+ const parseAt = result.indexOf("repair unreadable pages");
456
+ const danglingAt = result.indexOf("resolve dangling links");
457
+ expect(parseAt).toBeGreaterThan(0);
458
+ expect(danglingAt).toBeGreaterThan(parseAt);
459
+ expect(result).toContain("`memory/concepts/atl-1290.md`");
460
+ });
461
+
462
+ test("override with only the parse placeholder + dangling links: dangling section appended", () => {
463
+ const path = join(tmpWorkspace, "custom-prompt.md");
464
+ writeFileSync(path, "Top\n{{PARSE_FAILURES_SECTION}}Bottom\n");
465
+
466
+ const result = resolveConsolidationPrompt(path, CUTOFF, {
467
+ ...NO_CORE,
468
+ danglingLinks: SAMPLE_DANGLING,
469
+ });
470
+ expect(result.indexOf("Bottom")).toBeLessThan(
471
+ result.indexOf("resolve dangling links"),
472
+ );
473
+ });
474
+ });
475
+
319
476
  describe("resolveConsolidationPrompt — with override", () => {
320
477
  test("loads an absolute path verbatim and substitutes {{CUTOFF}}", () => {
321
478
  const path = join(tmpWorkspace, "custom-prompt.md");
@@ -50,8 +50,7 @@ mock.module("../../config.js", () => ({
50
50
  }),
51
51
  }));
52
52
 
53
- const { readMemoryV2StaticContent, shouldExposePersonalMemory } =
54
- await import("../static-context.js");
53
+ const { readMemoryV2StaticContent } = await import("../static-context.js");
55
54
 
56
55
  const MEMORY_FILES = [
57
56
  "essentials.md",
@@ -361,62 +360,3 @@ describe("readMemoryV2StaticContent Buffer cap", () => {
361
360
  expect(lines.some((line) => line.endsWith("entry-0"))).toBe(false);
362
361
  });
363
362
  });
364
-
365
- describe("shouldExposePersonalMemory", () => {
366
- test("allows guardian-trusted local conversations", () => {
367
- expect(
368
- shouldExposePersonalMemory({
369
- sourceChannel: "vellum",
370
- isTrustedActor: true,
371
- }),
372
- ).toBe(true);
373
- });
374
-
375
- test("allows local-channel conversations even when trust class is unknown (analyze runs, dev)", () => {
376
- expect(
377
- shouldExposePersonalMemory({
378
- sourceChannel: "vellum",
379
- isTrustedActor: false,
380
- }),
381
- ).toBe(true);
382
- });
383
-
384
- test("allows turns with no trust context (work-item task runs, internal background)", () => {
385
- expect(
386
- shouldExposePersonalMemory({
387
- sourceChannel: undefined,
388
- isTrustedActor: false,
389
- }),
390
- ).toBe(true);
391
- });
392
-
393
- const REMOTE_CHANNELS = [
394
- "phone",
395
- "slack",
396
- "telegram",
397
- "whatsapp",
398
- "email",
399
- ] as const;
400
-
401
- test("allows guardian-trusted remote channels (user's own phone/Slack)", () => {
402
- for (const channel of REMOTE_CHANNELS) {
403
- expect(
404
- shouldExposePersonalMemory({
405
- sourceChannel: channel,
406
- isTrustedActor: true,
407
- }),
408
- ).toBe(true);
409
- }
410
- });
411
-
412
- test("blocks non-guardian remote-channel actors (the leak this gate exists to prevent)", () => {
413
- for (const channel of REMOTE_CHANNELS) {
414
- expect(
415
- shouldExposePersonalMemory({
416
- sourceChannel: channel,
417
- isTrustedActor: false,
418
- }),
419
- ).toBe(false);
420
- }
421
- });
422
- });