@vellumai/assistant 0.12.4 → 0.12.5-dev.202609251017.e467a6c

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 (281) hide show
  1. package/docs/architecture/memory.md +9 -1
  2. package/docs/architecture/scheduling.md +13 -5
  3. package/docs/desktop-browser-cli.md +3 -2
  4. package/docs/guardian-request-flow.md +1 -1
  5. package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
  6. package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  10. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  11. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +2 -4
  12. package/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  13. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  14. package/openapi.yaml +152 -3
  15. package/package.json +1 -1
  16. package/src/__tests__/agent-loop.test.ts +76 -0
  17. package/src/__tests__/anthropic-error-formatting.test.ts +9 -0
  18. package/src/__tests__/background-shell-bash.test.ts +34 -0
  19. package/src/__tests__/background-shell-host-bash.test.ts +98 -1
  20. package/src/__tests__/background-tool-registry.test.ts +39 -0
  21. package/src/__tests__/channel-readiness-discord.test.ts +8 -0
  22. package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
  23. package/src/__tests__/client-os-metadata-persistence.test.ts +4 -20
  24. package/src/__tests__/conversation-agent-loop.test.ts +529 -1
  25. package/src/__tests__/conversation-error.test.ts +18 -0
  26. package/src/__tests__/conversation-pairing.test.ts +69 -0
  27. package/src/__tests__/conversation-queue.test.ts +257 -4
  28. package/src/__tests__/conversation-runtime-assembly.test.ts +27 -0
  29. package/src/__tests__/emit-signal-routing-intent.test.ts +62 -0
  30. package/src/__tests__/evict-conversations-for-reload.test.ts +21 -0
  31. package/src/__tests__/gemini-provider.test.ts +9 -0
  32. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  33. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  34. package/src/__tests__/introduction-card-resolver.test.ts +53 -8
  35. package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
  36. package/src/__tests__/non-member-access-request.test.ts +48 -0
  37. package/src/__tests__/notification-events-source-context-probe.test.ts +48 -1
  38. package/src/__tests__/notification-platform-adapter.test.ts +97 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +103 -3
  40. package/src/__tests__/platform-callback-registration.test.ts +46 -1
  41. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  42. package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
  43. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -0
  44. package/src/__tests__/scheduler-result-notification.test.ts +521 -7
  45. package/src/__tests__/scheduler-wake.test.ts +1 -0
  46. package/src/__tests__/skill-load-inline-command.test.ts +35 -0
  47. package/src/__tests__/subagent-cron-run-attribution.test.ts +41 -2
  48. package/src/__tests__/subagent-manager-notify.test.ts +48 -0
  49. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -6
  50. package/src/__tests__/usage-attribution.test.ts +31 -0
  51. package/src/__tests__/wake-schedule-escalation.test.ts +1 -0
  52. package/src/agent/loop.ts +23 -0
  53. package/src/api/ask-question-tool.ts +50 -0
  54. package/src/api/events/generation-handoff.ts +4 -0
  55. package/src/api/events/message-complete.ts +4 -0
  56. package/src/api/events/tool-result.ts +11 -0
  57. package/src/api/index.ts +11 -0
  58. package/src/api/requests/guardian-actions.test.ts +45 -0
  59. package/src/api/requests/guardian-actions.ts +20 -0
  60. package/src/api/responses/conversation-message.ts +4 -0
  61. package/src/api/responses/home.ts +27 -0
  62. package/src/approvals/guardian-decision-primitive.ts +6 -4
  63. package/src/approvals/guardian-request-resolvers.ts +35 -30
  64. package/src/avatar/notification-avatar.test.ts +42 -34
  65. package/src/avatar/notification-avatar.ts +1 -0
  66. package/src/calls/__tests__/call-controller.test.ts +18 -0
  67. package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
  68. package/src/calls/__tests__/voice-leg-coordinator.test.ts +70 -0
  69. package/src/calls/__tests__/voice-session-bridge.test.ts +116 -4
  70. package/src/calls/__tests__/voice-triage-escalate.test.ts +77 -11
  71. package/src/calls/call-controller.ts +3 -1
  72. package/src/calls/voice-control-protocol.ts +12 -4
  73. package/src/calls/voice-escalation-judge.ts +2 -2
  74. package/src/calls/voice-leg-coordinator.ts +23 -5
  75. package/src/calls/voice-screen-annotation.ts +44 -0
  76. package/src/calls/voice-session-bridge.ts +63 -20
  77. package/src/calls/voice-triage-escalate.ts +82 -27
  78. package/src/cli/commands/__tests__/conversations-clear.test.ts +156 -0
  79. package/src/cli/commands/conversations.help.ts +7 -2
  80. package/src/cli/commands/conversations.ts +36 -30
  81. package/src/cli/commands/oauth/index.help.ts +8 -2
  82. package/src/cli/commands/oauth/request.test.ts +32 -0
  83. package/src/cli/commands/platform/index.help.ts +2 -1
  84. package/src/cli/commands/platform/index.ts +8 -2
  85. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
  86. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
  87. package/src/cli/lib/bundled-marketplace.json +35 -0
  88. package/src/cli/lib/bundled-plugin-packages.json +21 -0
  89. package/src/cli/lib/plugin-catalog-cache.ts +17 -2
  90. package/src/cli/lib/plugin-catalog-platform.ts +48 -9
  91. package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
  92. package/src/config/__tests__/platform-identity.test.ts +59 -0
  93. package/src/config/__tests__/profile-order.test.ts +19 -0
  94. package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
  95. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  96. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  97. package/src/config/bundled-skills/screen-annotation/SKILL.md +41 -29
  98. package/src/config/bundled-skills/screen-annotation/TOOLS.json +5 -5
  99. package/src/config/call-site-defaults.ts +6 -0
  100. package/src/config/default-profile-catalog.ts +30 -1
  101. package/src/config/default-profile-names.ts +36 -2
  102. package/src/config/feature-flag-registry.json +9 -1
  103. package/src/config/platform-identity.ts +22 -5
  104. package/src/config/profile-order.ts +7 -1
  105. package/src/config/schemas/call-site-catalog.ts +7 -0
  106. package/src/config/schemas/llm.ts +21 -5
  107. package/src/config/sync-gated-profiles.ts +96 -44
  108. package/src/daemon/__tests__/abort-null-controller.test.ts +135 -1
  109. package/src/daemon/__tests__/auto-profile-router.test.ts +380 -0
  110. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +16 -0
  111. package/src/daemon/activity-trail.test.ts +206 -0
  112. package/src/daemon/activity-trail.ts +190 -0
  113. package/src/daemon/auto-profile-router.ts +431 -0
  114. package/src/daemon/computer-use-observation.test.ts +81 -0
  115. package/src/daemon/computer-use-observation.ts +23 -0
  116. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  117. package/src/daemon/conversation-agent-loop.ts +157 -32
  118. package/src/daemon/conversation-error.ts +19 -0
  119. package/src/daemon/conversation-lifecycle.ts +70 -7
  120. package/src/daemon/conversation-media-retry.ts +1 -2
  121. package/src/daemon/conversation-messaging.ts +31 -2
  122. package/src/daemon/conversation-process.ts +107 -16
  123. package/src/daemon/conversation-queue-manager.ts +7 -0
  124. package/src/daemon/conversation-runtime-assembly.ts +1 -0
  125. package/src/daemon/conversation-turn-finalize.ts +14 -0
  126. package/src/daemon/conversation.ts +25 -6
  127. package/src/daemon/event-loop-watchdog.test.ts +201 -2
  128. package/src/daemon/event-loop-watchdog.ts +182 -6
  129. package/src/daemon/host-cu-proxy.ts +34 -3
  130. package/src/daemon/pkb-reminder-builder.test.ts +10 -3
  131. package/src/daemon/pkb-reminder-builder.ts +1 -1
  132. package/src/daemon/wake-conversation-ops.ts +4 -2
  133. package/src/desktop/desktop-computer-use.test.ts +14 -4
  134. package/src/home/feed-types.ts +2 -0
  135. package/src/inbound/platform-callback-registration.ts +19 -6
  136. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
  137. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  138. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +48 -1
  139. package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
  140. package/src/live-voice/__tests__/protocol.test.ts +22 -0
  141. package/src/live-voice/continuation-judge.ts +1 -1
  142. package/src/live-voice/live-voice-session.ts +25 -19
  143. package/src/live-voice/protocol.ts +16 -5
  144. package/src/messaging/providers/discord/withdraw.ts +4 -5
  145. package/src/messaging/providers/gmail/client.ts +4 -0
  146. package/src/messaging/providers/slack/withdraw.ts +5 -5
  147. package/src/messaging/providers/telegram-bot/withdraw.ts +4 -5
  148. package/src/monitoring/__tests__/proc-fixtures.ts +28 -0
  149. package/src/monitoring/__tests__/proc-wait-state.test.ts +232 -0
  150. package/src/monitoring/__tests__/process-usage.test.ts +243 -0
  151. package/src/monitoring/proc-wait-state.ts +366 -0
  152. package/src/monitoring/process-memory.ts +4 -3
  153. package/src/monitoring/process-usage.ts +215 -0
  154. package/src/monitoring/resource-sample-types.ts +8 -0
  155. package/src/monitoring/resource-sampler.ts +17 -2
  156. package/src/monitoring/stall-capture.ts +12 -0
  157. package/src/notifications/README.md +88 -9
  158. package/src/notifications/__tests__/assistant-reply-producer.test.ts +390 -119
  159. package/src/notifications/__tests__/background-result-producer.test.ts +1272 -0
  160. package/src/notifications/__tests__/broadcaster.test.ts +292 -1
  161. package/src/notifications/__tests__/completion-policy.test.ts +243 -0
  162. package/src/notifications/__tests__/decision-engine.test.ts +51 -4
  163. package/src/notifications/__tests__/has-pending-background-work.test.ts +281 -0
  164. package/src/notifications/__tests__/home-feed-side-effect.test.ts +161 -3
  165. package/src/notifications/__tests__/resolve-visible-in-source.test.ts +110 -1
  166. package/src/notifications/__tests__/schedule-result-producer.test.ts +11 -4
  167. package/src/notifications/adapters/macos.ts +24 -12
  168. package/src/notifications/adapters/platform.ts +16 -5
  169. package/src/notifications/assistant-reply-producer.ts +96 -71
  170. package/src/notifications/background-result-producer.ts +369 -0
  171. package/src/notifications/broadcaster.ts +28 -8
  172. package/src/notifications/completion-policy.ts +115 -0
  173. package/src/notifications/completion-work.ts +46 -0
  174. package/src/notifications/conversation-pairing.ts +32 -13
  175. package/src/notifications/decision-engine.ts +18 -24
  176. package/src/notifications/emit-signal.ts +16 -1
  177. package/src/notifications/events-store.ts +4 -0
  178. package/src/notifications/has-pending-background-work.ts +91 -0
  179. package/src/notifications/home-feed-mirror.ts +44 -0
  180. package/src/notifications/home-feed-side-effect.ts +54 -28
  181. package/src/notifications/resolve-visible-in-source.ts +56 -2
  182. package/src/notifications/result-output.ts +154 -0
  183. package/src/notifications/schedule-result-producer.ts +6 -187
  184. package/src/notifications/types.ts +2 -0
  185. package/src/oauth/AGENTS.md +9 -1
  186. package/src/oauth/byo-connection.test.ts +28 -0
  187. package/src/persistence/__tests__/recent-conversation-messages.test.ts +41 -0
  188. package/src/persistence/conversation-crud.ts +26 -0
  189. package/src/persistence/conversation-types.ts +15 -37
  190. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +85 -14
  191. package/src/persistence/jobs-store.ts +229 -0
  192. package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
  193. package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
  194. package/src/persistence/steps.ts +12 -0
  195. package/src/platform/sync-avatar.test.ts +29 -0
  196. package/src/plugins/defaults/memory/AGENTS.md +5 -3
  197. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
  198. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
  199. package/src/plugins/defaults/memory/__tests__/turn-retrieval.test.ts +21 -0
  200. package/src/plugins/defaults/memory/graph/tools.ts +9 -5
  201. package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
  202. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +7 -3
  203. package/src/plugins/defaults/memory/job-handlers.ts +8 -2
  204. package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
  205. package/src/plugins/defaults/memory/tools.test.ts +17 -0
  206. package/src/plugins/defaults/memory/turn-retrieval.ts +8 -0
  207. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +24 -0
  208. package/src/plugins/defaults/memory/v3/injector.ts +4 -4
  209. package/src/plugins/types.ts +2 -0
  210. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  211. package/src/providers/anthropic/client.ts +4 -0
  212. package/src/providers/gemini/client.ts +7 -1
  213. package/src/providers/inference/auth.ts +25 -0
  214. package/src/providers/inference/profile-probe.ts +1 -0
  215. package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
  216. package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
  217. package/src/providers/openai/__tests__/api-error-normalization.test.ts +12 -0
  218. package/src/providers/openai/api-error-normalization.ts +4 -0
  219. package/src/providers/retry.ts +2 -0
  220. package/src/providers/search-provider-catalog.ts +10 -0
  221. package/src/providers/types.ts +7 -0
  222. package/src/providers/usage-tracking.ts +5 -3
  223. package/src/runtime/__tests__/agent-wake.test.ts +308 -0
  224. package/src/runtime/access-request-helper.ts +16 -9
  225. package/src/runtime/agent-wake-queue.ts +52 -0
  226. package/src/runtime/agent-wake.ts +111 -18
  227. package/src/runtime/channel-approval-types.ts +6 -38
  228. package/src/runtime/channel-approvals.ts +4 -5
  229. package/src/runtime/http-server.ts +4 -1
  230. package/src/runtime/introduction-policy.test.ts +17 -1
  231. package/src/runtime/introduction-policy.ts +25 -7
  232. package/src/runtime/migrations/vbundle-builder.ts +9 -0
  233. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +21 -1
  234. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
  235. package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
  236. package/src/runtime/routes/auto-profile-routes.ts +83 -0
  237. package/src/runtime/routes/conversation-management-routes.ts +1 -3
  238. package/src/runtime/routes/conversation-routes.ts +9 -0
  239. package/src/runtime/routes/guardian-action-routes.ts +8 -23
  240. package/src/runtime/routes/host-cu-routes.ts +3 -1
  241. package/src/runtime/routes/index.ts +2 -0
  242. package/src/runtime/routes/inference-profiles-routes.ts +5 -2
  243. package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
  244. package/src/runtime/routes/migration-routes.ts +8 -2
  245. package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
  246. package/src/runtime/routes/platform-routes.ts +6 -1
  247. package/src/runtime/routes/user-route-dispatcher.ts +1 -2
  248. package/src/runtime/web-presence.ts +3 -4
  249. package/src/schedule/schedule-store.ts +10 -0
  250. package/src/schedule/scheduler.ts +92 -24
  251. package/src/subagent/manager.ts +32 -12
  252. package/src/telegram/__tests__/webhook-health.test.ts +8 -0
  253. package/src/telemetry/telemetry-wire-source.json +1 -1
  254. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  255. package/src/tools/background-tool-registry.ts +30 -3
  256. package/src/tools/browser/cdp-client/local-cdp-client.ts +1 -2
  257. package/src/tools/host-terminal/host-shell.ts +7 -0
  258. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
  259. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
  260. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
  261. package/src/tools/network/__tests__/web-search.test.ts +96 -0
  262. package/src/tools/network/web-fetch.ts +30 -9
  263. package/src/tools/network/web-search.ts +235 -4
  264. package/src/tools/skills/load.ts +13 -17
  265. package/src/tools/skills/scaffold-managed.ts +86 -71
  266. package/src/tools/terminal/shell-launch.test.ts +27 -1
  267. package/src/tools/terminal/shell.ts +4 -0
  268. package/src/tools/workspace-tools/loader.ts +1 -2
  269. package/src/usage/attribution.ts +18 -1
  270. package/src/util/abort-reasons.ts +2 -0
  271. package/src/util/errors.ts +1 -0
  272. package/src/util/oauth-request-body.test.ts +5 -1
  273. package/src/util/provider-error-patterns.ts +11 -0
  274. package/src/util/single-flight.test.ts +6 -0
  275. package/src/util/single-flight.ts +2 -0
  276. package/src/util/worker-pipe.test.ts +56 -10
  277. package/src/util/worker-pipe.ts +9 -4
  278. package/src/workspace/git-service.ts +6 -1
  279. package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
  280. package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
  281. package/src/workspace/migrations/registry.ts +2 -0
@@ -0,0 +1,580 @@
1
+ /**
2
+ * Tests for the skill-update receipt job: the pending-row merge rules the
3
+ * burst boundary rests on, when a receipt is announced, the sibling path an
4
+ * entry landing mid-evaluation takes, and the conversation purge.
5
+ *
6
+ * The job queue runs for real on the test workspace's memory database; the
7
+ * notification pipeline and the conversation reads are stubbed at their
8
+ * module boundaries. `mock.module` is process-global and leaks into sibling
9
+ * files in a directory run, so each stub delegates to the real
10
+ * implementation unless this test is actively running.
11
+ */
12
+
13
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
14
+
15
+ import { eq } from "drizzle-orm";
16
+
17
+ import { setConfig } from "../../../../__tests__/helpers/set-config.js";
18
+ import { getMemoryDb } from "../../../../persistence/db-connection.js";
19
+ import { initializeDb } from "../../../../persistence/db-init.js";
20
+ import { memoryJobs } from "../../../../persistence/schema/index.js";
21
+
22
+ const realEmitSignal = {
23
+ ...(await import("../../../../notifications/emit-signal.js")),
24
+ };
25
+ const realPluginApi = { ...(await import("@vellumai/plugin-api")) };
26
+
27
+ let active = false;
28
+ /** What the stubbed pipeline answers, per emit, in order. */
29
+ let emitOutcomes: Array<{ pipelineFailed?: boolean; reason?: string }> = [];
30
+ const emits: Array<
31
+ Parameters<typeof realEmitSignal.emitNotificationSignal>[0]
32
+ > = [];
33
+ /** Runs that are processing; any other conversation reads as idle. */
34
+ const runs = new Map<string, "live" | "idle">();
35
+ /** Conversations, run or source, that no longer exist. */
36
+ const gone = new Set<string>();
37
+ /** Runs inside a liveness read, so a test can land an entry mid-evaluation. */
38
+ let onLivenessRead: (() => void) | null = null;
39
+
40
+ mock.module("../../../../notifications/emit-signal.js", () => ({
41
+ ...realEmitSignal,
42
+ emitNotificationSignal: async (
43
+ params: Parameters<typeof realEmitSignal.emitNotificationSignal>[0],
44
+ ) => {
45
+ if (!active) {
46
+ return realEmitSignal.emitNotificationSignal(params);
47
+ }
48
+ emits.push(params);
49
+ const outcome = emitOutcomes.shift() ?? {};
50
+ return {
51
+ signalId: `sig-${emits.length}`,
52
+ deduplicated: false,
53
+ dispatched: true,
54
+ reason: outcome.reason ?? "ok",
55
+ selectedChannels: [],
56
+ deliveryResults: [],
57
+ receiptClass: "unknown" as const,
58
+ pipelineFailed: outcome.pipelineFailed ?? false,
59
+ };
60
+ },
61
+ }));
62
+
63
+ mock.module("@vellumai/plugin-api", () => ({
64
+ ...realPluginApi,
65
+ getConversation: async (id: string) =>
66
+ active ? (gone.has(id) ? null : { id }) : realPluginApi.getConversation(id),
67
+ isConversationProcessing: async (id: string) => {
68
+ if (!active) {
69
+ return realPluginApi.isConversationProcessing(id);
70
+ }
71
+ onLivenessRead?.();
72
+ return runs.get(id) === "live";
73
+ },
74
+ }));
75
+
76
+ setConfig("memory", { enabled: true });
77
+ await initializeDb();
78
+
79
+ const {
80
+ parseSkillUpdateReceiptPayload,
81
+ removeSkillUpdateReceiptEntriesForConversation,
82
+ upsertSkillUpdateReceiptJob,
83
+ } = await import("../../../../persistence/jobs-store.js");
84
+ const {
85
+ composeSkillUpdateReceiptCopy,
86
+ decideSkillUpdateReceiptSeal,
87
+ recordSkillUpdate,
88
+ SKILL_UPDATE_RECEIPT_CAP_MS,
89
+ SKILL_UPDATE_RECEIPT_LIVE_RUN_RECHECK_MS,
90
+ SKILL_UPDATE_RECEIPT_QUIET_MS,
91
+ skillUpdateReceiptDedupeKey,
92
+ skillUpdateReceiptJob,
93
+ skillUpdateReceiptSourceContextId,
94
+ } = await import("../skill-update-receipt-job.js");
95
+
96
+ beforeEach(() => {
97
+ active = true;
98
+ getMemoryDb()!.delete(memoryJobs).run();
99
+ emitOutcomes = [];
100
+ emits.length = 0;
101
+ runs.clear();
102
+ gone.clear();
103
+ onLivenessRead = null;
104
+ });
105
+
106
+ afterAll(() => {
107
+ active = false;
108
+ });
109
+
110
+ interface Row {
111
+ id: string;
112
+ status: string;
113
+ runAfter: number;
114
+ payload: string;
115
+ }
116
+
117
+ function rows(): Row[] {
118
+ return getMemoryDb()!
119
+ .select({
120
+ id: memoryJobs.id,
121
+ status: memoryJobs.status,
122
+ runAfter: memoryJobs.runAfter,
123
+ payload: memoryJobs.payload,
124
+ })
125
+ .from(memoryJobs)
126
+ .where(eq(memoryJobs.type, "skill_update_receipt"))
127
+ .all();
128
+ }
129
+
130
+ function pending(): Row[] {
131
+ return rows().filter((row) => row.status === "pending");
132
+ }
133
+
134
+ function entryIds(row: Row): string[] {
135
+ return (parseSkillUpdateReceiptPayload(row.payload)?.entries ?? []).map(
136
+ (entry) => entry.entryId,
137
+ );
138
+ }
139
+
140
+ /** Claim the one pending row the way the worker does, and hand it to the handler. */
141
+ async function runClaimed(): Promise<void> {
142
+ const [row] = pending();
143
+ if (!row) {
144
+ throw new Error("no pending receipt to claim");
145
+ }
146
+ getMemoryDb()!
147
+ .update(memoryJobs)
148
+ .set({ status: "running", startedAt: Date.now() })
149
+ .where(eq(memoryJobs.id, row.id))
150
+ .run();
151
+ const resolution = await skillUpdateReceiptJob({
152
+ id: row.id,
153
+ type: "skill_update_receipt",
154
+ payload: JSON.parse(row.payload) as Record<string, unknown>,
155
+ status: "running",
156
+ attempts: 0,
157
+ deferrals: 0,
158
+ runAfter: row.runAfter,
159
+ lastError: null,
160
+ startedAt: Date.now(),
161
+ createdAt: 0,
162
+ updatedAt: 0,
163
+ });
164
+ getMemoryDb()!
165
+ .update(memoryJobs)
166
+ .set({
167
+ status:
168
+ resolution && resolution.queueResolution === "retryable"
169
+ ? "pending"
170
+ : "completed",
171
+ })
172
+ .where(eq(memoryJobs.id, row.id))
173
+ .run();
174
+ }
175
+
176
+ function record(
177
+ id: string,
178
+ skillId: string,
179
+ at: number,
180
+ extra: { run?: string; source?: string } = {},
181
+ ): void {
182
+ const run = extra.run ?? "run-1";
183
+ runs.set(run, runs.get(run) ?? "idle");
184
+ recordSkillUpdate(
185
+ {
186
+ entryId: id,
187
+ skillId,
188
+ name: `Skill ${skillId}`,
189
+ changeSummary: `summary ${id}`,
190
+ runConversationId: run,
191
+ ...(extra.source ? { sourceConversationId: extra.source } : {}),
192
+ },
193
+ at,
194
+ );
195
+ }
196
+
197
+ describe("upsertSkillUpdateReceiptJob", () => {
198
+ test("every append lands on the one pending row, keyed by entry id", () => {
199
+ record("e1", "a", 1_000);
200
+ record("e2", "b", 2_000);
201
+ record("e1", "a", 9_000);
202
+
203
+ expect(pending()).toHaveLength(1);
204
+ const [row] = pending();
205
+ expect(entryIds(row!)).toEqual(["e1", "e2"]);
206
+ expect(parseSkillUpdateReceiptPayload(row!.payload)).toMatchObject({
207
+ firstEntryAt: 1_000,
208
+ lastEntryAt: 9_000,
209
+ });
210
+ });
211
+
212
+ test("the merge keeps the earliest firstEntryAt, so a stream of rewrites cannot ratchet the cap", () => {
213
+ upsertSkillUpdateReceiptJob({
214
+ firstEntryAt: 5_000,
215
+ lastEntryAt: 5_000,
216
+ entries: [],
217
+ });
218
+ upsertSkillUpdateReceiptJob({
219
+ firstEntryAt: 1_000,
220
+ lastEntryAt: 1_000,
221
+ entries: [],
222
+ });
223
+ upsertSkillUpdateReceiptJob({
224
+ firstEntryAt: 7_000,
225
+ lastEntryAt: 7_000,
226
+ entries: [],
227
+ });
228
+
229
+ expect(parseSkillUpdateReceiptPayload(pending()[0]!.payload)).toMatchObject(
230
+ { firstEntryAt: 1_000, lastEntryAt: 7_000 },
231
+ );
232
+ });
233
+
234
+ test("the earliest runAfter wins, so a later append never pushes the check out", () => {
235
+ record("e1", "a", 1_000);
236
+ record("e2", "b", 50_000);
237
+
238
+ expect(pending()[0]!.runAfter).toBe(1_000 + SKILL_UPDATE_RECEIPT_QUIET_MS);
239
+ });
240
+
241
+ test("a running row never merges: an append during evaluation opens a sibling", () => {
242
+ record("e1", "a", 1_000);
243
+ getMemoryDb()!
244
+ .update(memoryJobs)
245
+ .set({ status: "running" })
246
+ .where(eq(memoryJobs.id, pending()[0]!.id))
247
+ .run();
248
+
249
+ record("e2", "b", 2_000);
250
+
251
+ expect(rows()).toHaveLength(2);
252
+ expect(pending()).toHaveLength(1);
253
+ expect(entryIds(pending()[0]!)).toEqual(["e2"]);
254
+ });
255
+ });
256
+
257
+ describe("decideSkillUpdateReceiptSeal", () => {
258
+ const receipt = { firstEntryAt: 0, lastEntryAt: 10_000 };
259
+
260
+ test("seals on the quiet window once every run has finished", () => {
261
+ expect(
262
+ decideSkillUpdateReceiptSeal({
263
+ receipt,
264
+ anyRunLive: false,
265
+ now: 10_000 + SKILL_UPDATE_RECEIPT_QUIET_MS,
266
+ }),
267
+ ).toEqual({ seal: "quiet" });
268
+ });
269
+
270
+ test("a live run holds the seal past the quiet window, on a short recheck", () => {
271
+ const now = 10_000 + SKILL_UPDATE_RECEIPT_QUIET_MS;
272
+ expect(
273
+ decideSkillUpdateReceiptSeal({ receipt, anyRunLive: true, now }),
274
+ ).toEqual({
275
+ seal: null,
276
+ nextCheckAt: now + SKILL_UPDATE_RECEIPT_LIVE_RUN_RECHECK_MS,
277
+ });
278
+ });
279
+
280
+ test("the cap seals a live, never-quiet burst", () => {
281
+ expect(
282
+ decideSkillUpdateReceiptSeal({
283
+ receipt: { firstEntryAt: 0, lastEntryAt: SKILL_UPDATE_RECEIPT_CAP_MS },
284
+ anyRunLive: true,
285
+ now: SKILL_UPDATE_RECEIPT_CAP_MS,
286
+ }),
287
+ ).toEqual({ seal: "cap" });
288
+ });
289
+ });
290
+
291
+ describe("copy and source context", () => {
292
+ test("one skill is named, several are counted, every summary is in the body", () => {
293
+ const entries = [
294
+ {
295
+ entryId: "e1",
296
+ skillId: "a",
297
+ name: "Skill a",
298
+ changeSummary: "s1",
299
+ runConversationId: "r",
300
+ createdAt: 1,
301
+ },
302
+ {
303
+ entryId: "e2",
304
+ skillId: "b",
305
+ name: "Skill b",
306
+ changeSummary: "s2",
307
+ runConversationId: "r",
308
+ createdAt: 2,
309
+ },
310
+ {
311
+ entryId: "e3",
312
+ skillId: "a",
313
+ name: "Skill a",
314
+ changeSummary: "s3",
315
+ runConversationId: "r",
316
+ createdAt: 3,
317
+ },
318
+ ];
319
+ expect(composeSkillUpdateReceiptCopy(entries)).toEqual({
320
+ title: "2 skills updated",
321
+ body: "- Skill a: s1\n- Skill b: s2\n- Skill a: s3",
322
+ });
323
+ expect(composeSkillUpdateReceiptCopy(entries.slice(0, 1)).title).toBe(
324
+ "Skill updated: Skill a",
325
+ );
326
+ expect(
327
+ skillUpdateReceiptSourceContextId("job", [
328
+ { ...entries[0]!, sourceConversationId: "conv-1" },
329
+ { ...entries[1]!, sourceConversationId: "conv-1" },
330
+ ]),
331
+ ).toBe("conv-1");
332
+ expect(
333
+ skillUpdateReceiptSourceContextId("job", [
334
+ { ...entries[0]!, sourceConversationId: "conv-1" },
335
+ { ...entries[1]!, sourceConversationId: "conv-2" },
336
+ ]),
337
+ ).toBe("skill-update-receipt:job");
338
+ // A rewrite of unknown lineage cannot be attributed to the known one.
339
+ expect(
340
+ skillUpdateReceiptSourceContextId("job", [
341
+ { ...entries[0]!, sourceConversationId: "conv-1" },
342
+ entries[1]!,
343
+ ]),
344
+ ).toBe("skill-update-receipt:job");
345
+ });
346
+ });
347
+
348
+ describe("skillUpdateReceiptJob", () => {
349
+ test("a quiet burst is announced once, with every entry, under the row's key", async () => {
350
+ const start = Date.now() - SKILL_UPDATE_RECEIPT_QUIET_MS - 10;
351
+ record("e1", "a", start, { source: "conv-1" });
352
+ record("e2", "b", start + 1, { source: "conv-2" });
353
+ record("e3", "a", start + 2);
354
+ const [row] = pending();
355
+
356
+ await runClaimed();
357
+
358
+ expect(emits).toHaveLength(1);
359
+ const emit = emits[0]!;
360
+ expect(emit.dedupeKey).toBe(skillUpdateReceiptDedupeKey(row!.id));
361
+ expect(emit.sourceContextId).toBe(`skill-update-receipt:${row!.id}`);
362
+ expect(emit.sourceChannel).toBe("assistant_tool");
363
+ const payload = emit.contextPayload as Record<string, unknown>;
364
+ expect(payload.title).toBe("2 skills updated");
365
+ expect(payload.updates).toEqual([
366
+ {
367
+ skillId: "a",
368
+ name: "Skill a",
369
+ summary: "summary e1",
370
+ conversationId: "conv-1",
371
+ },
372
+ {
373
+ skillId: "b",
374
+ name: "Skill b",
375
+ summary: "summary e2",
376
+ conversationId: "conv-2",
377
+ },
378
+ { skillId: "a", name: "Skill a", summary: "summary e3" },
379
+ ]);
380
+ expect(payload.skillId).toBeUndefined();
381
+ expect(pending()).toHaveLength(0);
382
+ });
383
+
384
+ test("a source conversation deleted while the row was claimed is left out of the announcement", async () => {
385
+ const start = Date.now() - SKILL_UPDATE_RECEIPT_QUIET_MS - 10;
386
+ record("e1", "a", start, { source: "conv-deleted" });
387
+ record("e2", "b", start + 1, { source: "conv-kept" });
388
+ record("e3", "c", start + 2);
389
+ onLivenessRead = () => gone.add("conv-deleted");
390
+
391
+ await runClaimed();
392
+
393
+ expect(emits).toHaveLength(1);
394
+ const payload = emits[0]!.contextPayload as Record<string, unknown>;
395
+ expect(
396
+ (payload.updates as Array<{ skillId: string }>).map((u) => u.skillId),
397
+ ).toEqual(["b", "c"]);
398
+ });
399
+
400
+ test("a receipt whose surviving entries share one source still links to it", async () => {
401
+ // Two sources, one deleted during evaluation: the announcement is the
402
+ // survivor's rewrites alone, so its context is the survivor's
403
+ // conversation, not the multi-source sentinel.
404
+ const start = Date.now() - SKILL_UPDATE_RECEIPT_QUIET_MS - 10;
405
+ record("e1", "a", start, { source: "conv-deleted" });
406
+ record("e2", "b", start + 1, { source: "conv-kept" });
407
+ record("e3", "b", start + 2, { source: "conv-kept" });
408
+ onLivenessRead = () => gone.add("conv-deleted");
409
+
410
+ await runClaimed();
411
+
412
+ expect(emits).toHaveLength(1);
413
+ expect(emits[0]!.sourceContextId).toBe("conv-kept");
414
+ expect((emits[0]!.contextPayload as Record<string, unknown>).skillId).toBe(
415
+ "b",
416
+ );
417
+ });
418
+
419
+ test("a receipt whose every source was deleted announces nothing", async () => {
420
+ record("e1", "a", Date.now() - SKILL_UPDATE_RECEIPT_QUIET_MS - 10, {
421
+ source: "conv-deleted",
422
+ });
423
+ gone.add("conv-deleted");
424
+
425
+ await runClaimed();
426
+
427
+ expect(emits).toHaveLength(0);
428
+ expect(pending()).toHaveLength(0);
429
+ });
430
+
431
+ test("a one-skill, one-source receipt carries both ids the footer links from", async () => {
432
+ const start = Date.now() - SKILL_UPDATE_RECEIPT_QUIET_MS - 10;
433
+ record("e1", "a", start, { source: "conv-1" });
434
+ record("e2", "a", start + 1, { source: "conv-1" });
435
+
436
+ await runClaimed();
437
+
438
+ expect(emits[0]!.sourceContextId).toBe("conv-1");
439
+ expect((emits[0]!.contextPayload as Record<string, unknown>).skillId).toBe(
440
+ "a",
441
+ );
442
+ });
443
+
444
+ test("nothing is announced before the quiet window; the payload moves to a row due at its end", async () => {
445
+ const start = Date.now() - 1_000;
446
+ record("e1", "a", start);
447
+
448
+ await runClaimed();
449
+
450
+ expect(emits).toHaveLength(0);
451
+ const [next] = pending();
452
+ expect(entryIds(next!)).toEqual(["e1"]);
453
+ expect(next!.runAfter).toBe(start + SKILL_UPDATE_RECEIPT_QUIET_MS);
454
+ });
455
+
456
+ test("a live contributing run holds the announcement; an unrelated live run and a deleted run do not", async () => {
457
+ const start = Date.now() - SKILL_UPDATE_RECEIPT_QUIET_MS - 10;
458
+ record("e1", "a", start, { run: "run-1" });
459
+ runs.set("run-1", "live");
460
+ runs.set("run-other", "live");
461
+
462
+ await runClaimed();
463
+ expect(emits).toHaveLength(0);
464
+ expect(pending()[0]!.runAfter - Date.now()).toBeLessThanOrEqual(
465
+ SKILL_UPDATE_RECEIPT_LIVE_RUN_RECHECK_MS,
466
+ );
467
+
468
+ gone.add("run-1");
469
+ await runClaimed();
470
+ expect(emits).toHaveLength(1);
471
+ });
472
+
473
+ test("the cap announces a burst whose run is still live", async () => {
474
+ record("e1", "a", Date.now() - SKILL_UPDATE_RECEIPT_CAP_MS, {
475
+ run: "run-1",
476
+ });
477
+ record("e2", "b", Date.now() - 1, { run: "run-1" });
478
+ runs.set("run-1", "live");
479
+
480
+ await runClaimed();
481
+
482
+ expect(emits).toHaveLength(1);
483
+ });
484
+
485
+ test("an entry landing mid-evaluation joins the burst: the row merges into the sibling and announces nothing", async () => {
486
+ const start = Date.now() - SKILL_UPDATE_RECEIPT_QUIET_MS - 10;
487
+ record("e1", "a", start, { run: "run-1" });
488
+ let landed = false;
489
+ onLivenessRead = () => {
490
+ if (!landed) {
491
+ landed = true;
492
+ record("late", "b", Date.now(), { run: "run-2" });
493
+ }
494
+ };
495
+
496
+ await runClaimed();
497
+
498
+ expect(emits).toHaveLength(0);
499
+ expect(pending()).toHaveLength(1);
500
+ // Chronological, although the sibling held the later entry first.
501
+ expect(entryIds(pending()[0]!)).toEqual(["e1", "late"]);
502
+ // The merged row keeps the burst's earliest stamp, so its cap is not
503
+ // pushed out by the late entry.
504
+ expect(
505
+ parseSkillUpdateReceiptPayload(pending()[0]!.payload)?.firstEntryAt,
506
+ ).toBe(start);
507
+ });
508
+
509
+ test("an entry after the announcement opens the next receipt", async () => {
510
+ record("e1", "a", Date.now() - SKILL_UPDATE_RECEIPT_QUIET_MS - 10);
511
+ await runClaimed();
512
+
513
+ record("e2", "b", Date.now());
514
+
515
+ expect(emits).toHaveLength(1);
516
+ expect(entryIds(pending()[0]!)).toEqual(["e2"]);
517
+ });
518
+
519
+ test("a failed pipeline is reported retryable and keeps the row", async () => {
520
+ record("e1", "a", Date.now() - SKILL_UPDATE_RECEIPT_QUIET_MS - 10);
521
+ emitOutcomes = [{ pipelineFailed: true, reason: "boom" }];
522
+
523
+ await runClaimed();
524
+
525
+ expect(emits).toHaveLength(1);
526
+ expect(pending()).toHaveLength(1);
527
+ expect(entryIds(pending()[0]!)).toEqual(["e1"]);
528
+ });
529
+
530
+ test("a malformed payload is dropped", async () => {
531
+ getMemoryDb()!
532
+ .insert(memoryJobs)
533
+ .values({
534
+ id: "bad",
535
+ type: "skill_update_receipt",
536
+ payload: JSON.stringify({ nope: true }),
537
+ status: "pending",
538
+ attempts: 0,
539
+ deferrals: 0,
540
+ runAfter: 0,
541
+ lastError: null,
542
+ createdAt: 0,
543
+ updatedAt: 0,
544
+ })
545
+ .run();
546
+
547
+ await runClaimed();
548
+
549
+ expect(emits).toHaveLength(0);
550
+ expect(pending()).toHaveLength(0);
551
+ });
552
+ });
553
+
554
+ describe("removeSkillUpdateReceiptEntriesForConversation", () => {
555
+ test("drops a deleted source conversation's entries, recomputes the bounds, and deletes an emptied row", () => {
556
+ record("e1", "a", 1_000, { source: "conv-gone" });
557
+ record("e2", "b", 2_000, { source: "conv-kept" });
558
+ record("e3", "c", 3_000, { source: "conv-gone" });
559
+
560
+ removeSkillUpdateReceiptEntriesForConversation("conv-gone");
561
+
562
+ expect(entryIds(pending()[0]!)).toEqual(["e2"]);
563
+ // Both edge entries went, so the burst's window is now e2's alone.
564
+ expect(parseSkillUpdateReceiptPayload(pending()[0]!.payload)).toMatchObject(
565
+ { firstEntryAt: 2_000, lastEntryAt: 2_000 },
566
+ );
567
+
568
+ removeSkillUpdateReceiptEntriesForConversation("conv-kept");
569
+ expect(rows()).toHaveLength(0);
570
+ });
571
+
572
+ test("a garbage-collected run conversation is not a purge key: its entries stay", () => {
573
+ record("e1", "a", 1, { run: "run-gc" });
574
+ record("e2", "b", 2, { run: "run-2" });
575
+
576
+ removeSkillUpdateReceiptEntriesForConversation("run-gc");
577
+
578
+ expect(entryIds(pending()[0]!)).toEqual(["e1", "e2"]);
579
+ });
580
+ });
@@ -0,0 +1,21 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ import { shouldRetrieveTurnMemory } from "../turn-retrieval.js";
4
+
5
+ test("fresh retrieval defaults on, except for front-door and explicitly skipped turns", () => {
6
+ expect(shouldRetrieveTurnMemory({})).toBe(true);
7
+ expect(shouldRetrieveTurnMemory({ callSite: "callAgent" })).toBe(true);
8
+ expect(shouldRetrieveTurnMemory({ callSite: "voiceFrontDoor" })).toBe(false);
9
+ expect(
10
+ shouldRetrieveTurnMemory({
11
+ callSite: "callAgent",
12
+ skipMemoryRetrieval: true,
13
+ }),
14
+ ).toBe(false);
15
+ expect(
16
+ shouldRetrieveTurnMemory({
17
+ callSite: "callAgent",
18
+ skipMemoryRetrieval: false,
19
+ }),
20
+ ).toBe(true);
21
+ });
@@ -20,13 +20,17 @@ export const graphRecallDefinition = {
20
20
  } satisfies ToolDefinition;
21
21
 
22
22
  /**
23
- * `remember` tool description. The retrospective pass catches what isn't
24
- * captured in the moment, so the in-conversation pressure stays at a
25
- * judgment framing: pause when something feels worth marking, not because
26
- * the volume is required.
23
+ * `remember` tool description. The in-conversation pressure stays at a
24
+ * judgment framing (pause when something feels worth marking, not because
25
+ * the volume is required), but later capture is conditional and the model
26
+ * must not rely on it: the retrospective enqueue gate skips `scheduled`
27
+ * conversations outright (`isLowYieldRetrospectiveSource` in
28
+ * `memory-retrospective-enqueue.ts`) and the substrate sweep excludes
29
+ * `scheduled` and `background` types, so a durable fact surfaced by
30
+ * automated work survives only if it is saved inline.
27
31
  */
28
32
  const REMEMBER_DESCRIPTION =
29
- "Remember anything concrete shared in conversation: corrections, plans, decisions, felt moments, names, dates, commitments, preferences. Corrections are the highest priority — call `remember` the same turn the correction lands. You don't have to call this on every turn; a retrospective pass reviews the conversation after each message-count / time interval and saves what you didn't capture. Use judgment: pause and remember when something feels worth marking, not because the volume is required.";
33
+ "Remember anything concrete shared in conversation: corrections, plans, decisions, felt moments, names, dates, commitments, preferences. Corrections are the highest priority: call `remember` the same turn the correction lands. A later pass may review an ordinary user conversation and save what you did not capture, but it is not guaranteed and never runs over scheduled work. If a fact must survive, especially in proactive, scheduled, or other automated work, save it now. Use judgment: pause and remember when something feels worth marking, not because the volume is required.";
30
34
 
31
35
  const REMEMBER_CONTENT_DESCRIPTION =
32
36
  "The fact(s) to remember. Pass a single string for one fact, or an array of strings to record several independent facts in one call. When a turn surfaces multiple unrelated facts, pass them all as an array in one call rather than calling `remember` once per fact. Write naturally — a preference, a detail, a commitment, a plan. No need to categorize.";
@@ -25,6 +25,7 @@ import type {
25
25
  HookFunction,
26
26
  } from "@vellumai/plugin-api";
27
27
 
28
+ import { removeSkillUpdateReceiptEntriesForConversation } from "../../../../persistence/jobs-store.js";
28
29
  import { purgeConversationMemoryTables } from "../conversation-memory-purge.js";
29
30
  import { memoryJobHandlers } from "../job-handlers.js";
30
31
  import { cancelPendingJobsForConversation } from "../task-memory-cleanup.js";
@@ -38,6 +39,9 @@ const conversationDeleted: HookFunction<ConversationDeletedContext> = async (
38
39
  ) => {
39
40
  cancelPendingJobsForConversation(ctx.conversationId, MEMORY_JOB_TYPES);
40
41
  purgeConversationMemoryTables(ctx.conversationId);
42
+ // The pending receipt is keyed to a burst, not a conversation, so the
43
+ // cancel above never matches it; its entries name their source.
44
+ removeSkillUpdateReceiptEntriesForConversation(ctx.conversationId);
41
45
  };
42
46
 
43
47
  export default conversationDeleted;
@@ -56,6 +56,7 @@ import { broadcastMessage } from "../../../../runtime/assistant-event-hub.js";
56
56
  import type { GraphMemoryResult } from "../graph/conversation-graph-memory.js";
57
57
  import { recordMemoryRecallLog } from "../memory-recall-log-store.js";
58
58
  import { stripTailInjectionsForReinjection } from "../tail-reinjection-strip.js";
59
+ import { shouldRetrieveTurnMemory } from "../turn-retrieval.js";
59
60
  import { injectionMetadataUpdates } from "./injection-metadata.js";
60
61
 
61
62
  /**
@@ -276,14 +277,17 @@ const userPromptSubmitMemoryRetrieval: HookFunction<
276
277
  // fallback — a v3 empty/failed selection yields no NEW injected memory that
277
278
  // turn (prior turns' frozen v3 sections still ride history).
278
279
  const memoryV3Live = isMemoryV3Live(config);
279
- const isVoiceFrontDoor = conversation?.currentCallSite === "voiceFrontDoor";
280
- if (isVoiceFrontDoor && conversation) {
280
+ const retrieveMemory = shouldRetrieveTurnMemory({
281
+ callSite: conversation?.currentCallSite,
282
+ skipMemoryRetrieval: conversation?.currentTurnSkipMemoryRetrieval,
283
+ });
284
+ if (!retrieveMemory && conversation) {
281
285
  conversation.graphMemory.recordPkbQueryVectors(undefined, undefined);
282
286
  }
283
287
  let v2BlockPersisted = false;
284
288
  if (
285
289
  shouldRunLegacyMemoryRetrieval({ isTrustedActor, memoryV3Live }) &&
286
- !isVoiceFrontDoor &&
290
+ retrieveMemory &&
287
291
  conversation &&
288
292
  abortSignal
289
293
  ) {