@vellumai/assistant 0.6.0 → 0.6.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 (285) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. package/src/util/clipboard.ts +0 -34
@@ -28,7 +28,10 @@ import { handleUserMessageSignal } from "../signals/user-message.js";
28
28
  import { DebouncerMap } from "../util/debounce.js";
29
29
  import { getLogger } from "../util/logger.js";
30
30
  import {
31
+ AVATAR_IMAGE_FILENAME,
32
+ getAvatarDir,
31
33
  getSignalsDir,
34
+ getSoundsDir,
32
35
  getWorkspaceDir,
33
36
  getWorkspaceSkillsDir,
34
37
  } from "../util/platform.js";
@@ -110,7 +113,12 @@ export class ConfigWatcher {
110
113
  * files change and conversations need to be evicted for reload.
111
114
  * `onIdentityChanged` is called when IDENTITY.md changes on disk.
112
115
  */
113
- start(onConversationEvict: () => void, onIdentityChanged?: () => void): void {
116
+ start(
117
+ onConversationEvict: () => void,
118
+ onIdentityChanged?: () => void,
119
+ onSoundsConfigChanged?: () => void,
120
+ onAvatarChanged?: () => void,
121
+ ): void {
114
122
  const workspaceDir = getWorkspaceDir();
115
123
 
116
124
  const workspaceHandlers: Record<string, () => void> = {
@@ -175,6 +183,13 @@ export class ConfigWatcher {
175
183
  "workspace directory for config/prompt changes",
176
184
  );
177
185
 
186
+ if (onSoundsConfigChanged) {
187
+ this.startSoundsWatcher(onSoundsConfigChanged);
188
+ }
189
+ if (onAvatarChanged) {
190
+ this.startAvatarWatcher(onAvatarChanged);
191
+ }
192
+
178
193
  this.startFeatureFlagsWatcher();
179
194
  this.startSignalsWatcher();
180
195
  this.startSkillsWatchers(onConversationEvict);
@@ -188,6 +203,69 @@ export class ConfigWatcher {
188
203
  this.watchers = [];
189
204
  }
190
205
 
206
+ private startSoundsWatcher(onSoundsConfigChanged: () => void): void {
207
+ const soundsDir = getSoundsDir();
208
+ try {
209
+ if (!existsSync(soundsDir)) {
210
+ mkdirSync(soundsDir, { recursive: true });
211
+ }
212
+ } catch {
213
+ // If we can't create it, watching will also fail — handled below.
214
+ }
215
+
216
+ try {
217
+ const watcher = watch(soundsDir, (_eventType, filename) => {
218
+ if (!filename) return;
219
+ this.debounceTimers.schedule("file:sounds", () => {
220
+ log.info(
221
+ { file: String(filename) },
222
+ "Sounds directory changed, notifying clients",
223
+ );
224
+ onSoundsConfigChanged();
225
+ });
226
+ });
227
+ this.watchers.push(watcher);
228
+ log.info({ dir: soundsDir }, "Watching sounds directory for changes");
229
+ } catch (err) {
230
+ log.warn(
231
+ { err, dir: soundsDir },
232
+ "Failed to watch sounds directory. Sound config changes will require a restart.",
233
+ );
234
+ }
235
+ }
236
+
237
+ private startAvatarWatcher(onAvatarChanged: () => void): void {
238
+ const avatarDir = getAvatarDir();
239
+ try {
240
+ if (!existsSync(avatarDir)) {
241
+ mkdirSync(avatarDir, { recursive: true });
242
+ }
243
+ } catch {
244
+ // If we can't create it, watching will also fail — handled below.
245
+ }
246
+
247
+ try {
248
+ const watcher = watch(avatarDir, (_eventType, filename) => {
249
+ if (!filename) return;
250
+ if (String(filename) !== AVATAR_IMAGE_FILENAME) return;
251
+ this.debounceTimers.schedule("file:avatar", () => {
252
+ log.info(
253
+ { file: String(filename) },
254
+ "Avatar image changed, notifying clients",
255
+ );
256
+ onAvatarChanged();
257
+ });
258
+ });
259
+ this.watchers.push(watcher);
260
+ log.info({ dir: avatarDir }, "Watching avatar directory for changes");
261
+ } catch (err) {
262
+ log.warn(
263
+ { err, dir: avatarDir },
264
+ "Failed to watch avatar directory. Avatar changes will require a restart.",
265
+ );
266
+ }
267
+ }
268
+
191
269
  private startFeatureFlagsWatcher(): void {
192
270
  const protectedDir = process.env.GATEWAY_SECURITY_DIR
193
271
  ? process.env.GATEWAY_SECURITY_DIR
@@ -27,6 +27,7 @@ import {
27
27
  } from "../memory/llm-request-log-store.js";
28
28
  import { backfillMemoryRecallLogMessageId } from "../memory/memory-recall-log-store.js";
29
29
  import type { ContentBlock, ImageContent } from "../providers/types.js";
30
+ import { ProviderError } from "../util/errors.js";
30
31
  import { getLogger } from "../util/logger.js";
31
32
  import type { DirectiveRequest } from "./assistant-attachments.js";
32
33
  import {
@@ -630,6 +631,25 @@ export function handleError(
630
631
  state.orderingErrorDetected = true;
631
632
  state.deferredOrderingError = event.error.message;
632
633
  } else {
634
+ if (classified.errorCategory === "provider_api_error") {
635
+ log.error(
636
+ {
637
+ conversationId: deps.ctx.conversationId,
638
+ errorCode: classified.code,
639
+ errorCategory: classified.errorCategory,
640
+ statusCode:
641
+ event.error instanceof ProviderError
642
+ ? event.error.statusCode
643
+ : undefined,
644
+ provider:
645
+ event.error instanceof ProviderError
646
+ ? event.error.provider
647
+ : undefined,
648
+ errorMessage: event.error.message,
649
+ },
650
+ "Provider rejected request with unclassified 4xx error",
651
+ );
652
+ }
633
653
  deps.onEvent(
634
654
  buildConversationErrorMessage(deps.ctx.conversationId, classified),
635
655
  );
@@ -44,10 +44,7 @@ import {
44
44
  updateConversationTitle,
45
45
  updateMessageMetadata,
46
46
  } from "../memory/conversation-crud.js";
47
- import {
48
- rebuildConversationDiskViewFromDbState,
49
- syncMessageToDisk,
50
- } from "../memory/conversation-disk-view.js";
47
+ import { syncMessageToDisk } from "../memory/conversation-disk-view.js";
51
48
  import {
52
49
  isReplaceableTitle,
53
50
  queueGenerateConversationTitle,
@@ -91,30 +88,30 @@ import {
91
88
  classifyConversationError,
92
89
  isUserCancellation,
93
90
  } from "./conversation-error.js";
94
- import { consolidateAssistantMessages } from "./conversation-history.js";
95
91
  import { raceWithTimeout } from "./conversation-media-retry.js";
96
92
  import type { MessageQueue } from "./conversation-queue-manager.js";
97
93
  import type { QueueDrainReason } from "./conversation-queue-manager.js";
98
94
  import type {
99
95
  ActiveSurfaceContext,
100
96
  ChannelCapabilities,
101
- ChannelTurnContextParams,
102
97
  InboundActorContext,
103
98
  InjectionMode,
104
- InterfaceTurnContextParams,
105
99
  TrustContext,
106
100
  } from "./conversation-runtime-assembly.js";
107
101
  import {
108
102
  applyRuntimeInjections,
103
+ buildUnifiedTurnContextBlock,
104
+ findLastInjectedNowContent,
109
105
  inboundActorContextFromTrust,
110
106
  inboundActorContextFromTrustContext,
111
107
  readNowScratchpad,
112
- stripInjectedContext,
108
+ readPkbContext,
109
+ stripInjectionsForCompaction,
113
110
  } from "./conversation-runtime-assembly.js";
114
111
  import type { SkillProjectionCache } from "./conversation-skill-tools.js";
115
112
  import { resolveTrustClass } from "./conversation-tool-setup.js";
116
113
  import { recordUsage } from "./conversation-usage.js";
117
- import { buildTemporalContext } from "./date-context.js";
114
+ import { formatTurnTimestamp } from "./date-context.js";
118
115
  import { deepRepairHistory, repairHistory } from "./history-repair.js";
119
116
  import type {
120
117
  DynamicPageSurfaceData,
@@ -123,6 +120,7 @@ import type {
123
120
  SurfaceType,
124
121
  UsageStats,
125
122
  } from "./message-protocol.js";
123
+ import type { MemoryRecalled } from "./message-types/memory.js";
126
124
  import type { TraceEmitter } from "./trace-emitter.js";
127
125
 
128
126
  const log = getLogger("conversation-agent-loop");
@@ -502,6 +500,7 @@ export async function runAgentLoopImpl(
502
500
  }
503
501
 
504
502
  const isFirstMessage = ctx.messages.length === 1;
503
+ let shouldInjectWorkspace = isFirstMessage;
505
504
 
506
505
  const compactCheck = ctx.contextWindowManager.shouldCompact(ctx.messages);
507
506
  if (compactCheck.needed) {
@@ -556,6 +555,7 @@ export async function runAgentLoopImpl(
556
555
  compacted.summaryCacheReadInputTokens ?? 0,
557
556
  collapseRawResponses(compacted.summaryRawResponses),
558
557
  );
558
+ shouldInjectWorkspace = true;
559
559
  }
560
560
 
561
561
  const state = createEventHandlerState();
@@ -599,8 +599,8 @@ export async function runAgentLoopImpl(
599
599
 
600
600
  let runMessages = ctx.messages;
601
601
 
602
- // Memory graph retrieval — dispatches to context-load / per-turn / refresh
603
- // based on conversation state.
602
+ // Memory graph retrieval — dispatches to context-load / per-turn based on
603
+ // conversation state.
604
604
  const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
605
605
  if (isTrustedActor) {
606
606
  const graphResult = await ctx.graphMemory.prepareMemory(
@@ -627,26 +627,65 @@ export async function runAgentLoopImpl(
627
627
  }
628
628
  }
629
629
 
630
+ const m = graphResult.metrics;
631
+
630
632
  try {
631
633
  recordMemoryRecallLog({
632
634
  conversationId: ctx.conversationId,
633
635
  enabled: true,
634
636
  degraded: false,
635
- semanticHits: 0,
636
- mergedCount: 0,
637
- selectedCount: 0,
638
- tier1Count: 0,
639
- tier2Count: 0,
640
- hybridSearchLatencyMs: 0,
641
- sparseVectorUsed: false,
637
+ provider: m?.embeddingProvider ?? undefined,
638
+ model: m?.embeddingModel ?? undefined,
639
+ semanticHits: m?.semanticHits ?? 0,
640
+ mergedCount: m?.mergedCount ?? 0,
641
+ selectedCount: m?.selectedCount ?? 0,
642
+ tier1Count: m?.tier1Count ?? 0,
643
+ tier2Count: m?.tier2Count ?? 0,
644
+ hybridSearchLatencyMs: m?.hybridSearchLatencyMs ?? 0,
645
+ sparseVectorUsed: m?.sparseVectorUsed ?? false,
642
646
  injectedTokens: graphResult.injectedTokens,
643
647
  latencyMs: graphResult.latencyMs,
644
- topCandidatesJson: [],
648
+ topCandidatesJson: (m?.topCandidates ?? []).map((c) => ({
649
+ key: c.nodeId,
650
+ type: c.type,
651
+ kind: "graph",
652
+ finalScore: c.score,
653
+ semantic: c.semanticSimilarity,
654
+ recency: c.recencyBoost,
655
+ })),
656
+ injectedText: graphResult.injectedBlockText ?? undefined,
645
657
  reason: `graph:${graphResult.mode}`,
658
+ queryContext: m?.queryContext ?? undefined,
646
659
  });
647
660
  } catch (err) {
648
661
  log.warn({ err }, "Failed to persist memory recall log (non-fatal)");
649
662
  }
663
+
664
+ if (m) {
665
+ const memoryRecalledEvent: MemoryRecalled = {
666
+ type: "memory_recalled",
667
+ provider: m.embeddingProvider ?? "unknown",
668
+ model: m.embeddingModel ?? "unknown",
669
+ semanticHits: m.semanticHits,
670
+ mergedCount: m.mergedCount,
671
+ selectedCount: m.selectedCount,
672
+ tier1Count: m.tier1Count,
673
+ tier2Count: m.tier2Count,
674
+ hybridSearchLatencyMs: m.hybridSearchLatencyMs,
675
+ sparseVectorUsed: m.sparseVectorUsed,
676
+ injectedTokens: graphResult.injectedTokens,
677
+ latencyMs: graphResult.latencyMs,
678
+ topCandidates: m.topCandidates.map((c) => ({
679
+ key: c.nodeId,
680
+ type: c.type,
681
+ kind: "graph",
682
+ finalScore: c.score,
683
+ semantic: c.semanticSimilarity,
684
+ recency: c.recencyBoost,
685
+ })),
686
+ };
687
+ onEvent(memoryRecalledEvent);
688
+ }
650
689
  }
651
690
 
652
691
  // Build active surface context
@@ -678,37 +717,20 @@ export async function runAgentLoopImpl(
678
717
 
679
718
  ctx.refreshWorkspaceTopLevelContextIfNeeded();
680
719
 
681
- // Compute fresh temporal context each turn for date grounding.
720
+ // Compute fresh turn timestamp for date grounding.
682
721
  // Absolute "now" is always anchored to assistant host clock, while local
683
722
  // date semantics prefer configured user timezone, then recalled memory.
684
723
  const hostTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
685
724
  const configuredUserTimeZone = getConfig().ui.userTimezone ?? null;
686
725
  const recalledUserTimeZone = null;
687
- const temporalContext = buildTemporalContext({
726
+ const timestamp = formatTurnTimestamp({
688
727
  hostTimeZone,
689
728
  configuredUserTimeZone,
690
729
  userTimeZone: recalledUserTimeZone,
691
730
  });
692
731
 
693
- // Use the channel/interface context captured at the top of this function
694
- // so it reflects the channel/interface that originally sent *this* turn's
695
- // message, even if a newer message from a different channel arrived since.
696
- const channelTurnContext: ChannelTurnContextParams = {
697
- turnContext: capturedTurnChannelContext,
698
- conversationOriginChannel: getConversationOriginChannel(
699
- ctx.conversationId,
700
- ),
701
- };
702
-
703
- const interfaceTurnContext: InterfaceTurnContextParams = {
704
- turnContext: capturedTurnInterfaceContext,
705
- conversationOriginInterface: getConversationOriginInterface(
706
- ctx.conversationId,
707
- ),
708
- };
709
-
710
- // Resolve the inbound actor context for the model's <inbound_actor_context>
711
- // block. When the conversation carries enough identity info, use the unified
732
+ // Resolve the inbound actor context for the unified <turn_context> block.
733
+ // When the conversation carries enough identity info, use the unified
712
734
  // actor trust resolver so member status/policy and guardian binding details
713
735
  // are fresh for this turn. The conversation runtime context remains the source
714
736
  // for policy gating; this block is model-facing grounding metadata.
@@ -729,22 +751,53 @@ export async function runAgentLoopImpl(
729
751
  }
730
752
  }
731
753
 
754
+ // Build unified turn context block that replaces the separate temporal,
755
+ // channel, interface, and actor context blocks.
756
+ const interfaceName =
757
+ capturedTurnInterfaceContext.userMessageInterface ?? undefined;
758
+ const channelName =
759
+ capturedTurnChannelContext?.userMessageChannel ?? undefined;
760
+ const isGuardian =
761
+ resolvedInboundActorContext?.trustClass === "guardian" ||
762
+ !resolvedInboundActorContext;
763
+ const unifiedTurnContextStr = buildUnifiedTurnContextBlock(
764
+ isGuardian
765
+ ? { timestamp, interfaceName, channelName }
766
+ : {
767
+ timestamp,
768
+ interfaceName,
769
+ channelName,
770
+ actorContext: resolvedInboundActorContext,
771
+ },
772
+ );
773
+
732
774
  // The `remember` tool handles scratchpad-style memory writes directly to the graph.
733
775
 
734
776
  const isInteractiveResolved =
735
777
  options?.isInteractive ?? (!ctx.hasNoClient && !ctx.headlessLock);
736
778
 
779
+ // Only inject NOW.md if it changed since the last injection in the
780
+ // conversation. Keeping the previous injection in place avoids mutating
781
+ // historical user messages and preserves the cached prefix.
782
+ const currentNowContent = readNowScratchpad();
783
+ const lastInjectedNow = findLastInjectedNowContent(ctx.messages);
784
+ const nowScratchpad =
785
+ currentNowContent !== lastInjectedNow ? currentNowContent : null;
786
+
787
+ // Read PKB always-loaded files (INDEX, essentials, threads, buffer)
788
+ const currentPkbContent = readPkbContext();
789
+
737
790
  // Shared injection options — reused whenever we need to re-inject after reduction.
738
791
  const injectionOpts = {
739
792
  activeSurface,
740
- workspaceTopLevelContext: ctx.workspaceTopLevelContext,
793
+ workspaceTopLevelContext: shouldInjectWorkspace
794
+ ? ctx.workspaceTopLevelContext
795
+ : null,
741
796
  channelCapabilities: ctx.channelCapabilities ?? null,
742
797
  channelCommandContext: ctx.commandIntent ?? null,
743
- channelTurnContext,
744
- interfaceTurnContext,
745
- inboundActorContext: resolvedInboundActorContext,
746
- temporalContext,
747
- nowScratchpad: readNowScratchpad(),
798
+ unifiedTurnContext: unifiedTurnContextStr,
799
+ pkbContext: currentPkbContent,
800
+ nowScratchpad,
748
801
  voiceCallControlPrompt: ctx.voiceCallControlPrompt ?? null,
749
802
  transportHints: ctx.transportHints ?? null,
750
803
  isNonInteractive: !isInteractiveResolved,
@@ -860,11 +913,20 @@ export async function runAgentLoopImpl(
860
913
  ctx.graphMemory.onCompacted(
861
914
  step.compactionResult.compactedPersistedMessages,
862
915
  );
916
+ shouldInjectWorkspace = true;
863
917
  }
864
918
 
865
- // Re-inject with potentially downgraded injection mode
919
+ // Re-inject with potentially downgraded injection mode.
920
+ // When compaction ran it strips existing NOW.md / PKB blocks, so we
921
+ // must re-inject the current content. Otherwise rely on the deduplicated
922
+ // value from injectionOpts to avoid duplicate injection.
866
923
  runMessages = applyRuntimeInjections(ctx.messages, {
867
924
  ...injectionOpts,
925
+ pkbContext: currentPkbContent,
926
+ ...(step.compactionResult?.compacted && { nowScratchpad: currentNowContent }),
927
+ workspaceTopLevelContext: shouldInjectWorkspace
928
+ ? ctx.workspaceTopLevelContext
929
+ : null,
868
930
  mode: currentInjectionMode,
869
931
  });
870
932
  if (isTrustedActor && currentInjectionMode !== "minimal") {
@@ -994,7 +1056,7 @@ export async function runAgentLoopImpl(
994
1056
 
995
1057
  // Strip injected context from updated history before compacting,
996
1058
  // so we compact the "raw" persistent messages.
997
- const rawHistory = stripInjectedContext(updatedHistory);
1059
+ const rawHistory = stripInjectionsForCompaction(updatedHistory);
998
1060
  ctx.messages = rawHistory;
999
1061
 
1000
1062
  ctx.emitActivityState(
@@ -1048,14 +1110,21 @@ export async function runAgentLoopImpl(
1048
1110
  midLoopCompact.summaryCacheReadInputTokens ?? 0,
1049
1111
  collapseRawResponses(midLoopCompact.summaryRawResponses),
1050
1112
  );
1051
- ctx.graphMemory.onCompacted(
1052
- midLoopCompact.compactedPersistedMessages,
1053
- );
1113
+ ctx.graphMemory.onCompacted(midLoopCompact.compactedPersistedMessages);
1114
+ shouldInjectWorkspace = true;
1054
1115
  }
1055
1116
 
1056
- // Re-inject runtime context and re-enter the agent loop
1117
+ // Re-inject runtime context and re-enter the agent loop.
1118
+ // stripInjectionsForCompaction() unconditionally removed the existing
1119
+ // NOW.md block from ctx.messages above, so we must always re-inject
1120
+ // the current content regardless of whether compaction actually ran.
1057
1121
  runMessages = applyRuntimeInjections(ctx.messages, {
1058
1122
  ...injectionOpts,
1123
+ pkbContext: currentPkbContent,
1124
+ nowScratchpad: currentNowContent,
1125
+ workspaceTopLevelContext: shouldInjectWorkspace
1126
+ ? ctx.workspaceTopLevelContext
1127
+ : null,
1059
1128
  mode: currentInjectionMode,
1060
1129
  });
1061
1130
  if (isTrustedActor && currentInjectionMode !== "minimal") {
@@ -1134,7 +1203,7 @@ export async function runAgentLoopImpl(
1134
1203
  // convergence loop operates on the full (larger) history.
1135
1204
  if (state.contextTooLargeDetected) {
1136
1205
  if (updatedHistory.length > preRunHistoryLength) {
1137
- ctx.messages = stripInjectedContext(updatedHistory);
1206
+ ctx.messages = stripInjectionsForCompaction(updatedHistory);
1138
1207
  preRepairMessages = updatedHistory;
1139
1208
  preRunHistoryLength = updatedHistory.length;
1140
1209
  }
@@ -1254,10 +1323,18 @@ export async function runAgentLoopImpl(
1254
1323
  ctx.graphMemory.onCompacted(
1255
1324
  step.compactionResult.compactedPersistedMessages,
1256
1325
  );
1326
+ shouldInjectWorkspace = true;
1257
1327
  }
1258
1328
 
1329
+ // ctx.messages has been stripped (line 1206/1373) so NOW.md must
1330
+ // always be re-injected regardless of whether compaction ran.
1259
1331
  runMessages = applyRuntimeInjections(ctx.messages, {
1260
1332
  ...injectionOpts,
1333
+ pkbContext: currentPkbContent,
1334
+ nowScratchpad: currentNowContent,
1335
+ workspaceTopLevelContext: shouldInjectWorkspace
1336
+ ? ctx.workspaceTopLevelContext
1337
+ : null,
1261
1338
  mode: currentInjectionMode,
1262
1339
  });
1263
1340
  if (isTrustedActor && currentInjectionMode !== "minimal") {
@@ -1295,7 +1372,7 @@ export async function runAgentLoopImpl(
1295
1372
  // tier operates on up-to-date history instead of stale
1296
1373
  // pre-rerun messages.
1297
1374
  if (updatedHistory.length > preRunHistoryLength) {
1298
- ctx.messages = stripInjectedContext(updatedHistory);
1375
+ ctx.messages = stripInjectionsForCompaction(updatedHistory);
1299
1376
  preRepairMessages = updatedHistory;
1300
1377
  preRunHistoryLength = updatedHistory.length;
1301
1378
  }
@@ -1368,14 +1445,23 @@ export async function runAgentLoopImpl(
1368
1445
  ctx.graphMemory.onCompacted(
1369
1446
  emergencyCompact.compactedPersistedMessages,
1370
1447
  );
1448
+ shouldInjectWorkspace = true;
1371
1449
  }
1372
1450
 
1451
+ // ctx.messages was already stripped before the convergence
1452
+ // loop, so NOW.md must always be re-injected here.
1373
1453
  runMessages = applyRuntimeInjections(ctx.messages, {
1374
1454
  ...injectionOpts,
1455
+ pkbContext: currentPkbContent,
1456
+ nowScratchpad: currentNowContent,
1457
+ workspaceTopLevelContext: shouldInjectWorkspace
1458
+ ? ctx.workspaceTopLevelContext
1459
+ : null,
1375
1460
  mode: currentInjectionMode,
1376
1461
  });
1377
1462
  if (isTrustedActor && currentInjectionMode !== "minimal") {
1378
- const memResult = ctx.graphMemory.reinjectCachedMemory(runMessages);
1463
+ const memResult =
1464
+ ctx.graphMemory.reinjectCachedMemory(runMessages);
1379
1465
  runMessages = memResult.runMessages;
1380
1466
  }
1381
1467
  preRepairMessages = runMessages;
@@ -1479,10 +1565,18 @@ export async function runAgentLoopImpl(
1479
1565
  ctx.graphMemory.onCompacted(
1480
1566
  emergencyCompact.compactedPersistedMessages,
1481
1567
  );
1568
+ shouldInjectWorkspace = true;
1482
1569
  }
1483
1570
 
1571
+ // ctx.messages was already stripped before the convergence
1572
+ // loop, so NOW.md must always be re-injected here.
1484
1573
  runMessages = applyRuntimeInjections(ctx.messages, {
1485
1574
  ...injectionOpts,
1575
+ pkbContext: currentPkbContent,
1576
+ nowScratchpad: currentNowContent,
1577
+ workspaceTopLevelContext: shouldInjectWorkspace
1578
+ ? ctx.workspaceTopLevelContext
1579
+ : null,
1486
1580
  mode: currentInjectionMode,
1487
1581
  });
1488
1582
  if (isTrustedActor && currentInjectionMode !== "minimal") {
@@ -1626,7 +1720,11 @@ export async function runAgentLoopImpl(
1626
1720
  { providerName: ctx.provider.name, toolTokenBudget },
1627
1721
  );
1628
1722
 
1629
- ctx.messages = stripInjectedContext(restoredHistory);
1723
+ // Persist injections in history: runtime-injected context stays on
1724
+ // historical user messages so the conversation prefix is stable for
1725
+ // Anthropic's prefix caching. Stripping only happens during
1726
+ // compaction/overflow recovery (where a cache miss is expected).
1727
+ ctx.messages = restoredHistory;
1630
1728
 
1631
1729
  emitUsage(
1632
1730
  ctx,
@@ -1877,15 +1975,10 @@ export async function runAgentLoopImpl(
1877
1975
  // Clear at turn end so they never leak into subsequent unrelated messages.
1878
1976
  ctx.commandIntent = undefined;
1879
1977
 
1880
- if (userMessageId) {
1881
- const didMutateHistory = consolidateAssistantMessages(
1882
- ctx.conversationId,
1883
- userMessageId,
1884
- );
1885
- if (didMutateHistory) {
1886
- rebuildConversationDiskViewFromDbState(ctx.conversationId);
1887
- }
1888
- }
1978
+ // Consolidation deferred to compaction: keeping assistant + tool_result
1979
+ // messages unconsolidated preserves the exact message structure sent to
1980
+ // the API, enabling stable prefix caching across turns. Compaction
1981
+ // consolidates when it summarizes old messages (cache miss is expected).
1889
1982
 
1890
1983
  ctx.drainQueue(yieldedForHandoff ? "checkpoint_handoff" : "loop_complete");
1891
1984
 
@@ -68,14 +68,13 @@ export function findLastUndoableUserMessageIndex(messages: Message[]): number {
68
68
  // ── Qdrant Vector Cleanup ────────────────────────────────────────────
69
69
 
70
70
  /**
71
- * Delete Qdrant vector entries for the given segment and item IDs.
71
+ * Delete Qdrant vector entries for the given segment IDs.
72
72
  * Individual deletion failures are logged and enqueued as retry jobs
73
73
  * to prevent silently orphaned vectors.
74
74
  */
75
75
  export async function cleanupQdrantVectors(
76
76
  conversationId: string,
77
77
  segmentIds: string[],
78
- orphanedItemIds: string[],
79
78
  ): Promise<void> {
80
79
  let qdrant: ReturnType<typeof getQdrantClient>;
81
80
  try {
@@ -84,15 +83,12 @@ export async function cleanupQdrantVectors(
84
83
  return; // Qdrant not initialized — nothing to clean up.
85
84
  }
86
85
 
87
- if (segmentIds.length === 0 && orphanedItemIds.length === 0) return;
86
+ if (segmentIds.length === 0) return;
88
87
 
89
88
  const targets: Array<{ targetType: string; targetId: string }> = [];
90
89
  for (const segId of segmentIds) {
91
90
  targets.push({ targetType: "segment", targetId: segId });
92
91
  }
93
- for (const itemId of orphanedItemIds) {
94
- targets.push({ targetType: "item", targetId: itemId });
95
- }
96
92
 
97
93
  const results = await Promise.allSettled(
98
94
  targets.map((t) =>
@@ -124,7 +120,6 @@ export async function cleanupQdrantVectors(
124
120
  succeeded,
125
121
  failed,
126
122
  segments: segmentIds.length,
127
- items: orphanedItemIds.length,
128
123
  },
129
124
  "Cleaned up Qdrant vectors after regenerate",
130
125
  );
@@ -187,20 +182,17 @@ export function consolidateAssistantMessages(
187
182
  // Still delete internal tool_result messages even if only one assistant message,
188
183
  // and collect IDs for vector cleanup
189
184
  const allSegmentIds: string[] = [];
190
- const allOrphanedItemIds: string[] = [];
191
185
  for (const id of messagesToDelete) {
192
186
  const deleted = deleteMessageById(id);
193
187
  didMutate = true;
194
188
  allSegmentIds.push(...deleted.segmentIds);
195
- allOrphanedItemIds.push(...deleted.orphanedItemIds);
196
189
  }
197
190
 
198
191
  // Clean up Qdrant vectors (fire-and-forget)
199
- if (allSegmentIds.length > 0 || allOrphanedItemIds.length > 0) {
192
+ if (allSegmentIds.length > 0) {
200
193
  cleanupQdrantVectors(
201
194
  conversationId,
202
195
  allSegmentIds,
203
- allOrphanedItemIds,
204
196
  ).catch((err) => {
205
197
  log.warn(
206
198
  { err, conversationId },
@@ -322,24 +314,20 @@ export function consolidateAssistantMessages(
322
314
  // Delete the other assistant messages and internal tool_result messages,
323
315
  // and collect IDs for vector cleanup
324
316
  const allSegmentIds: string[] = [];
325
- const allOrphanedItemIds: string[] = [];
326
317
  for (let i = 1; i < messagesToConsolidate.length; i++) {
327
318
  const deleted = deleteMessageById(messagesToConsolidate[i].id);
328
319
  allSegmentIds.push(...deleted.segmentIds);
329
- allOrphanedItemIds.push(...deleted.orphanedItemIds);
330
320
  }
331
321
  for (const id of messagesToDelete) {
332
322
  const deleted = deleteMessageById(id);
333
323
  allSegmentIds.push(...deleted.segmentIds);
334
- allOrphanedItemIds.push(...deleted.orphanedItemIds);
335
324
  }
336
325
 
337
326
  // Clean up Qdrant vectors (fire-and-forget)
338
- if (allSegmentIds.length > 0 || allOrphanedItemIds.length > 0) {
327
+ if (allSegmentIds.length > 0) {
339
328
  cleanupQdrantVectors(
340
329
  conversationId,
341
330
  allSegmentIds,
342
- allOrphanedItemIds,
343
331
  ).catch((err) => {
344
332
  log.warn(
345
333
  { err, conversationId },
@@ -539,18 +527,15 @@ export async function regenerate(
539
527
 
540
528
  // Delete each message via deleteMessageById and collect IDs for Qdrant cleanup.
541
529
  const allSegmentIds: string[] = [];
542
- const allOrphanedItemIds: string[] = [];
543
530
  for (const msg of messagesToDelete) {
544
531
  const deleted = deleteMessageById(msg.id);
545
532
  allSegmentIds.push(...deleted.segmentIds);
546
- allOrphanedItemIds.push(...deleted.orphanedItemIds);
547
533
  }
548
534
 
549
535
  // Clean up Qdrant vectors (fire-and-forget).
550
536
  cleanupQdrantVectors(
551
537
  conversation.conversationId,
552
538
  allSegmentIds,
553
- allOrphanedItemIds,
554
539
  ).catch((err) => {
555
540
  log.warn(
556
541
  { err, conversationId: conversation.conversationId },