@vellumai/assistant 0.4.3 → 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 (183) hide show
  1. package/.env.example +3 -0
  2. package/ARCHITECTURE.md +40 -3
  3. package/README.md +43 -35
  4. package/package.json +1 -1
  5. package/scripts/ipc/generate-swift.ts +1 -0
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
  7. package/src/__tests__/actor-token-service.test.ts +1099 -0
  8. package/src/__tests__/agent-loop.test.ts +51 -0
  9. package/src/__tests__/approval-routes-http.test.ts +2 -0
  10. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
  11. package/src/__tests__/assistant-id-boundary-guard.test.ts +125 -0
  12. package/src/__tests__/call-controller.test.ts +49 -0
  13. package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
  14. package/src/__tests__/call-pointer-messages.test.ts +93 -3
  15. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
  16. package/src/__tests__/callback-handoff-copy.test.ts +186 -0
  17. package/src/__tests__/channel-approval-routes.test.ts +133 -12
  18. package/src/__tests__/channel-guardian.test.ts +0 -87
  19. package/src/__tests__/channel-readiness-service.test.ts +10 -16
  20. package/src/__tests__/checker.test.ts +33 -12
  21. package/src/__tests__/config-schema.test.ts +4 -0
  22. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
  23. package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
  24. package/src/__tests__/conversation-routes.test.ts +12 -3
  25. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  26. package/src/__tests__/daemon-server-session-init.test.ts +4 -0
  27. package/src/__tests__/guardian-actions-endpoint.test.ts +19 -14
  28. package/src/__tests__/guardian-dispatch.test.ts +8 -0
  29. package/src/__tests__/guardian-outbound-http.test.ts +4 -4
  30. package/src/__tests__/guardian-question-mode.test.ts +200 -0
  31. package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
  32. package/src/__tests__/guardian-routing-state.test.ts +525 -0
  33. package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
  34. package/src/__tests__/handlers-telegram-config.test.ts +0 -83
  35. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
  36. package/src/__tests__/headless-browser-navigate.test.ts +2 -0
  37. package/src/__tests__/ipc-snapshot.test.ts +18 -51
  38. package/src/__tests__/non-member-access-request.test.ts +131 -8
  39. package/src/__tests__/notification-decision-fallback.test.ts +129 -4
  40. package/src/__tests__/notification-decision-strategy.test.ts +62 -2
  41. package/src/__tests__/notification-guardian-path.test.ts +3 -0
  42. package/src/__tests__/recording-intent-handler.test.ts +1 -0
  43. package/src/__tests__/relay-server.test.ts +841 -39
  44. package/src/__tests__/send-endpoint-busy.test.ts +5 -0
  45. package/src/__tests__/session-agent-loop.test.ts +1 -0
  46. package/src/__tests__/session-confirmation-signals.test.ts +523 -0
  47. package/src/__tests__/session-init.benchmark.test.ts +0 -1
  48. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -1
  49. package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
  50. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
  51. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
  52. package/src/__tests__/tool-executor.test.ts +21 -2
  53. package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
  54. package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
  55. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
  56. package/src/__tests__/twilio-config.test.ts +2 -13
  57. package/src/agent/loop.ts +1 -1
  58. package/src/approvals/guardian-decision-primitive.ts +10 -2
  59. package/src/approvals/guardian-request-resolvers.ts +128 -9
  60. package/src/calls/call-constants.ts +21 -0
  61. package/src/calls/call-controller.ts +9 -2
  62. package/src/calls/call-domain.ts +28 -7
  63. package/src/calls/call-pointer-message-composer.ts +154 -0
  64. package/src/calls/call-pointer-messages.ts +106 -27
  65. package/src/calls/guardian-dispatch.ts +4 -2
  66. package/src/calls/relay-server.ts +424 -12
  67. package/src/calls/twilio-config.ts +4 -11
  68. package/src/calls/twilio-routes.ts +1 -1
  69. package/src/calls/types.ts +3 -1
  70. package/src/cli.ts +5 -4
  71. package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
  72. package/src/config/bundled-skills/app-builder/SKILL.md +146 -10
  73. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
  74. package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
  75. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
  76. package/src/config/bundled-skills/messaging/SKILL.md +61 -12
  77. package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
  78. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
  79. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
  80. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
  81. package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
  82. package/src/config/bundled-skills/twitter/SKILL.md +3 -3
  83. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +1 -0
  84. package/src/config/calls-schema.ts +24 -0
  85. package/src/config/env.ts +22 -0
  86. package/src/config/feature-flag-registry.json +8 -0
  87. package/src/config/schema.ts +2 -2
  88. package/src/config/skills.ts +11 -0
  89. package/src/config/system-prompt.ts +11 -1
  90. package/src/config/templates/SOUL.md +2 -0
  91. package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
  92. package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -9
  93. package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
  94. package/src/daemon/call-pointer-generators.ts +59 -0
  95. package/src/daemon/computer-use-session.ts +2 -5
  96. package/src/daemon/handlers/apps.ts +76 -20
  97. package/src/daemon/handlers/config-channels.ts +5 -55
  98. package/src/daemon/handlers/config-inbox.ts +9 -3
  99. package/src/daemon/handlers/config-ingress.ts +28 -3
  100. package/src/daemon/handlers/config-telegram.ts +12 -0
  101. package/src/daemon/handlers/config.ts +2 -6
  102. package/src/daemon/handlers/pairing.ts +2 -0
  103. package/src/daemon/handlers/sessions.ts +48 -3
  104. package/src/daemon/handlers/shared.ts +17 -2
  105. package/src/daemon/ipc-contract/integrations.ts +1 -99
  106. package/src/daemon/ipc-contract/messages.ts +47 -1
  107. package/src/daemon/ipc-contract/notifications.ts +11 -0
  108. package/src/daemon/ipc-contract-inventory.json +2 -4
  109. package/src/daemon/lifecycle.ts +17 -0
  110. package/src/daemon/server.ts +14 -1
  111. package/src/daemon/session-agent-loop-handlers.ts +20 -0
  112. package/src/daemon/session-agent-loop.ts +22 -11
  113. package/src/daemon/session-lifecycle.ts +1 -1
  114. package/src/daemon/session-process.ts +11 -1
  115. package/src/daemon/session-runtime-assembly.ts +3 -0
  116. package/src/daemon/session-surfaces.ts +3 -2
  117. package/src/daemon/session.ts +88 -1
  118. package/src/daemon/tool-side-effects.ts +22 -0
  119. package/src/home-base/prebuilt/brain-graph.html +1483 -0
  120. package/src/home-base/prebuilt/index.html +40 -0
  121. package/src/inbound/platform-callback-registration.ts +157 -0
  122. package/src/memory/canonical-guardian-store.ts +1 -1
  123. package/src/memory/db-init.ts +4 -0
  124. package/src/memory/migrations/038-actor-token-records.ts +39 -0
  125. package/src/memory/migrations/index.ts +1 -0
  126. package/src/memory/schema.ts +16 -0
  127. package/src/messaging/provider-types.ts +24 -0
  128. package/src/messaging/provider.ts +7 -0
  129. package/src/messaging/providers/gmail/adapter.ts +127 -0
  130. package/src/messaging/providers/sms/adapter.ts +40 -37
  131. package/src/notifications/adapters/macos.ts +45 -2
  132. package/src/notifications/broadcaster.ts +16 -0
  133. package/src/notifications/copy-composer.ts +39 -1
  134. package/src/notifications/decision-engine.ts +22 -9
  135. package/src/notifications/destination-resolver.ts +16 -2
  136. package/src/notifications/emit-signal.ts +16 -8
  137. package/src/notifications/guardian-question-mode.ts +419 -0
  138. package/src/notifications/signal.ts +14 -3
  139. package/src/permissions/checker.ts +13 -1
  140. package/src/permissions/prompter.ts +14 -0
  141. package/src/providers/anthropic/client.ts +20 -0
  142. package/src/providers/provider-send-message.ts +15 -3
  143. package/src/runtime/access-request-helper.ts +71 -1
  144. package/src/runtime/actor-token-service.ts +234 -0
  145. package/src/runtime/actor-token-store.ts +236 -0
  146. package/src/runtime/channel-approvals.ts +5 -3
  147. package/src/runtime/channel-readiness-service.ts +23 -64
  148. package/src/runtime/channel-readiness-types.ts +3 -4
  149. package/src/runtime/channel-retry-sweep.ts +4 -1
  150. package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
  151. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  152. package/src/runtime/guardian-context-resolver.ts +82 -0
  153. package/src/runtime/guardian-outbound-actions.ts +0 -3
  154. package/src/runtime/guardian-reply-router.ts +67 -30
  155. package/src/runtime/guardian-vellum-migration.ts +57 -0
  156. package/src/runtime/http-server.ts +65 -12
  157. package/src/runtime/http-types.ts +13 -0
  158. package/src/runtime/invite-redemption-service.ts +8 -0
  159. package/src/runtime/local-actor-identity.ts +76 -0
  160. package/src/runtime/middleware/actor-token.ts +271 -0
  161. package/src/runtime/routes/approval-routes.ts +82 -7
  162. package/src/runtime/routes/brain-graph-routes.ts +222 -0
  163. package/src/runtime/routes/channel-readiness-routes.ts +71 -0
  164. package/src/runtime/routes/conversation-routes.ts +140 -52
  165. package/src/runtime/routes/events-routes.ts +20 -5
  166. package/src/runtime/routes/guardian-action-routes.ts +45 -3
  167. package/src/runtime/routes/guardian-approval-interception.ts +29 -0
  168. package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
  169. package/src/runtime/routes/inbound-message-handler.ts +143 -2
  170. package/src/runtime/routes/integration-routes.ts +7 -15
  171. package/src/runtime/routes/pairing-routes.ts +163 -0
  172. package/src/runtime/routes/twilio-routes.ts +934 -0
  173. package/src/runtime/tool-grant-request-helper.ts +3 -1
  174. package/src/security/oauth2.ts +27 -2
  175. package/src/security/token-manager.ts +46 -10
  176. package/src/tools/browser/browser-execution.ts +4 -3
  177. package/src/tools/browser/browser-handoff.ts +10 -18
  178. package/src/tools/browser/browser-manager.ts +80 -25
  179. package/src/tools/browser/browser-screencast.ts +35 -119
  180. package/src/tools/permission-checker.ts +15 -4
  181. package/src/tools/tool-approval-handler.ts +242 -18
  182. package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
  183. package/src/daemon/handlers/config-twilio.ts +0 -1082
@@ -5,6 +5,12 @@
5
5
  * The adapter broadcasts a `notification_intent` message that the Vellum
6
6
  * client can use to display a native notification (e.g. NSUserNotification
7
7
  * or UNUserNotificationCenter).
8
+ *
9
+ * Guardian-sensitive notifications (approval requests, escalation alerts)
10
+ * are annotated with `targetGuardianPrincipalId` so that only clients
11
+ * bound to the guardian identity display them. Non-guardian clients
12
+ * should ignore notifications with a `targetGuardianPrincipalId` that
13
+ * does not match their own identity.
8
14
  */
9
15
 
10
16
  import type { ServerMessage } from '../../daemon/ipc-contract.js';
@@ -21,6 +27,24 @@ const log = getLogger('notif-adapter-vellum');
21
27
 
22
28
  export type BroadcastFn = (msg: ServerMessage) => void;
23
29
 
30
+ /**
31
+ * Event name prefixes that carry guardian-sensitive content (approval
32
+ * requests, escalation alerts, access requests). Notifications for
33
+ * these events are scoped to bound guardian devices via
34
+ * `targetGuardianPrincipalId`.
35
+ */
36
+ const GUARDIAN_SENSITIVE_EVENT_PREFIXES = [
37
+ 'guardian.question',
38
+ 'ingress.escalation',
39
+ 'ingress.access_request',
40
+ ] as const;
41
+
42
+ export function isGuardianSensitiveEvent(sourceEventName: string): boolean {
43
+ return GUARDIAN_SENSITIVE_EVENT_PREFIXES.some(
44
+ (prefix) => sourceEventName === prefix || sourceEventName.startsWith(prefix + '.'),
45
+ );
46
+ }
47
+
24
48
  export class VellumAdapter implements ChannelAdapter {
25
49
  readonly channel: NotificationChannel = 'vellum';
26
50
 
@@ -30,8 +54,22 @@ export class VellumAdapter implements ChannelAdapter {
30
54
  this.broadcast = broadcast;
31
55
  }
32
56
 
33
- async send(payload: ChannelDeliveryPayload, _destination: ChannelDestination): Promise<DeliveryResult> {
57
+ async send(payload: ChannelDeliveryPayload, destination: ChannelDestination): Promise<DeliveryResult> {
34
58
  try {
59
+ // For guardian-sensitive events, annotate the outbound message with
60
+ // the target guardian identity so clients can filter. The
61
+ // guardianPrincipalId comes from the vellum binding resolved by
62
+ // the destination resolver.
63
+ const guardianPrincipalId =
64
+ typeof destination.metadata?.guardianPrincipalId === 'string'
65
+ ? destination.metadata.guardianPrincipalId
66
+ : undefined;
67
+
68
+ const targetGuardianPrincipalId =
69
+ guardianPrincipalId && isGuardianSensitiveEvent(payload.sourceEventName)
70
+ ? guardianPrincipalId
71
+ : undefined;
72
+
35
73
  this.broadcast({
36
74
  type: 'notification_intent',
37
75
  deliveryId: payload.deliveryId,
@@ -39,10 +77,15 @@ export class VellumAdapter implements ChannelAdapter {
39
77
  title: payload.copy.title,
40
78
  body: payload.copy.body,
41
79
  deepLinkMetadata: payload.deepLinkTarget,
80
+ targetGuardianPrincipalId,
42
81
  } as ServerMessage);
43
82
 
44
83
  log.info(
45
- { sourceEventName: payload.sourceEventName, title: payload.copy.title },
84
+ {
85
+ sourceEventName: payload.sourceEventName,
86
+ title: payload.copy.title,
87
+ guardianScoped: targetGuardianPrincipalId != null,
88
+ },
46
89
  'Vellum notification intent broadcast',
47
90
  );
48
91
 
@@ -12,6 +12,7 @@
12
12
  import { v4 as uuid } from 'uuid';
13
13
 
14
14
  import { getLogger } from '../util/logger.js';
15
+ import { isGuardianSensitiveEvent } from './adapters/macos.js';
15
16
  import { pairDeliveryWithConversation } from './conversation-pairing.js';
16
17
  import { composeFallbackCopy } from './copy-composer.js';
17
18
  import { createDelivery, findDeliveryByDecisionAndChannel, updateDeliveryStatus } from './deliveries-store.js';
@@ -34,6 +35,8 @@ export interface ThreadCreatedInfo {
34
35
  conversationId: string;
35
36
  title: string;
36
37
  sourceEventName: string;
38
+ /** Present when the thread is for a guardian-sensitive notification. */
39
+ targetGuardianPrincipalId?: string;
37
40
  }
38
41
  export type OnThreadCreatedFn = (info: ThreadCreatedInfo) => void;
39
42
  export interface BroadcastDecisionOptions {
@@ -163,6 +166,18 @@ export class NotificationBroadcaster {
163
166
  if (channel === 'vellum' && pairing.conversationId) {
164
167
  deepLinkTarget = { ...deepLinkTarget, conversationId: pairing.conversationId };
165
168
 
169
+ // Resolve guardian scoping for thread-created events so clients
170
+ // can filter guardian-sensitive threads the same way they filter
171
+ // guardian-sensitive notification intents.
172
+ const guardianPrincipalId =
173
+ typeof destination.metadata?.guardianPrincipalId === 'string'
174
+ ? destination.metadata.guardianPrincipalId
175
+ : undefined;
176
+ const targetGuardianPrincipalId =
177
+ guardianPrincipalId && isGuardianSensitiveEvent(signal.sourceEventName)
178
+ ? guardianPrincipalId
179
+ : undefined;
180
+
166
181
  const threadTitle =
167
182
  copy.threadTitle ??
168
183
  copy.title ??
@@ -171,6 +186,7 @@ export class NotificationBroadcaster {
171
186
  conversationId: pairing.conversationId,
172
187
  title: threadTitle,
173
188
  sourceEventName: signal.sourceEventName,
189
+ targetGuardianPrincipalId,
174
190
  };
175
191
 
176
192
  // The per-dispatch onThreadCreated callback fires whenever a vellum
@@ -9,6 +9,10 @@
9
9
  * values from the context payload.
10
10
  */
11
11
 
12
+ import {
13
+ buildGuardianRequestCodeInstruction,
14
+ resolveGuardianQuestionInstructionMode,
15
+ } from './guardian-question-mode.js';
12
16
  import type { NotificationSignal } from './signal.js';
13
17
  import type { NotificationChannel, RenderedChannelCopy } from './types.js';
14
18
 
@@ -48,9 +52,11 @@ const TEMPLATES: Record<string, CopyTemplate> = {
48
52
  }
49
53
 
50
54
  const normalizedCode = requestCode.toUpperCase();
55
+ const modeResolution = resolveGuardianQuestionInstructionMode(payload);
56
+ const instruction = buildGuardianRequestCodeInstruction(normalizedCode, modeResolution.mode);
51
57
  return {
52
58
  title: 'Guardian Question',
53
- body: `${question}\n\nReference code: ${normalizedCode}. Reply "${normalizedCode} approve" or "${normalizedCode} reject".`,
59
+ body: `${question}\n\n${instruction}`,
54
60
  };
55
61
  },
56
62
 
@@ -59,6 +65,9 @@ const TEMPLATES: Record<string, CopyTemplate> = {
59
65
  const requestCode = nonEmpty(typeof payload.requestCode === 'string' ? payload.requestCode : undefined);
60
66
  const sourceChannel = typeof payload.sourceChannel === 'string' ? payload.sourceChannel : undefined;
61
67
  const callerName = nonEmpty(typeof payload.senderName === 'string' ? payload.senderName : undefined);
68
+ const previousMemberStatus = typeof payload.previousMemberStatus === 'string'
69
+ ? payload.previousMemberStatus
70
+ : undefined;
62
71
  const lines: string[] = [];
63
72
 
64
73
  // Voice-originated access requests include caller name context
@@ -67,6 +76,9 @@ const TEMPLATES: Record<string, CopyTemplate> = {
67
76
  } else {
68
77
  lines.push(`${requester} is requesting access to the assistant.`);
69
78
  }
79
+ if (previousMemberStatus === 'revoked') {
80
+ lines.push('Note: this user was previously revoked.');
81
+ }
70
82
 
71
83
  if (requestCode) {
72
84
  const code = requestCode.toUpperCase();
@@ -79,6 +91,32 @@ const TEMPLATES: Record<string, CopyTemplate> = {
79
91
  };
80
92
  },
81
93
 
94
+ 'ingress.access_request.callback_handoff': (payload) => {
95
+ const callerName = nonEmpty(typeof payload.callerName === 'string' ? payload.callerName : undefined);
96
+ const callerPhone = nonEmpty(typeof payload.callerPhoneNumber === 'string' ? payload.callerPhoneNumber : undefined);
97
+ const requestCode = nonEmpty(typeof payload.requestCode === 'string' ? payload.requestCode : undefined);
98
+ const memberId = nonEmpty(typeof payload.requesterMemberId === 'string' ? payload.requesterMemberId : undefined);
99
+
100
+ const callerIdentity = callerName && callerPhone
101
+ ? `${callerName} (${callerPhone})`
102
+ : callerName ?? callerPhone ?? 'An unknown caller';
103
+
104
+ const lines: string[] = [];
105
+ lines.push(`${callerIdentity} called and requested a callback while you were unreachable.`);
106
+
107
+ if (requestCode) {
108
+ lines.push(`Request code: ${requestCode.toUpperCase()}`);
109
+ }
110
+ if (memberId) {
111
+ lines.push(`This caller is a trusted contact (member ID: ${memberId}).`);
112
+ }
113
+
114
+ return {
115
+ title: 'Callback Requested',
116
+ body: lines.join('\n'),
117
+ };
118
+ },
119
+
82
120
  'ingress.escalation': (payload) => ({
83
121
  title: 'Escalation',
84
122
  body: str(payload.senderIdentifier, 'An incoming message') + ' needs attention',
@@ -18,6 +18,12 @@ import type { ModelIntent } from '../providers/types.js';
18
18
  import { getLogger } from '../util/logger.js';
19
19
  import { composeFallbackCopy } from './copy-composer.js';
20
20
  import { createDecision } from './decisions-store.js';
21
+ import {
22
+ buildGuardianRequestCodeInstruction,
23
+ hasGuardianRequestCodeInstruction,
24
+ resolveGuardianQuestionInstructionMode,
25
+ stripConflictingGuardianRequestInstructions,
26
+ } from './guardian-question-mode.js';
21
27
  import { getPreferenceSummary } from './preference-summary.js';
22
28
  import type { NotificationSignal, RoutingIntent } from './signal.js';
23
29
  import { buildThreadCandidates, serializeCandidatesForPrompt,type ThreadCandidateSet } from './thread-candidates.js';
@@ -409,18 +415,15 @@ export function validateThreadActions(
409
415
  function ensureGuardianRequestCodeInCopy(
410
416
  copy: RenderedChannelCopy,
411
417
  requestCode: string,
418
+ mode: 'approval' | 'answer',
412
419
  ): RenderedChannelCopy {
413
- const instruction = `Reference code: ${requestCode}. Reply "${requestCode} approve" or "${requestCode} reject".`;
414
- const hasParserCompatibleInstructions = (text: string | undefined): boolean => {
415
- if (typeof text !== 'string') return false;
416
- const upper = text.toUpperCase();
417
- return upper.includes(`${requestCode} APPROVE`) && upper.includes(`${requestCode} REJECT`);
418
- };
420
+ const instruction = buildGuardianRequestCodeInstruction(requestCode, mode);
419
421
 
420
422
  const ensureText = (text: string | undefined): string => {
421
423
  const base = typeof text === 'string' ? text.trim() : '';
422
- if (hasParserCompatibleInstructions(base)) return base;
423
- return base.length > 0 ? `${base}\n\n${instruction}` : instruction;
424
+ const sanitized = stripConflictingGuardianRequestInstructions(base, requestCode, mode);
425
+ if (hasGuardianRequestCodeInstruction(sanitized, requestCode, mode)) return sanitized;
426
+ return sanitized.length > 0 ? `${sanitized}\n\n${instruction}` : instruction;
424
427
  };
425
428
 
426
429
  return {
@@ -445,6 +448,16 @@ function enforceGuardianRequestCode(
445
448
  if (typeof rawCode !== 'string' || rawCode.trim().length === 0) return decision;
446
449
 
447
450
  const requestCode = rawCode.trim().toUpperCase();
451
+ const modeResolution = resolveGuardianQuestionInstructionMode(signal.contextPayload);
452
+ if (modeResolution.legacyFallbackUsed) {
453
+ log.warn(
454
+ {
455
+ signalId: signal.signalId,
456
+ requestKind: modeResolution.requestKind,
457
+ },
458
+ 'guardian.question payload missing/invalid typed fields; using legacy instruction-mode fallback',
459
+ );
460
+ }
448
461
  const nextCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>> = {
449
462
  ...decision.renderedCopy,
450
463
  };
@@ -452,7 +465,7 @@ function enforceGuardianRequestCode(
452
465
  for (const channel of Object.keys(nextCopy) as NotificationChannel[]) {
453
466
  const copy = nextCopy[channel];
454
467
  if (!copy) continue;
455
- nextCopy[channel] = ensureGuardianRequestCodeInCopy(copy, requestCode);
468
+ nextCopy[channel] = ensureGuardianRequestCodeInCopy(copy, requestCode, modeResolution.mode);
456
469
  }
457
470
 
458
471
  return {
@@ -2,7 +2,10 @@
2
2
  * Resolves per-channel destination endpoints for notification delivery.
3
3
  *
4
4
  * - Vellum: no external endpoint needed — delivery goes through the IPC
5
- * broadcast mechanism to connected desktop/mobile clients.
5
+ * broadcast mechanism to connected desktop/mobile clients. The
6
+ * guardianPrincipalId from the vellum binding is included in metadata
7
+ * so downstream adapters can scope guardian-sensitive notifications to
8
+ * bound guardian devices only.
6
9
  * - Binding-based channels (telegram, sms): require a chat/delivery ID
7
10
  * sourced from the guardian binding for the assistant.
8
11
  */
@@ -35,7 +38,18 @@ export function resolveDestinations(
35
38
  switch (channel as NotificationChannel) {
36
39
  case 'vellum': {
37
40
  // Vellum delivery is local IPC — no external endpoint required.
38
- result.set('vellum', { channel: 'vellum' });
41
+ // Include the guardianPrincipalId from the vellum binding so the
42
+ // adapter can annotate guardian-sensitive notifications for scoped
43
+ // delivery to bound guardian devices.
44
+ const vellumBinding = getActiveBinding(assistantId, 'vellum');
45
+ const metadata: Record<string, unknown> = {};
46
+ if (vellumBinding) {
47
+ metadata.guardianPrincipalId = vellumBinding.guardianExternalUserId;
48
+ }
49
+ result.set('vellum', {
50
+ channel: 'vellum',
51
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
52
+ });
39
53
  break;
40
54
  }
41
55
  case 'telegram':
@@ -24,7 +24,12 @@ import { updateDecision } from './decisions-store.js';
24
24
  import { type DeterministicCheckContext, runDeterministicChecks } from './deterministic-checks.js';
25
25
  import { createEvent, updateEventDedupeKey } from './events-store.js';
26
26
  import { dispatchDecision } from './runtime-dispatch.js';
27
- import type { AttentionHints, NotificationSignal, RoutingIntent } from './signal.js';
27
+ import type {
28
+ AttentionHints,
29
+ NotificationContextPayload,
30
+ NotificationSignal,
31
+ RoutingIntent,
32
+ } from './signal.js';
28
33
  import type { NotificationChannel, NotificationDeliveryResult } from './types.js';
29
34
 
30
35
  const log = getLogger('emit-signal');
@@ -67,9 +72,10 @@ function getBroadcaster(): NotificationBroadcaster {
67
72
  conversationId: info.conversationId,
68
73
  title: info.title,
69
74
  sourceEventName: info.sourceEventName,
75
+ targetGuardianPrincipalId: info.targetGuardianPrincipalId,
70
76
  });
71
77
  log.info(
72
- { conversationId: info.conversationId },
78
+ { conversationId: info.conversationId, guardianScoped: info.targetGuardianPrincipalId != null },
73
79
  'Emitted notification_thread_created push event',
74
80
  );
75
81
  });
@@ -117,9 +123,9 @@ function getConnectedChannels(assistantId: string): NotificationChannel[] {
117
123
 
118
124
  // ── Public API ─────────────────────────────────────────────────────────
119
125
 
120
- export interface EmitSignalParams {
126
+ export interface EmitSignalParams<TEventName extends string = string> {
121
127
  /** Free-form event name, e.g. 'reminder.fired', 'schedule.complete'. */
122
- sourceEventName: string;
128
+ sourceEventName: TEventName;
123
129
  /** Source channel that produced the event. */
124
130
  sourceChannel: string;
125
131
  /** Session or conversation ID from the source context. */
@@ -129,7 +135,7 @@ export interface EmitSignalParams {
129
135
  /** Attention hints for the decision engine. */
130
136
  attentionHints: AttentionHints;
131
137
  /** Arbitrary context payload passed to the decision engine. */
132
- contextPayload?: Record<string, unknown>;
138
+ contextPayload?: NotificationContextPayload<TEventName>;
133
139
  /** Routing intent from the source (e.g. reminder). Controls post-decision channel enforcement. */
134
140
  routingIntent?: RoutingIntent;
135
141
  /** Free-form hints from the source for the decision engine. */
@@ -169,18 +175,20 @@ export interface EmitSignalResult {
169
175
  * Fire-and-forget safe by default: errors are caught and logged unless
170
176
  * `throwOnError` is enabled by the caller.
171
177
  */
172
- export async function emitNotificationSignal(params: EmitSignalParams): Promise<EmitSignalResult> {
178
+ export async function emitNotificationSignal<TEventName extends string>(
179
+ params: EmitSignalParams<TEventName>,
180
+ ): Promise<EmitSignalResult> {
173
181
  const signalId = uuid();
174
182
  const assistantId = params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
175
183
 
176
- const signal: NotificationSignal = {
184
+ const signal: NotificationSignal<TEventName> = {
177
185
  signalId,
178
186
  assistantId,
179
187
  createdAt: Date.now(),
180
188
  sourceChannel: params.sourceChannel,
181
189
  sourceSessionId: params.sourceSessionId,
182
190
  sourceEventName: params.sourceEventName,
183
- contextPayload: params.contextPayload ?? {},
191
+ contextPayload: (params.contextPayload ?? {}) as NotificationContextPayload<TEventName>,
184
192
  attentionHints: params.attentionHints,
185
193
  routingIntent: params.routingIntent,
186
194
  routingHints: params.routingHints,