@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
@@ -488,6 +488,24 @@
488
488
  </div>
489
489
  </section>
490
490
 
491
+ <!-- Brain Knowledge -->
492
+ <section class="section" id="home-base-brain-lane">
493
+ <div class="section-header anim anim-d6"><h2>Knowledge</h2></div>
494
+
495
+ <div class="card-stack">
496
+ <div class="card feature anim anim-d6">
497
+ <div class="card-icon">🧠</div>
498
+ <div class="card-body">
499
+ <div class="task">Knowledge Brain</div>
500
+ <div class="task-meta" id="home-base-brain-count">Loading...</div>
501
+ <div class="task-controls">
502
+ <a href="/v1/brain-graph-ui" class="task-button primary" id="home-base-brain-view">View →</a>
503
+ </div>
504
+ </div>
505
+ </div>
506
+ </div>
507
+ </section>
508
+
491
509
  <!-- Onboarding -->
492
510
  <section class="section" id="home-base-onboarding-lane">
493
511
  <div class="section-header anim anim-d6"><h2>Set up your assistant</h2></div>
@@ -656,6 +674,28 @@
656
674
  });
657
675
  });
658
676
  }
677
+
678
+ // Fetch brain graph count for the Knowledge Brain card.
679
+ (function () {
680
+ var countEl = byId('home-base-brain-count');
681
+ if (!countEl) return;
682
+ var apiToken = document.querySelector('meta[name="api-token"]');
683
+ var headers = {};
684
+ if (apiToken && apiToken.content) headers['Authorization'] = 'Bearer ' + apiToken.content;
685
+ fetch('/v1/brain-graph', { headers: headers })
686
+ .then(function (res) {
687
+ if (!res.ok) throw new Error('HTTP ' + res.status);
688
+ return res.json();
689
+ })
690
+ .then(function (data) {
691
+ var entities = (data.entities || []).length;
692
+ var relations = (data.relations || []).length;
693
+ countEl.textContent = entities + ' nodes \u2022 ' + relations + ' relations';
694
+ })
695
+ .catch(function () {
696
+ countEl.textContent = '\u2014';
697
+ });
698
+ })();
659
699
  })();
660
700
  </script>
661
701
  </body>
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Platform callback route registration for containerized deployments.
3
+ *
4
+ * When the assistant daemon runs inside a container (IS_CONTAINERIZED=true)
5
+ * with a configured PLATFORM_BASE_URL and PLATFORM_ASSISTANT_ID, external
6
+ * service callbacks (Twilio webhooks, OAuth redirects, Telegram webhooks, etc.)
7
+ * must route through the platform's gateway proxy instead of hitting the
8
+ * assistant directly.
9
+ *
10
+ * This module registers callback routes with the platform's internal
11
+ * gateway endpoint so the platform knows how to forward inbound provider
12
+ * webhooks to the correct containerized assistant instance.
13
+ *
14
+ * The platform endpoint is:
15
+ * POST {PLATFORM_BASE_URL}/v1/internal/gateway/callback-routes/register/
16
+ *
17
+ * It accepts { assistant_id, callback_path, type } and returns a stable
18
+ * callback_url that external services should use.
19
+ */
20
+
21
+ import { getPlatformAssistantId, getPlatformBaseUrl, getPlatformInternalApiKey } from '../config/env.js';
22
+ import { getIsContainerized } from '../config/env-registry.js';
23
+ import { getLogger } from '../util/logger.js';
24
+
25
+ const log = getLogger('platform-callback-registration');
26
+
27
+ /**
28
+ * Whether the daemon should register callback routes with the platform.
29
+ * True when IS_CONTAINERIZED, PLATFORM_BASE_URL, and PLATFORM_ASSISTANT_ID
30
+ * are all set.
31
+ */
32
+ export function shouldUsePlatformCallbacks(): boolean {
33
+ return getIsContainerized() && !!getPlatformBaseUrl() && !!getPlatformAssistantId();
34
+ }
35
+
36
+ interface RegisterCallbackRouteResponse {
37
+ callback_url: string;
38
+ callback_path: string;
39
+ type: string;
40
+ assistant_id: string;
41
+ }
42
+
43
+ /**
44
+ * Register a callback route with the platform's internal gateway endpoint.
45
+ *
46
+ * @param callbackPath - The path portion after the ingress base URL
47
+ * (e.g. "webhooks/twilio/voice"). Leading/trailing slashes are stripped
48
+ * by the platform.
49
+ * @param type - The route type identifier (e.g. "twilio_voice", "twilio_sms",
50
+ * "twilio_status", "oauth", "telegram").
51
+ * @returns The platform-provided callback URL that external services should use.
52
+ * @throws If the platform request fails.
53
+ */
54
+ export async function registerCallbackRoute(
55
+ callbackPath: string,
56
+ type: string,
57
+ ): Promise<string> {
58
+ const platformBaseUrl = getPlatformBaseUrl().replace(/\/+$/, '');
59
+ const assistantId = getPlatformAssistantId();
60
+ const apiKey = getPlatformInternalApiKey();
61
+
62
+ const url = `${platformBaseUrl}/v1/internal/gateway/callback-routes/register/`;
63
+
64
+ const headers: Record<string, string> = {
65
+ 'Content-Type': 'application/json',
66
+ };
67
+
68
+ if (apiKey) {
69
+ headers['Authorization'] = `Bearer ${apiKey}`;
70
+ }
71
+
72
+ const body = JSON.stringify({
73
+ assistant_id: assistantId,
74
+ callback_path: callbackPath,
75
+ type,
76
+ });
77
+
78
+ log.debug({ callbackPath, type }, 'Registering platform callback route');
79
+
80
+ const response = await fetch(url, {
81
+ method: 'POST',
82
+ headers,
83
+ body,
84
+ signal: AbortSignal.timeout(10_000),
85
+ });
86
+
87
+ if (!response.ok) {
88
+ const detail = await response.text().catch(() => '');
89
+ throw new Error(
90
+ `Platform callback route registration failed (HTTP ${response.status}): ${detail}`,
91
+ );
92
+ }
93
+
94
+ const data = (await response.json()) as RegisterCallbackRouteResponse;
95
+
96
+ log.info(
97
+ { callbackPath, type, callbackUrl: data.callback_url },
98
+ 'Platform callback route registered',
99
+ );
100
+
101
+ return data.callback_url;
102
+ }
103
+
104
+ /**
105
+ * Resolve a callback URL, registering with the platform when containerized.
106
+ *
107
+ * When platform callbacks are enabled, registers the route and returns the
108
+ * platform's stable callback URL (optionally with query parameters appended).
109
+ * Otherwise evaluates the lazy direct URL supplier and returns that value.
110
+ *
111
+ * The `directUrl` parameter is a **lazy supplier** (a function returning a
112
+ * string) rather than an eagerly-evaluated string. This is critical because
113
+ * the direct URL builders (e.g. `getTwilioVoiceWebhookUrl`) call
114
+ * `getPublicBaseUrl()` which throws when no public ingress URL is configured.
115
+ * In containerized environments that rely solely on platform callbacks, the
116
+ * direct URL is never needed — deferring evaluation avoids the throw.
117
+ *
118
+ * @param directUrl - Lazy supplier for the direct callback URL.
119
+ * @param callbackPath - The path to register (e.g. "webhooks/twilio/voice").
120
+ * @param type - The route type identifier.
121
+ * @param queryParams - Optional query parameters to append to the resolved URL.
122
+ * @returns The resolved callback URL.
123
+ */
124
+ export async function resolveCallbackUrl(
125
+ directUrl: () => string,
126
+ callbackPath: string,
127
+ type: string,
128
+ queryParams?: Record<string, string>,
129
+ ): Promise<string> {
130
+ if (!shouldUsePlatformCallbacks()) {
131
+ return directUrl();
132
+ }
133
+
134
+ try {
135
+ let url = await registerCallbackRoute(callbackPath, type);
136
+ if (queryParams && Object.keys(queryParams).length > 0) {
137
+ const params = new URLSearchParams(queryParams);
138
+ const separator = url.includes('?') ? '&' : '?';
139
+ url = `${url}${separator}${params.toString()}`;
140
+ }
141
+ return url;
142
+ } catch (err) {
143
+ log.warn(
144
+ { err, callbackPath, type },
145
+ 'Failed to register platform callback route, falling back to direct URL',
146
+ );
147
+ try {
148
+ return directUrl();
149
+ } catch (fallbackErr) {
150
+ log.error(
151
+ { fallbackErr, callbackPath, type },
152
+ 'Direct URL fallback also failed after platform registration failure',
153
+ );
154
+ throw err;
155
+ }
156
+ }
157
+ }
@@ -292,7 +292,7 @@ export interface UpdateCanonicalGuardianRequestParams {
292
292
  status?: CanonicalRequestStatus;
293
293
  answerText?: string;
294
294
  decidedByExternalUserId?: string;
295
- followupState?: string;
295
+ followupState?: string | null;
296
296
  expiresAt?: string;
297
297
  }
298
298
 
@@ -7,6 +7,7 @@ import { parseChannelId, parseInterfaceId } from '../channels/types.js';
7
7
  import { CHANNEL_IDS, INTERFACE_IDS, isChannelId } from '../channels/types.js';
8
8
  import { getConfig } from '../config/loader.js';
9
9
  import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
10
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
10
11
  import { getLogger } from '../util/logger.js';
11
12
  import { createRowMapper } from '../util/row-mapper.js';
12
13
  import { deleteOrphanAttachments } from './attachments-store.js';
@@ -299,7 +300,7 @@ export async function addMessage(conversationId: string, role: string, content:
299
300
  try {
300
301
  projectAssistantMessage({
301
302
  conversationId,
302
- assistantId: 'self',
303
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
303
304
  messageId: message.id,
304
305
  messageAt: message.createdAt,
305
306
  });
@@ -286,7 +286,7 @@ function buildTitlePrompt(
286
286
  assistantResponse?: string,
287
287
  ): string {
288
288
  const parts: string[] = [
289
- 'Generate a very short title for this conversation. Rules: at most 5 words, at most 40 characters, no quotes.',
289
+ 'Generate a very short title for this conversation. Rules: at most 5 words, at most 40 characters, no quotes, no markdown formatting.',
290
290
  ];
291
291
 
292
292
  if (context) {
@@ -313,12 +313,26 @@ function buildTitlePrompt(
313
313
 
314
314
  function normalizeTitle(raw: string): string {
315
315
  let title = raw.trim().replace(/^["']|["']$/g, '');
316
+ title = stripMarkdown(title);
316
317
  const words = title.split(/\s+/);
317
318
  if (words.length > 5) title = words.slice(0, 5).join(' ');
318
319
  if (title.length > 40) title = title.slice(0, 40).trimEnd();
319
320
  return title;
320
321
  }
321
322
 
323
+ /** Strip common markdown formatting so titles render as plain text. */
324
+ function stripMarkdown(text: string): string {
325
+ return text
326
+ .replace(/\*\*(.+?)\*\*/g, '$1') // **bold**
327
+ .replace(/__(.+?)__/g, '$1') // __bold__
328
+ .replace(/\*(.+?)\*/g, '$1') // *italic*
329
+ .replace(/(?<!\w)_(.+?)_(?!\w)/g, '$1') // _italic_ (word-boundary-aware to preserve snake_case)
330
+ .replace(/~~(.+?)~~/g, '$1') // ~~strikethrough~~
331
+ .replace(/`(.+?)`/g, '$1') // `code`
332
+ .replace(/\[(.+?)\]\(.+?\)/g, '$1') // [link](url)
333
+ .replace(/^#{1,6}\s+/gm, ''); // # headings
334
+ }
335
+
322
336
  function deriveFallbackTitle(context?: TitleContext): string | null {
323
337
  if (!context) return null;
324
338
  if (context.systemHint) return truncate(context.systemHint, 40, '');
@@ -328,7 +342,7 @@ function deriveFallbackTitle(context?: TitleContext): string | null {
328
342
 
329
343
  function buildRegenerationPrompt(recentMessages: MessageRow[]): string {
330
344
  const parts: string[] = [
331
- 'Generate a very short title for this conversation based on the recent messages below. Rules: at most 5 words, at most 40 characters, no quotes.',
345
+ 'Generate a very short title for this conversation based on the recent messages below. Rules: at most 5 words, at most 40 characters, no quotes, no markdown formatting.',
332
346
  '',
333
347
  'Recent messages:',
334
348
  ];
@@ -1,6 +1,7 @@
1
1
  import { getDb } from './db-connection.js';
2
2
  import {
3
3
  addCoreColumns,
4
+ createActorTokenRecordsTable,
4
5
  createAssistantInboxTables,
5
6
  createCallSessionsTables,
6
7
  createCanonicalGuardianTables,
@@ -37,6 +38,7 @@ import {
37
38
  migrateReminderRoutingIntent,
38
39
  migrateSchemaIndexesAndColumns,
39
40
  migrateVoiceInviteColumns,
41
+ migrateVoiceInviteDisplayMetadata,
40
42
  recoverCrashedMigrations,
41
43
  runComplexMigrations,
42
44
  runLateMigrations,
@@ -165,5 +167,11 @@ export function initializeDb(): void {
165
167
  // 26. Voice invite columns on assistant_ingress_invites
166
168
  migrateVoiceInviteColumns(database);
167
169
 
170
+ // 27. Voice invite display metadata (friend_name, guardian_name) for personalized prompts
171
+ migrateVoiceInviteDisplayMetadata(database);
172
+
173
+ // 28. Actor token records (hash-only actor token persistence)
174
+ createActorTokenRecordsTable(database);
175
+
168
176
  validateMigrationState(database);
169
177
  }
@@ -8,6 +8,7 @@
8
8
  import { and, desc, eq, isNotNull } from 'drizzle-orm';
9
9
  import { v4 as uuid } from 'uuid';
10
10
 
11
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
11
12
  import { getConversationByKey, getOrCreateConversation, setConversationKeyIfAbsent } from './conversation-key-store.js';
12
13
  import { getDb } from './db.js';
13
14
  import { channelInboundEvents, conversations } from './schema.js';
@@ -73,7 +74,7 @@ export function recordInbound(
73
74
  const scopedMapping = assistantId ? getConversationByKey(scopedKey) : null;
74
75
  if (scopedMapping) {
75
76
  mapping = { conversationId: scopedMapping.conversationId, created: false };
76
- } else if (assistantId === 'self') {
77
+ } else if (assistantId === DAEMON_INTERNAL_ASSISTANT_ID) {
77
78
  const legacyMapping = getConversationByKey(legacyKey);
78
79
  if (legacyMapping) {
79
80
  mapping = { conversationId: legacyMapping.conversationId, created: false };
@@ -10,6 +10,7 @@
10
10
  import { and, desc, eq, inArray, lt } from 'drizzle-orm';
11
11
  import { v4 as uuid } from 'uuid';
12
12
 
13
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
13
14
  import { getLogger } from '../util/logger.js';
14
15
  import { getDb, rawChanges } from './db.js';
15
16
  import {
@@ -160,7 +161,7 @@ export function createGuardianActionRequest(params: {
160
161
 
161
162
  const row = {
162
163
  id,
163
- assistantId: params.assistantId ?? 'self',
164
+ assistantId: params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
164
165
  kind: params.kind,
165
166
  sourceChannel: params.sourceChannel,
166
167
  sourceConversationId: params.sourceConversationId,
@@ -9,6 +9,7 @@
9
9
  import { and, count, desc, eq, gt, lte } from 'drizzle-orm';
10
10
  import { v4 as uuid } from 'uuid';
11
11
 
12
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
12
13
  import { getDb } from './db.js';
13
14
  import { channelGuardianApprovalRequests } from './schema.js';
14
15
 
@@ -100,7 +101,7 @@ export function createApprovalRequest(params: {
100
101
  runId: params.runId,
101
102
  requestId: params.requestId ?? null,
102
103
  conversationId: params.conversationId,
103
- assistantId: params.assistantId ?? 'self',
104
+ assistantId: params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
104
105
  channel: params.channel,
105
106
  requesterExternalUserId: params.requesterExternalUserId,
106
107
  requesterChatId: params.requesterChatId,
@@ -402,7 +403,7 @@ export function listPendingApprovalRequests(params: {
402
403
  const db = getDb();
403
404
 
404
405
  const conditions = [
405
- eq(channelGuardianApprovalRequests.assistantId, params.assistantId ?? 'self'),
406
+ eq(channelGuardianApprovalRequests.assistantId, params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID),
406
407
  ];
407
408
  if (params.channel) {
408
409
  conditions.push(eq(channelGuardianApprovalRequests.channel, params.channel));
@@ -10,6 +10,7 @@ import { createHash, randomBytes, randomUUID } from 'node:crypto';
10
10
 
11
11
  import { and, desc, eq } from 'drizzle-orm';
12
12
 
13
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
13
14
  import { getDb } from './db.js';
14
15
  import { assistantIngressInvites, assistantIngressMembers } from './schema.js';
15
16
 
@@ -37,6 +38,9 @@ export interface IngressInvite {
37
38
  expectedExternalUserId: string | null;
38
39
  voiceCodeHash: string | null;
39
40
  voiceCodeDigits: number | null;
41
+ // Display metadata for personalized voice prompts (null for non-voice invites)
42
+ friendName: string | null;
43
+ guardianName: string | null;
40
44
  createdAt: number;
41
45
  updatedAt: number;
42
46
  }
@@ -97,6 +101,8 @@ function rowToInvite(row: typeof assistantIngressInvites.$inferSelect): IngressI
97
101
  expectedExternalUserId: row.expectedExternalUserId,
98
102
  voiceCodeHash: row.voiceCodeHash,
99
103
  voiceCodeDigits: row.voiceCodeDigits,
104
+ friendName: row.friendName,
105
+ guardianName: row.guardianName,
100
106
  createdAt: row.createdAt,
101
107
  updatedAt: row.updatedAt,
102
108
  };
@@ -138,6 +144,8 @@ export function createInvite(params: {
138
144
  expectedExternalUserId?: string;
139
145
  voiceCodeHash?: string;
140
146
  voiceCodeDigits?: number;
147
+ friendName?: string;
148
+ guardianName?: string;
141
149
  }): { invite: IngressInvite; rawToken: string } {
142
150
  const db = getDb();
143
151
  const now = Date.now();
@@ -147,7 +155,7 @@ export function createInvite(params: {
147
155
 
148
156
  const row = {
149
157
  id,
150
- assistantId: params.assistantId ?? 'self',
158
+ assistantId: params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
151
159
  sourceChannel: params.sourceChannel,
152
160
  tokenHash: tokenH,
153
161
  createdBySessionId: params.createdBySessionId ?? null,
@@ -162,6 +170,8 @@ export function createInvite(params: {
162
170
  expectedExternalUserId: params.expectedExternalUserId ?? null,
163
171
  voiceCodeHash: params.voiceCodeHash ?? null,
164
172
  voiceCodeDigits: params.voiceCodeDigits ?? null,
173
+ friendName: params.friendName ?? null,
174
+ guardianName: params.guardianName ?? null,
165
175
  createdAt: now,
166
176
  updatedAt: now,
167
177
  };
@@ -183,7 +193,7 @@ export function listInvites(params: {
183
193
  offset?: number;
184
194
  }): IngressInvite[] {
185
195
  const db = getDb();
186
- const assistantId = params.assistantId ?? 'self';
196
+ const assistantId = params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
187
197
 
188
198
  const conditions = [eq(assistantIngressInvites.assistantId, assistantId)];
189
199
 
@@ -8,6 +8,7 @@
8
8
  import { and, desc, eq, or } from 'drizzle-orm';
9
9
  import { v4 as uuid } from 'uuid';
10
10
 
11
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
11
12
  import { getDb } from './db.js';
12
13
  import { assistantIngressMembers } from './schema.js';
13
14
 
@@ -78,7 +79,7 @@ export function upsertMember(params: {
78
79
  createdBySessionId?: string;
79
80
  assistantId?: string;
80
81
  }): IngressMember {
81
- const assistantId = params.assistantId ?? 'self';
82
+ const assistantId = params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
82
83
 
83
84
  if (!params.externalUserId && !params.externalChatId) {
84
85
  throw new Error('At least one of externalUserId or externalChatId must be provided');
@@ -181,7 +182,7 @@ export function listMembers(params?: {
181
182
  offset?: number;
182
183
  }): IngressMember[] {
183
184
  const db = getDb();
184
- const assistantId = params?.assistantId ?? 'self';
185
+ const assistantId = params?.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
185
186
 
186
187
  const conditions = [eq(assistantIngressMembers.assistantId, assistantId)];
187
188
  if (params?.sourceChannel) {
@@ -304,7 +305,7 @@ export function findMember(params: {
304
305
  }
305
306
 
306
307
  const db = getDb();
307
- const assistantId = params.assistantId ?? 'self';
308
+ const assistantId = params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
308
309
 
309
310
  // Prefer lookup by externalUserId when available, fall back to externalChatId
310
311
  const matchConditions = [];
@@ -0,0 +1,39 @@
1
+ import type { DrizzleDb } from '../db-connection.js';
2
+
3
+ /**
4
+ * Create the actor_token_records table for hash-only actor token persistence.
5
+ *
6
+ * Stores the SHA-256 hash of each actor token alongside metadata for
7
+ * verification and revocation. The raw token plaintext is never stored.
8
+ */
9
+ export function createActorTokenRecordsTable(database: DrizzleDb): void {
10
+ database.run(/*sql*/ `
11
+ CREATE TABLE IF NOT EXISTS actor_token_records (
12
+ id TEXT PRIMARY KEY,
13
+ token_hash TEXT NOT NULL,
14
+ assistant_id TEXT NOT NULL,
15
+ guardian_principal_id TEXT NOT NULL,
16
+ hashed_device_id TEXT NOT NULL,
17
+ platform TEXT NOT NULL,
18
+ status TEXT NOT NULL DEFAULT 'active',
19
+ issued_at INTEGER NOT NULL,
20
+ expires_at INTEGER,
21
+ created_at INTEGER NOT NULL,
22
+ updated_at INTEGER NOT NULL
23
+ )
24
+ `);
25
+
26
+ // Unique active token per device binding
27
+ database.run(
28
+ /*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_actor_tokens_active_device
29
+ ON actor_token_records(assistant_id, guardian_principal_id, hashed_device_id)
30
+ WHERE status = 'active'`,
31
+ );
32
+
33
+ // Token hash lookup for verification
34
+ database.run(
35
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_actor_tokens_hash
36
+ ON actor_token_records(token_hash)
37
+ WHERE status = 'active'`,
38
+ );
39
+ }
@@ -0,0 +1,14 @@
1
+ import type { DrizzleDb } from '../db-connection.js';
2
+
3
+ /**
4
+ * Add display metadata columns to assistant_ingress_invites for personalized
5
+ * voice invite prompts. Both columns are nullable to keep existing invite
6
+ * rows compatible.
7
+ *
8
+ * - friend_name: the name of the person being invited (used in welcome prompt)
9
+ * - guardian_name: the name of the guardian who created the invite (used in prompts)
10
+ */
11
+ export function migrateVoiceInviteDisplayMetadata(database: DrizzleDb): void {
12
+ try { database.run(/*sql*/ `ALTER TABLE assistant_ingress_invites ADD COLUMN friend_name TEXT`); } catch { /* already exists */ }
13
+ try { database.run(/*sql*/ `ALTER TABLE assistant_ingress_invites ADD COLUMN guardian_name TEXT`); } catch { /* already exists */ }
14
+ }
@@ -39,6 +39,7 @@ export { migrateGuardianActionToolMetadata } from './034-guardian-action-tool-me
39
39
  export { migrateGuardianActionSupersession } from './035-guardian-action-supersession.js';
40
40
  export { migrateNormalizePhoneIdentities } from './036-normalize-phone-identities.js';
41
41
  export { migrateVoiceInviteColumns } from './037-voice-invite-columns.js';
42
+ export { createActorTokenRecordsTable } from './038-actor-token-records.js';
42
43
  export { createCoreTables } from './100-core-tables.js';
43
44
  export { createWatchersAndLogsTables } from './101-watchers-and-logs.js';
44
45
  export { addCoreColumns } from './102-alter-table-columns.js';
@@ -63,6 +64,7 @@ export { migrateFkCascadeRebuilds } from './120-fk-cascade-rebuilds.js';
63
64
  export { createCanonicalGuardianTables } from './121-canonical-guardian-requests.js';
64
65
  export { migrateCanonicalGuardianRequesterChatId } from './122-canonical-guardian-requester-chat-id.js';
65
66
  export { migrateCanonicalGuardianDeliveriesDestinationIndex } from './123-canonical-guardian-deliveries-destination-index.js';
67
+ export { migrateVoiceInviteDisplayMetadata } from './124-voice-invite-display-metadata.js';
66
68
  export {
67
69
  MIGRATION_REGISTRY,
68
70
  type MigrationRegistryEntry,
@@ -1,5 +1,7 @@
1
1
  import { blob, index,integer, real, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
2
2
 
3
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
4
+
3
5
  export const conversations = sqliteTable('conversations', {
4
6
  id: text('id').primaryKey(),
5
7
  title: text('title'),
@@ -683,7 +685,7 @@ export const channelGuardianApprovalRequests = sqliteTable('channel_guardian_app
683
685
  runId: text('run_id').notNull(),
684
686
  requestId: text('request_id'),
685
687
  conversationId: text('conversation_id').notNull(),
686
- assistantId: text('assistant_id').notNull().default('self'),
688
+ assistantId: text('assistant_id').notNull().default(DAEMON_INTERNAL_ASSISTANT_ID),
687
689
  channel: text('channel').notNull(),
688
690
  requesterExternalUserId: text('requester_external_user_id').notNull(),
689
691
  requesterChatId: text('requester_chat_id').notNull(),
@@ -819,7 +821,7 @@ export const mediaEventFeedback = sqliteTable('media_event_feedback', {
819
821
 
820
822
  export const guardianActionRequests = sqliteTable('guardian_action_requests', {
821
823
  id: text('id').primaryKey(),
822
- assistantId: text('assistant_id').notNull().default('self'),
824
+ assistantId: text('assistant_id').notNull().default(DAEMON_INTERNAL_ASSISTANT_ID),
823
825
  kind: text('kind').notNull(), // 'ask_guardian'
824
826
  sourceChannel: text('source_channel').notNull(), // 'voice'
825
827
  sourceConversationId: text('source_conversation_id').notNull(),
@@ -930,7 +932,7 @@ export const canonicalGuardianDeliveries = sqliteTable('canonical_guardian_deliv
930
932
 
931
933
  export const assistantIngressInvites = sqliteTable('assistant_ingress_invites', {
932
934
  id: text('id').primaryKey(),
933
- assistantId: text('assistant_id').notNull().default('self'),
935
+ assistantId: text('assistant_id').notNull().default(DAEMON_INTERNAL_ASSISTANT_ID),
934
936
  sourceChannel: text('source_channel').notNull(),
935
937
  tokenHash: text('token_hash').notNull(),
936
938
  createdBySessionId: text('created_by_session_id'),
@@ -946,13 +948,16 @@ export const assistantIngressInvites = sqliteTable('assistant_ingress_invites',
946
948
  expectedExternalUserId: text('expected_external_user_id'),
947
949
  voiceCodeHash: text('voice_code_hash'),
948
950
  voiceCodeDigits: integer('voice_code_digits'),
951
+ // Display metadata for personalized voice prompts (nullable — non-voice invites leave these NULL)
952
+ friendName: text('friend_name'),
953
+ guardianName: text('guardian_name'),
949
954
  createdAt: integer('created_at').notNull(),
950
955
  updatedAt: integer('updated_at').notNull(),
951
956
  });
952
957
 
953
958
  export const assistantIngressMembers = sqliteTable('assistant_ingress_members', {
954
959
  id: text('id').primaryKey(),
955
- assistantId: text('assistant_id').notNull().default('self'),
960
+ assistantId: text('assistant_id').notNull().default(DAEMON_INTERNAL_ASSISTANT_ID),
956
961
  sourceChannel: text('source_channel').notNull(),
957
962
  externalUserId: text('external_user_id'),
958
963
  externalChatId: text('external_chat_id'),
@@ -974,7 +979,7 @@ export const assistantInboxThreadState = sqliteTable('assistant_inbox_thread_sta
974
979
  conversationId: text('conversation_id')
975
980
  .primaryKey()
976
981
  .references(() => conversations.id, { onDelete: 'cascade' }),
977
- assistantId: text('assistant_id').notNull().default('self'),
982
+ assistantId: text('assistant_id').notNull().default(DAEMON_INTERNAL_ASSISTANT_ID),
978
983
  sourceChannel: text('source_channel').notNull(),
979
984
  externalChatId: text('external_chat_id').notNull(),
980
985
  externalUserId: text('external_user_id'),
@@ -1138,6 +1143,22 @@ export const conversationAssistantAttentionState = sqliteTable('conversation_ass
1138
1143
  index('idx_conv_attn_state_assistant_last_seen').on(table.assistantId, table.lastSeenAssistantMessageAt),
1139
1144
  ]);
1140
1145
 
1146
+ // ── Actor Token Records ──────────────────────────────────────────────
1147
+
1148
+ export const actorTokenRecords = sqliteTable('actor_token_records', {
1149
+ id: text('id').primaryKey(),
1150
+ tokenHash: text('token_hash').notNull(),
1151
+ assistantId: text('assistant_id').notNull(),
1152
+ guardianPrincipalId: text('guardian_principal_id').notNull(),
1153
+ hashedDeviceId: text('hashed_device_id').notNull(),
1154
+ platform: text('platform').notNull(),
1155
+ status: text('status').notNull().default('active'),
1156
+ issuedAt: integer('issued_at').notNull(),
1157
+ expiresAt: integer('expires_at'),
1158
+ createdAt: integer('created_at').notNull(),
1159
+ updatedAt: integer('updated_at').notNull(),
1160
+ });
1161
+
1141
1162
  // ── Scoped Approval Grants ──────────────────────────────────────────
1142
1163
 
1143
1164
  export const scopedApprovalGrants = sqliteTable('scoped_approval_grants', {
@@ -81,3 +81,27 @@ export interface SendOptions {
81
81
  /** Optional assistant scope for multi-assistant channels. */
82
82
  assistantId?: string;
83
83
  }
84
+
85
+ /** Result from a sender digest scan — groups messages by sender for bulk cleanup. */
86
+ export interface SenderDigestEntry {
87
+ id: string;
88
+ displayName: string;
89
+ email: string;
90
+ messageCount: number;
91
+ hasUnsubscribe: boolean;
92
+ newestMessageId: string;
93
+ searchQuery: string;
94
+ messageIds: string[];
95
+ hasMore: boolean;
96
+ }
97
+
98
+ export interface SenderDigestResult {
99
+ senders: SenderDigestEntry[];
100
+ totalScanned: number;
101
+ queryUsed: string;
102
+ }
103
+
104
+ export interface ArchiveResult {
105
+ archived: number;
106
+ truncated?: boolean;
107
+ }