@vellumai/assistant 0.12.2 → 0.12.3-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 (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -43,11 +43,11 @@
43
43
  import {
44
44
  addMessage,
45
45
  type AgentLoopExitReason,
46
- type ContentBlock,
47
46
  type ConversationRow,
48
47
  deleteConversation,
49
48
  getConversation,
50
49
  getConversationProcessingStartedAt,
50
+ getRecordedConversationToolSurface,
51
51
  isConversationProcessing,
52
52
  } from "@vellumai/plugin-api";
53
53
 
@@ -105,6 +105,12 @@ import {
105
105
  upsertRetrospectiveState,
106
106
  } from "./memory-retrospective-state.js";
107
107
  import { effectiveSweepLookbackMs } from "./memory-retrospective-sweep.js";
108
+ import {
109
+ collectSuccessfulToolResultIds,
110
+ countDurableToolUses,
111
+ extractRememberContents,
112
+ hasCommittedTextReply,
113
+ } from "./memory-run-evidence.js";
108
114
 
109
115
  const log = getLogger("memory-retrospective-job");
110
116
 
@@ -500,10 +506,9 @@ export async function runForkBasedRetrospective(
500
506
  // Persona + tool-context parity pins derived from the source conversation
501
507
  // (see `resolveSourceParityPins`), both passed unconditionally. The persona
502
508
  // override keeps the system-prompt prefix in parity (and is a review-quality
503
- // fix on its own); the tool-context pin keeps the wire tool surface in
504
- // parity the fork always runs execution gate mode below, so the source's
505
- // full tool surface stays on the wire while the allowlist holds at
506
- // execution time.
509
+ // fix on its own); the tool-context pin resolves the fork's execution-side
510
+ // tool inventory under the source's client context and carries the wire
511
+ // surface when the source has no recorded one (`sourceToolSurface` below).
507
512
  // Warm both guardian-delivery cache keys (vellum + unfiltered) so the sync
508
513
  // slug resolution inside resolveSourceParityPins (resolveUserSlug(undefined)),
509
514
  // including its any-channel fallback, hits fresh keys instead of falling
@@ -514,6 +519,16 @@ export async function runForkBasedRetrospective(
514
519
  newMessages,
515
520
  );
516
521
 
522
+ // The tools array the source's most recent live turn actually sent,
523
+ // replayed verbatim on the fork. The wire tool block is the first tier of
524
+ // the provider cache prefix (tools → system → messages), and re-deriving it
525
+ // on the fork cannot reproduce the source's bytes: this worker's registry
526
+ // has no user-plugin tools, host-tool gates read connected clients the
527
+ // worker never has, and presence derived from a persisted interface stamp
528
+ // misreads clientless turns. `null` (no live turn has recorded a surface)
529
+ // leaves the pin's derivation as the wire surface.
530
+ const sourceToolSurface = await readSourceToolSurface(sourceConversationId);
531
+
517
532
  // `skipHintInjection: true` because the instruction is already a
518
533
  // persisted message — the wake's hint sandwich would only duplicate it.
519
534
  let wakeSucceeded = false;
@@ -542,16 +557,32 @@ export async function runForkBasedRetrospective(
542
557
  "find_similar_skills",
543
558
  ]
544
559
  : ["remember"],
545
- // Always keep the source's full tool surface on the wire and resolve it
546
- // under the source's client context (`toolContextPin`). The wire tool
547
- // block is the first tier of the provider cache prefix
548
- // (tools system messages), so a wire filter busts cache parity with
549
- // the source's live turns re-creating the cached prefix instead of
550
- // reading it. The allowlist still holds at execution time: non-allowlisted
551
- // calls are rejected before any executor or side effect runs. See
560
+ // Keep the source's full tool surface on the wire: replay the array its
561
+ // last live turn sent when one is recorded, otherwise resolve the fork's
562
+ // own surface under the source's client context (`toolContextPin`). The
563
+ // wire tool block is the first tier of the provider cache prefix
564
+ // (tools system messages), so any wire difference busts cache parity
565
+ // with the source's live turns, re-creating the cached prefix instead of
566
+ // reading it. The recorded delegation-section state rides with the array
567
+ // into the fork's system prompt (the second tier) for the same reason:
568
+ // this wake's own scope cannot spawn, so deriving the section here
569
+ // renders it off where an interactive source rendered it on. The
570
+ // allowlist still holds at execution time: non-allowlisted calls are
571
+ // rejected before any executor or side effect runs. See
552
572
  // {@link SubagentToolGateMode} and {@link WakeToolContextPin}.
553
573
  toolGateMode: "execution" as const,
554
574
  toolContextPin,
575
+ ...(sourceToolSurface
576
+ ? {
577
+ wireToolDefinitions: sourceToolSurface.tools,
578
+ ...(sourceToolSurface.delegateIndependentTasks === null
579
+ ? {}
580
+ : {
581
+ delegateIndependentTasks:
582
+ sourceToolSurface.delegateIndependentTasks,
583
+ }),
584
+ }
585
+ : {}),
555
586
  // Preactivate skill-management so its authoring tools (`find_similar_skills`
556
587
  // / `scaffold_managed_skill` / the `skill_load` target) are in the turn's
557
588
  // active set from turn 1; the checker's origin-scoped grant then makes them
@@ -731,6 +762,25 @@ function enqueueFollowUpJobs(): string[] {
731
762
  return followUpJobIds;
732
763
  }
733
764
 
765
+ /**
766
+ * The source's recorded wire surface (its tool array and the delegation
767
+ * section state of its system prompt), or `null` when none is recorded or the
768
+ * read fails (logged; the fork then derives both for itself).
769
+ */
770
+ async function readSourceToolSurface(
771
+ sourceConversationId: string,
772
+ ): ReturnType<typeof getRecordedConversationToolSurface> {
773
+ try {
774
+ return await getRecordedConversationToolSurface(sourceConversationId);
775
+ } catch (err) {
776
+ log.warn(
777
+ { err, sourceConversationId },
778
+ "memory-retrospective (fork): failed to read the source's recorded tool surface; deriving the wire surface instead",
779
+ );
780
+ return null;
781
+ }
782
+ }
783
+
734
784
  /**
735
785
  * The source-derived parity pins the fork wake runs under: the system-prompt
736
786
  * persona override and the tool-resolution context pin. Both exist so the
@@ -1252,177 +1302,16 @@ async function collectRetrospectiveRunEvidence(
1252
1302
  const succeededIds = collectSuccessfulToolResultIds(runMessages);
1253
1303
  return {
1254
1304
  remembers: extractRememberContents(runMessages, succeededIds),
1255
- durableToolCallCount: countDurableToolUses(runMessages, succeededIds),
1256
- durableToolAttemptCount: countDurableToolUses(runMessages, null),
1305
+ durableToolCallCount: countDurableToolUses(
1306
+ runMessages,
1307
+ DURABLE_RETROSPECTIVE_TOOLS,
1308
+ succeededIds,
1309
+ ),
1310
+ durableToolAttemptCount: countDurableToolUses(
1311
+ runMessages,
1312
+ DURABLE_RETROSPECTIVE_TOOLS,
1313
+ null,
1314
+ ),
1257
1315
  committedTextReply: hasCommittedTextReply(runMessages),
1258
1316
  };
1259
1317
  }
1260
-
1261
- /**
1262
- * Whether the LAST persisted assistant row on the run's tail carries a text
1263
- * block with non-whitespace content. Paired with a model-driven stop and
1264
- * zero memory-write attempts, that closing reply is the persisted artifact
1265
- * of a pass that read its window and had nothing to save: the model spoke
1266
- * and then chose to end the run. Reading only the final row separates it
1267
- * from a run whose narration went live but whose actual conclusion was
1268
- * empty, as well as from a response that committed nothing at all
1269
- * (thinking-only output, an empty content array).
1270
- */
1271
- function hasCommittedTextReply(messages: MessageLike[]): boolean {
1272
- for (let i = messages.length - 1; i >= 0; i--) {
1273
- const msg = messages[i]!;
1274
- if (msg.role !== "assistant") {
1275
- continue;
1276
- }
1277
- const blocks = parseMessageBlocks(msg);
1278
- if (blocks === null) {
1279
- return false;
1280
- }
1281
- return blocks.some(
1282
- (b) =>
1283
- b.type === "text" && typeof b.text === "string" && b.text.trim() !== "",
1284
- );
1285
- }
1286
- return false;
1287
- }
1288
-
1289
- /**
1290
- * Ids of `tool_result` blocks on the run's user rows whose execution did not
1291
- * report an error. Robust to malformed content JSON the same way
1292
- * `extractRememberContents` is.
1293
- */
1294
- function collectSuccessfulToolResultIds(messages: MessageLike[]): Set<string> {
1295
- const ids = new Set<string>();
1296
- for (const msg of messages) {
1297
- if (msg.role !== "user") {
1298
- continue;
1299
- }
1300
- for (const b of parseMessageBlocks(msg) ?? []) {
1301
- // guard:allow-tool-result-only: success evidence for locally-executed
1302
- // durable memory tools; server-side web_search_tool_result never
1303
- // corresponds to a durable write and carries no is_error flag.
1304
- if (
1305
- b.type === "tool_result" &&
1306
- typeof b.tool_use_id === "string" &&
1307
- b.is_error !== true
1308
- ) {
1309
- ids.add(b.tool_use_id);
1310
- }
1311
- }
1312
- }
1313
- return ids;
1314
- }
1315
-
1316
- /**
1317
- * Count persisted `tool_use` blocks whose `name` is in
1318
- * {@link DURABLE_RETROSPECTIVE_TOOLS} across the run's assistant rows.
1319
- * With a `succeededIds` set, only calls whose id has a matching successful
1320
- * `tool_result` count (verified executions); with `null`, every attempt
1321
- * counts regardless of outcome.
1322
- */
1323
- function countDurableToolUses(
1324
- messages: MessageLike[],
1325
- succeededIds: ReadonlySet<string> | null,
1326
- ): number {
1327
- let count = 0;
1328
- for (const msg of messages) {
1329
- if (msg.role !== "assistant") {
1330
- continue;
1331
- }
1332
- for (const b of parseMessageBlocks(msg) ?? []) {
1333
- if (
1334
- b.type === "tool_use" &&
1335
- DURABLE_RETROSPECTIVE_TOOLS.has(String(b.name)) &&
1336
- (succeededIds === null ||
1337
- (typeof b.id === "string" && succeededIds.has(b.id)))
1338
- ) {
1339
- count += 1;
1340
- }
1341
- }
1342
- }
1343
- return count;
1344
- }
1345
-
1346
- interface MessageLike {
1347
- role: string;
1348
- content: string | ContentBlock[];
1349
- }
1350
-
1351
- /**
1352
- * Parse a message row's content into its block objects, or `null` when the
1353
- * content is malformed (unparseable JSON, not an array). Non-object entries
1354
- * are dropped. Every evidence reader in this module goes through this so
1355
- * malformed rows degrade the same way everywhere: skipped, not propagated.
1356
- */
1357
- function parseMessageBlocks(
1358
- msg: MessageLike,
1359
- ): Record<string, unknown>[] | null {
1360
- let blocks: unknown = msg.content;
1361
- if (typeof blocks === "string") {
1362
- try {
1363
- blocks = JSON.parse(blocks);
1364
- } catch {
1365
- return null;
1366
- }
1367
- }
1368
- if (!Array.isArray(blocks)) {
1369
- return null;
1370
- }
1371
- return blocks.filter(
1372
- (block): block is Record<string, unknown> =>
1373
- typeof block === "object" && block !== null,
1374
- );
1375
- }
1376
-
1377
- /**
1378
- * Scan an array of message rows for `tool_use` blocks where `name` is
1379
- * `"remember"` and return the `input.content` strings in order. Robust to
1380
- * malformed content JSON — unparseable rows are skipped, not propagated.
1381
- */
1382
- function extractRememberContents(
1383
- messages: MessageLike[],
1384
- succeededIds?: ReadonlySet<string>,
1385
- ): string[] {
1386
- const contents: string[] = [];
1387
- for (const msg of messages) {
1388
- if (msg.role !== "assistant") {
1389
- continue;
1390
- }
1391
- for (const b of parseMessageBlocks(msg) ?? []) {
1392
- if (b.type !== "tool_use") {
1393
- continue;
1394
- }
1395
- if (b.name !== "remember") {
1396
- continue;
1397
- }
1398
- // When a success set is provided, only executions that reported a
1399
- // non-error tool_result contribute facts: a failed remember never
1400
- // wrote the buffer, and logging its facts would suppress the retry's
1401
- // re-save via <already_remembered>.
1402
- if (
1403
- succeededIds !== undefined &&
1404
- (typeof b.id !== "string" || !succeededIds.has(b.id))
1405
- ) {
1406
- continue;
1407
- }
1408
- const input = b.input;
1409
- if (!input || typeof input !== "object") {
1410
- continue;
1411
- }
1412
- const content = (input as Record<string, unknown>).content;
1413
- // `remember` accepts a single string or an array of facts (batch form);
1414
- // flatten both so batched saves still feed the dedup baseline.
1415
- const facts = Array.isArray(content) ? content : [content];
1416
- for (const fact of facts) {
1417
- if (typeof fact !== "string") {
1418
- continue;
1419
- }
1420
- const trimmed = fact.trim();
1421
- if (trimmed.length > 0) {
1422
- contents.push(trimmed);
1423
- }
1424
- }
1425
- }
1426
- }
1427
- return contents;
1428
- }
@@ -0,0 +1,213 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Memory run evidence: what a background memory run durably produced.
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // A background run's persisted message rows are the one record of what it
6
+ // did. A `tool_use` block proves only that the model asked; the durable write
7
+ // happens inside the executor, so a call counts as evidence only when a
8
+ // matching non-error `tool_result` is persisted on the same rows. The
9
+ // readers here apply that rule uniformly so every memory job that gates a
10
+ // state transition on "the run verifiably wrote something" (the
11
+ // retrospective's cursor advance, consolidation's buffer consumption)
12
+ // reads the same evidence the same way.
13
+ //
14
+ // This module sits at the plugin root (shared infra, not a tier) and imports
15
+ // nothing from the plugin, so `substrate/` and the spine can both reach it
16
+ // without a tier importing spine.
17
+
18
+ import type { ContentBlock } from "@vellumai/plugin-api";
19
+
20
+ /**
21
+ * Whether the LAST persisted assistant row on the run's tail carries a text
22
+ * block with non-whitespace content. Paired with a model-driven stop and
23
+ * zero memory-write attempts, that closing reply is the persisted artifact
24
+ * of a pass that read its window and had nothing to save: the model spoke
25
+ * and then chose to end the run. Reading only the final row separates it
26
+ * from a run whose narration went live but whose actual conclusion was
27
+ * empty, as well as from a response that committed nothing at all
28
+ * (thinking-only output, an empty content array).
29
+ */
30
+ export function hasCommittedTextReply(messages: MessageLike[]): boolean {
31
+ for (let i = messages.length - 1; i >= 0; i--) {
32
+ const msg = messages[i]!;
33
+ if (msg.role !== "assistant") {
34
+ continue;
35
+ }
36
+ const blocks = parseMessageBlocks(msg);
37
+ if (blocks === null) {
38
+ return false;
39
+ }
40
+ return blocks.some(
41
+ (b) =>
42
+ b.type === "text" && typeof b.text === "string" && b.text.trim() !== "",
43
+ );
44
+ }
45
+ return false;
46
+ }
47
+
48
+ /**
49
+ * Whether the run's FINAL persisted row is an assistant reply in its own
50
+ * words: text with non-whitespace content and no `tool_use` block. This is
51
+ * the shape of a run the model ended itself, since the loop stops when the
52
+ * assistant answers without asking for a tool. It is stricter than
53
+ * {@link hasCommittedTextReply} on purpose: narration on a row that also
54
+ * calls a tool ("fixing that page now" followed by `file_write`) is not a
55
+ * conclusion, and a run whose last row is a tool result stopped mid-loop.
56
+ */
57
+ export function endsWithTextReply(messages: MessageLike[]): boolean {
58
+ const last = messages[messages.length - 1];
59
+ if (last === undefined || last.role !== "assistant") {
60
+ return false;
61
+ }
62
+ const blocks = parseMessageBlocks(last);
63
+ if (blocks === null || blocks.some((b) => b.type === "tool_use")) {
64
+ return false;
65
+ }
66
+ return blocks.some(
67
+ (b) =>
68
+ b.type === "text" && typeof b.text === "string" && b.text.trim() !== "",
69
+ );
70
+ }
71
+
72
+ /**
73
+ * Ids of `tool_result` blocks on the run's user rows whose execution did not
74
+ * report an error. Robust to malformed content JSON the same way
75
+ * `extractRememberContents` is.
76
+ */
77
+ export function collectSuccessfulToolResultIds(
78
+ messages: MessageLike[],
79
+ ): Set<string> {
80
+ const ids = new Set<string>();
81
+ for (const msg of messages) {
82
+ if (msg.role !== "user") {
83
+ continue;
84
+ }
85
+ for (const b of parseMessageBlocks(msg) ?? []) {
86
+ // guard:allow-tool-result-only: success evidence for locally-executed
87
+ // durable memory tools; server-side web_search_tool_result never
88
+ // corresponds to a durable write and carries no is_error flag.
89
+ if (
90
+ b.type === "tool_result" &&
91
+ typeof b.tool_use_id === "string" &&
92
+ b.is_error !== true
93
+ ) {
94
+ ids.add(b.tool_use_id);
95
+ }
96
+ }
97
+ }
98
+ return ids;
99
+ }
100
+
101
+ /**
102
+ * Count persisted `tool_use` blocks whose `name` is in `durableTools` across
103
+ * the run's assistant rows. With a `succeededIds` set, only calls whose id
104
+ * has a matching successful `tool_result` count (verified executions); with
105
+ * `null`, every attempt counts regardless of outcome.
106
+ */
107
+ export function countDurableToolUses(
108
+ messages: MessageLike[],
109
+ durableTools: ReadonlySet<string>,
110
+ succeededIds: ReadonlySet<string> | null,
111
+ ): number {
112
+ let count = 0;
113
+ for (const msg of messages) {
114
+ if (msg.role !== "assistant") {
115
+ continue;
116
+ }
117
+ for (const b of parseMessageBlocks(msg) ?? []) {
118
+ if (
119
+ b.type === "tool_use" &&
120
+ durableTools.has(String(b.name)) &&
121
+ (succeededIds === null ||
122
+ (typeof b.id === "string" && succeededIds.has(b.id)))
123
+ ) {
124
+ count += 1;
125
+ }
126
+ }
127
+ }
128
+ return count;
129
+ }
130
+
131
+ export interface MessageLike {
132
+ role: string;
133
+ content: string | ContentBlock[];
134
+ }
135
+
136
+ /**
137
+ * Parse a message row's content into its block objects, or `null` when the
138
+ * content is malformed (unparseable JSON, not an array). Non-object entries
139
+ * are dropped. Every evidence reader in this module goes through this so
140
+ * malformed rows degrade the same way everywhere: skipped, not propagated.
141
+ */
142
+ function parseMessageBlocks(
143
+ msg: MessageLike,
144
+ ): Record<string, unknown>[] | null {
145
+ let blocks: unknown = msg.content;
146
+ if (typeof blocks === "string") {
147
+ try {
148
+ blocks = JSON.parse(blocks);
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ if (!Array.isArray(blocks)) {
154
+ return null;
155
+ }
156
+ return blocks.filter(
157
+ (block): block is Record<string, unknown> =>
158
+ typeof block === "object" && block !== null,
159
+ );
160
+ }
161
+
162
+ /**
163
+ * Scan an array of message rows for `tool_use` blocks where `name` is
164
+ * `"remember"` and return the `input.content` strings in order. Robust to
165
+ * malformed content JSON: unparseable rows are skipped, not propagated.
166
+ */
167
+ export function extractRememberContents(
168
+ messages: MessageLike[],
169
+ succeededIds?: ReadonlySet<string>,
170
+ ): string[] {
171
+ const contents: string[] = [];
172
+ for (const msg of messages) {
173
+ if (msg.role !== "assistant") {
174
+ continue;
175
+ }
176
+ for (const b of parseMessageBlocks(msg) ?? []) {
177
+ if (b.type !== "tool_use") {
178
+ continue;
179
+ }
180
+ if (b.name !== "remember") {
181
+ continue;
182
+ }
183
+ // When a success set is provided, only executions that reported a
184
+ // non-error tool_result contribute facts: a failed remember never
185
+ // wrote the buffer, and logging its facts would suppress the retry's
186
+ // re-save via <already_remembered>.
187
+ if (
188
+ succeededIds !== undefined &&
189
+ (typeof b.id !== "string" || !succeededIds.has(b.id))
190
+ ) {
191
+ continue;
192
+ }
193
+ const input = b.input;
194
+ if (!input || typeof input !== "object") {
195
+ continue;
196
+ }
197
+ const content = (input as Record<string, unknown>).content;
198
+ // `remember` accepts a single string or an array of facts (batch form);
199
+ // flatten both so batched saves still feed the dedup baseline.
200
+ const facts = Array.isArray(content) ? content : [content];
201
+ for (const fact of facts) {
202
+ if (typeof fact !== "string") {
203
+ continue;
204
+ }
205
+ const trimmed = fact.trim();
206
+ if (trimmed.length > 0) {
207
+ contents.push(trimmed);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ return contents;
213
+ }
@@ -203,7 +203,7 @@ function insertItem(opts: {
203
203
  describe("Memory Item Routes", () => {
204
204
  beforeAll(async () => {
205
205
  await initializeDb();
206
- });
206
+ }, 30_000);
207
207
 
208
208
  beforeEach(() => {
209
209
  // Keep memory v2 disabled so the v1 paths under test stay active.