@vellumai/assistant 0.10.3 → 0.10.4-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/openapi.yaml +73 -56
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  4. package/src/__tests__/assistant-stream-state.test.ts +3 -76
  5. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
  6. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  7. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  8. package/src/__tests__/channel-guardian.test.ts +82 -32
  9. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  10. package/src/__tests__/channel-reply-delivery.test.ts +6 -2
  11. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +148 -0
  13. package/src/__tests__/consult-deadline.test.ts +60 -0
  14. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  15. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  16. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  17. package/src/__tests__/contacts-write.test.ts +0 -2
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +98 -7
  20. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  21. package/src/__tests__/conversation-error.test.ts +18 -0
  22. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  23. package/src/__tests__/conversation-title-service.test.ts +222 -201
  24. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  25. package/src/__tests__/delete-propagation.test.ts +5 -3
  26. package/src/__tests__/dm-backfill.test.ts +6 -4
  27. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  28. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  29. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  30. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  31. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  32. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  33. package/src/__tests__/helpers/mock-logger.ts +1 -0
  34. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  35. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  36. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  37. package/src/__tests__/invite-routes-http.test.ts +34 -0
  38. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  39. package/src/__tests__/list-messages-page-latest.test.ts +173 -4
  40. package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
  41. package/src/__tests__/non-member-access-request.test.ts +15 -13
  42. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  43. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  44. package/src/__tests__/persona-resolver.test.ts +75 -45
  45. package/src/__tests__/plugin-bootstrap.test.ts +13 -5
  46. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  47. package/src/__tests__/provider-usage-tracking.test.ts +1 -1
  48. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  49. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  50. package/src/__tests__/reaction-persistence.test.ts +51 -4
  51. package/src/__tests__/relay-server.test.ts +88 -31
  52. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  53. package/src/__tests__/settings-routes.test.ts +32 -0
  54. package/src/__tests__/slack-block-formatting.test.ts +1 -38
  55. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  56. package/src/__tests__/stt-hints.test.ts +6 -3
  57. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  58. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  59. package/src/__tests__/subagent-role-registry.test.ts +17 -4
  60. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  61. package/src/__tests__/subagent-tools.test.ts +398 -3
  62. package/src/__tests__/thread-backfill.test.ts +3 -3
  63. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
  64. package/src/__tests__/tool-start-timestamp.test.ts +4 -3
  65. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  66. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  67. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  68. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  69. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  70. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  71. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  72. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  73. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
  74. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  75. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  76. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  77. package/src/agent/loop-exclusive-tool.test.ts +19 -15
  78. package/src/agent/loop-native-web-search.test.ts +200 -0
  79. package/src/agent/loop.ts +108 -1
  80. package/src/api/responses/conversation-message.ts +9 -0
  81. package/src/approvals/guardian-request-resolvers.ts +16 -4
  82. package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
  83. package/src/calls/guardian-dispatch.ts +14 -11
  84. package/src/calls/inbound-trust-reader.ts +7 -1
  85. package/src/calls/relay-access-wait.ts +6 -6
  86. package/src/calls/relay-server.ts +22 -2
  87. package/src/calls/relay-setup-router.ts +10 -10
  88. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  89. package/src/cli/commands/contacts.ts +10 -7
  90. package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
  91. package/src/cli/commands/memory/worker.ts +97 -30
  92. package/src/cli/commands/plugins.ts +3 -146
  93. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
  94. package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
  95. package/src/cli/lib/publish-plugin.ts +231 -1
  96. package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
  97. package/src/config/bundled-skills/subagent/SKILL.md +16 -1
  98. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  99. package/src/config/call-site-defaults.ts +0 -6
  100. package/src/config/llm-resolver.ts +0 -3
  101. package/src/config/schemas/call-site-catalog.ts +0 -7
  102. package/src/config/schemas/heartbeat.ts +2 -5
  103. package/src/config/schemas/llm.ts +3 -12
  104. package/src/config/schemas/memory-lifecycle.ts +1 -1
  105. package/src/config/seed-inference-profiles.ts +76 -35
  106. package/src/config/sync-gated-profiles.ts +0 -3
  107. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  108. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  109. package/src/contacts/contact-store.ts +27 -237
  110. package/src/contacts/contacts-write.ts +18 -58
  111. package/src/contacts/gateway-channel-read.ts +51 -0
  112. package/src/contacts/member-write-relay.ts +25 -31
  113. package/src/contacts/types.ts +3 -15
  114. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  115. package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
  116. package/src/daemon/conversation-agent-loop.ts +68 -61
  117. package/src/daemon/conversation-error.ts +7 -10
  118. package/src/daemon/conversation-tool-setup.ts +0 -10
  119. package/src/daemon/conversation.ts +10 -0
  120. package/src/daemon/external-plugins-bootstrap.ts +8 -2
  121. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  122. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  123. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  124. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  125. package/src/daemon/handlers/config-channels.ts +14 -29
  126. package/src/daemon/lifecycle.ts +16 -4
  127. package/src/daemon/message-types/surfaces.ts +2 -0
  128. package/src/heartbeat/heartbeat-service.ts +5 -0
  129. package/src/home/relationship-state-writer.ts +5 -0
  130. package/src/memory/__tests__/embedding-cache.test.ts +136 -0
  131. package/src/memory/compaction-ledger-store.ts +107 -0
  132. package/src/memory/conversation-crud.ts +136 -61
  133. package/src/memory/conversation-title-service.ts +173 -24
  134. package/src/memory/embedding-backend.ts +8 -1
  135. package/src/memory/embedding-cache.ts +139 -0
  136. package/src/memory/jobs-worker.ts +75 -29
  137. package/src/memory/memory-retrospective-job.ts +5 -0
  138. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
  139. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  140. package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
  141. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
  142. package/src/memory/schema/contacts.ts +6 -2
  143. package/src/memory/schema/conversations.ts +39 -0
  144. package/src/memory/steps.ts +1090 -367
  145. package/src/memory/worker-control.ts +104 -18
  146. package/src/memory/worker-process.ts +17 -0
  147. package/src/messaging/channel-binding-metadata.ts +31 -0
  148. package/src/messaging/channel-binding-schema.ts +51 -0
  149. package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
  150. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
  151. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
  152. package/src/messaging/providers/a2a/deliver.ts +5 -1
  153. package/src/messaging/providers/a2a/transport.ts +10 -0
  154. package/src/messaging/providers/callback-routing.ts +48 -0
  155. package/src/messaging/providers/channel-transport.ts +55 -0
  156. package/src/messaging/providers/index.ts +65 -241
  157. package/src/messaging/providers/slack/binding-metadata.ts +62 -0
  158. package/src/messaging/providers/slack/transport.ts +92 -0
  159. package/src/messaging/providers/telegram-bot/transport.ts +51 -0
  160. package/src/messaging/providers/whatsapp/transport.ts +38 -0
  161. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  162. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  163. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  164. package/src/notifications/destination-resolver.ts +7 -23
  165. package/src/notifications/emit-signal.ts +5 -11
  166. package/src/plugins/defaults/index.ts +0 -35
  167. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
  168. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
  169. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
  170. package/src/plugins/disabled-state.ts +31 -0
  171. package/src/plugins/registry.ts +55 -12
  172. package/src/prompts/persona-resolver.ts +43 -11
  173. package/src/providers/call-site-routing.ts +41 -0
  174. package/src/providers/provider-send-message.ts +6 -0
  175. package/src/providers/ratelimit.ts +6 -0
  176. package/src/providers/registry.ts +1 -1
  177. package/src/providers/retry.ts +6 -0
  178. package/src/providers/types.ts +13 -0
  179. package/src/providers/usage-tracking.ts +6 -0
  180. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  181. package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
  182. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  183. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
  184. package/src/runtime/access-request-helper.ts +1 -2
  185. package/src/runtime/actor-trust-resolver.ts +44 -17
  186. package/src/runtime/anchored-guardian.test.ts +7 -54
  187. package/src/runtime/anchored-guardian.ts +4 -53
  188. package/src/runtime/assistant-stream-state.ts +12 -74
  189. package/src/runtime/channel-reply-delivery.ts +3 -8
  190. package/src/runtime/guardian-vellum-migration.ts +18 -16
  191. package/src/runtime/invite-redemption-service.ts +25 -10
  192. package/src/runtime/local-actor-identity.test.ts +108 -0
  193. package/src/runtime/local-actor-identity.ts +27 -20
  194. package/src/runtime/member-verdict-cache.ts +0 -0
  195. package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
  196. package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
  197. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  198. package/src/runtime/routes/contact-routes.ts +40 -25
  199. package/src/runtime/routes/conversation-list-routes.ts +1 -29
  200. package/src/runtime/routes/conversation-routes.ts +27 -7
  201. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  202. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  203. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  204. package/src/runtime/routes/settings-routes.ts +8 -3
  205. package/src/runtime/services/conversation-serializer.ts +6 -49
  206. package/src/runtime/slack-block-formatting.ts +0 -15
  207. package/src/runtime/trust-verdict-consumer.ts +36 -41
  208. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  209. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  210. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
  211. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  212. package/src/subagent/index.ts +1 -1
  213. package/src/subagent/manager.ts +245 -33
  214. package/src/subagent/types.ts +8 -1
  215. package/src/tools/registry.ts +10 -3
  216. package/src/tools/subagent/consult-deadline.ts +49 -0
  217. package/src/tools/subagent/spawn.ts +234 -5
  218. package/src/util/logger.ts +9 -0
  219. package/src/util/platform.ts +14 -0
  220. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  221. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  222. package/src/workspace/migrations/registry.ts +2 -0
  223. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  224. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  225. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  226. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
  227. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  228. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  229. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  230. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  231. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  232. package/src/plugins/defaults/advisor/config.ts +0 -21
  233. package/src/plugins/defaults/advisor/consult.ts +0 -197
  234. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  235. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  236. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  237. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  238. package/src/plugins/defaults/advisor/package.json +0 -14
  239. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
@@ -520,12 +520,14 @@ export async function runAgentLoopImpl(
520
520
  let turnStarted = false;
521
521
  const state = createEventHandlerState();
522
522
  let persistedErrorAssistantMessage = false;
523
+ let deletedReservedAssistantMessage = false;
523
524
 
524
525
  const publishLoopMessagesChanged = (): void => {
525
526
  if (
526
527
  state.lastAssistantMessageId ||
527
528
  state.persistedToolUseIds.size > 0 ||
528
- persistedErrorAssistantMessage
529
+ persistedErrorAssistantMessage ||
530
+ deletedReservedAssistantMessage
529
531
  ) {
530
532
  publishConversationMessagesChanged(ctx.conversationId);
531
533
  }
@@ -1151,23 +1153,22 @@ export async function runAgentLoopImpl(
1151
1153
  !abortController.signal.aborted &&
1152
1154
  !yieldedForHandoff
1153
1155
  ) {
1154
- // Drop any reservation stranded by the failed LLM call before
1155
- // inserting the synthetic error message. The B3 pre-allocation
1156
- // path reserves an empty assistant row at `llm_call_started`;
1157
- // when the call exits through the provider-error branch (no
1158
- // `message_complete`), `assistantRowAwaitingFinalization` stays
1159
- // true. Without this delete the transcript would carry both the
1160
- // empty reserved row AND the error message — and downstream sync
1161
- // (`syncLastAssistantMessageToDisk`) would mis-target the empty
1162
- // row. After delete we set `lastAssistantMessageId` to the new
1163
- // error row's id so the post-loop emission paths still point at
1164
- // a real message.
1156
+ // Drop any reservation stranded by the failed LLM call. The B3
1157
+ // pre-allocation path reserves an empty assistant row at
1158
+ // `llm_call_started`; when the call exits through the provider-error
1159
+ // branch (no `message_complete`), `assistantRowAwaitingFinalization`
1160
+ // stays true. Without this delete the transcript would carry an empty
1161
+ // reserved row, and downstream sync (`syncLastAssistantMessageToDisk`)
1162
+ // would target it.
1165
1163
  if (
1166
1164
  state.assistantRowAwaitingFinalization &&
1167
1165
  state.lastAssistantMessageId
1168
1166
  ) {
1169
1167
  try {
1170
1168
  deleteMessageById(state.lastAssistantMessageId);
1169
+ deletedReservedAssistantMessage = true;
1170
+ state.lastAssistantMessageId = undefined;
1171
+ state.assistantRowAwaitingFinalization = false;
1171
1172
  } catch (err) {
1172
1173
  rlog.warn(
1173
1174
  { err, messageId: state.lastAssistantMessageId },
@@ -1175,57 +1176,63 @@ export async function runAgentLoopImpl(
1175
1176
  );
1176
1177
  }
1177
1178
  }
1178
- const errChannelMeta = {
1179
- ...provenanceFromTrustContext(ctx.trustContext),
1180
- userMessageChannel: capturedTurnChannelContext.userMessageChannel,
1181
- assistantMessageChannel:
1182
- capturedTurnChannelContext.assistantMessageChannel,
1183
- userMessageInterface: capturedTurnInterfaceContext.userMessageInterface,
1184
- assistantMessageInterface:
1185
- capturedTurnInterfaceContext.assistantMessageInterface,
1186
- };
1187
- const errorAssistantMessage = createAssistantMessage(
1188
- state.providerErrorUserMessage,
1189
- );
1190
- const errorRow = await addMessage(
1191
- ctx.conversationId,
1192
- "assistant",
1193
- JSON.stringify(errorAssistantMessage.content),
1194
- { metadata: errChannelMeta },
1195
- );
1196
- persistedErrorAssistantMessage = true;
1197
- // Repoint `lastAssistantMessageId` at the synthetic error row so the
1198
- // post-loop sync, attachment resolution, and `message_complete`/
1199
- // `generation_handoff` emissions all reference a real, persisted
1200
- // message id. The previous reservation (if any) was already deleted
1201
- // above. Mark finalization complete so the next LLM call in this run
1202
- // (or a downstream handler) doesn't try to clean up an id that
1203
- // already corresponds to a finalized row.
1204
- state.lastAssistantMessageId = errorRow.id;
1205
- state.assistantRowAwaitingFinalization = false;
1206
- newMessages.push(errorAssistantMessage);
1207
- // Pipe the just-assigned message id into any orphaned LLM request log
1208
- // row(s) for this turn. The success path links rows via
1209
- // `handleMessageComplete` -> `backfillMessageIdOnLogs`, but provider-
1210
- // failure turns never fire `message_complete` (the synthetic assistant
1211
- // message is persisted directly above), so without this call the rows
1212
- // from `handleProviderError` stay with `message_id IS NULL` and a
1213
- // later turn's backfill sweep would wrong-attach them to that turn's
1214
- // assistant message. Scope is per-conversation, so concurrent runs on
1215
- // other conversations cannot collide. Non-fatal — a DB hiccup must
1216
- // not escalate a provider rejection into a turn-level throw.
1217
- try {
1218
- backfillMessageIdOnLogs(ctx.conversationId, errorRow.id);
1219
- } catch (err) {
1220
- rlog.warn(
1221
- { err },
1222
- "Failed to backfill message_id on provider-error LLM request logs (non-fatal)",
1179
+ if (!state.persistProviderErrorAsAssistantMessage) {
1180
+ state.assistantRowAwaitingFinalization = false;
1181
+ state.lastAssistantMessageId = undefined;
1182
+ } else {
1183
+ const errChannelMeta = {
1184
+ ...provenanceFromTrustContext(ctx.trustContext),
1185
+ userMessageChannel: capturedTurnChannelContext.userMessageChannel,
1186
+ assistantMessageChannel:
1187
+ capturedTurnChannelContext.assistantMessageChannel,
1188
+ userMessageInterface:
1189
+ capturedTurnInterfaceContext.userMessageInterface,
1190
+ assistantMessageInterface:
1191
+ capturedTurnInterfaceContext.assistantMessageInterface,
1192
+ };
1193
+ const errorAssistantMessage = createAssistantMessage(
1194
+ state.providerErrorUserMessage,
1195
+ );
1196
+ const errorRow = await addMessage(
1197
+ ctx.conversationId,
1198
+ "assistant",
1199
+ JSON.stringify(errorAssistantMessage.content),
1200
+ { metadata: errChannelMeta },
1223
1201
  );
1202
+ persistedErrorAssistantMessage = true;
1203
+ // Repoint `lastAssistantMessageId` at the synthetic error row so the
1204
+ // post-loop sync, attachment resolution, and `message_complete`/
1205
+ // `generation_handoff` emissions all reference a real, persisted
1206
+ // message id. The previous reservation (if any) was already deleted
1207
+ // above. Mark finalization complete so the next LLM call in this run
1208
+ // (or a downstream handler) doesn't try to clean up an id that
1209
+ // already corresponds to a finalized row.
1210
+ state.lastAssistantMessageId = errorRow.id;
1211
+ state.assistantRowAwaitingFinalization = false;
1212
+ newMessages.push(errorAssistantMessage);
1213
+ // Pipe the just-assigned message id into any orphaned LLM request log
1214
+ // row(s) for this turn. The success path links rows via
1215
+ // `handleMessageComplete` -> `backfillMessageIdOnLogs`, but provider-
1216
+ // failure turns never fire `message_complete` (the synthetic assistant
1217
+ // message is persisted directly above), so without this call the rows
1218
+ // from `handleProviderError` stay with `message_id IS NULL` and a
1219
+ // later turn's backfill sweep would wrong-attach them to that turn's
1220
+ // assistant message. Scope is per-conversation, so concurrent runs on
1221
+ // other conversations cannot collide. Non-fatal — a DB hiccup must
1222
+ // not escalate a provider rejection into a turn-level throw.
1223
+ try {
1224
+ backfillMessageIdOnLogs(ctx.conversationId, errorRow.id);
1225
+ } catch (err) {
1226
+ rlog.warn(
1227
+ { err },
1228
+ "Failed to backfill message_id on provider-error LLM request logs (non-fatal)",
1229
+ );
1230
+ }
1231
+ // Do NOT send assistant_text_delta here — handleProviderError already
1232
+ // emitted a conversation_error event for this same error text, and the
1233
+ // client renders it as an InlineChatErrorAlert. Sending a text delta
1234
+ // would create a duplicate plain-text bubble below the alert card.
1224
1235
  }
1225
- // Do NOT send assistant_text_delta here — handleProviderError already
1226
- // emitted a conversation_error event for this same error text, and the
1227
- // client renders it as an InlineChatErrorAlert. Sending a text delta
1228
- // would create a duplicate plain-text bubble below the alert card.
1229
1236
  }
1230
1237
 
1231
1238
  // Base persisted into `ctx.messages` is the loop's own returned history
@@ -315,20 +315,17 @@ function classifyCore(
315
315
  }
316
316
  if (error.statusCode === 401 || error.statusCode === 403) {
317
317
  // Both managed-proxy and user-key 401/403s reach this branch.
318
- // Managed-proxy routes through the assistant API key (stale → re-
319
- // provision) and emits `MANAGED_KEY_INVALID`; everything else is a
320
- // user-set credential that the upstream provider rejected → emit
321
- // `PROVIDER_INVALID_KEY` so the macOS chat banner renders an
322
- // "Invalid API key" surface (distinct from "API key required"
323
- // which only fires when the key is genuinely missing — see
324
- // `providerNotConfiguredClassification`).
318
+ // Managed-proxy routes through the assistant API key; if that
319
+ // credential is stale, the user cannot fix it from model settings.
320
+ // Everything else is a user-set credential that the upstream provider
321
+ // rejected, so emit `PROVIDER_INVALID_KEY` and let the chat banner point
322
+ // at Settings.
325
323
  const providerName = error.provider;
326
324
  if (getProviderRoutingSource(providerName) === "managed-proxy") {
327
325
  return {
328
326
  code: "MANAGED_KEY_INVALID",
329
- userMessage:
330
- "The assistant API key is invalid. Attempting to re-provision…",
331
- retryable: true,
327
+ userMessage: "Couldn't refresh assistant credentials.",
328
+ retryable: false,
332
329
  errorCategory: "managed_key_invalid",
333
330
  };
334
331
  }
@@ -17,7 +17,6 @@ import type { LLMCallSite } from "../config/schemas/llm.js";
17
17
  import { getBindingByConversation } from "../memory/external-conversation-store.js";
18
18
  import type { PermissionPrompter } from "../permissions/prompter.js";
19
19
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
20
- import { advisorEnabledForProfile } from "../plugins/defaults/advisor/advisor-gate.js";
21
20
  import type { Message, ToolDefinition } from "../providers/types.js";
22
21
  import { assistantEventHub } from "../runtime/assistant-event-hub.js";
23
22
  import { registerConversationSender } from "../tools/browser/browser-screencast.js";
@@ -575,15 +574,6 @@ export function isToolActiveForContext(
575
574
  return true;
576
575
  }
577
576
  }
578
- if (name === "advisor") {
579
- // Gated per chat-profile (`ProfileEntry.advisorEnabled`): when the resolved
580
- // profile disables the advisor, omit the tool from the wire list so the
581
- // model never sees a tool it can only no-op on. Resolves the profile the
582
- // same way the advisor's execution-time guard does (the per-turn override,
583
- // else the active profile). The wire list is fixed before PRE_MODEL_CALL
584
- // hooks run, so later profile changes from hooks are not reflected here.
585
- return advisorEnabledForProfile(ctx.currentTurnOverrideProfile ?? null);
586
- }
587
577
  if (UI_SURFACE_TOOL_NAMES.has(name)) {
588
578
  if (
589
579
  channelCapabilities?.channel === "slack" &&
@@ -243,6 +243,14 @@ export interface ConversationConstructorOptions {
243
243
  speedOverride?: Speed;
244
244
  cacheTtl?: "5m" | "1h";
245
245
  modelOverride?: string;
246
+ /**
247
+ * Give this conversation's LLM calls provider-native (server-side) web
248
+ * search when the resolved provider supports it (see
249
+ * {@link AgentLoopConfig.enableNativeWebSearch}). Set by the subagent manager
250
+ * for the tool-less advisor consult so it can ground guidance with live web
251
+ * access; non-native providers get nothing. Defaults to false.
252
+ */
253
+ enableNativeWebSearch?: boolean;
246
254
  }
247
255
 
248
256
  export class Conversation {
@@ -592,6 +600,7 @@ export class Conversation {
592
600
  options?: ConversationConstructorOptions,
593
601
  ) {
594
602
  const { maxTokens, speedOverride, cacheTtl, modelOverride } = options ?? {};
603
+ const enableNativeWebSearch = options?.enableNativeWebSearch ?? false;
595
604
  this.conversationId = conversationId;
596
605
  this.systemPrompt = systemPrompt;
597
606
  this.provider = provider;
@@ -690,6 +699,7 @@ export class Conversation {
690
699
  ? { speed: resolvedSpeed }
691
700
  : {}),
692
701
  ...(cacheTtl ? { cacheTtl } : {}),
702
+ ...(enableNativeWebSearch ? { enableNativeWebSearch: true } : {}),
693
703
  };
694
704
  if (configuredMaxTokens !== undefined) {
695
705
  agentLoopConfig.maxTokens = configuredMaxTokens;
@@ -247,7 +247,14 @@ export async function bootstrapPlugins(): Promise<void> {
247
247
  // out-of-band kill switch — the operator creates a directory named
248
248
  // after the plugin's manifest name (e.g. `plugins/default-advisor/`)
249
249
  // and drops a `.disabled` file inside it. Runs before init so no
250
- // hooks, tools, or routes from the disabled plugin are ever wired.
250
+ // tools or routes from the disabled plugin are ever wired.
251
+ //
252
+ // Unlike the feature-flag path above, we do NOT call
253
+ // `unregisterPlugin(name)` here. The plugin's hooks stay in the hook
254
+ // registry and are filtered at read time by `isPluginDisabled` in
255
+ // `getHooksFor`. This means `assistant plugins enable <name>` takes
256
+ // effect on the next turn without a restart — the hooks are already
257
+ // registered, they just need the sentinel removed to be included.
251
258
  const disabledSentinelPath = join(
252
259
  getWorkspacePluginsDir(),
253
260
  name,
@@ -258,7 +265,6 @@ export async function bootstrapPlugins(): Promise<void> {
258
265
  { plugin: name, sentinel: disabledSentinelPath },
259
266
  `skipping plugin ${name}: disabled via .disabled sentinel`,
260
267
  );
261
- unregisterPlugin(name);
262
268
  continue;
263
269
  }
264
270
 
@@ -174,7 +174,6 @@ describe("acceptA2AInvite", () => {
174
174
  expect(contact).not.toBeNull();
175
175
  expect(contact!.channels).toHaveLength(1);
176
176
  expect(contact!.channels[0]!.type).toBe("a2a");
177
- expect(contact!.channels[0]!.status).toBe("active");
178
177
 
179
178
  // Verify assistant metadata
180
179
  const metadata = getAssistantContactMetadata(result.contactId!);
@@ -100,8 +100,6 @@ describe("completeA2AInvite", () => {
100
100
  expect(contact!.displayName).toBe("Acceptor Bot");
101
101
  expect(contact!.channels).toHaveLength(1);
102
102
  expect(contact!.channels[0]!.type).toBe("a2a");
103
- expect(contact!.channels[0]!.status).toBe("active");
104
- expect(contact!.channels[0]!.policy).toBe("allow");
105
103
  });
106
104
 
107
105
  test("contact channel address is acceptor.assistantId.toLowerCase()", () => {
@@ -71,8 +71,6 @@ describe("redeemA2AInvite", () => {
71
71
  expect(contact!.displayName).toBe("Sender Bot");
72
72
  expect(contact!.channels).toHaveLength(1);
73
73
  expect(contact!.channels[0]!.type).toBe("a2a");
74
- expect(contact!.channels[0]!.status).toBe("active");
75
- expect(contact!.channels[0]!.policy).toBe("allow");
76
74
  });
77
75
 
78
76
  test("idempotency: already-connected sender returns alreadyConnected", () => {
@@ -20,11 +20,14 @@ mock.module("../../../contacts/guardian-delivery-reader.js", () => ({
20
20
  input.channelTypes!.includes(g.channelType),
21
21
  );
22
22
  },
23
+ guardianForChannel: (
24
+ list: GuardianDelivery[],
25
+ channelType: string,
26
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
23
27
  }));
24
28
 
25
29
  mock.module("../../../contacts/contact-store.js", () => ({
26
30
  findContactChannel: () => mockContactChannel,
27
- findGuardianForChannel: () => null,
28
31
  getChannelById: () => mockChannel,
29
32
  getContact: () => ({ id: "contact-1", displayName: "Pat" }),
30
33
  }));
@@ -123,15 +126,7 @@ function channel(overrides: Partial<ContactChannel> = {}): ContactChannel {
123
126
  address: "user-123",
124
127
  isPrimary: true,
125
128
  externalChatId: "chat-123",
126
- // DB columns are intentionally a terminal state to prove the gates ignore
127
- // them and read from the gateway delivery instead.
128
- status: "revoked",
129
- policy: {} as ContactChannel["policy"],
130
- verifiedAt: null,
131
- verifiedVia: null,
132
129
  inviteId: null,
133
- revokedReason: null,
134
- blockedReason: null,
135
130
  lastSeenAt: null,
136
131
  interactionCount: 0,
137
132
  lastInteraction: null,
@@ -168,9 +163,9 @@ describe("revokeVerificationForChannel", () => {
168
163
  });
169
164
 
170
165
  test("skips a redundant revoke when the gateway delivery is already revoked", async () => {
171
- // Local DB status is the live "active" here, but the gateway (SoT) says
172
- // revoked the gate must follow the gateway and not relay.
173
- mockContactChannel = { channel: channel({ status: "active" }) };
166
+ // The gateway (SoT) says revoked — the gate must follow the gateway and
167
+ // not relay regardless of local state.
168
+ mockContactChannel = { channel: channel() };
174
169
  mockGuardians = [delivery({ status: "revoked" })];
175
170
  await revokeVerificationForChannel("telegram");
176
171
  expect(ipcCalls.map((c) => c.method)).not.toContain("mark_channel_revoked");
@@ -208,8 +203,8 @@ describe("verifyTrustedContact already-verified gate", () => {
208
203
  });
209
204
 
210
205
  test("does not short-circuit when the gateway channel has no verifiedAt", async () => {
211
- // DB column says verified, but the gateway channel is unverified — proceed.
212
- mockChannel = channel({ status: "active", verifiedAt: 1700000000 });
206
+ // The gateway channel is unverified — proceed regardless of local state.
207
+ mockChannel = channel();
213
208
  mockGwContactChannels = [
214
209
  { id: "ch-1", status: "pending", verifiedAt: null },
215
210
  ];
@@ -1,21 +1,21 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
 
3
3
  import type { GuardianDelivery } from "@vellumai/gateway-client";
4
- import {
5
- GetContactIpcResponseSchema,
6
- MarkChannelRevokedIpcResponseSchema,
7
- } from "@vellumai/gateway-client/gateway-ipc-contracts";
4
+ import { MarkChannelRevokedIpcResponseSchema } from "@vellumai/gateway-client/gateway-ipc-contracts";
8
5
 
9
6
  import { startVerificationCall } from "../../calls/call-domain.js";
10
7
  import type { ChannelId } from "../../channels/types.js";
11
8
  import { emitContactChange } from "../../contacts/contact-events.js";
12
9
  import {
13
10
  findContactChannel,
14
- findGuardianForChannel,
15
11
  getChannelById,
16
12
  getContact,
17
13
  } from "../../contacts/contact-store.js";
18
- import { getGuardianDelivery } from "../../contacts/guardian-delivery-reader.js";
14
+ import { gatewayContactChannelState } from "../../contacts/gateway-channel-read.js";
15
+ import {
16
+ getGuardianDelivery,
17
+ guardianForChannel,
18
+ } from "../../contacts/guardian-delivery-reader.js";
19
19
  import type { ContactChannel } from "../../contacts/types.js";
20
20
  import { ipcCallPersistent } from "../../ipc/gateway-client.js";
21
21
  import { getBindingByChannelChat } from "../../memory/external-conversation-store.js";
@@ -105,25 +105,6 @@ async function deliveryForChannel(
105
105
  );
106
106
  }
107
107
 
108
- /**
109
- * Read a contact channel's verified state from the gateway contact-channel read
110
- * (ACL source of truth). Covers all contacts, not just guardian deliveries.
111
- * Returns `undefined` when the gateway is unreachable or has no such channel.
112
- */
113
- async function gatewayContactChannelState(
114
- channel: Pick<ContactChannel, "id" | "contactId">,
115
- ): Promise<{ status: string; verifiedAt: number | null } | undefined> {
116
- const result = await ipcCallPersistent("contacts_get_rich", {
117
- contactId: channel.contactId,
118
- });
119
- if (!result || (result as { contact?: unknown }).contact == null) {
120
- return undefined;
121
- }
122
- const { contact } = GetContactIpcResponseSchema.parse(result);
123
- const ch = contact.channels.find((c) => c.id === channel.id);
124
- return ch ? { status: ch.status, verifiedAt: ch.verifiedAt } : undefined;
125
- }
126
-
127
108
  // ---------------------------------------------------------------------------
128
109
  // Extracted business logic functions
129
110
  // ---------------------------------------------------------------------------
@@ -170,10 +151,14 @@ export async function getVerificationStatus(
170
151
 
171
152
  const binding = await getGuardianBinding(resolvedAssistantId, resolvedChannel);
172
153
 
173
- // Read the contact directly to get displayName — getGuardianBinding is a
174
- // compatibility shim that doesn't carry metadataJson.
175
- const guardianResult = findGuardianForChannel(resolvedChannel);
176
- const bindingDisplayName = guardianResult?.contact.displayName;
154
+ // Read the guardian displayName from the gateway delivery — getGuardianBinding
155
+ // is a compatibility shim that doesn't carry metadataJson.
156
+ const guardians = await getGuardianDelivery({
157
+ channelTypes: [resolvedChannel],
158
+ });
159
+ const bindingDisplayName = guardians
160
+ ? (guardianForChannel(guardians, resolvedChannel)?.displayName ?? undefined)
161
+ : undefined;
177
162
  const guardianDisplayName = resolveGuardianName(bindingDisplayName);
178
163
 
179
164
  // Resolve username from external conversation store.
@@ -74,6 +74,7 @@ import {
74
74
  resolveSigningKey,
75
75
  } from "../runtime/auth/token-service.js";
76
76
  import { RuntimeHttpServer } from "../runtime/http-server.js";
77
+ import { warmLocalGuardianPrincipalCache } from "../runtime/local-actor-identity.js";
77
78
  import { recoverInterruptedImport } from "../runtime/migrations/vbundle-streaming-importer.js";
78
79
  import { registerSecretsDeps } from "../runtime/routes/secrets-deps.js";
79
80
  import {
@@ -824,6 +825,16 @@ export async function runDaemon(): Promise<void> {
824
825
 
825
826
  await server.start();
826
827
  log.info("Daemon startup: DaemonServer started");
828
+
829
+ // Warm the gateway guardian-delivery cache so the SSE eager-subscribe path
830
+ // (sync, IO-free) resolves the local actor principal on the FIRST client
831
+ // registration. Without this, a cold cache regresses host-proxy same-user
832
+ // targeting until a later reconnect. Non-blocking: failures aren't cached
833
+ // and the async hot paths re-warm on their next read.
834
+ void warmLocalGuardianPrincipalCache().catch((err) =>
835
+ log.warn({ err }, "Guardian principal cache warm failed — continuing"),
836
+ );
837
+
827
838
  startDiskPressureGuardForLifecycle();
828
839
  startOrphanReaper();
829
840
  startEventLoopWatchdog();
@@ -966,10 +977,11 @@ export async function runDaemon(): Promise<void> {
966
977
  }
967
978
  }
968
979
 
969
- // `startMemoryJobsWorker` selects the worker implementation based on
970
- // `memory.worker.enabled` (in-process vs. a separate OS process).
971
- // Shutdown stops whichever worker is actually running see
972
- // shutdown-handlers.ts.
980
+ // `startMemoryJobsWorker` starts the in-process supervisor (which owns
981
+ // the synchronous runner and stands down when an out-of-process worker is
982
+ // live) and spawns the out-of-process worker at boot when
983
+ // `memory.worker.enabled` is set. Shutdown stops whichever worker is
984
+ // actually running — see shutdown-handlers.ts.
973
985
  log.info("Daemon startup: starting memory worker");
974
986
  bgRefs.memoryWorker = startMemoryJobsWorker();
975
987
 
@@ -116,6 +116,8 @@ export interface FormSurfaceData {
116
116
  submitLabel?: string;
117
117
  pages?: FormPage[];
118
118
  pageLabels?: { next?: string; back?: string; submit?: string };
119
+ /** Progress indicator style for multi-page forms: segment bar or labeled tabs. */
120
+ progressStyle?: "bar" | "tabs";
119
121
  }
120
122
 
121
123
  export interface ListItem {
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
 
4
4
  import { getConfig } from "../config/loader.js";
5
5
  import type { HeartbeatConfig } from "../config/schemas/heartbeat.js";
6
+ import { getGuardianDelivery } from "../contacts/guardian-delivery-reader.js";
6
7
  import {
7
8
  checkDiskPressureBackgroundGate,
8
9
  diskPressureBackgroundSkipLogFields,
@@ -761,6 +762,10 @@ export class HeartbeatService {
761
762
 
762
763
  const checklist = this.readChecklist();
763
764
  const completedRunCount = countCompletedHeartbeatRuns();
765
+ // Warm the vellum guardian-delivery cache so buildPrompt's sync guardian
766
+ // persona read (isShallowProfile → resolveGuardianPersona) hits a fresh key
767
+ // instead of falling back to default.md on a cold/TTL-expired cache.
768
+ await getGuardianDelivery({ channelTypes: ["vellum"] });
764
769
  const { prompt, includedReengagement } = this.buildPrompt(
765
770
  checklist,
766
771
  unhealthyProviders,
@@ -21,6 +21,7 @@
21
21
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
22
  import { join } from "node:path";
23
23
 
24
+ import { getGuardianDelivery } from "../contacts/guardian-delivery-reader.js";
24
25
  import { countConversations as countConversationsDb } from "../memory/conversation-queries.js";
25
26
  import { listConnections } from "../oauth/oauth-store.js";
26
27
  import { resolveGuardianPersonaPath } from "../prompts/persona-resolver.js";
@@ -162,6 +163,10 @@ export async function computeRelationshipState(): Promise<RelationshipState> {
162
163
  // old workspaces that never ran migration 031.
163
164
  // 3. Empty string → extraction yields [] and `userName` is undefined.
164
165
  // Every step is guarded because the writer must never throw.
166
+ // Warm the vellum guardian-delivery cache so the sync persona resolution in
167
+ // resolveGuardianUserContent hits a fresh key instead of falling back to
168
+ // default.md on a cold/TTL-expired cache.
169
+ await getGuardianDelivery({ channelTypes: ["vellum"] });
165
170
  const userMd = resolveGuardianUserContent();
166
171
  const soulMd = safeRead(getWorkspacePromptPath("SOUL.md"));
167
172
  const identityPath = getWorkspacePromptPath("IDENTITY.md");