@vellumai/assistant 0.3.27 → 0.4.0

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 (247) hide show
  1. package/ARCHITECTURE.md +81 -4
  2. package/Dockerfile +2 -2
  3. package/bun.lock +4 -1
  4. package/docs/trusted-contact-access.md +9 -2
  5. package/package.json +6 -3
  6. package/scripts/ipc/generate-swift.ts +9 -5
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  8. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  9. package/src/__tests__/agent-loop.test.ts +119 -0
  10. package/src/__tests__/approval-routes-http.test.ts +13 -5
  11. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  12. package/src/__tests__/asset-search-tool.test.ts +2 -0
  13. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  14. package/src/__tests__/attachments-store.test.ts +2 -0
  15. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  16. package/src/__tests__/bundled-asset.test.ts +107 -0
  17. package/src/__tests__/call-controller.test.ts +30 -29
  18. package/src/__tests__/call-routes-http.test.ts +34 -32
  19. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  20. package/src/__tests__/canonical-guardian-store.test.ts +636 -0
  21. package/src/__tests__/channel-approval-routes.test.ts +174 -1
  22. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  23. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  24. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  25. package/src/__tests__/clarification-resolver.test.ts +2 -0
  26. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  27. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  28. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  29. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  30. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  31. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  32. package/src/__tests__/config-schema.test.ts +5 -5
  33. package/src/__tests__/config-watcher.test.ts +3 -1
  34. package/src/__tests__/connection-policy.test.ts +14 -5
  35. package/src/__tests__/contacts-tools.test.ts +3 -1
  36. package/src/__tests__/contradiction-checker.test.ts +2 -0
  37. package/src/__tests__/conversation-pairing.test.ts +10 -0
  38. package/src/__tests__/conversation-routes.test.ts +1 -1
  39. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  40. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  41. package/src/__tests__/credential-vault.test.ts +5 -4
  42. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  43. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  44. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  45. package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
  46. package/src/__tests__/encrypted-store.test.ts +10 -5
  47. package/src/__tests__/followup-tools.test.ts +3 -1
  48. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  49. package/src/__tests__/gmail-integration.test.ts +0 -1
  50. package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
  51. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  52. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
  53. package/src/__tests__/guardian-dispatch.test.ts +21 -19
  54. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  55. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  56. package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
  57. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  58. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  59. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  60. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  61. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  63. package/src/__tests__/heartbeat-service.test.ts +20 -0
  64. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  65. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  66. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  67. package/src/__tests__/intent-routing.test.ts +2 -0
  68. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  69. package/src/__tests__/mcp-cli.test.ts +77 -0
  70. package/src/__tests__/media-generate-image.test.ts +21 -0
  71. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  72. package/src/__tests__/memory-regressions.test.ts +20 -20
  73. package/src/__tests__/non-member-access-request.test.ts +212 -36
  74. package/src/__tests__/notification-decision-fallback.test.ts +63 -3
  75. package/src/__tests__/notification-decision-strategy.test.ts +78 -0
  76. package/src/__tests__/notification-guardian-path.test.ts +15 -15
  77. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  78. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  79. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  80. package/src/__tests__/onboarding-template-contract.test.ts +116 -21
  81. package/src/__tests__/pairing-routes.test.ts +171 -0
  82. package/src/__tests__/playbook-execution.test.ts +3 -1
  83. package/src/__tests__/playbook-tools.test.ts +3 -1
  84. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  85. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  86. package/src/__tests__/recording-handler.test.ts +11 -0
  87. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  88. package/src/__tests__/recording-state-machine.test.ts +13 -2
  89. package/src/__tests__/registry.test.ts +7 -3
  90. package/src/__tests__/relay-server.test.ts +148 -28
  91. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  92. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  93. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  94. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  95. package/src/__tests__/schedule-tools.test.ts +3 -1
  96. package/src/__tests__/secret-scanner-executor.test.ts +59 -0
  97. package/src/__tests__/secret-scanner.test.ts +8 -0
  98. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  99. package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
  100. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  101. package/src/__tests__/session-agent-loop.test.ts +16 -0
  102. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  103. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  104. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  105. package/src/__tests__/session-profile-injection.test.ts +21 -0
  106. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  107. package/src/__tests__/session-queue.test.ts +23 -0
  108. package/src/__tests__/session-runtime-assembly.test.ts +126 -59
  109. package/src/__tests__/session-skill-tools.test.ts +27 -5
  110. package/src/__tests__/session-slash-known.test.ts +23 -0
  111. package/src/__tests__/session-slash-queue.test.ts +23 -0
  112. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  113. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  114. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  115. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  116. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  117. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  118. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  119. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  120. package/src/__tests__/skills.test.ts +8 -4
  121. package/src/__tests__/slack-channel-config.test.ts +3 -1
  122. package/src/__tests__/subagent-tools.test.ts +19 -0
  123. package/src/__tests__/swarm-recursion.test.ts +2 -0
  124. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  125. package/src/__tests__/swarm-tool.test.ts +2 -0
  126. package/src/__tests__/system-prompt.test.ts +3 -1
  127. package/src/__tests__/task-compiler.test.ts +3 -1
  128. package/src/__tests__/task-management-tools.test.ts +3 -1
  129. package/src/__tests__/task-tools.test.ts +3 -1
  130. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  131. package/src/__tests__/terminal-tools.test.ts +2 -0
  132. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  133. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  134. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  135. package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  138. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  139. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  140. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  141. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  142. package/src/__tests__/view-image-tool.test.ts +3 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  145. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  146. package/src/__tests__/work-item-output.test.ts +3 -1
  147. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  148. package/src/agent/loop.ts +46 -3
  149. package/src/approvals/guardian-decision-primitive.ts +285 -0
  150. package/src/approvals/guardian-request-resolvers.ts +539 -0
  151. package/src/calls/call-controller.ts +26 -23
  152. package/src/calls/guardian-action-sweep.ts +10 -2
  153. package/src/calls/guardian-dispatch.ts +46 -40
  154. package/src/calls/relay-server.ts +358 -24
  155. package/src/calls/types.ts +1 -1
  156. package/src/calls/voice-session-bridge.ts +3 -3
  157. package/src/cli.ts +12 -0
  158. package/src/config/agent-schema.ts +14 -3
  159. package/src/config/calls-schema.ts +6 -6
  160. package/src/config/core-schema.ts +3 -3
  161. package/src/config/feature-flag-registry.json +8 -0
  162. package/src/config/mcp-schema.ts +1 -1
  163. package/src/config/memory-schema.ts +27 -19
  164. package/src/config/schema.ts +21 -21
  165. package/src/config/skills-schema.ts +7 -7
  166. package/src/config/system-prompt.ts +2 -1
  167. package/src/config/templates/BOOTSTRAP.md +47 -31
  168. package/src/config/templates/USER.md +5 -0
  169. package/src/config/update-bulletin-template-path.ts +4 -1
  170. package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
  171. package/src/daemon/handlers/config-inbox.ts +4 -4
  172. package/src/daemon/handlers/guardian-actions.ts +45 -66
  173. package/src/daemon/handlers/sessions.ts +148 -4
  174. package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
  175. package/src/daemon/ipc-contract/messages.ts +16 -0
  176. package/src/daemon/ipc-contract-inventory.json +1 -0
  177. package/src/daemon/lifecycle.ts +22 -16
  178. package/src/daemon/pairing-store.ts +86 -3
  179. package/src/daemon/server.ts +18 -0
  180. package/src/daemon/session-agent-loop-handlers.ts +5 -4
  181. package/src/daemon/session-agent-loop.ts +33 -6
  182. package/src/daemon/session-lifecycle.ts +25 -17
  183. package/src/daemon/session-memory.ts +2 -2
  184. package/src/daemon/session-process.ts +68 -326
  185. package/src/daemon/session-runtime-assembly.ts +119 -25
  186. package/src/daemon/session-tool-setup.ts +3 -2
  187. package/src/daemon/session.ts +4 -3
  188. package/src/home-base/prebuilt/seed.ts +2 -1
  189. package/src/hooks/templates.ts +2 -1
  190. package/src/memory/canonical-guardian-store.ts +586 -0
  191. package/src/memory/channel-guardian-store.ts +2 -0
  192. package/src/memory/conversation-crud.ts +7 -7
  193. package/src/memory/db-init.ts +20 -0
  194. package/src/memory/embedding-local.ts +257 -39
  195. package/src/memory/embedding-runtime-manager.ts +471 -0
  196. package/src/memory/guardian-action-store.ts +7 -60
  197. package/src/memory/guardian-approvals.ts +9 -4
  198. package/src/memory/guardian-bindings.ts +25 -1
  199. package/src/memory/indexer.ts +3 -3
  200. package/src/memory/ingress-invite-store.ts +45 -0
  201. package/src/memory/job-handlers/backfill.ts +16 -9
  202. package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
  203. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  204. package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
  205. package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
  206. package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
  207. package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
  208. package/src/memory/migrations/index.ts +5 -0
  209. package/src/memory/migrations/registry.ts +5 -0
  210. package/src/memory/qdrant-client.ts +31 -22
  211. package/src/memory/schema-migration.ts +1 -0
  212. package/src/memory/schema.ts +56 -0
  213. package/src/notifications/copy-composer.ts +31 -4
  214. package/src/notifications/decision-engine.ts +57 -0
  215. package/src/permissions/defaults.ts +2 -0
  216. package/src/runtime/access-request-helper.ts +173 -0
  217. package/src/runtime/actor-trust-resolver.ts +221 -0
  218. package/src/runtime/channel-guardian-service.ts +12 -4
  219. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  220. package/src/runtime/channel-retry-sweep.ts +18 -6
  221. package/src/runtime/guardian-context-resolver.ts +38 -71
  222. package/src/runtime/guardian-decision-types.ts +6 -0
  223. package/src/runtime/guardian-reply-router.ts +717 -0
  224. package/src/runtime/http-server.ts +8 -0
  225. package/src/runtime/ingress-service.ts +80 -3
  226. package/src/runtime/invite-redemption-service.ts +141 -2
  227. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
  228. package/src/runtime/routes/channel-route-shared.ts +1 -1
  229. package/src/runtime/routes/channel-routes.ts +1 -1
  230. package/src/runtime/routes/conversation-routes.ts +20 -2
  231. package/src/runtime/routes/guardian-action-routes.ts +100 -109
  232. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  233. package/src/runtime/routes/inbound-message-handler.ts +205 -529
  234. package/src/runtime/routes/ingress-routes.ts +52 -4
  235. package/src/runtime/routes/pairing-routes.ts +3 -0
  236. package/src/runtime/tool-grant-request-helper.ts +195 -0
  237. package/src/tools/executor.ts +13 -1
  238. package/src/tools/guardian-control-plane-policy.ts +2 -2
  239. package/src/tools/sensitive-output-placeholders.ts +203 -0
  240. package/src/tools/tool-approval-handler.ts +53 -10
  241. package/src/tools/types.ts +13 -2
  242. package/src/util/bundled-asset.ts +31 -0
  243. package/src/util/canonicalize-identity.ts +52 -0
  244. package/src/util/logger.ts +20 -8
  245. package/src/util/platform.ts +10 -0
  246. package/src/util/voice-code.ts +29 -0
  247. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -7,36 +7,20 @@
7
7
  */
8
8
 
9
9
  import { createAssistantMessage,createUserMessage } from '../agent/message-types.js';
10
- import { answerCall } from '../calls/call-domain.js';
11
- import { isTerminalState } from '../calls/call-state-machine.js';
12
- import { getCallSession } from '../calls/call-store.js';
13
10
  import type { TurnChannelContext, TurnInterfaceContext } from '../channels/types.js';
14
11
  import { parseChannelId, parseInterfaceId } from '../channels/types.js';
15
12
  import { getConfig } from '../config/loader.js';
13
+ import {
14
+ listCanonicalGuardianRequests,
15
+ listPendingCanonicalGuardianRequestsByDestinationConversation,
16
+ } from '../memory/canonical-guardian-store.js';
16
17
  import * as conversationStore from '../memory/conversation-store.js';
17
18
  import { provenanceFromGuardianContext } from '../memory/conversation-store.js';
18
- import {
19
- finalizeFollowup,
20
- getDeliveriesByRequestId,
21
- getExpiredDeliveriesByConversation,
22
- getFollowupDeliveriesByConversation,
23
- getGuardianActionRequest,
24
- getPendingDeliveriesByConversation,
25
- getPendingRequestByCallSessionId,
26
- progressFollowupState,
27
- resolveGuardianActionRequest,
28
- startFollowupFromExpiredRequest,
29
- } from '../memory/guardian-action-store.js';
30
19
  import { extractPreferences } from '../notifications/preference-extractor.js';
31
20
  import { createPreference } from '../notifications/preferences-store.js';
32
21
  import type { Message } from '../providers/types.js';
33
- import { processGuardianFollowUpTurn } from '../runtime/guardian-action-conversation-turn.js';
34
- import { executeFollowupAction } from '../runtime/guardian-action-followup-executor.js';
35
- import { tryMintGuardianActionGrant } from '../runtime/guardian-action-grant-minter.js';
36
- import { composeGuardianActionMessageGenerative } from '../runtime/guardian-action-message-composer.js';
37
- import type { ApprovalConversationGenerator, GuardianActionCopyGenerator, GuardianFollowUpConversationGenerator } from '../runtime/http-types.js';
22
+ import { routeGuardianReply } from '../runtime/guardian-reply-router.js';
38
23
  import { getLogger } from '../util/logger.js';
39
- import { resolveGuardianInviteIntent } from './guardian-invite-intent.js';
40
24
  import { resolveGuardianVerificationIntent } from './guardian-verification-intent.js';
41
25
  import type { UsageStats } from './ipc-contract.js';
42
26
  import type { ServerMessage, UserMessageAttachment } from './ipc-protocol.js';
@@ -48,31 +32,6 @@ import type { TraceEmitter } from './trace-emitter.js';
48
32
 
49
33
  const log = getLogger('session-process');
50
34
 
51
- // ---------------------------------------------------------------------------
52
- // Module-level generator injection
53
- // ---------------------------------------------------------------------------
54
- // The daemon lifecycle creates the generator once and injects it here so the
55
- // mac/IPC channel path can classify follow-up replies without threading the
56
- // generator through Session / DaemonServer constructors.
57
- let _guardianFollowUpGenerator: GuardianFollowUpConversationGenerator | undefined;
58
- let _guardianActionCopyGenerator: GuardianActionCopyGenerator | undefined;
59
- let _approvalConversationGenerator: ApprovalConversationGenerator | undefined;
60
-
61
- /** Inject the guardian follow-up conversation generator (called from lifecycle.ts). */
62
- export function setGuardianFollowUpConversationGenerator(gen: GuardianFollowUpConversationGenerator): void {
63
- _guardianFollowUpGenerator = gen;
64
- }
65
-
66
- /** Inject the guardian action copy generator (called from lifecycle.ts). */
67
- export function setGuardianActionCopyGenerator(gen: GuardianActionCopyGenerator): void {
68
- _guardianActionCopyGenerator = gen;
69
- }
70
-
71
- /** Inject the approval conversation generator (called from lifecycle.ts). */
72
- export function setApprovalConversationGenerator(gen: ApprovalConversationGenerator): void {
73
- _approvalConversationGenerator = gen;
74
- }
75
-
76
35
  /** Build a model_info event with fresh config data. */
77
36
  function buildModelInfoEvent(): ServerMessage {
78
37
  const config = getConfig();
@@ -306,15 +265,6 @@ export async function drainQueue(session: ProcessSessionContext, reason: QueueDr
306
265
  log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted in queue — forcing skill flow');
307
266
  agentLoopContent = guardianIntent.rewrittenContent;
308
267
  session.preactivatedSkillIds = ['guardian-verify-setup'];
309
- } else {
310
- // Guardian invite intent interception — force invite management
311
- // requests into the trusted-contacts skill flow.
312
- const inviteIntent = resolveGuardianInviteIntent(resolvedContent);
313
- if (inviteIntent.kind === 'invite_management') {
314
- log.info({ conversationId: session.conversationId, action: inviteIntent.action }, 'Guardian invite intent intercepted in queue — forcing skill flow');
315
- agentLoopContent = inviteIntent.rewrittenContent;
316
- session.preactivatedSkillIds = ['trusted-contacts'];
317
- }
318
268
  }
319
269
  }
320
270
 
@@ -404,275 +354,76 @@ export async function processMessage(
404
354
  await session.ensureActorScopedHistory();
405
355
  session.currentActiveSurfaceId = activeSurfaceId;
406
356
  session.currentPage = currentPage;
357
+ const trimmedContent = content.trim();
358
+ const canonicalPendingRequestIdsForConversation = trimmedContent.length > 0
359
+ ? Array.from(new Set([
360
+ ...listPendingCanonicalGuardianRequestsByDestinationConversation(session.conversationId, 'vellum').map((request) => request.id),
361
+ ...listCanonicalGuardianRequests({
362
+ status: 'pending',
363
+ conversationId: session.conversationId,
364
+ }).map((request) => request.id),
365
+ ]))
366
+ : [];
367
+
368
+ // ── Canonical guardian reply router (desktop/session path) ──
369
+ // Desktop/session guardian replies are canonical-only. Messages consumed
370
+ // by the router never hit the general agent loop.
371
+ if (trimmedContent.length > 0) {
372
+ const routerResult = await routeGuardianReply({
373
+ messageText: trimmedContent,
374
+ channel: 'vellum',
375
+ actor: {
376
+ externalUserId: undefined,
377
+ channel: 'vellum',
378
+ isTrusted: true,
379
+ },
380
+ conversationId: session.conversationId,
381
+ pendingRequestIds: canonicalPendingRequestIdsForConversation,
382
+ // Desktop path: disable NL classification to avoid consuming non-decision
383
+ // messages while a tool confirmation is pending. Deterministic code-prefix
384
+ // and callback parsing remain active.
385
+ approvalConversationGenerator: undefined,
386
+ });
407
387
 
408
- // ── Unified guardian action answer interception (mac channel) ──
409
- // Deterministic priority matching: pending → follow-up → expired.
410
- // When the guardian includes an explicit request code, match it across all
411
- // states in priority order. When only one actionable request exists,
412
- // auto-match without requiring a code prefix.
413
- {
414
- const allPending = getPendingDeliveriesByConversation(session.conversationId);
415
- const allFollowup = getFollowupDeliveriesByConversation(session.conversationId);
416
- const allExpired = getExpiredDeliveriesByConversation(session.conversationId);
417
- const totalActionable = allPending.length + allFollowup.length + allExpired.length;
418
-
419
- if (totalActionable > 0) {
388
+ if (routerResult.consumed) {
420
389
  const guardianIfCtx = session.getTurnInterfaceContext();
421
- const guardianChannelMeta = { userMessageChannel: 'vellum' as const, assistantMessageChannel: 'vellum' as const, userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum', assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum', provenanceActorRole: 'guardian' as const };
422
-
423
- // Try to parse an explicit request code from the message, in priority order
424
- type CodeMatch = { delivery: typeof allPending[0]; request: NonNullable<ReturnType<typeof getGuardianActionRequest>>; state: 'pending' | 'followup' | 'expired'; answerText: string };
425
- let codeMatch: CodeMatch | null = null;
426
- const upperContent = content.toUpperCase();
427
- const orderedSets: Array<{ deliveries: typeof allPending; state: 'pending' | 'followup' | 'expired' }> = [
428
- { deliveries: allPending, state: 'pending' },
429
- { deliveries: allFollowup, state: 'followup' },
430
- { deliveries: allExpired, state: 'expired' },
431
- ];
432
- for (const { deliveries, state } of orderedSets) {
433
- for (const d of deliveries) {
434
- const req = getGuardianActionRequest(d.requestId);
435
- if (req && upperContent.startsWith(req.requestCode)) {
436
- codeMatch = { delivery: d, request: req, state, answerText: content.slice(req.requestCode.length).trim() };
437
- break;
438
- }
439
- }
440
- if (codeMatch) break;
441
- }
442
-
443
- // Explicit code targets a non-pending state: handle terminal superseded
444
- if (codeMatch && codeMatch.state !== 'pending') {
445
- const targetReq = codeMatch.request;
446
- if (targetReq.status === 'expired' && targetReq.expiredReason === 'superseded') {
447
- const callSession = getCallSession(targetReq.callSessionId);
448
- const callStillActive = callSession && !isTerminalState(callSession.status);
449
- if (!callStillActive) {
450
- const userMsg = createUserMessage(content, attachments);
451
- const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
452
- session.messages.push(userMsg);
453
- const staleText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_superseded' }, {}, _guardianActionCopyGenerator);
454
- const staleMsg = createAssistantMessage(staleText);
455
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(staleMsg.content), guardianChannelMeta);
456
- session.messages.push(staleMsg);
457
- onEvent({ type: 'assistant_text_delta', text: staleText });
458
- onEvent({ type: 'message_complete', sessionId: session.conversationId });
459
- return persisted.id;
460
- }
461
- }
462
- }
463
-
464
- // Auto-match: single actionable request across all states
465
- if (!codeMatch && totalActionable === 1) {
466
- const singleDelivery = allPending[0] ?? allFollowup[0] ?? allExpired[0];
467
- const singleReq = getGuardianActionRequest(singleDelivery.requestId);
468
- if (singleReq) {
469
- const state: 'pending' | 'followup' | 'expired' = allPending.length === 1 ? 'pending' : allFollowup.length === 1 ? 'followup' : 'expired';
470
- let text = content;
471
- if (upperContent.startsWith(singleReq.requestCode)) {
472
- text = content.slice(singleReq.requestCode.length).trim();
473
- }
474
- codeMatch = { delivery: singleDelivery, request: singleReq, state, answerText: text };
475
- }
476
- }
390
+ const routerChannelMeta = {
391
+ userMessageChannel: 'vellum' as const,
392
+ assistantMessageChannel: 'vellum' as const,
393
+ userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum',
394
+ assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum',
395
+ provenanceTrustClass: 'guardian' as const,
396
+ };
477
397
 
478
- // Unknown code: message starts with a 6-char alphanumeric token that doesn't match
479
- if (!codeMatch && totalActionable > 0) {
480
- const possibleCodeMatch = content.match(/^([A-F0-9]{6})\s/i);
481
- if (possibleCodeMatch) {
482
- const candidateCode = possibleCodeMatch[1].toUpperCase();
483
- const allDeliveries = [...allPending, ...allFollowup, ...allExpired];
484
- const knownCodes = allDeliveries
485
- .map((d) => { const req = getGuardianActionRequest(d.requestId); return req?.requestCode; })
486
- .filter((code): code is string => typeof code === 'string');
487
- if (!knownCodes.includes(candidateCode)) {
488
- const userMsg = createUserMessage(content, attachments);
489
- const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
490
- session.messages.push(userMsg);
491
- const unknownText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_unknown_code', unknownCode: candidateCode }, {}, _guardianActionCopyGenerator);
492
- const unknownMsg = createAssistantMessage(unknownText);
493
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(unknownMsg.content), guardianChannelMeta);
494
- session.messages.push(unknownMsg);
495
- onEvent({ type: 'assistant_text_delta', text: unknownText });
496
- onEvent({ type: 'message_complete', sessionId: session.conversationId });
497
- return persisted.id;
498
- }
499
- }
500
- }
398
+ const userMsg = createUserMessage(content, attachments);
399
+ const persisted = await conversationStore.addMessage(
400
+ session.conversationId,
401
+ 'user',
402
+ JSON.stringify(userMsg.content),
403
+ routerChannelMeta,
404
+ );
405
+ session.messages.push(userMsg);
501
406
 
502
- // No match and multiple actionable requests → disambiguation
503
- if (!codeMatch && totalActionable > 1) {
504
- const userMsg = createUserMessage(content, attachments);
505
- const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
506
- session.messages.push(userMsg);
507
- const allDeliveries = [...allPending, ...allFollowup, ...allExpired];
508
- const codes = allDeliveries
509
- .map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
510
- .filter((code): code is string => typeof code === 'string' && code.length > 0);
511
- const disambiguationScenario = allPending.length > 0
512
- ? 'guardian_pending_disambiguation' as const
513
- : allFollowup.length > 0
514
- ? 'guardian_followup_disambiguation' as const
515
- : 'guardian_expired_disambiguation' as const;
516
- const disambiguationText = await composeGuardianActionMessageGenerative(
517
- { scenario: disambiguationScenario, requestCodes: codes },
518
- { requiredKeywords: codes },
519
- _guardianActionCopyGenerator,
520
- );
521
- const disambiguationMsg = createAssistantMessage(disambiguationText);
522
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(disambiguationMsg.content), guardianChannelMeta);
523
- session.messages.push(disambiguationMsg);
524
- onEvent({ type: 'assistant_text_delta', text: disambiguationText });
525
- onEvent({ type: 'message_complete', sessionId: session.conversationId });
526
- return persisted.id;
527
- }
407
+ const replyText = routerResult.replyText
408
+ ?? (routerResult.decisionApplied ? 'Decision applied.' : 'Request already resolved.');
409
+ const assistantMsg = createAssistantMessage(replyText);
410
+ await conversationStore.addMessage(
411
+ session.conversationId,
412
+ 'assistant',
413
+ JSON.stringify(assistantMsg.content),
414
+ routerChannelMeta,
415
+ );
416
+ session.messages.push(assistantMsg);
528
417
 
529
- // Dispatch matched delivery by state
530
- if (codeMatch) {
531
- const { request, state, answerText } = codeMatch;
532
-
533
- // PENDING state handler
534
- if (state === 'pending' && request.status === 'pending') {
535
- const userMsg = createUserMessage(content, attachments);
536
- const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
537
- session.messages.push(userMsg);
538
-
539
- const answerResult = await answerCall({ callSessionId: request.callSessionId, answer: answerText, pendingQuestionId: request.pendingQuestionId });
540
-
541
- if ('ok' in answerResult && answerResult.ok) {
542
- const resolved = resolveGuardianActionRequest(request.id, answerText, 'vellum');
543
- if (resolved) {
544
- await tryMintGuardianActionGrant({ request, answerText, decisionChannel: 'vellum', approvalConversationGenerator: _approvalConversationGenerator });
545
- }
546
- const replyText = resolved
547
- ? 'Your answer has been relayed to the call.'
548
- : await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_answered' }, {}, _guardianActionCopyGenerator);
549
- const replyMsg = createAssistantMessage(replyText);
550
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(replyMsg.content), guardianChannelMeta);
551
- session.messages.push(replyMsg);
552
- onEvent({ type: 'assistant_text_delta', text: replyText });
553
- } else {
554
- const errorDetail = 'error' in answerResult ? answerResult.error : 'Unknown error';
555
- log.warn({ callSessionId: request.callSessionId, error: errorDetail }, 'answerCall failed for mac guardian answer');
556
- const failureText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_answer_delivery_failed' }, {}, _guardianActionCopyGenerator);
557
- const failMsg = createAssistantMessage(failureText);
558
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(failMsg.content), guardianChannelMeta);
559
- session.messages.push(failMsg);
560
- onEvent({ type: 'assistant_text_delta', text: failureText });
561
- }
562
- onEvent({ type: 'message_complete', sessionId: session.conversationId });
563
- return persisted.id;
564
- }
418
+ onEvent({ type: 'assistant_text_delta', text: replyText });
419
+ onEvent({ type: 'message_complete', sessionId: session.conversationId });
565
420
 
566
- // FOLLOW-UP state handler
567
- if (state === 'followup' && request.followupState === 'awaiting_guardian_choice') {
568
- const userMsg = createUserMessage(content, attachments);
569
- const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
570
- session.messages.push(userMsg);
571
-
572
- const turnResult = await processGuardianFollowUpTurn(
573
- { questionText: request.questionText, lateAnswerText: request.lateAnswerText ?? '', guardianReply: answerText },
574
- _guardianFollowUpGenerator,
575
- );
576
-
577
- let stateApplied = true;
578
- if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
579
- stateApplied = progressFollowupState(request.id, 'dispatching', turnResult.disposition) !== null;
580
- } else if (turnResult.disposition === 'decline') {
581
- stateApplied = finalizeFollowup(request.id, 'declined') !== null;
582
- }
583
-
584
- if (!stateApplied) {
585
- log.warn({ requestId: request.id, disposition: turnResult.disposition }, 'Follow-up state transition failed (already resolved)');
586
- }
587
-
588
- const replyText = stateApplied
589
- ? turnResult.replyText
590
- : await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_followup' }, {}, _guardianActionCopyGenerator);
591
- const replyMsg = createAssistantMessage(replyText);
592
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(replyMsg.content), guardianChannelMeta);
593
- session.messages.push(replyMsg);
594
- onEvent({ type: 'assistant_text_delta', text: replyText });
595
- onEvent({ type: 'message_complete', sessionId: session.conversationId });
596
-
597
- if (stateApplied && (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back')) {
598
- void (async () => {
599
- try {
600
- const execResult = await executeFollowupAction(request.id, turnResult.disposition as 'call_back' | 'message_back', _guardianActionCopyGenerator);
601
- const completionMsg = createAssistantMessage(execResult.guardianReplyText);
602
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(completionMsg.content), guardianChannelMeta);
603
- session.messages.push(completionMsg);
604
- onEvent({ type: 'assistant_text_delta', text: execResult.guardianReplyText });
605
- onEvent({ type: 'message_complete', sessionId: session.conversationId });
606
- } catch (execErr) {
607
- log.error({ err: execErr, requestId: request.id }, 'Follow-up action execution or completion message failed');
608
- }
609
- })();
610
- }
611
- return persisted.id;
612
- }
421
+ log.info(
422
+ { conversationId: session.conversationId, routerType: routerResult.type, requestId: routerResult.requestId },
423
+ 'Session guardian reply routed through canonical pipeline',
424
+ );
613
425
 
614
- // EXPIRED state handler
615
- if (state === 'expired' && request.status === 'expired' && request.followupState === 'none') {
616
- const userMsg = createUserMessage(content, attachments);
617
- const persisted = await conversationStore.addMessage(session.conversationId, 'user', JSON.stringify(userMsg.content), guardianChannelMeta);
618
- session.messages.push(userMsg);
619
-
620
- // Superseded remap
621
- if (request.expiredReason === 'superseded') {
622
- const callSession = getCallSession(request.callSessionId);
623
- const callStillActive = callSession && !isTerminalState(callSession.status);
624
- const currentPending = callStillActive ? getPendingRequestByCallSessionId(request.callSessionId) : null;
625
-
626
- if (callStillActive && currentPending) {
627
- const currentDeliveries = getDeliveriesByRequestId(currentPending.id);
628
- const guardianExtUserId = session.guardianContext?.guardianExternalUserId;
629
- // When guardianExternalUserId is present, verify the sender has a
630
- // matching delivery on the current pending request. When it's absent
631
- // (trusted Vellum/HTTP session), allow the remap without delivery check.
632
- const senderHasDelivery = guardianExtUserId
633
- ? currentDeliveries.some((d) => d.destinationExternalUserId === guardianExtUserId)
634
- : true;
635
- if (!senderHasDelivery) {
636
- log.info({ supersededRequestId: request.id, currentRequestId: currentPending.id, guardianExternalUserId: guardianExtUserId }, 'Superseded remap skipped: sender has no delivery on current pending request');
637
- } else {
638
- const remapResult = await answerCall({ callSessionId: currentPending.callSessionId, answer: answerText, pendingQuestionId: currentPending.pendingQuestionId });
639
- if ('ok' in remapResult && remapResult.ok) {
640
- const resolved = resolveGuardianActionRequest(currentPending.id, answerText, 'vellum');
641
- if (resolved) {
642
- await tryMintGuardianActionGrant({ request: currentPending, answerText, decisionChannel: 'vellum', approvalConversationGenerator: _approvalConversationGenerator });
643
- }
644
- const remapText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_superseded_remap', questionText: currentPending.questionText }, {}, _guardianActionCopyGenerator);
645
- const remapMsg = createAssistantMessage(remapText);
646
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(remapMsg.content), guardianChannelMeta);
647
- session.messages.push(remapMsg);
648
- onEvent({ type: 'assistant_text_delta', text: remapText });
649
- onEvent({ type: 'message_complete', sessionId: session.conversationId });
650
- log.info({ supersededRequestId: request.id, remappedToRequestId: currentPending.id }, 'Late approval for superseded request remapped to current pending request');
651
- return persisted.id;
652
- }
653
- log.warn({ callSessionId: currentPending.callSessionId, error: 'error' in remapResult ? remapResult.error : 'unknown' }, 'Superseded remap answerCall failed, falling through to follow-up');
654
- }
655
- }
656
- }
657
-
658
- const followupResult = startFollowupFromExpiredRequest(request.id, answerText);
659
- if (followupResult) {
660
- const followupText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_late_answer_followup', questionText: request.questionText, lateAnswerText: answerText }, {}, _guardianActionCopyGenerator);
661
- const replyMsg = createAssistantMessage(followupText);
662
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(replyMsg.content), guardianChannelMeta);
663
- session.messages.push(replyMsg);
664
- onEvent({ type: 'assistant_text_delta', text: followupText });
665
- } else {
666
- const staleText = await composeGuardianActionMessageGenerative({ scenario: 'guardian_stale_expired' }, {}, _guardianActionCopyGenerator);
667
- const staleMsg = createAssistantMessage(staleText);
668
- await conversationStore.addMessage(session.conversationId, 'assistant', JSON.stringify(staleMsg.content), guardianChannelMeta);
669
- session.messages.push(staleMsg);
670
- onEvent({ type: 'assistant_text_delta', text: staleText });
671
- }
672
- onEvent({ type: 'message_complete', sessionId: session.conversationId });
673
- return persisted.id;
674
- }
675
- }
426
+ return persisted.id;
676
427
  }
677
428
  }
678
429
 
@@ -759,15 +510,6 @@ export async function processMessage(
759
510
  log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted — forcing skill flow');
760
511
  agentLoopContent = guardianIntent.rewrittenContent;
761
512
  session.preactivatedSkillIds = ['guardian-verify-setup'];
762
- } else {
763
- // Guardian invite intent interception — force invite management
764
- // requests into the trusted-contacts skill flow.
765
- const inviteIntent = resolveGuardianInviteIntent(resolvedContent);
766
- if (inviteIntent.kind === 'invite_management') {
767
- log.info({ conversationId: session.conversationId, action: inviteIntent.action }, 'Guardian invite intent intercepted — forcing skill flow');
768
- agentLoopContent = inviteIntent.rewrittenContent;
769
- session.preactivatedSkillIds = ['trusted-contacts'];
770
- }
771
513
  }
772
514
  }
773
515