@vellumai/assistant 0.4.49 → 0.4.50

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 (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -12,6 +12,8 @@ export interface ClassifiedSessionError {
12
12
  userMessage: string;
13
13
  retryable: boolean;
14
14
  debugDetails?: string;
15
+ /** Machine-readable error category for log report metadata and triage. */
16
+ errorCategory: string;
15
17
  }
16
18
 
17
19
  // Network-level error patterns (connection refused, timeout, DNS, reset)
@@ -68,6 +70,21 @@ const PROVIDER_API_PATTERNS = [
68
70
  /gateway timeout/i,
69
71
  ];
70
72
 
73
+ // Provider ordering error patterns (tool_use/tool_result mismatches)
74
+ const ORDERING_ERROR_PATTERNS = [
75
+ /tool_result.*not immediately after.*tool_use/i,
76
+ /tool_use.*must have.*tool_result/i,
77
+ /tool_use_id.*without.*tool_result/i,
78
+ /tool_result.*tool_use_id.*not found/i,
79
+ /messages.*invalid.*order/i,
80
+ ];
81
+
82
+ // Web-search-specific ordering error patterns
83
+ const WEB_SEARCH_ORDERING_PATTERNS = [
84
+ /web_search.*tool_use.*without/i,
85
+ /web_search.*tool_result/i,
86
+ ];
87
+
71
88
  // User-initiated cancellation patterns — these should NOT produce session_error
72
89
  const CANCEL_PATTERNS = [/abort/i, /cancel/i];
73
90
 
@@ -131,6 +148,7 @@ export function classifySessionError(
131
148
  userMessage: `Could not regenerate the response. ${base.userMessage}`,
132
149
  retryable: true,
133
150
  debugDetails,
151
+ errorCategory: `regenerate:${base.errorCategory}`,
134
152
  };
135
153
  }
136
154
 
@@ -155,8 +173,10 @@ function classifyCore(
155
173
  if (error.statusCode === 413) {
156
174
  return {
157
175
  code: "CONTEXT_TOO_LARGE",
158
- userMessage: "This conversation exceeds the model's context limit.",
176
+ userMessage:
177
+ "This conversation is too long. Please start a new thread.",
159
178
  retryable: false,
179
+ errorCategory: "context_too_large",
160
180
  };
161
181
  }
162
182
  if (error.statusCode === 401) {
@@ -164,13 +184,15 @@ function classifyCore(
164
184
  code: "PROVIDER_BILLING",
165
185
  userMessage: "Your API key is invalid or expired.",
166
186
  retryable: false,
187
+ errorCategory: "provider_billing",
167
188
  };
168
189
  }
169
190
  if (error.statusCode === 429) {
170
191
  return {
171
192
  code: "PROVIDER_RATE_LIMIT",
172
- userMessage: "The AI provider is rate limiting requests.",
193
+ userMessage: "The AI provider is busy. Please try again in a moment.",
173
194
  retryable: true,
195
+ errorCategory: "rate_limit",
174
196
  };
175
197
  }
176
198
  if (error.statusCode >= 500) {
@@ -178,15 +200,35 @@ function classifyCore(
178
200
  code: "PROVIDER_API",
179
201
  userMessage: "The AI provider returned a server error.",
180
202
  retryable: true,
203
+ errorCategory: "provider_server_error",
181
204
  };
182
205
  }
183
- // 4xx (non-429) — check for context-too-large before generic fallback
206
+ // 4xx (non-429) — check for context-too-large, ordering errors, then generic fallback
184
207
  if (error.statusCode >= 400) {
185
208
  if (isContextTooLarge(message)) {
186
209
  return {
187
210
  code: "CONTEXT_TOO_LARGE",
188
- userMessage: "This conversation exceeds the model's context limit.",
211
+ userMessage:
212
+ "This conversation is too long. Please start a new thread.",
189
213
  retryable: false,
214
+ errorCategory: "context_too_large",
215
+ };
216
+ }
217
+ if (isWebSearchOrderingError(message)) {
218
+ return {
219
+ code: "PROVIDER_WEB_SEARCH",
220
+ userMessage:
221
+ "An internal error occurred with web search. Retrying...",
222
+ retryable: true,
223
+ errorCategory: "web_search_ordering",
224
+ };
225
+ }
226
+ if (isOrderingError(message)) {
227
+ return {
228
+ code: "PROVIDER_ORDERING",
229
+ userMessage: "An internal error occurred. Retrying...",
230
+ retryable: true,
231
+ errorCategory: "tool_ordering",
190
232
  };
191
233
  }
192
234
  if (/credit balance is too low|insufficient.*credits?/i.test(message)) {
@@ -194,6 +236,7 @@ function classifyCore(
194
236
  code: "PROVIDER_BILLING",
195
237
  userMessage: "Your API key has insufficient credits.",
196
238
  retryable: false,
239
+ errorCategory: "provider_billing",
197
240
  };
198
241
  }
199
242
  if (
@@ -205,12 +248,14 @@ function classifyCore(
205
248
  code: "PROVIDER_BILLING",
206
249
  userMessage: "Your API key is invalid.",
207
250
  retryable: false,
251
+ errorCategory: "provider_billing",
208
252
  };
209
253
  }
210
254
  return {
211
255
  code: "PROVIDER_API",
212
256
  userMessage: "The AI provider rejected the request.",
213
257
  retryable: true,
258
+ errorCategory: "provider_api_error",
214
259
  };
215
260
  }
216
261
  }
@@ -224,6 +269,16 @@ export function isContextTooLarge(message: string): boolean {
224
269
  return CONTEXT_TOO_LARGE_PATTERNS.some((p) => p.test(message));
225
270
  }
226
271
 
272
+ /** Check whether an error message indicates a web-search-specific ordering failure. */
273
+ export function isWebSearchOrderingError(message: string): boolean {
274
+ return WEB_SEARCH_ORDERING_PATTERNS.some((p) => p.test(message));
275
+ }
276
+
277
+ /** Check whether an error message indicates a tool_use/tool_result ordering failure. */
278
+ export function isOrderingError(message: string): boolean {
279
+ return ORDERING_ERROR_PATTERNS.some((p) => p.test(message));
280
+ }
281
+
227
282
  function classifyByMessage(
228
283
  message: string,
229
284
  ): Omit<ClassifiedSessionError, "debugDetails"> {
@@ -231,8 +286,9 @@ function classifyByMessage(
231
286
  if (isContextTooLarge(message)) {
232
287
  return {
233
288
  code: "CONTEXT_TOO_LARGE",
234
- userMessage: "This conversation exceeds the model's context limit.",
289
+ userMessage: "This conversation is too long. Please start a new thread.",
235
290
  retryable: false,
291
+ errorCategory: "context_too_large",
236
292
  };
237
293
  }
238
294
 
@@ -241,12 +297,33 @@ function classifyByMessage(
241
297
  if (pattern.test(message)) {
242
298
  return {
243
299
  code: "PROVIDER_RATE_LIMIT",
244
- userMessage: "The AI provider is rate limiting requests.",
300
+ userMessage: "The AI provider is busy. Please try again in a moment.",
245
301
  retryable: true,
302
+ errorCategory: "rate_limit",
246
303
  };
247
304
  }
248
305
  }
249
306
 
307
+ // Web-search ordering errors (before general ordering errors)
308
+ if (isWebSearchOrderingError(message)) {
309
+ return {
310
+ code: "PROVIDER_WEB_SEARCH",
311
+ userMessage: "An internal error occurred with web search. Retrying...",
312
+ retryable: true,
313
+ errorCategory: "web_search_ordering",
314
+ };
315
+ }
316
+
317
+ // General tool_use/tool_result ordering errors
318
+ if (isOrderingError(message)) {
319
+ return {
320
+ code: "PROVIDER_ORDERING",
321
+ userMessage: "An internal error occurred. Retrying...",
322
+ retryable: true,
323
+ errorCategory: "tool_ordering",
324
+ };
325
+ }
326
+
250
327
  // Network errors (before timeout so "connection timeout" is classified as network)
251
328
  for (const pattern of NETWORK_PATTERNS) {
252
329
  if (pattern.test(message)) {
@@ -254,6 +331,7 @@ function classifyByMessage(
254
331
  code: "PROVIDER_NETWORK",
255
332
  userMessage: "Could not connect to the AI provider.",
256
333
  retryable: true,
334
+ errorCategory: "provider_network",
257
335
  };
258
336
  }
259
337
  }
@@ -265,6 +343,7 @@ function classifyByMessage(
265
343
  code: "PROVIDER_API",
266
344
  userMessage: "The AI provider returned a server error.",
267
345
  retryable: true,
346
+ errorCategory: "provider_server_error",
268
347
  };
269
348
  }
270
349
  }
@@ -277,6 +356,7 @@ function classifyByMessage(
277
356
  code: "PROVIDER_API",
278
357
  userMessage: "The request to the AI provider timed out.",
279
358
  retryable: true,
359
+ errorCategory: "provider_timeout",
280
360
  };
281
361
  }
282
362
  }
@@ -288,6 +368,7 @@ function classifyByMessage(
288
368
  code: "SESSION_ABORTED",
289
369
  userMessage: "The request was interrupted.",
290
370
  retryable: true,
371
+ errorCategory: "session_aborted",
291
372
  };
292
373
  }
293
374
  }
@@ -308,6 +389,7 @@ function classifyByMessage(
308
389
  code: "SESSION_PROCESSING_FAILED",
309
390
  userMessage,
310
391
  retryable: false,
392
+ errorCategory: "processing_failed",
311
393
  };
312
394
  }
313
395
 
@@ -325,5 +407,6 @@ export function buildSessionErrorMessage(
325
407
  userMessage: classified.userMessage,
326
408
  retryable: classified.retryable,
327
409
  debugDetails: classified.debugDetails,
410
+ errorCategory: classified.errorCategory,
328
411
  };
329
412
  }
@@ -21,6 +21,14 @@ const log = getLogger("session-history");
21
21
 
22
22
  // ── Helpers ──────────────────────────────────────────────────────────
23
23
 
24
+ function isToolResultBlock(
25
+ block: ContentBlock | Record<string, unknown>,
26
+ ): boolean {
27
+ return (
28
+ block.type === "tool_result" || block.type === "web_search_tool_result"
29
+ );
30
+ }
31
+
24
32
  function isUndoableUserMessage(message: Message): boolean {
25
33
  if (message.role !== "user") return false;
26
34
  if (getSummaryFromContextMessage(message) != null) return false;
@@ -30,7 +38,7 @@ function isUndoableUserMessage(message: Message): boolean {
30
38
  // (e.g. after repairHistory merges a tool_result turn with a user prompt) are still
31
39
  // undoable because they contain real user content.
32
40
  const hasNonToolResultContent = message.content.some(
33
- (block) => block.type !== "tool_result",
41
+ (block) => !isToolResultBlock(block),
34
42
  );
35
43
  if (!hasNonToolResultContent) return false;
36
44
  return true;
@@ -143,7 +151,9 @@ export function consolidateAssistantMessages(
143
151
  const content = JSON.parse(msg.content);
144
152
  const isToolResultOnly =
145
153
  Array.isArray(content) &&
146
- content.every((block) => block.type === "tool_result") &&
154
+ content.every((block: Record<string, unknown>) =>
155
+ isToolResultBlock(block),
156
+ ) &&
147
157
  content.length > 0;
148
158
  if (isToolResultOnly) {
149
159
  internalToolResultMessages.push(msg);
@@ -229,8 +239,8 @@ export function consolidateAssistantMessages(
229
239
  try {
230
240
  const content = JSON.parse(msg.content);
231
241
  if (Array.isArray(content)) {
232
- const toolResultBlocks = content.filter(
233
- (b: Record<string, unknown>) => b.type === "tool_result",
242
+ const toolResultBlocks = content.filter((b: Record<string, unknown>) =>
243
+ isToolResultBlock(b),
234
244
  );
235
245
  log.info(
236
246
  {
@@ -253,8 +263,8 @@ export function consolidateAssistantMessages(
253
263
  const toolUseBlocksInConsolidated = consolidatedContent.filter(
254
264
  (b) => b.type === "tool_use",
255
265
  ).length;
256
- const toolResultBlocksInConsolidated = consolidatedContent.filter(
257
- (b) => b.type === "tool_result",
266
+ const toolResultBlocksInConsolidated = consolidatedContent.filter((b) =>
267
+ isToolResultBlock(b),
258
268
  ).length;
259
269
  log.info(
260
270
  {
@@ -471,7 +481,7 @@ export async function regenerate(
471
481
  if (
472
482
  Array.isArray(parsed) &&
473
483
  parsed.length > 0 &&
474
- parsed.every((b: Record<string, unknown>) => b.type === "tool_result")
484
+ parsed.every((b: Record<string, unknown>) => isToolResultBlock(b))
475
485
  ) {
476
486
  continue; // Skip tool_result-only user messages
477
487
  }
@@ -69,7 +69,7 @@ export function stripMediaPayloadsForRetry(messages: Message[]): {
69
69
  }
70
70
 
71
71
  if (
72
- block.type === "tool_result" &&
72
+ block.type === "tool_result" && // guard:allow-tool-result-only — web_search_tool_result has no contentBlocks
73
73
  block.contentBlocks &&
74
74
  block.contentBlocks.length > 0
75
75
  ) {
@@ -143,7 +143,10 @@ function fileBlockToStub(
143
143
  function isToolResultOnlyMessage(message: Message): boolean {
144
144
  return (
145
145
  message.content.length > 0 &&
146
- message.content.every((block) => block.type === "tool_result")
146
+ message.content.every(
147
+ (block) =>
148
+ block.type === "tool_result" || block.type === "web_search_tool_result",
149
+ )
147
150
  );
148
151
  }
149
152
 
@@ -158,6 +161,7 @@ export function countMediaBlocks(messages: Message[]): number {
158
161
  if (block.type === "image" || block.type === "file") {
159
162
  count++;
160
163
  } else if (block.type === "tool_result" && block.contentBlocks) {
164
+ // guard:allow-tool-result-only — web_search_tool_result has no contentBlocks
161
165
  for (const cb of block.contentBlocks) {
162
166
  if (cb.type === "image" || cb.type === "file") {
163
167
  count++;
@@ -1,30 +1,19 @@
1
1
  import { getConfig } from "../config/loader.js";
2
2
  import { estimatePromptTokens } from "../context/token-estimator.js";
3
- import { getMemoryConflictAndCleanupStats } from "../memory/admin.js";
4
- import { compileDynamicProfile } from "../memory/profile-compiler.js";
5
3
  import { buildMemoryQuery } from "../memory/query-builder.js";
6
4
  import { computeRecallBudget } from "../memory/retrieval-budget.js";
7
5
  import {
8
6
  buildMemoryRecall,
9
7
  injectMemoryRecallAsSeparateMessage,
10
- injectMemoryRecallIntoUserMessage,
11
8
  } from "../memory/retriever.js";
12
9
  import type { ScopePolicyOverride } from "../memory/search/types.js";
13
10
  import type { Message } from "../providers/types.js";
14
11
  import type { Provider } from "../providers/types.js";
15
12
  import type { ServerMessage } from "./message-protocol.js";
16
- import type { ConflictGate } from "./session-conflict-gate.js";
17
- import { injectDynamicProfileIntoUserMessage } from "./session-dynamic-profile.js";
18
-
19
- export type RecallInjectionStrategy =
20
- | "prepend_user_block"
21
- | "separate_context_message";
22
13
 
23
14
  export interface MemoryRecallResult {
24
15
  runMessages: Message[];
25
16
  recall: Awaited<ReturnType<typeof buildMemoryRecall>>;
26
- dynamicProfile: { text: string };
27
- recallInjectionStrategy: RecallInjectionStrategy;
28
17
  }
29
18
 
30
19
  export interface MemoryPrepareContext {
@@ -32,12 +21,9 @@ export interface MemoryPrepareContext {
32
21
  messages: Message[];
33
22
  systemPrompt: string;
34
23
  provider: Provider;
35
- conflictGate: ConflictGate;
36
24
  scopeId: string;
37
25
  includeDefaultFallback: boolean;
38
26
  trustClass: "guardian" | "trusted_contact" | "unknown";
39
- /** When false (e.g. scheduled tasks), skip conflict gate evaluation. */
40
- isInteractive?: boolean;
41
27
  }
42
28
 
43
29
  /**
@@ -48,14 +34,43 @@ function isToolResultOnlyUserTurn(message: Message | undefined): boolean {
48
34
  return (
49
35
  message?.role === "user" &&
50
36
  message.content.length > 0 &&
51
- message.content.every((block) => block.type === "tool_result")
37
+ message.content.every(
38
+ (block) =>
39
+ block.type === "tool_result" || block.type === "web_search_tool_result",
40
+ )
52
41
  );
53
42
  }
54
43
 
55
44
  /**
56
- * Build memory recall, dynamic profile, and conflict gate evaluation
57
- * for a single agent loop turn. Returns the augmented run messages and
58
- * metadata for downstream event emission.
45
+ * Fast gate that determines whether the current turn warrants memory
46
+ * retrieval. Returns `false` for mechanical no-ops (empty content,
47
+ * tool-result-only) so the full memory pipeline can be skipped.
48
+ * Runs in microseconds — no external calls.
49
+ *
50
+ * Note: We intentionally avoid character-length heuristics here.
51
+ * Short messages like "What did I say?" or "My preferences?" are
52
+ * legitimate memory queries. Per AGENTS.md, judgement calls about
53
+ * message value should be routed through the daemon, not hardcoded.
54
+ */
55
+ export function needsMemory(messages: Message[], content: string): boolean {
56
+ // Empty or whitespace-only content — mechanical validation, nothing to query
57
+ if (!content || content.trim().length === 0) return false;
58
+
59
+ // Tool-result-only turns (assistant tool loop)
60
+ const latestMessage = messages[messages.length - 1];
61
+ if (isToolResultOnlyUserTurn(latestMessage)) return false;
62
+
63
+ return true;
64
+ }
65
+
66
+ /**
67
+ * Build memory recall for a single agent loop turn using the V2 hybrid
68
+ * pipeline. Returns the augmented run messages and metadata for
69
+ * downstream event emission.
70
+ *
71
+ * The V2 pipeline always uses `separate_context_message` injection
72
+ * strategy (user + assistant ack pair). When injection text is empty,
73
+ * no synthetic messages are added.
59
74
  */
60
75
  export async function prepareMemoryContext(
61
76
  ctx: MemoryPrepareContext,
@@ -65,104 +80,42 @@ export async function prepareMemoryContext(
65
80
  onEvent: (msg: ServerMessage) => void,
66
81
  ): Promise<MemoryRecallResult> {
67
82
  // Provenance-based trust gating: untrusted actors skip all memory operations
68
- // (recall, dynamic profile, conflict gate) to prevent untrusted content from
69
- // influencing memory-augmented responses.
83
+ // to prevent untrusted content from influencing memory-augmented responses.
70
84
  const isTrustedActor = ctx.trustClass === "guardian";
71
85
 
86
+ // Build a no-op result that skips the entire memory pipeline.
87
+ const noopResult = (): MemoryRecallResult => ({
88
+ runMessages: ctx.messages,
89
+ recall: {
90
+ enabled: false,
91
+ degraded: false,
92
+ injectedText: "",
93
+ semanticHits: 0,
94
+ recencyHits: 0,
95
+ mergedCount: 0,
96
+ selectedCount: 0,
97
+ injectedTokens: 0,
98
+ latencyMs: 0,
99
+ topCandidates: [],
100
+ tier1Count: 0,
101
+ tier2Count: 0,
102
+ } as Awaited<ReturnType<typeof buildMemoryRecall>>,
103
+ });
104
+
72
105
  if (!isTrustedActor) {
73
- return {
74
- runMessages: ctx.messages,
75
- recall: {
76
- enabled: false,
77
- degraded: false,
78
- injectedText: "",
79
- lexicalHits: 0,
80
- semanticHits: 0,
81
- recencyHits: 0,
82
- entityHits: 0,
83
- relationSeedEntityCount: 0,
84
- relationTraversedEdgeCount: 0,
85
- relationNeighborEntityCount: 0,
86
- relationExpandedItemCount: 0,
87
- earlyTerminated: false,
88
- mergedCount: 0,
89
- selectedCount: 0,
90
- rerankApplied: false,
91
- injectedTokens: 0,
92
- latencyMs: 0,
93
- topCandidates: [],
94
- } as Awaited<ReturnType<typeof buildMemoryRecall>>,
95
- dynamicProfile: { text: "" },
96
- recallInjectionStrategy: "prepend_user_block",
97
- };
106
+ return noopResult();
98
107
  }
99
108
 
100
- // Internal tool-result turns (assistant tool loop) should not trigger
101
- // memory retrieval/profile injection. Injecting memory here repeats the
102
- // same long recall block on every tool step and dramatically inflates
103
- // per-step prompt size/latency.
104
- const latestMessage = ctx.messages[ctx.messages.length - 1];
105
- if (isToolResultOnlyUserTurn(latestMessage)) {
106
- return {
107
- runMessages: ctx.messages,
108
- recall: {
109
- enabled: false,
110
- degraded: false,
111
- injectedText: "",
112
- lexicalHits: 0,
113
- semanticHits: 0,
114
- recencyHits: 0,
115
- entityHits: 0,
116
- relationSeedEntityCount: 0,
117
- relationTraversedEdgeCount: 0,
118
- relationNeighborEntityCount: 0,
119
- relationExpandedItemCount: 0,
120
- earlyTerminated: false,
121
- mergedCount: 0,
122
- selectedCount: 0,
123
- rerankApplied: false,
124
- injectedTokens: 0,
125
- latencyMs: 0,
126
- topCandidates: [],
127
- } as Awaited<ReturnType<typeof buildMemoryRecall>>,
128
- dynamicProfile: { text: "" },
129
- recallInjectionStrategy: "prepend_user_block",
130
- };
109
+ // Gate: skip the entire memory pipeline for mechanical no-ops (empty
110
+ // content, tool-result-only turns).
111
+ if (!needsMemory(ctx.messages, content)) {
112
+ return noopResult();
131
113
  }
132
114
 
133
115
  const runtimeConfig = getConfig();
134
- const memoryEnabled = runtimeConfig.memory?.enabled !== false;
135
116
 
136
- // Conflict gate evaluate for side effects (background resolution/dismissal)
137
- // but do not return any user-facing payload. Non-interactive sessions skip
138
- // entirely since there is no human context for conflict evaluation.
139
- const isInteractive = ctx.isInteractive !== false;
140
- const conflictConfig =
141
- memoryEnabled && isInteractive
142
- ? runtimeConfig.memory?.conflicts
143
- : undefined;
144
- if (conflictConfig) {
145
- await ctx.conflictGate.evaluate(content, conflictConfig, ctx.scopeId);
146
- }
147
-
148
- // Dynamic profile
149
- const profileConfig = memoryEnabled
150
- ? runtimeConfig.memory?.profile
151
- : undefined;
152
- const dynamicProfile = profileConfig?.enabled
153
- ? compileDynamicProfile({
154
- scopeId: ctx.scopeId,
155
- includeDefaultFallback: ctx.includeDefaultFallback,
156
- maxInjectTokensOverride: profileConfig.maxInjectTokens,
157
- })
158
- : { text: "" };
159
-
160
- // Memory recall
117
+ // Memory recall via the V2 hybrid pipeline
161
118
  const recallQuery = buildMemoryQuery(content, ctx.messages);
162
- const recallInjectionStrategy: RecallInjectionStrategy =
163
- (runtimeConfig.memory?.retrieval?.injectionStrategy as
164
- | RecallInjectionStrategy
165
- | undefined) ?? "prepend_user_block";
166
119
  const dynamicBudgetConfig = runtimeConfig.memory?.retrieval?.dynamicBudget;
167
120
  const recallBudget = dynamicBudgetConfig?.enabled
168
121
  ? computeRecallBudget({
@@ -196,8 +149,6 @@ export async function prepareMemoryContext(
196
149
  scopePolicyOverride,
197
150
  },
198
151
  );
199
- const memoryStatus = getMemoryConflictAndCleanupStats();
200
-
201
152
  onEvent({
202
153
  type: "memory_status",
203
154
  enabled: recall.enabled,
@@ -212,32 +163,18 @@ export async function prepareMemoryContext(
212
163
  reason: recall.reason,
213
164
  provider: recall.provider,
214
165
  model: recall.model,
215
- conflictsPending: memoryStatus.conflicts.pending,
216
- conflictsResolved: memoryStatus.conflicts.resolved,
217
- oldestPendingConflictAgeMs: memoryStatus.conflicts.oldestPendingAgeMs,
218
- cleanupResolvedJobsPending: memoryStatus.cleanup.resolvedBacklog,
219
- cleanupSupersededJobsPending: memoryStatus.cleanup.supersededBacklog,
220
- cleanupResolvedJobsCompleted24h: memoryStatus.cleanup.resolvedCompleted24h,
221
- cleanupSupersededJobsCompleted24h:
222
- memoryStatus.cleanup.supersededCompleted24h,
223
166
  });
224
167
 
225
- // Inject recall into messages
168
+ // Inject recall into messages using separate_context_message strategy.
169
+ // When injection text is empty, skip injection entirely (no synthetic messages).
226
170
  let runMessages = ctx.messages;
227
171
  if (recall.injectedText.length > 0) {
228
172
  const userTail = ctx.messages[ctx.messages.length - 1];
229
173
  if (userTail && userTail.role === "user") {
230
- if (recallInjectionStrategy === "separate_context_message") {
231
- runMessages = injectMemoryRecallAsSeparateMessage(
232
- ctx.messages,
233
- recall.injectedText,
234
- );
235
- } else {
236
- runMessages = [
237
- ...ctx.messages.slice(0, -1),
238
- injectMemoryRecallIntoUserMessage(userTail, recall.injectedText),
239
- ];
240
- }
174
+ runMessages = injectMemoryRecallAsSeparateMessage(
175
+ ctx.messages,
176
+ recall.injectedText,
177
+ );
241
178
  onEvent({
242
179
  type: "memory_recalled",
243
180
  provider: recall.provider ?? "unknown",
@@ -249,18 +186,14 @@ export async function prepareMemoryContext(
249
186
  fallbackSources: [...recall.degradation.fallbackSources],
250
187
  }
251
188
  : undefined,
252
- lexicalHits: recall.lexicalHits,
253
189
  semanticHits: recall.semanticHits,
254
190
  recencyHits: recall.recencyHits,
255
- entityHits: recall.entityHits,
256
- relationSeedEntityCount: recall.relationSeedEntityCount,
257
- relationTraversedEdgeCount: recall.relationTraversedEdgeCount,
258
- relationNeighborEntityCount: recall.relationNeighborEntityCount,
259
- relationExpandedItemCount: recall.relationExpandedItemCount,
260
- earlyTerminated: recall.earlyTerminated,
191
+ tier1Count: recall.tier1Count ?? 0,
192
+ tier2Count: recall.tier2Count ?? 0,
193
+ hybridSearchLatencyMs: recall.hybridSearchMs ?? 0,
194
+ sparseVectorUsed: recall.sparseVectorUsed ?? false,
261
195
  mergedCount: recall.mergedCount,
262
196
  selectedCount: recall.selectedCount,
263
- rerankApplied: recall.rerankApplied,
264
197
  injectedTokens: recall.injectedTokens,
265
198
  latencyMs: recall.latencyMs,
266
199
  topCandidates: recall.topCandidates,
@@ -268,21 +201,8 @@ export async function prepareMemoryContext(
268
201
  }
269
202
  }
270
203
 
271
- // Inject dynamic profile
272
- if (dynamicProfile.text.length > 0) {
273
- const userTail = runMessages[runMessages.length - 1];
274
- if (userTail && userTail.role === "user") {
275
- runMessages = [
276
- ...runMessages.slice(0, -1),
277
- injectDynamicProfileIntoUserMessage(userTail, dynamicProfile.text),
278
- ];
279
- }
280
- }
281
-
282
204
  return {
283
205
  runMessages,
284
206
  recall,
285
- dynamicProfile,
286
- recallInjectionStrategy,
287
207
  };
288
208
  }
@@ -92,6 +92,8 @@ export interface ProcessSessionContext {
92
92
  readonly usageStats: UsageStats;
93
93
  /** Request-scoped skill IDs preactivated via slash resolution. */
94
94
  preactivatedSkillIds?: string[];
95
+ /** Add a skill ID to the preactivated set without replacing existing entries. */
96
+ addPreactivatedSkillId(id: string): void;
95
97
  /** Assistant identity — used for scoping notification preferences. */
96
98
  readonly assistantId?: string;
97
99
  trustContext?: TrustContext;
@@ -140,7 +142,8 @@ export interface ProcessSessionContext {
140
142
  | "message_complete"
141
143
  | "generation_cancelled"
142
144
  | "error_terminal"
143
- | "preview_start",
145
+ | "preview_start"
146
+ | "context_compacting",
144
147
  anchor?: "assistant_turn" | "user_turn" | "global",
145
148
  requestId?: string,
146
149
  statusText?: string,
@@ -224,6 +227,11 @@ export async function drainQueue(
224
227
  const next = session.queue.shift();
225
228
  if (!next) return;
226
229
 
230
+ // Reset per-turn preactivation so a prior iteration (e.g. an unknown-slash
231
+ // from a desktop source that skips runAgentLoop) can't leak CU preactivation
232
+ // into the next queued message.
233
+ session.preactivatedSkillIds = undefined;
234
+
227
235
  log.info(
228
236
  {
229
237
  conversationId: session.conversationId,
@@ -283,6 +291,7 @@ export async function drainQueue(
283
291
  const sourceInterface = interfaceCtx?.userMessageInterface;
284
292
  if (sourceInterface === "macos" || sourceInterface === "ios") {
285
293
  session.restoreProxyAvailability();
294
+ session.addPreactivatedSkillId("computer-use");
286
295
  }
287
296
  }
288
297