@vellumai/assistant 0.4.2 → 0.4.4

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 (221) hide show
  1. package/.env.example +3 -0
  2. package/ARCHITECTURE.md +124 -10
  3. package/README.md +43 -35
  4. package/docs/trusted-contact-access.md +20 -0
  5. package/package.json +1 -1
  6. package/scripts/ipc/generate-swift.ts +1 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
  8. package/src/__tests__/access-request-decision.test.ts +0 -1
  9. package/src/__tests__/actor-token-service.test.ts +1099 -0
  10. package/src/__tests__/agent-loop.test.ts +51 -0
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
  14. package/src/__tests__/call-controller.test.ts +49 -0
  15. package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
  16. package/src/__tests__/call-pointer-messages.test.ts +93 -3
  17. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
  18. package/src/__tests__/call-routes-http.test.ts +0 -25
  19. package/src/__tests__/callback-handoff-copy.test.ts +186 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +133 -12
  21. package/src/__tests__/channel-guardian.test.ts +0 -86
  22. package/src/__tests__/channel-readiness-service.test.ts +10 -16
  23. package/src/__tests__/checker.test.ts +33 -12
  24. package/src/__tests__/config-schema.test.ts +6 -0
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
  26. package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
  27. package/src/__tests__/conversation-routes.test.ts +12 -3
  28. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  29. package/src/__tests__/daemon-server-session-init.test.ts +4 -0
  30. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  31. package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
  32. package/src/__tests__/guardian-dispatch.test.ts +8 -0
  33. package/src/__tests__/guardian-outbound-http.test.ts +4 -5
  34. package/src/__tests__/guardian-question-mode.test.ts +200 -0
  35. package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
  36. package/src/__tests__/guardian-routing-state.test.ts +525 -0
  37. package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
  38. package/src/__tests__/handlers-telegram-config.test.ts +0 -83
  39. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
  40. package/src/__tests__/headless-browser-navigate.test.ts +2 -0
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  42. package/src/__tests__/ingress-routes-http.test.ts +55 -0
  43. package/src/__tests__/ipc-snapshot.test.ts +18 -51
  44. package/src/__tests__/non-member-access-request.test.ts +159 -9
  45. package/src/__tests__/notification-decision-fallback.test.ts +129 -4
  46. package/src/__tests__/notification-decision-strategy.test.ts +106 -2
  47. package/src/__tests__/notification-guardian-path.test.ts +3 -0
  48. package/src/__tests__/recording-intent-handler.test.ts +1 -0
  49. package/src/__tests__/relay-server.test.ts +1475 -33
  50. package/src/__tests__/send-endpoint-busy.test.ts +5 -0
  51. package/src/__tests__/session-agent-loop.test.ts +1 -0
  52. package/src/__tests__/session-confirmation-signals.test.ts +523 -0
  53. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  54. package/src/__tests__/session-runtime-assembly.test.ts +4 -1
  55. package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
  56. package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
  57. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
  58. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
  59. package/src/__tests__/tool-executor.test.ts +21 -2
  60. package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
  61. package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
  62. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
  63. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
  64. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  65. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  66. package/src/__tests__/twilio-config.test.ts +2 -13
  67. package/src/__tests__/twilio-routes.test.ts +4 -3
  68. package/src/__tests__/update-bulletin.test.ts +0 -1
  69. package/src/agent/loop.ts +1 -1
  70. package/src/approvals/guardian-decision-primitive.ts +12 -3
  71. package/src/approvals/guardian-request-resolvers.ts +169 -11
  72. package/src/calls/call-constants.ts +29 -0
  73. package/src/calls/call-controller.ts +11 -3
  74. package/src/calls/call-domain.ts +33 -11
  75. package/src/calls/call-pointer-message-composer.ts +154 -0
  76. package/src/calls/call-pointer-messages.ts +106 -27
  77. package/src/calls/guardian-dispatch.ts +4 -2
  78. package/src/calls/relay-server.ts +921 -112
  79. package/src/calls/twilio-config.ts +4 -11
  80. package/src/calls/twilio-routes.ts +4 -6
  81. package/src/calls/types.ts +3 -1
  82. package/src/calls/voice-session-bridge.ts +4 -3
  83. package/src/cli/core-commands.ts +7 -4
  84. package/src/cli.ts +5 -4
  85. package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
  86. package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
  87. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
  88. package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
  89. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
  90. package/src/config/bundled-skills/messaging/SKILL.md +61 -12
  91. package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
  92. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
  93. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
  94. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
  96. package/src/config/bundled-skills/twitter/SKILL.md +3 -3
  97. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
  98. package/src/config/calls-schema.ts +36 -0
  99. package/src/config/env.ts +22 -0
  100. package/src/config/feature-flag-registry.json +8 -8
  101. package/src/config/schema.ts +2 -2
  102. package/src/config/skills.ts +11 -0
  103. package/src/config/system-prompt.ts +11 -1
  104. package/src/config/templates/SOUL.md +2 -0
  105. package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
  106. package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
  107. package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
  108. package/src/daemon/call-pointer-generators.ts +59 -0
  109. package/src/daemon/computer-use-session.ts +2 -5
  110. package/src/daemon/handlers/apps.ts +76 -20
  111. package/src/daemon/handlers/config-channels.ts +9 -61
  112. package/src/daemon/handlers/config-inbox.ts +11 -3
  113. package/src/daemon/handlers/config-ingress.ts +28 -3
  114. package/src/daemon/handlers/config-telegram.ts +12 -0
  115. package/src/daemon/handlers/config.ts +2 -6
  116. package/src/daemon/handlers/index.ts +2 -1
  117. package/src/daemon/handlers/pairing.ts +2 -0
  118. package/src/daemon/handlers/publish.ts +11 -46
  119. package/src/daemon/handlers/sessions.ts +59 -5
  120. package/src/daemon/handlers/shared.ts +17 -2
  121. package/src/daemon/ipc-contract/apps.ts +1 -0
  122. package/src/daemon/ipc-contract/inbox.ts +4 -0
  123. package/src/daemon/ipc-contract/integrations.ts +1 -97
  124. package/src/daemon/ipc-contract/messages.ts +47 -1
  125. package/src/daemon/ipc-contract/notifications.ts +11 -0
  126. package/src/daemon/ipc-contract-inventory.json +2 -4
  127. package/src/daemon/lifecycle.ts +17 -0
  128. package/src/daemon/server.ts +16 -2
  129. package/src/daemon/session-agent-loop-handlers.ts +20 -0
  130. package/src/daemon/session-agent-loop.ts +24 -12
  131. package/src/daemon/session-lifecycle.ts +1 -1
  132. package/src/daemon/session-process.ts +11 -1
  133. package/src/daemon/session-runtime-assembly.ts +6 -1
  134. package/src/daemon/session-surfaces.ts +32 -3
  135. package/src/daemon/session.ts +88 -1
  136. package/src/daemon/tool-side-effects.ts +22 -0
  137. package/src/home-base/prebuilt/brain-graph.html +1483 -0
  138. package/src/home-base/prebuilt/index.html +40 -0
  139. package/src/inbound/platform-callback-registration.ts +157 -0
  140. package/src/memory/canonical-guardian-store.ts +1 -1
  141. package/src/memory/conversation-crud.ts +2 -1
  142. package/src/memory/conversation-title-service.ts +16 -2
  143. package/src/memory/db-init.ts +8 -0
  144. package/src/memory/delivery-crud.ts +2 -1
  145. package/src/memory/guardian-action-store.ts +2 -1
  146. package/src/memory/guardian-approvals.ts +3 -2
  147. package/src/memory/ingress-invite-store.ts +12 -2
  148. package/src/memory/ingress-member-store.ts +4 -3
  149. package/src/memory/migrations/038-actor-token-records.ts +39 -0
  150. package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
  151. package/src/memory/migrations/index.ts +2 -0
  152. package/src/memory/schema.ts +26 -5
  153. package/src/messaging/provider-types.ts +24 -0
  154. package/src/messaging/provider.ts +7 -0
  155. package/src/messaging/providers/gmail/adapter.ts +127 -0
  156. package/src/messaging/providers/sms/adapter.ts +40 -37
  157. package/src/notifications/adapters/macos.ts +45 -2
  158. package/src/notifications/broadcaster.ts +16 -0
  159. package/src/notifications/copy-composer.ts +50 -2
  160. package/src/notifications/decision-engine.ts +22 -9
  161. package/src/notifications/destination-resolver.ts +16 -2
  162. package/src/notifications/emit-signal.ts +18 -9
  163. package/src/notifications/guardian-question-mode.ts +419 -0
  164. package/src/notifications/signal.ts +14 -3
  165. package/src/permissions/checker.ts +13 -1
  166. package/src/permissions/prompter.ts +14 -0
  167. package/src/providers/anthropic/client.ts +20 -0
  168. package/src/providers/provider-send-message.ts +15 -3
  169. package/src/runtime/access-request-helper.ts +82 -4
  170. package/src/runtime/actor-token-service.ts +234 -0
  171. package/src/runtime/actor-token-store.ts +236 -0
  172. package/src/runtime/actor-trust-resolver.ts +2 -2
  173. package/src/runtime/assistant-scope.ts +10 -0
  174. package/src/runtime/channel-approvals.ts +5 -3
  175. package/src/runtime/channel-readiness-service.ts +23 -64
  176. package/src/runtime/channel-readiness-types.ts +3 -4
  177. package/src/runtime/channel-retry-sweep.ts +4 -1
  178. package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
  179. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  180. package/src/runtime/guardian-context-resolver.ts +82 -0
  181. package/src/runtime/guardian-outbound-actions.ts +5 -7
  182. package/src/runtime/guardian-reply-router.ts +67 -30
  183. package/src/runtime/guardian-vellum-migration.ts +57 -0
  184. package/src/runtime/http-server.ts +75 -31
  185. package/src/runtime/http-types.ts +13 -0
  186. package/src/runtime/ingress-service.ts +14 -0
  187. package/src/runtime/invite-redemption-service.ts +10 -1
  188. package/src/runtime/local-actor-identity.ts +76 -0
  189. package/src/runtime/middleware/actor-token.ts +271 -0
  190. package/src/runtime/middleware/twilio-validation.ts +2 -4
  191. package/src/runtime/routes/approval-routes.ts +82 -7
  192. package/src/runtime/routes/brain-graph-routes.ts +222 -0
  193. package/src/runtime/routes/call-routes.ts +2 -1
  194. package/src/runtime/routes/channel-readiness-routes.ts +71 -0
  195. package/src/runtime/routes/channel-route-shared.ts +3 -3
  196. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  197. package/src/runtime/routes/conversation-routes.ts +142 -53
  198. package/src/runtime/routes/events-routes.ts +22 -8
  199. package/src/runtime/routes/guardian-action-routes.ts +45 -3
  200. package/src/runtime/routes/guardian-approval-interception.ts +29 -0
  201. package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
  202. package/src/runtime/routes/inbound-conversation.ts +4 -3
  203. package/src/runtime/routes/inbound-message-handler.ts +147 -5
  204. package/src/runtime/routes/ingress-routes.ts +2 -0
  205. package/src/runtime/routes/integration-routes.ts +7 -15
  206. package/src/runtime/routes/pairing-routes.ts +163 -0
  207. package/src/runtime/routes/twilio-routes.ts +934 -0
  208. package/src/runtime/tool-grant-request-helper.ts +3 -1
  209. package/src/security/oauth2.ts +27 -2
  210. package/src/security/token-manager.ts +46 -10
  211. package/src/tools/browser/browser-execution.ts +4 -3
  212. package/src/tools/browser/browser-handoff.ts +10 -18
  213. package/src/tools/browser/browser-manager.ts +80 -25
  214. package/src/tools/browser/browser-screencast.ts +35 -119
  215. package/src/tools/calls/call-start.ts +2 -1
  216. package/src/tools/permission-checker.ts +15 -4
  217. package/src/tools/terminal/parser.ts +12 -0
  218. package/src/tools/tool-approval-handler.ts +244 -19
  219. package/src/workspace/git-service.ts +19 -0
  220. package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
  221. package/src/daemon/handlers/config-twilio.ts +0 -1082
@@ -9,11 +9,13 @@ import { getTwilioUserPhoneNumber } from '../config/env.js';
9
9
  import { loadConfig } from '../config/loader.js';
10
10
  import { VALID_CALLER_IDENTITY_MODES } from '../config/schema.js';
11
11
  import type { AssistantConfig } from '../config/types.js';
12
+ import { resolveCallbackUrl } from '../inbound/platform-callback-registration.js';
12
13
  import { getTwilioStatusCallbackUrl,getTwilioVoiceWebhookUrl } from '../inbound/public-ingress-urls.js';
13
14
  import { getOrCreateConversation } from '../memory/conversation-key-store.js';
14
15
  import { queueGenerateConversationTitle } from '../memory/conversation-title-service.js';
15
16
  import { upsertBinding } from '../memory/external-conversation-store.js';
16
17
  import { revokeScopedApprovalGrantsForContext } from '../memory/scoped-approval-grants.js';
18
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
17
19
  import { isGuardian } from '../runtime/channel-guardian-service.js';
18
20
  import { getSecureKey } from '../security/secure-keys.js';
19
21
  import { getLogger } from '../util/logger.js';
@@ -100,8 +102,7 @@ export type CallerIdentityResult =
100
102
  * - Otherwise, always use `assistant_number` (implicit default).
101
103
  *
102
104
  * For `assistant_number`: uses the Twilio phone number from
103
- * `getTwilioConfig(assistantId)` so multi-assistant mappings are honored.
104
- * No eligibility check is performed — this is a fast path.
105
+ * `getTwilioConfig()`. No eligibility check is performed — this is a fast path.
105
106
  * For `user_number`: uses `config.calls.callerIdentity.userNumber` or the
106
107
  * secure key `credential:twilio:user_phone_number`, then validates that the
107
108
  * number is usable as an outbound caller ID via the Twilio API.
@@ -133,7 +134,7 @@ export async function resolveCallerIdentity(
133
134
 
134
135
  if (mode === 'assistant_number') {
135
136
  const twilioConfig = getTwilioConfig(assistantId);
136
- log.info({ mode, source, fromNumber: twilioConfig.phoneNumber, assistantId }, 'Resolved caller identity');
137
+ log.info({ mode, source, fromNumber: twilioConfig.phoneNumber }, 'Resolved caller identity');
137
138
  return { ok: true, mode, fromNumber: twilioConfig.phoneNumber, source };
138
139
  }
139
140
 
@@ -208,7 +209,7 @@ export type CreateInboundVoiceSessionResult = {
208
209
  export function createInboundVoiceSession(
209
210
  input: CreateInboundVoiceSessionInput,
210
211
  ): CreateInboundVoiceSessionResult {
211
- const { callSid, fromNumber, toNumber, assistantId = 'self' } = input;
212
+ const { callSid, fromNumber, toNumber, assistantId = DAEMON_INTERNAL_ASSISTANT_ID } = input;
212
213
 
213
214
  // Check if a session already exists for this CallSid (replay protection)
214
215
  const existing = getCallSessionByCallSid(callSid);
@@ -219,7 +220,7 @@ export function createInboundVoiceSession(
219
220
 
220
221
  // Create a dedicated voice conversation keyed by CallSid so inbound calls
221
222
  // get their own conversation thread.
222
- const voiceConvKey = assistantId && assistantId !== 'self'
223
+ const voiceConvKey = assistantId && assistantId !== DAEMON_INTERNAL_ASSISTANT_ID
223
224
  ? `asst:${assistantId}:voice:inbound:${callSid}`
224
225
  : `voice:inbound:${callSid}`;
225
226
  const { conversationId: voiceConversationId } = getOrCreateConversation(voiceConvKey);
@@ -272,7 +273,7 @@ export function createInboundVoiceSession(
272
273
  * Initiate a new outbound call.
273
274
  */
274
275
  export async function startCall(input: StartCallInput): Promise<StartCallResult | CallError> {
275
- const { phoneNumber, task, context: callContext, conversationId, callerIdentityMode, assistantId = 'self' } = input;
276
+ const { phoneNumber, task, context: callContext, conversationId, callerIdentityMode, assistantId = DAEMON_INTERNAL_ASSISTANT_ID } = input;
276
277
 
277
278
  if (!phoneNumber || typeof phoneNumber !== 'string') {
278
279
  return { ok: false, error: 'phone_number is required and must be a string', status: 400 };
@@ -357,11 +358,23 @@ export async function startCall(input: StartCallInput): Promise<StartCallResult
357
358
 
358
359
  log.info({ callSessionId: session.id, voiceConversationId, initiatedFrom: conversationId, to: phoneNumber, from: fromNumber, task }, 'Initiating outbound call');
359
360
 
361
+ const webhookUrl = await resolveCallbackUrl(
362
+ () => getTwilioVoiceWebhookUrl(ingressConfig, session.id),
363
+ 'webhooks/twilio/voice',
364
+ 'twilio_voice',
365
+ { callSessionId: session.id },
366
+ );
367
+ const statusCallbackUrl = await resolveCallbackUrl(
368
+ () => getTwilioStatusCallbackUrl(ingressConfig),
369
+ 'webhooks/twilio/status',
370
+ 'twilio_status',
371
+ );
372
+
360
373
  const { callSid } = await provider.initiateCall({
361
374
  from: fromNumber,
362
375
  to: phoneNumber,
363
- webhookUrl: getTwilioVoiceWebhookUrl(ingressConfig, session.id),
364
- statusCallbackUrl: getTwilioStatusCallbackUrl(ingressConfig),
376
+ webhookUrl,
377
+ statusCallbackUrl,
365
378
  });
366
379
 
367
380
  updateCallSession(session.id, { providerCallSid: callSid });
@@ -644,7 +657,7 @@ export type StartGuardianVerificationCallResult =
644
657
  export async function startGuardianVerificationCall(
645
658
  input: StartGuardianVerificationCallInput,
646
659
  ): Promise<StartGuardianVerificationCallResult> {
647
- const { phoneNumber, guardianVerificationSessionId, assistantId = 'self', originConversationId } = input;
660
+ const { phoneNumber, guardianVerificationSessionId, assistantId = DAEMON_INTERNAL_ASSISTANT_ID, originConversationId } = input;
648
661
 
649
662
  if (!phoneNumber || !E164_REGEX.test(phoneNumber)) {
650
663
  return { ok: false, error: 'phone_number must be in E.164 format', status: 400 };
@@ -686,8 +699,17 @@ export async function startGuardianVerificationCall(
686
699
  });
687
700
  sessionId = session.id;
688
701
 
689
- const webhookUrl = getTwilioVoiceWebhookUrl(config, session.id);
690
- const statusCallbackUrl = getTwilioStatusCallbackUrl(config);
702
+ const webhookUrl = await resolveCallbackUrl(
703
+ () => getTwilioVoiceWebhookUrl(config, session.id),
704
+ 'webhooks/twilio/voice',
705
+ 'twilio_voice',
706
+ { callSessionId: session.id },
707
+ );
708
+ const statusCallbackUrl = await resolveCallbackUrl(
709
+ () => getTwilioStatusCallbackUrl(config),
710
+ 'webhooks/twilio/status',
711
+ 'twilio_status',
712
+ );
691
713
 
692
714
  const { callSid } = await provider.initiateCall({
693
715
  from: identityResult.fromNumber,
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Layered call pointer message composition system.
3
+ *
4
+ * Generates pointer/status copy through a priority chain:
5
+ * 1. Generator-produced text (when provided by daemon and audience is trusted)
6
+ * 2. Deterministic fallback templates (preserving existing semantics)
7
+ *
8
+ * Follows the same pattern as approval-message-composer.ts and
9
+ * guardian-action-message-composer.ts.
10
+ */
11
+ import type { PointerCopyGenerator } from '../runtime/http-types.js';
12
+ import { getLogger } from '../util/logger.js';
13
+
14
+ const log = getLogger('call-pointer-message-composer');
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export type CallPointerMessageScenario =
21
+ | 'started'
22
+ | 'completed'
23
+ | 'failed'
24
+ | 'guardian_verification_succeeded'
25
+ | 'guardian_verification_failed';
26
+
27
+ export interface CallPointerMessageContext {
28
+ scenario: CallPointerMessageScenario;
29
+ phoneNumber: string;
30
+ duration?: string;
31
+ reason?: string;
32
+ verificationCode?: string;
33
+ channel?: string;
34
+ }
35
+
36
+ export interface ComposeCallPointerMessageOptions {
37
+ fallbackText?: string;
38
+ requiredFacts?: string[];
39
+ maxTokens?: number;
40
+ timeoutMs?: number;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Constants (exported for the daemon-injected generator implementation)
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export const POINTER_COPY_TIMEOUT_MS = 3_000;
48
+ export const POINTER_COPY_MAX_TOKENS = 120;
49
+ export const POINTER_COPY_SYSTEM_PROMPT =
50
+ 'You are an assistant writing a brief status update about a phone call. '
51
+ + 'Keep it concise (1-2 sentences), natural, and informative. '
52
+ + 'Preserve all factual details exactly (phone numbers, durations, failure reasons, verification status). '
53
+ + 'Do not mention internal systems or technical details. '
54
+ + 'Return plain text only.';
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Public API
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Compose pointer copy using the daemon-injected generator when available,
62
+ * with deterministic fallback for reliability.
63
+ *
64
+ * The generator parameter is the daemon-provided function that knows about
65
+ * providers. When absent (or in test env), only the deterministic fallback
66
+ * is used.
67
+ */
68
+ export async function composeCallPointerMessageGenerative(
69
+ context: CallPointerMessageContext,
70
+ options: ComposeCallPointerMessageOptions = {},
71
+ generator?: PointerCopyGenerator,
72
+ ): Promise<string> {
73
+ const fallbackText = options.fallbackText?.trim() || getPointerFallbackMessage(context);
74
+
75
+ if (process.env.NODE_ENV === 'test') {
76
+ return fallbackText;
77
+ }
78
+
79
+ if (generator) {
80
+ try {
81
+ const generated = await generator(context, options);
82
+ if (generated) return generated;
83
+ } catch (err) {
84
+ log.warn({ err, scenario: context.scenario }, 'Failed to generate pointer copy, using fallback');
85
+ }
86
+ }
87
+
88
+ return fallbackText;
89
+ }
90
+
91
+ /** @internal Exported for use by the daemon-injected generator implementation. */
92
+ export function buildPointerGenerationPrompt(
93
+ context: CallPointerMessageContext,
94
+ fallbackText: string,
95
+ requiredFacts: string[] | undefined,
96
+ ): string {
97
+ const factClause = requiredFacts && requiredFacts.length > 0
98
+ ? `Required facts to include: ${requiredFacts.join(', ')}.\n`
99
+ : '';
100
+ return [
101
+ 'Rewrite the following call status message as a natural, conversational update.',
102
+ 'Keep the same concrete facts (phone number, duration, failure reason, verification status).',
103
+ factClause,
104
+ `Context JSON: ${JSON.stringify(context)}`,
105
+ `Fallback message: ${fallbackText}`,
106
+ ].filter(Boolean).join('\n\n');
107
+ }
108
+
109
+ /** @internal Exported for use by the daemon-injected generator implementation. */
110
+ export function includesRequiredFacts(text: string, requiredFacts: string[] | undefined): boolean {
111
+ if (!requiredFacts || requiredFacts.length === 0) return true;
112
+ return requiredFacts.every((fact) => text.includes(fact));
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Deterministic fallback templates
117
+ // ---------------------------------------------------------------------------
118
+
119
+ /**
120
+ * Return a scenario-specific deterministic fallback message.
121
+ *
122
+ * These preserve the exact semantics of the original hard-coded pointer
123
+ * templates from call-pointer-messages.ts.
124
+ */
125
+ export function getPointerFallbackMessage(context: CallPointerMessageContext): string {
126
+ switch (context.scenario) {
127
+ case 'started':
128
+ return context.verificationCode
129
+ ? `\u{1F4DE} Call to ${context.phoneNumber} started. Verification code: ${context.verificationCode}`
130
+ : `\u{1F4DE} Call to ${context.phoneNumber} started.`;
131
+ case 'completed':
132
+ return context.duration
133
+ ? `\u{1F4DE} Call to ${context.phoneNumber} completed (${context.duration}).`
134
+ : `\u{1F4DE} Call to ${context.phoneNumber} completed.`;
135
+ case 'failed':
136
+ return context.reason
137
+ ? `\u{1F4DE} Call to ${context.phoneNumber} failed: ${context.reason}.`
138
+ : `\u{1F4DE} Call to ${context.phoneNumber} failed.`;
139
+ case 'guardian_verification_succeeded': {
140
+ const ch = context.channel ?? 'voice';
141
+ return `\u{2705} Guardian verification (${ch}) for ${context.phoneNumber} succeeded.`;
142
+ }
143
+ case 'guardian_verification_failed': {
144
+ const ch = context.channel ?? 'voice';
145
+ return context.reason
146
+ ? `\u{274C} Guardian verification (${ch}) for ${context.phoneNumber} failed: ${context.reason}.`
147
+ : `\u{274C} Guardian verification (${ch}) for ${context.phoneNumber} failed.`;
148
+ }
149
+ default: {
150
+ const _exhaustive: never = context.scenario;
151
+ return `Call status update. ${String(_exhaustive)}`;
152
+ }
153
+ }
154
+ }
@@ -2,47 +2,126 @@
2
2
  * Concise pointer/status messages posted to the initiating conversation
3
3
  * so the user sees call lifecycle events without the full transcript
4
4
  * (which lives in the dedicated voice conversation).
5
+ *
6
+ * Trust-aware: trusted audiences receive assistant-generated copy when a
7
+ * generator is available; untrusted/unknown audiences always receive
8
+ * deterministic fallback text.
5
9
  */
6
10
 
7
11
  import * as conversationStore from '../memory/conversation-store.js';
12
+ import type { PointerCopyGenerator } from '../runtime/http-types.js';
13
+ import { getLogger } from '../util/logger.js';
14
+ import {
15
+ type CallPointerMessageContext,
16
+ composeCallPointerMessageGenerative,
17
+ getPointerFallbackMessage,
18
+ } from './call-pointer-message-composer.js';
19
+
20
+ const log = getLogger('call-pointer-messages');
8
21
 
9
22
  export type PointerEvent = 'started' | 'completed' | 'failed' | 'guardian_verification_succeeded' | 'guardian_verification_failed';
10
23
 
24
+ export type PointerAudienceMode = 'auto' | 'trusted' | 'untrusted';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Module-level generator injection (set by daemon lifecycle at startup)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ let pointerCopyGenerator: PointerCopyGenerator | undefined;
31
+
32
+ /**
33
+ * Inject the daemon-provided pointer copy generator.
34
+ * Called from daemon/lifecycle.ts at startup, following the same pattern
35
+ * as setRelayBroadcast.
36
+ */
37
+ export function setPointerCopyGenerator(generator: PointerCopyGenerator): void {
38
+ pointerCopyGenerator = generator;
39
+ }
40
+
41
+ /** @internal Reset for tests. */
42
+ export function resetPointerCopyGenerator(): void {
43
+ pointerCopyGenerator = undefined;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Trust resolution
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * Resolve whether the audience for a pointer message is trusted.
52
+ *
53
+ * Trusted when:
54
+ * - conversation threadType is 'private' (local desktop-origin context)
55
+ * - conversation origin channel is 'vellum' (desktop app)
56
+ *
57
+ * Untrusted by default when insufficient evidence.
58
+ */
59
+ function resolvePointerAudienceTrust(conversationId: string): boolean {
60
+ try {
61
+ const threadType = conversationStore.getConversationThreadType(conversationId);
62
+ if (threadType === 'private') return true;
63
+
64
+ const originChannel = conversationStore.getConversationOriginChannel(conversationId);
65
+ if (originChannel === 'vellum') return true;
66
+ } catch {
67
+ // Conversation may not exist or DB may be unavailable — default untrusted.
68
+ }
69
+
70
+ return false;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Public API
75
+ // ---------------------------------------------------------------------------
76
+
11
77
  export async function addPointerMessage(
12
78
  conversationId: string,
13
79
  event: PointerEvent,
14
80
  phoneNumber: string,
15
81
  extra?: { duration?: string; reason?: string; verificationCode?: string; channel?: string },
82
+ audienceMode: PointerAudienceMode = 'auto',
16
83
  ): Promise<void> {
84
+ const context: CallPointerMessageContext = {
85
+ scenario: event,
86
+ phoneNumber,
87
+ duration: extra?.duration,
88
+ reason: extra?.reason,
89
+ verificationCode: extra?.verificationCode,
90
+ channel: extra?.channel,
91
+ };
92
+
93
+ // Build required-facts list so generated text cannot drop key details.
94
+ const requiredFacts: string[] = [phoneNumber];
95
+ if (extra?.duration) requiredFacts.push(extra.duration);
96
+ if (extra?.verificationCode) requiredFacts.push(extra.verificationCode);
97
+ if (extra?.reason) requiredFacts.push(extra.reason);
98
+
99
+ // Enforce lifecycle outcome keywords so the LLM cannot rewrite e.g. a
100
+ // "failed" event as a success — the generated text must contain the
101
+ // outcome word verbatim.
102
+ const eventOutcomeKeywords: Record<PointerEvent, string | undefined> = {
103
+ started: 'started',
104
+ completed: 'completed',
105
+ failed: 'failed',
106
+ guardian_verification_succeeded: 'succeeded',
107
+ guardian_verification_failed: 'failed',
108
+ };
109
+ const outcomeKeyword = eventOutcomeKeywords[event];
110
+ if (outcomeKeyword) requiredFacts.push(outcomeKeyword);
111
+
17
112
  let text: string;
18
- switch (event) {
19
- case 'started':
20
- text = extra?.verificationCode
21
- ? `\u{1F4DE} Call to ${phoneNumber} started. Verification code: ${extra.verificationCode}`
22
- : `\u{1F4DE} Call to ${phoneNumber} started.`;
23
- break;
24
- case 'completed':
25
- text = extra?.duration
26
- ? `\u{1F4DE} Call to ${phoneNumber} completed (${extra.duration}).`
27
- : `\u{1F4DE} Call to ${phoneNumber} completed.`;
28
- break;
29
- case 'failed':
30
- text = extra?.reason
31
- ? `\u{1F4DE} Call to ${phoneNumber} failed: ${extra.reason}.`
32
- : `\u{1F4DE} Call to ${phoneNumber} failed.`;
33
- break;
34
- case 'guardian_verification_succeeded': {
35
- const ch = extra?.channel ?? 'voice';
36
- text = `\u{2705} Guardian verification (${ch}) for ${phoneNumber} succeeded.`;
37
- break;
38
- }
39
- case 'guardian_verification_failed': {
40
- const ch = extra?.channel ?? 'voice';
41
- text = extra?.reason
42
- ? `\u{274C} Guardian verification (${ch}) for ${phoneNumber} failed: ${extra.reason}.`
43
- : `\u{274C} Guardian verification (${ch}) for ${phoneNumber} failed.`;
44
- break;
113
+
114
+ const isTrusted =
115
+ audienceMode === 'trusted' ||
116
+ (audienceMode === 'auto' && resolvePointerAudienceTrust(conversationId));
117
+
118
+ if (isTrusted && pointerCopyGenerator) {
119
+ text = await composeCallPointerMessageGenerative(context, { requiredFacts }, pointerCopyGenerator);
120
+ } else {
121
+ if (!isTrusted && pointerCopyGenerator) {
122
+ log.debug({ event, conversationId }, 'Untrusted audience — using deterministic pointer copy');
45
123
  }
124
+ text = getPointerFallbackMessage(context);
46
125
  }
47
126
 
48
127
  // Pointer messages are assistant-generated status updates in the initiating
@@ -149,6 +149,7 @@ async function dispatchGuardianQuestionInner(params: GuardianDispatchParams): Pr
149
149
  // Route through the canonical notification pipeline. The paired vellum
150
150
  // conversation from this pipeline is the canonical guardian thread.
151
151
  let vellumDeliveryId: string | null = null;
152
+ const requestCode = request.requestCode ?? request.id.slice(0, 6).toUpperCase();
152
153
  const signalResult = await emitNotificationSignal({
153
154
  sourceEventName: 'guardian.question',
154
155
  sourceChannel: 'voice',
@@ -163,10 +164,11 @@ async function dispatchGuardianQuestionInner(params: GuardianDispatchParams): Pr
163
164
  },
164
165
  contextPayload: {
165
166
  requestId: request.id,
166
- requestCode: request.requestCode,
167
+ requestKind: 'pending_question',
168
+ requestCode,
167
169
  callSessionId,
170
+ toolName,
168
171
  questionText: pendingQuestion.questionText,
169
- pendingQuestionId: pendingQuestion.id,
170
172
  activeGuardianRequestCount,
171
173
  },
172
174
  conversationAffinityHint,