@vellumai/assistant 0.3.15 → 0.3.16

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 (290) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +1 -1
  3. package/README.md +5 -5
  4. package/docs/architecture/http-token-refresh.md +252 -0
  5. package/docs/architecture/memory.md +5 -4
  6. package/docs/architecture/scheduling.md +4 -88
  7. package/docs/runbook-trusted-contacts.md +283 -0
  8. package/docs/trusted-contact-access.md +247 -0
  9. package/package.json +1 -1
  10. package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
  11. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
  12. package/src/__tests__/access-request-decision.test.ts +331 -0
  13. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  14. package/src/__tests__/asset-search-tool.test.ts +15 -15
  15. package/src/__tests__/attachments-store.test.ts +13 -13
  16. package/src/__tests__/call-controller.test.ts +150 -4
  17. package/src/__tests__/call-conversation-messages.test.ts +2 -2
  18. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  19. package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +108 -12
  21. package/src/__tests__/channel-guardian.test.ts +16 -14
  22. package/src/__tests__/checker.test.ts +24 -0
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +358 -0
  25. package/src/__tests__/conversation-pairing.test.ts +24 -24
  26. package/src/__tests__/conversation-store.test.ts +36 -36
  27. package/src/__tests__/date-context.test.ts +179 -1
  28. package/src/__tests__/db-migration-rollback.test.ts +4 -7
  29. package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
  30. package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
  31. package/src/__tests__/gateway-only-guard.test.ts +188 -0
  32. package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
  33. package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
  34. package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
  35. package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
  36. package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  39. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  40. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  41. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  43. package/src/__tests__/hooks-runner.test.ts +13 -4
  44. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  45. package/src/__tests__/intent-routing.test.ts +14 -0
  46. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  47. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  48. package/src/__tests__/memory-regressions.test.ts +16 -12
  49. package/src/__tests__/non-member-access-request.test.ts +282 -0
  50. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  51. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  52. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  53. package/src/__tests__/recording-intent.test.ts +1 -0
  54. package/src/__tests__/recording-state-machine.test.ts +328 -17
  55. package/src/__tests__/registry.test.ts +17 -8
  56. package/src/__tests__/relay-server.test.ts +105 -0
  57. package/src/__tests__/reminder.test.ts +13 -0
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  59. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  60. package/src/__tests__/server-history-render.test.ts +8 -8
  61. package/src/__tests__/session-agent-loop.test.ts +1 -0
  62. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  63. package/src/__tests__/session-skill-tools.test.ts +1 -0
  64. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  65. package/src/__tests__/slack-channel-config.test.ts +230 -0
  66. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  67. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  68. package/src/__tests__/system-prompt.test.ts +43 -0
  69. package/src/__tests__/task-management-tools.test.ts +3 -3
  70. package/src/__tests__/task-tools.test.ts +3 -3
  71. package/src/__tests__/trust-store.test.ts +17 -1
  72. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  73. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  74. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  75. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  76. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  77. package/src/__tests__/update-bulletin.test.ts +260 -0
  78. package/src/__tests__/update-template-contract.test.ts +29 -0
  79. package/src/agent/loop.ts +2 -2
  80. package/src/amazon/client.ts +2 -3
  81. package/src/calls/call-controller.ts +115 -34
  82. package/src/calls/call-conversation-messages.ts +2 -2
  83. package/src/calls/call-domain.ts +10 -3
  84. package/src/calls/call-pointer-messages.ts +17 -5
  85. package/src/calls/guardian-action-sweep.ts +77 -36
  86. package/src/calls/relay-server.ts +51 -12
  87. package/src/calls/twilio-routes.ts +3 -1
  88. package/src/calls/types.ts +1 -1
  89. package/src/calls/voice-session-bridge.ts +4 -4
  90. package/src/cli/core-commands.ts +3 -3
  91. package/src/cli/map.ts +8 -5
  92. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  93. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  94. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  95. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  96. package/src/config/computer-use-prompt.ts +1 -0
  97. package/src/config/core-schema.ts +16 -0
  98. package/src/config/env-registry.ts +1 -0
  99. package/src/config/env.ts +16 -1
  100. package/src/config/memory-schema.ts +5 -0
  101. package/src/config/schema.ts +4 -0
  102. package/src/config/system-prompt.ts +69 -2
  103. package/src/config/templates/BOOTSTRAP.md +1 -1
  104. package/src/config/templates/IDENTITY.md +8 -4
  105. package/src/config/templates/SOUL.md +14 -0
  106. package/src/config/templates/UPDATES.md +16 -0
  107. package/src/config/templates/USER.md +5 -1
  108. package/src/config/types.ts +1 -0
  109. package/src/config/update-bulletin-format.ts +52 -0
  110. package/src/config/update-bulletin-state.ts +49 -0
  111. package/src/config/update-bulletin.ts +82 -0
  112. package/src/config/vellum-skills/catalog.json +6 -0
  113. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  114. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  115. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  116. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  117. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  118. package/src/context/window-manager.ts +43 -3
  119. package/src/daemon/config-watcher.ts +1 -0
  120. package/src/daemon/connection-policy.ts +21 -1
  121. package/src/daemon/daemon-control.ts +164 -7
  122. package/src/daemon/date-context.ts +174 -1
  123. package/src/daemon/guardian-action-generators.ts +175 -0
  124. package/src/daemon/guardian-verification-intent.ts +120 -0
  125. package/src/daemon/handlers/apps.ts +1 -3
  126. package/src/daemon/handlers/config-channels.ts +2 -2
  127. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  128. package/src/daemon/handlers/config-inbox.ts +55 -159
  129. package/src/daemon/handlers/config-ingress.ts +1 -1
  130. package/src/daemon/handlers/config-integrations.ts +1 -1
  131. package/src/daemon/handlers/config-platform.ts +1 -1
  132. package/src/daemon/handlers/config-scheduling.ts +2 -2
  133. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  134. package/src/daemon/handlers/config-telegram.ts +1 -1
  135. package/src/daemon/handlers/config-twilio.ts +1 -1
  136. package/src/daemon/handlers/config-voice.ts +100 -0
  137. package/src/daemon/handlers/config.ts +3 -0
  138. package/src/daemon/handlers/misc.ts +83 -5
  139. package/src/daemon/handlers/navigate-settings.ts +27 -0
  140. package/src/daemon/handlers/recording.ts +270 -144
  141. package/src/daemon/handlers/sessions.ts +100 -17
  142. package/src/daemon/handlers/subagents.ts +3 -3
  143. package/src/daemon/handlers/work-items.ts +10 -7
  144. package/src/daemon/ipc-contract/integrations.ts +9 -1
  145. package/src/daemon/ipc-contract/messages.ts +4 -0
  146. package/src/daemon/ipc-contract/sessions.ts +1 -1
  147. package/src/daemon/ipc-contract/settings.ts +26 -0
  148. package/src/daemon/ipc-contract/shared.ts +2 -0
  149. package/src/daemon/ipc-contract/work-items.ts +1 -7
  150. package/src/daemon/ipc-contract-inventory.json +5 -1
  151. package/src/daemon/ipc-contract.ts +5 -1
  152. package/src/daemon/lifecycle.ts +306 -266
  153. package/src/daemon/recording-intent.ts +0 -41
  154. package/src/daemon/response-tier.ts +2 -2
  155. package/src/daemon/server.ts +6 -6
  156. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  157. package/src/daemon/session-agent-loop.ts +15 -8
  158. package/src/daemon/session-history.ts +3 -2
  159. package/src/daemon/session-media-retry.ts +3 -0
  160. package/src/daemon/session-messaging.ts +38 -4
  161. package/src/daemon/session-notifiers.ts +2 -2
  162. package/src/daemon/session-process.ts +256 -23
  163. package/src/daemon/session-queue-manager.ts +2 -0
  164. package/src/daemon/session-runtime-assembly.ts +39 -0
  165. package/src/daemon/session-skill-tools.ts +13 -4
  166. package/src/daemon/session-tool-setup.ts +5 -6
  167. package/src/daemon/session.ts +19 -8
  168. package/src/daemon/tls-certs.ts +55 -13
  169. package/src/daemon/tool-side-effects.ts +13 -5
  170. package/src/gallery/default-gallery.ts +32 -9
  171. package/src/influencer/client.ts +2 -1
  172. package/src/memory/channel-delivery-store.ts +37 -567
  173. package/src/memory/channel-guardian-store.ts +66 -1317
  174. package/src/memory/conflict-store.ts +4 -4
  175. package/src/memory/conversation-attention-store.ts +0 -3
  176. package/src/memory/conversation-crud.ts +668 -0
  177. package/src/memory/conversation-queries.ts +361 -0
  178. package/src/memory/conversation-store.ts +45 -983
  179. package/src/memory/db-connection.ts +3 -0
  180. package/src/memory/db-init.ts +25 -0
  181. package/src/memory/delivery-channels.ts +175 -0
  182. package/src/memory/delivery-crud.ts +211 -0
  183. package/src/memory/delivery-status.ts +199 -0
  184. package/src/memory/embedding-backend.ts +70 -4
  185. package/src/memory/embedding-local.ts +12 -2
  186. package/src/memory/entity-extractor.ts +3 -8
  187. package/src/memory/fts-reconciler.ts +121 -0
  188. package/src/memory/guardian-action-store.ts +366 -3
  189. package/src/memory/guardian-approvals.ts +569 -0
  190. package/src/memory/guardian-bindings.ts +130 -0
  191. package/src/memory/guardian-rate-limits.ts +196 -0
  192. package/src/memory/guardian-verification.ts +520 -0
  193. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  194. package/src/memory/job-utils.ts +8 -5
  195. package/src/memory/jobs-store.ts +66 -6
  196. package/src/memory/jobs-worker.ts +23 -1
  197. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  198. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  199. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  200. package/src/memory/migrations/100-core-tables.ts +1 -1
  201. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  202. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  203. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  204. package/src/memory/migrations/113-late-migrations.ts +1 -1
  205. package/src/memory/migrations/116-messages-fts.ts +13 -0
  206. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  207. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  208. package/src/memory/migrations/index.ts +8 -3
  209. package/src/memory/migrations/validate-migration-state.ts +114 -15
  210. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  211. package/src/memory/retriever.ts +46 -13
  212. package/src/memory/schema-migration.ts +3 -0
  213. package/src/memory/schema.ts +25 -7
  214. package/src/memory/search/semantic.ts +8 -90
  215. package/src/notifications/README.md +1 -1
  216. package/src/notifications/broadcaster.ts +20 -2
  217. package/src/notifications/conversation-pairing.ts +3 -3
  218. package/src/notifications/decision-engine.ts +173 -8
  219. package/src/notifications/deliveries-store.ts +27 -8
  220. package/src/notifications/preferences-store.ts +7 -7
  221. package/src/notifications/thread-candidates.ts +234 -0
  222. package/src/notifications/types.ts +18 -0
  223. package/src/permissions/defaults.ts +11 -1
  224. package/src/permissions/prompter.ts +17 -0
  225. package/src/permissions/trust-store.ts +2 -0
  226. package/src/providers/failover.ts +19 -0
  227. package/src/providers/registry.ts +46 -1
  228. package/src/runtime/approval-message-composer.ts +1 -1
  229. package/src/runtime/channel-guardian-service.ts +15 -3
  230. package/src/runtime/channel-retry-sweep.ts +7 -2
  231. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  232. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  233. package/src/runtime/guardian-action-message-composer.ts +245 -0
  234. package/src/runtime/guardian-outbound-actions.ts +26 -6
  235. package/src/runtime/guardian-verification-templates.ts +15 -9
  236. package/src/runtime/http-errors.ts +93 -0
  237. package/src/runtime/http-server.ts +133 -44
  238. package/src/runtime/http-types.ts +53 -0
  239. package/src/runtime/ingress-service.ts +237 -0
  240. package/src/runtime/middleware/error-handler.ts +4 -3
  241. package/src/runtime/middleware/rate-limiter.ts +160 -0
  242. package/src/runtime/middleware/request-logger.ts +71 -0
  243. package/src/runtime/middleware/twilio-validation.ts +7 -6
  244. package/src/runtime/pending-interactions.ts +12 -0
  245. package/src/runtime/routes/access-request-decision.ts +215 -0
  246. package/src/runtime/routes/app-routes.ts +25 -18
  247. package/src/runtime/routes/approval-routes.ts +18 -47
  248. package/src/runtime/routes/attachment-routes.ts +15 -41
  249. package/src/runtime/routes/call-routes.ts +20 -20
  250. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  251. package/src/runtime/routes/contact-routes.ts +4 -9
  252. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  253. package/src/runtime/routes/conversation-routes.ts +26 -57
  254. package/src/runtime/routes/debug-routes.ts +71 -0
  255. package/src/runtime/routes/events-routes.ts +3 -2
  256. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  257. package/src/runtime/routes/identity-routes.ts +14 -10
  258. package/src/runtime/routes/inbound-conversation.ts +3 -2
  259. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  260. package/src/runtime/routes/ingress-routes.ts +174 -0
  261. package/src/runtime/routes/integration-routes.ts +78 -16
  262. package/src/runtime/routes/pairing-routes.ts +11 -10
  263. package/src/runtime/routes/secret-routes.ts +10 -18
  264. package/src/runtime/verification-rate-limiter.ts +83 -0
  265. package/src/schedule/schedule-store.ts +13 -1
  266. package/src/schedule/scheduler.ts +1 -1
  267. package/src/security/secret-ingress.ts +5 -2
  268. package/src/security/secret-scanner.ts +72 -6
  269. package/src/subagent/manager.ts +6 -4
  270. package/src/swarm/plan-validator.ts +4 -1
  271. package/src/tasks/task-runner.ts +3 -1
  272. package/src/tools/browser/api-map.ts +9 -6
  273. package/src/tools/calls/call-start.ts +20 -0
  274. package/src/tools/executor.ts +50 -568
  275. package/src/tools/permission-checker.ts +272 -0
  276. package/src/tools/registry.ts +14 -6
  277. package/src/tools/reminder/reminder-store.ts +7 -7
  278. package/src/tools/reminder/reminder.ts +6 -3
  279. package/src/tools/secret-detection-handler.ts +301 -0
  280. package/src/tools/subagent/message.ts +1 -1
  281. package/src/tools/system/voice-config.ts +62 -0
  282. package/src/tools/tasks/index.ts +3 -3
  283. package/src/tools/tasks/work-item-list.ts +3 -3
  284. package/src/tools/tasks/work-item-update.ts +4 -5
  285. package/src/tools/tool-approval-handler.ts +192 -0
  286. package/src/tools/tool-manifest.ts +2 -0
  287. package/src/watcher/watcher-store.ts +9 -9
  288. package/src/work-items/work-item-runner.ts +9 -6
  289. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  290. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -7,8 +7,6 @@
7
7
  // Internal helpers (detect/strip/classify) are kept as private utilities
8
8
  // consumed only by `resolveRecordingIntent`.
9
9
 
10
- type RecordingIntentClass = 'start_only' | 'stop_only' | 'mixed' | 'none';
11
-
12
10
  export type RecordingIntentResult =
13
11
  | { kind: 'none' }
14
12
  | { kind: 'start_only' }
@@ -370,45 +368,6 @@ function isInterrogative(text: string, dynamicNames?: string[]): boolean {
370
368
  return false;
371
369
  }
372
370
 
373
- // ─── Unified classification ─────────────────────────────────────────────────
374
-
375
- /**
376
- * Classifies the recording intent of a user message into one of four categories:
377
- * - 'start_only': the prompt is purely about starting a recording
378
- * - 'stop_only': the prompt is purely about stopping a recording
379
- * - 'mixed': the prompt contains recording intent mixed with other tasks,
380
- * or contains both start and stop recording patterns
381
- * - 'none': no recording intent detected
382
- *
383
- * If `dynamicNames` are provided, they are stripped from the beginning of the
384
- * text before classification (e.g., "Nova, record my screen" -> "record my screen").
385
- */
386
- function _classifyRecordingIntent(
387
- taskText: string,
388
- dynamicNames?: string[],
389
- ): RecordingIntentClass {
390
- const normalized =
391
- dynamicNames && dynamicNames.length > 0
392
- ? stripDynamicNames(taskText, dynamicNames)
393
- : taskText;
394
-
395
- const hasStart = detectRecordingIntent(normalized);
396
- const hasStop = detectStopRecordingIntent(normalized);
397
-
398
- // Both start and stop patterns present -> mixed
399
- if (hasStart && hasStop) return 'mixed';
400
-
401
- if (hasStop) {
402
- return isStopRecordingOnly(normalized) ? 'stop_only' : 'mixed';
403
- }
404
-
405
- if (hasStart) {
406
- return isRecordingOnly(normalized) ? 'start_only' : 'mixed';
407
- }
408
-
409
- return 'none';
410
- }
411
-
412
371
  // ─── Structured intent resolver ─────────────────────────────────────────────
413
372
 
414
373
  /**
@@ -226,12 +226,12 @@ export function tierMaxTokens(tier: ResponseTier, configuredMax: number): number
226
226
 
227
227
  /**
228
228
  * Map for Anthropic provider: tier → model.
229
- * low → haiku (fastest TTFT)
229
+ * low → sonnet (balanced)
230
230
  * medium → sonnet (balanced)
231
231
  * high → undefined (use configured default, typically opus)
232
232
  */
233
233
  const ANTHROPIC_TIER_MODELS: Record<ResponseTier, string | undefined> = {
234
- low: 'claude-haiku-4-5-20251001',
234
+ low: 'claude-sonnet-4-6',
235
235
  medium: 'claude-sonnet-4-6',
236
236
  high: undefined, // use configured default
237
237
  };
@@ -188,7 +188,7 @@ export class DaemonServer {
188
188
  const children = getSubagentManager().getChildrenOf(sessionId);
189
189
  return children.some((c) => c.status === 'running' || c.status === 'pending');
190
190
  };
191
- getSubagentManager().onSubagentFinished = (parentSessionId, message, sendToClient, notification) => {
191
+ getSubagentManager().onSubagentFinished = async (parentSessionId, message, sendToClient, notification) => {
192
192
  const parentSession = this.sessions.get(parentSessionId);
193
193
  if (!parentSession) {
194
194
  log.warn({ parentSessionId }, 'Subagent finished but parent session not found');
@@ -202,7 +202,7 @@ export class DaemonServer {
202
202
  return;
203
203
  }
204
204
  if (!enqueueResult.queued) {
205
- const messageId = parentSession.persistUserMessage(message, [], undefined, metadata);
205
+ const messageId = await parentSession.persistUserMessage(message, [], undefined, metadata);
206
206
  parentSession.runAgentLoop(message, messageId, sendToClient).catch((err) => {
207
207
  log.error({ parentSessionId, err }, 'Failed to process subagent notification in parent');
208
208
  });
@@ -809,7 +809,7 @@ export class DaemonServer {
809
809
  );
810
810
 
811
811
  const requestId = crypto.randomUUID();
812
- const messageId = session.persistUserMessage(content, attachments, requestId);
812
+ const messageId = await session.persistUserMessage(content, attachments, requestId);
813
813
 
814
814
  // Register pending interactions so channel approval interception can
815
815
  // find the session by requestId when confirmation/secret events fire.
@@ -865,7 +865,7 @@ export class DaemonServer {
865
865
  : {}),
866
866
  };
867
867
  const userMsg = createUserMessage(content, attachments);
868
- const persisted = conversationStore.addMessage(
868
+ const persisted = await conversationStore.addMessage(
869
869
  conversationId,
870
870
  'user',
871
871
  JSON.stringify(userMsg.content),
@@ -889,7 +889,7 @@ export class DaemonServer {
889
889
  }
890
890
 
891
891
  const assistantMsg = createAssistantMessage(slashResult.message);
892
- conversationStore.addMessage(
892
+ await conversationStore.addMessage(
893
893
  conversationId,
894
894
  'assistant',
895
895
  JSON.stringify(assistantMsg.content),
@@ -908,7 +908,7 @@ export class DaemonServer {
908
908
  const requestId = crypto.randomUUID();
909
909
  let messageId: string;
910
910
  try {
911
- messageId = session.persistUserMessage(resolvedContent, attachments, requestId);
911
+ messageId = await session.persistUserMessage(resolvedContent, attachments, requestId);
912
912
  } catch (err) {
913
913
  session.setPreactivatedSkillIds(undefined);
914
914
  throw err;
@@ -104,6 +104,26 @@ export function emitLlmCallStartedIfNeeded(
104
104
  });
105
105
  }
106
106
 
107
+ // ── IPC Size Caps ────────────────────────────────────────────────────
108
+ // The client truncates tool results anyway (2 000 chars in ChatViewModel),
109
+ // but the full string can be megabytes (file_read, bash output). Capping
110
+ // here avoids sending oversized payloads over IPC which get decoded on
111
+ // the client's main thread.
112
+
113
+ const TOOL_RESULT_MAX_CHARS = 2_000;
114
+ const TOOL_RESULT_TRUNCATION_SUFFIX = '...[truncated]';
115
+
116
+ // tool_input_delta streams accumulated JSON as tools run. For non-app
117
+ // tools the client discards it (extractCodePreview only handles app tools),
118
+ // so we cap it aggressively to avoid excessive IPC traffic.
119
+ const TOOL_INPUT_DELTA_MAX_CHARS = 50_000;
120
+ const APP_TOOL_NAMES = new Set(['app_create', 'app_update']);
121
+
122
+ function truncateForIpc(value: string, maxChars: number, suffix: string): string {
123
+ if (value.length <= maxChars) return value;
124
+ return value.slice(0, maxChars - suffix.length) + suffix;
125
+ }
126
+
107
127
  // ── Individual Handlers ──────────────────────────────────────────────
108
128
 
109
129
  export function handleTextDelta(
@@ -194,7 +214,12 @@ export function handleInputJsonDelta(
194
214
  deps: EventHandlerDeps,
195
215
  event: Extract<AgentEvent, { type: 'input_json_delta' }>,
196
216
  ): void {
197
- deps.onEvent({ type: 'tool_input_delta', toolName: event.toolName, content: event.accumulatedJson, sessionId: deps.ctx.conversationId });
217
+ // Cap non-app tool input deltas the client only uses this data for
218
+ // app_create/app_update code previews; all other tools discard it.
219
+ const content = APP_TOOL_NAMES.has(event.toolName)
220
+ ? event.accumulatedJson
221
+ : truncateForIpc(event.accumulatedJson, TOOL_INPUT_DELTA_MAX_CHARS, TOOL_RESULT_TRUNCATION_SUFFIX);
222
+ deps.onEvent({ type: 'tool_input_delta', toolName: event.toolName, content, sessionId: deps.ctx.conversationId });
198
223
  }
199
224
 
200
225
  export function handleToolResult(
@@ -206,7 +231,7 @@ export function handleToolResult(
206
231
  deps.onEvent({
207
232
  type: 'tool_result',
208
233
  toolName: '',
209
- result: event.content,
234
+ result: truncateForIpc(event.content, TOOL_RESULT_MAX_CHARS, TOOL_RESULT_TRUNCATION_SUFFIX),
210
235
  isError: event.isError,
211
236
  diff: event.diff,
212
237
  status: event.status,
@@ -256,11 +281,11 @@ export function handleError(
256
281
  }
257
282
  }
258
283
 
259
- export function handleMessageComplete(
284
+ export async function handleMessageComplete(
260
285
  state: EventHandlerState,
261
286
  deps: EventHandlerDeps,
262
287
  event: Extract<AgentEvent, { type: 'message_complete' }>,
263
- ): void {
288
+ ): Promise<void> {
264
289
  // Flush any remaining directive display buffer
265
290
  if (state.pendingDirectiveDisplayBuffer.length > 0) {
266
291
  deps.onEvent({
@@ -290,7 +315,7 @@ export function handleMessageComplete(
290
315
  userMessageInterface: deps.turnInterfaceContext.userMessageInterface,
291
316
  assistantMessageInterface: deps.turnInterfaceContext.assistantMessageInterface,
292
317
  };
293
- conversationStore.addMessage(
318
+ await conversationStore.addMessage(
294
319
  deps.ctx.conversationId,
295
320
  'user',
296
321
  JSON.stringify(toolResultBlocks),
@@ -335,7 +360,7 @@ export function handleMessageComplete(
335
360
  userMessageInterface: deps.turnInterfaceContext.userMessageInterface,
336
361
  assistantMessageInterface: deps.turnInterfaceContext.assistantMessageInterface,
337
362
  };
338
- const assistantMsg = conversationStore.addMessage(
363
+ const assistantMsg = await conversationStore.addMessage(
339
364
  deps.ctx.conversationId,
340
365
  'assistant',
341
366
  JSON.stringify(contentWithSurfaces),
@@ -399,11 +424,11 @@ export function handleUsage(
399
424
  // ── Dispatcher ───────────────────────────────────────────────────────
400
425
 
401
426
  /** Routes an AgentEvent to the appropriate handler. */
402
- export function dispatchAgentEvent(
427
+ export async function dispatchAgentEvent(
403
428
  state: EventHandlerState,
404
429
  deps: EventHandlerDeps,
405
430
  event: AgentEvent,
406
- ): void {
431
+ ): Promise<void> {
407
432
  switch (event.type) {
408
433
  case 'text_delta':
409
434
  handleTextDelta(state, deps, event);
@@ -427,7 +452,7 @@ export function dispatchAgentEvent(
427
452
  handleError(state, deps, event);
428
453
  break;
429
454
  case 'message_complete':
430
- handleMessageComplete(state, deps, event);
455
+ await handleMessageComplete(state, deps, event);
431
456
  break;
432
457
  case 'usage':
433
458
  handleUsage(state, deps, event);
@@ -34,7 +34,7 @@ import {
34
34
  type AssistantAttachmentDraft,
35
35
  cleanAssistantContent,
36
36
  } from './assistant-attachments.js';
37
- import { buildTemporalContext } from './date-context.js';
37
+ import { buildTemporalContext, extractUserTimeZoneFromDynamicProfile } from './date-context.js';
38
38
  import { deepRepairHistory,repairHistory } from './history-repair.js';
39
39
  import type { DynamicPageSurfaceData,ServerMessage, SurfaceData, SurfaceType, UsageStats } from './ipc-protocol.js';
40
40
  import {
@@ -132,7 +132,7 @@ export interface AgentLoopSessionContext {
132
132
  getQueueDepth(): number;
133
133
  hasQueuedMessages(): boolean;
134
134
  canHandoffAtCheckpoint(): boolean;
135
- drainQueue(reason: QueueDrainReason): void;
135
+ drainQueue(reason: QueueDrainReason): Promise<void>;
136
136
  getTurnChannelContext(): TurnChannelContext | null;
137
137
  getTurnInterfaceContext(): TurnInterfaceContext | null;
138
138
  }
@@ -144,7 +144,7 @@ export async function runAgentLoopImpl(
144
144
  content: string,
145
145
  userMessageId: string,
146
146
  onEvent: (msg: ServerMessage) => void,
147
- options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean },
147
+ options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; titleText?: string },
148
148
  ): Promise<void> {
149
149
  if (!ctx.abortController) {
150
150
  throw new Error('runAgentLoop called without prior persistUserMessage');
@@ -272,7 +272,7 @@ export async function runAgentLoopImpl(
272
272
  assistantMessageInterface: capturedTurnInterfaceContext.assistantMessageInterface,
273
273
  };
274
274
  const assistantMessage = createAssistantMessage(memoryResult.conflictClarification);
275
- conversationStore.addMessage(
275
+ await conversationStore.addMessage(
276
276
  ctx.conversationId,
277
277
  'assistant',
278
278
  JSON.stringify(assistantMessage.content),
@@ -325,8 +325,15 @@ export async function runAgentLoopImpl(
325
325
  ctx.refreshWorkspaceTopLevelContextIfNeeded();
326
326
 
327
327
  // Compute fresh temporal context each turn for date grounding.
328
+ // Absolute "now" is always anchored to assistant host clock, while local
329
+ // date semantics prefer configured user timezone, then profile memory.
330
+ const hostTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
331
+ const userTimeZone = extractUserTimeZoneFromDynamicProfile(dynamicProfile.text);
332
+ const configuredUserTimeZone = getConfig().ui.userTimezone ?? null;
328
333
  const temporalContext = buildTemporalContext({
329
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
334
+ hostTimeZone,
335
+ configuredUserTimeZone,
336
+ userTimeZone,
330
337
  });
331
338
 
332
339
  // Use the channel/interface context captured at the top of this function
@@ -584,7 +591,7 @@ export async function runAgentLoopImpl(
584
591
  userMessageInterface: capturedTurnInterfaceContext.userMessageInterface,
585
592
  assistantMessageInterface: capturedTurnInterfaceContext.assistantMessageInterface,
586
593
  };
587
- conversationStore.addMessage(
594
+ await conversationStore.addMessage(
588
595
  ctx.conversationId,
589
596
  'user',
590
597
  JSON.stringify(toolResultBlocks),
@@ -610,7 +617,7 @@ export async function runAgentLoopImpl(
610
617
  assistantMessageInterface: capturedTurnInterfaceContext.assistantMessageInterface,
611
618
  };
612
619
  const errorAssistantMessage = createAssistantMessage(state.providerErrorUserMessage);
613
- conversationStore.addMessage(
620
+ await conversationStore.addMessage(
614
621
  ctx.conversationId,
615
622
  'assistant',
616
623
  JSON.stringify(errorAssistantMessage.content),
@@ -695,7 +702,7 @@ export async function runAgentLoopImpl(
695
702
  queueGenerateConversationTitle({
696
703
  conversationId: ctx.conversationId,
697
704
  provider: ctx.provider,
698
- userMessage: content,
705
+ userMessage: options?.titleText ?? content,
699
706
  assistantResponse: state.firstAssistantText || undefined,
700
707
  onTitleUpdated: (title) => {
701
708
  onEvent({
@@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid';
3
3
  import { getSummaryFromContextMessage } from '../context/window-manager.js';
4
4
  import * as conversationStore from '../memory/conversation-store.js';
5
5
  import { enqueueMemoryJob } from '../memory/jobs-store.js';
6
+ import { withQdrantBreaker } from '../memory/qdrant-circuit-breaker.js';
6
7
  import { getQdrantClient } from '../memory/qdrant-client.js';
7
8
  import type { ContentBlock,Message } from '../providers/types.js';
8
9
  import { getLogger } from '../util/logger.js';
@@ -67,7 +68,7 @@ export async function cleanupQdrantVectors(
67
68
  }
68
69
 
69
70
  const results = await Promise.allSettled(
70
- targets.map((t) => qdrant.deleteByTarget(t.targetType, t.targetId)),
71
+ targets.map((t) => withQdrantBreaker(() => qdrant.deleteByTarget(t.targetType, t.targetId))),
71
72
  );
72
73
 
73
74
  let succeeded = 0;
@@ -263,7 +264,7 @@ export interface HistorySessionContext {
263
264
  content: string,
264
265
  userMessageId: string,
265
266
  onEvent: (msg: ServerMessage) => void,
266
- options?: { skipPreMessageRollback?: boolean },
267
+ options?: { skipPreMessageRollback?: boolean; titleText?: string },
267
268
  ): Promise<void>;
268
269
  }
269
270
 
@@ -30,6 +30,9 @@ export function stripMediaPayloadsForRetry(messages: Message[]): { messages: Mes
30
30
  const nextMessages = messages.map((msg, msgIndex) => {
31
31
  const nextContent: ContentBlock[] = [];
32
32
  for (const block of msg.content) {
33
+ // Top-level image blocks are user-uploaded attachments. Keep the latest
34
+ // few (in the most recent user message) and strip older ones so the
35
+ // retry can actually reduce context size when images are the cause.
33
36
  if (block.type === 'image') {
34
37
  const keep = latestUserIndex === msgIndex && keptLatestMediaBlocks < RETRY_KEEP_LATEST_MEDIA_BLOCKS;
35
38
  if (keep) {
@@ -10,6 +10,7 @@ import { v4 as uuid } from 'uuid';
10
10
  import { createUserMessage } from '../agent/message-types.js';
11
11
  import type { TurnChannelContext, TurnInterfaceContext } from '../channels/types.js';
12
12
  import { parseChannelId, parseInterfaceId } from '../channels/types.js';
13
+ import { AttachmentUploadError, linkAttachmentToMessage, uploadAttachment, validateAttachmentUpload } from '../memory/attachments-store.js';
13
14
  import * as conversationStore from '../memory/conversation-store.js';
14
15
  import { provenanceFromGuardianContext } from '../memory/conversation-store.js';
15
16
  import type { SecretPrompter } from '../permissions/secret-prompter.js';
@@ -124,6 +125,7 @@ export function enqueueMessage(
124
125
  currentPage?: string,
125
126
  metadata?: Record<string, unknown>,
126
127
  options?: { isInteractive?: boolean },
128
+ displayContent?: string,
127
129
  ): { queued: boolean; rejected?: boolean; requestId: string } {
128
130
  if (!ctx.processing) {
129
131
  return { queued: false, requestId };
@@ -143,6 +145,7 @@ export function enqueueMessage(
143
145
  turnInterfaceContext,
144
146
  isInteractive: options?.isInteractive,
145
147
  queuedAt: Date.now(),
148
+ displayContent,
146
149
  });
147
150
  if (!pushed) {
148
151
  return { queued: false, rejected: true, requestId };
@@ -152,13 +155,14 @@ export function enqueueMessage(
152
155
 
153
156
  // ── persistUserMessage ───────────────────────────────────────────────
154
157
 
155
- export function persistUserMessage(
158
+ export async function persistUserMessage(
156
159
  ctx: MessagingSessionContext,
157
160
  content: string,
158
161
  attachments: UserMessageAttachment[],
159
162
  requestId?: string,
160
163
  metadata?: Record<string, unknown>,
161
- ): string {
164
+ displayContent?: string,
165
+ ): Promise<string> {
162
166
  if (ctx.processing) {
163
167
  throw new Error('Session is already processing a message');
164
168
  }
@@ -202,10 +206,19 @@ export function persistUserMessage(
202
206
  : {}),
203
207
  };
204
208
 
205
- const persistedUserMessage = conversationStore.addMessage(
209
+ // When displayContent is provided (e.g. original text before recording
210
+ // intent stripping), persist that to DB so users see the full message
211
+ // after restart. The in-memory userMessage (sent to the LLM) still uses
212
+ // the stripped content.
213
+ const contentToPersist = displayContent
214
+ ? JSON.stringify(createUserMessage(displayContent, attachments.map((a) => ({
215
+ id: a.id, filename: a.filename, mimeType: a.mimeType, data: a.data, extractedText: a.extractedText,
216
+ }))).content)
217
+ : JSON.stringify(userMessage.content);
218
+ const persistedUserMessage = await conversationStore.addMessage(
206
219
  ctx.conversationId,
207
220
  'user',
208
- JSON.stringify(userMessage.content),
221
+ contentToPersist,
209
222
  mergedMetadata,
210
223
  );
211
224
 
@@ -220,6 +233,27 @@ export function persistUserMessage(
220
233
  throw new Error('Failed to persist user message');
221
234
  }
222
235
 
236
+ // Index user attachments in the attachments table so asset_search can find them.
237
+ for (let i = 0; i < attachments.length; i++) {
238
+ const a = attachments[i];
239
+ if (!a.data) continue;
240
+ try {
241
+ const validation = validateAttachmentUpload(a.filename, a.mimeType);
242
+ if (!validation.ok) {
243
+ log.warn({ filename: a.filename, error: validation.error }, 'Skipping user attachment indexing: validation failed');
244
+ continue;
245
+ }
246
+ const stored = uploadAttachment(a.filename, a.mimeType, a.data);
247
+ linkAttachmentToMessage(persistedUserMessage.id, stored.id, i);
248
+ } catch (err) {
249
+ if (err instanceof AttachmentUploadError) {
250
+ log.warn({ filename: a.filename, error: err.message }, 'Skipping user attachment indexing');
251
+ } else {
252
+ log.error({ filename: a.filename, err }, 'Failed to index user attachment');
253
+ }
254
+ }
255
+ }
256
+
223
257
  return persistedUserMessage.id;
224
258
  } catch (err) {
225
259
  ctx.messages.pop();
@@ -96,12 +96,12 @@ export function registerSessionNotifiers(
96
96
  }
97
97
  });
98
98
 
99
- registerCallQuestionNotifier(conversationId, (callSessionId: string, question: string) => {
99
+ registerCallQuestionNotifier(conversationId, async (callSessionId: string, question: string) => {
100
100
  const callSession = getCallSession(callSessionId);
101
101
  const callee = callSession?.toNumber ?? 'the caller';
102
102
  const questionText = `**Live call question** (to ${callee}):\n\n${question}\n\n_Use the call answer API to respond._`;
103
103
 
104
- conversationStore.addMessage(
104
+ await conversationStore.addMessage(
105
105
  conversationId,
106
106
  'assistant',
107
107
  JSON.stringify([{ type: 'text', text: questionText }]),