@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
@@ -0,0 +1,59 @@
1
+ import {
2
+ buildPointerGenerationPrompt,
3
+ getPointerFallbackMessage,
4
+ includesRequiredFacts,
5
+ POINTER_COPY_MAX_TOKENS,
6
+ POINTER_COPY_SYSTEM_PROMPT,
7
+ POINTER_COPY_TIMEOUT_MS,
8
+ } from '../calls/call-pointer-message-composer.js';
9
+ import { loadConfig } from '../config/loader.js';
10
+ import { getFailoverProvider } from '../providers/registry.js';
11
+ import type { PointerCopyGenerator } from '../runtime/http-types.js';
12
+
13
+ /**
14
+ * Create the daemon-owned pointer copy generator that resolves providers
15
+ * and calls `provider.sendMessage` to generate pointer message text.
16
+ * This keeps all provider awareness in the daemon lifecycle, away from
17
+ * the runtime composer.
18
+ */
19
+ export function createPointerCopyGenerator(): PointerCopyGenerator {
20
+ return async (context, options = {}) => {
21
+ const config = loadConfig();
22
+ let provider;
23
+ try {
24
+ provider = getFailoverProvider(config.provider, config.providerOrder);
25
+ } catch {
26
+ return null;
27
+ }
28
+
29
+ const fallbackText = options.fallbackText?.trim() || getPointerFallbackMessage(context);
30
+ const requiredFacts = options.requiredFacts
31
+ ?.map((f) => f.trim())
32
+ .filter((f) => f.length > 0);
33
+ const prompt = buildPointerGenerationPrompt(context, fallbackText, requiredFacts);
34
+
35
+ const response = await provider.sendMessage(
36
+ [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
37
+ [],
38
+ POINTER_COPY_SYSTEM_PROMPT,
39
+ {
40
+ config: {
41
+ max_tokens: options.maxTokens ?? POINTER_COPY_MAX_TOKENS,
42
+ modelIntent: 'latency-optimized',
43
+ },
44
+ signal: AbortSignal.timeout(options.timeoutMs ?? POINTER_COPY_TIMEOUT_MS),
45
+ },
46
+ );
47
+
48
+ const block = response.content.find((entry) => entry.type === 'text');
49
+ const text = block && 'text' in block ? block.text.trim() : '';
50
+ if (!text) return null;
51
+ const cleaned = text
52
+ .replace(/^["'`]+/, '')
53
+ .replace(/["'`]+$/, '')
54
+ .trim();
55
+ if (!cleaned) return null;
56
+ if (!includesRequiredFacts(cleaned, requiredFacts)) return null;
57
+ return cleaned;
58
+ };
59
+ }
@@ -79,7 +79,7 @@ export class ComputerUseSession {
79
79
  private pendingSurfaceActions = new Map<string, {
80
80
  resolve: (result: ToolExecutionResult) => void;
81
81
  }>();
82
- private surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>();
82
+ private surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>();
83
83
  private terminalNotified = false;
84
84
  private prompter: PermissionPrompter | null = null;
85
85
 
@@ -337,7 +337,7 @@ export class ComputerUseSession {
337
337
  const awaitAction = (input.await_action as boolean) ?? isInteractive;
338
338
 
339
339
  // Track surface state for ui_update merging
340
- this.surfaceState.set(surfaceId, { surfaceType, data });
340
+ this.surfaceState.set(surfaceId, { surfaceType, data, title });
341
341
 
342
342
  this.sendToClient({
343
343
  type: 'ui_surface_show',
@@ -532,9 +532,6 @@ export class ComputerUseSession {
532
532
  maxTokens: 4096,
533
533
  maxInputTokens: cuConfig.contextWindow.maxInputTokens,
534
534
  toolChoice: { type: 'any' },
535
- // Allow MAX_STEPS non-terminal actions plus one terminal turn
536
- // (computer_use_done/computer_use_respond), since AgentLoop caps tool turns globally.
537
- maxToolUseTurns: MAX_STEPS + 1,
538
535
  },
539
536
  toolDefs,
540
537
  toolExecutor,
@@ -7,6 +7,8 @@ import { v4 as uuid } from 'uuid';
7
7
 
8
8
  import { packageApp } from '../../bundler/app-bundler.js';
9
9
  import { defaultGallery } from '../../gallery/default-gallery.js';
10
+ import { resolveHomeBaseAppId } from '../../home-base/bootstrap.js';
11
+ import { isPrebuiltHomeBaseApp } from '../../home-base/prebuilt-home-base-updater.js';
10
12
  import { getAppDiff, getAppFileAtVersion, getAppHistory, restoreAppVersion } from '../../memory/app-git-service.js';
11
13
  import { createApp, createAppRecord, deleteAppRecord, getApp, getAppPreview, listApps, queryAppRecords, updateApp,updateAppRecord } from '../../memory/app-store.js';
12
14
  import { createSharedAppLink } from '../../memory/shared-app-links-store.js';
@@ -77,28 +79,54 @@ export function handleAppDataRequest(
77
79
  }
78
80
 
79
81
  export function handleAppOpenRequest(msg: { appId: string }, socket: net.Socket, ctx: HandlerContext): void {
80
- const appId = msg.appId;
81
- if (!appId) {
82
- ctx.send(socket, { type: 'error', message: 'app_open_request requires appId' });
83
- return;
84
- }
82
+ try {
83
+ const appId = msg.appId;
84
+ if (!appId) {
85
+ ctx.send(socket, { type: 'error', message: 'app_open_request requires appId' });
86
+ return;
87
+ }
88
+
89
+ const app = getApp(appId);
90
+ if (app) {
91
+ const surfaceId = `app-open-${uuid()}`;
92
+ ctx.send(socket, {
93
+ type: 'ui_surface_show',
94
+ sessionId: 'app-panel',
95
+ surfaceId,
96
+ surfaceType: 'dynamic_page',
97
+ title: app.name,
98
+ data: { html: app.htmlDefinition, appId: app.id, appType: app.appType },
99
+ display: 'panel',
100
+ } as UiSurfaceShow);
101
+ return;
102
+ }
103
+
104
+ // Fallback: the ID might be a surfaceId from an ephemeral ui_show surface
105
+ // (not a persistent app). Search active sessions for cached surface data.
106
+ for (const session of ctx.sessions.values()) {
107
+ const cached = session.surfaceState.get(appId);
108
+ if (cached && cached.surfaceType === 'dynamic_page') {
109
+ const newSurfaceId = `app-open-${uuid()}`;
110
+ ctx.send(socket, {
111
+ type: 'ui_surface_show',
112
+ sessionId: 'app-panel',
113
+ surfaceId: newSurfaceId,
114
+ surfaceType: 'dynamic_page',
115
+ title: cached.title ?? (cached.data as { preview?: { title?: string } }).preview?.title,
116
+ data: cached.data,
117
+ display: 'panel',
118
+ } as UiSurfaceShow);
119
+ return;
120
+ }
121
+ }
85
122
 
86
- const app = getApp(appId);
87
- if (!app) {
123
+ log.warn({ appId }, 'App not found in store or session surfaces');
88
124
  ctx.send(socket, { type: 'error', message: `App not found: ${appId}` });
89
- return;
125
+ } catch (err) {
126
+ const message = err instanceof Error ? err.message : String(err);
127
+ log.error({ err, appId: msg.appId }, 'Failed to handle app open request');
128
+ ctx.send(socket, { type: 'error', message: `Failed to open app: ${message}` });
90
129
  }
91
-
92
- const surfaceId = `app-open-${uuid()}`;
93
- ctx.send(socket, {
94
- type: 'ui_surface_show',
95
- sessionId: 'app-panel',
96
- surfaceId,
97
- surfaceType: 'dynamic_page',
98
- title: app.name,
99
- data: { html: app.htmlDefinition, appId: app.id, appType: app.appType },
100
- display: 'panel',
101
- } as UiSurfaceShow);
102
130
  }
103
131
 
104
132
  export function handleAppUpdatePreview(msg: AppUpdatePreviewRequest, socket: net.Socket, ctx: HandlerContext): void {
@@ -114,7 +142,35 @@ export function handleAppUpdatePreview(msg: AppUpdatePreviewRequest, socket: net
114
142
 
115
143
  export function handleAppsList(socket: net.Socket, ctx: HandlerContext): void {
116
144
  try {
117
- const apps = listApps();
145
+ const allApps = listApps();
146
+ const homeBaseId = resolveHomeBaseAppId();
147
+
148
+ // When no home base was found by ID, do a single targeted search for an app
149
+ // matching the HTML marker. listApps() returns metadata-only (no htmlDefinition),
150
+ // so the HTML marker check requires loading the full app from disk. We limit
151
+ // this expensive operation to the case where homeBaseId is null.
152
+ const excludeIds = new Set<string>();
153
+ if (homeBaseId) {
154
+ excludeIds.add(homeBaseId);
155
+ } else {
156
+ for (const a of allApps) {
157
+ if (isPrebuiltHomeBaseApp(a)) {
158
+ excludeIds.add(a.id);
159
+ continue;
160
+ }
161
+ const fullApp = getApp(a.id);
162
+ if (fullApp && isPrebuiltHomeBaseApp(fullApp)) {
163
+ excludeIds.add(a.id);
164
+ continue;
165
+ }
166
+ }
167
+ }
168
+
169
+ const apps = allApps.filter((a) => {
170
+ if (excludeIds.has(a.id)) return false;
171
+ if (isPrebuiltHomeBaseApp(a)) return false;
172
+ return true;
173
+ });
118
174
  ctx.send(socket, {
119
175
  type: 'apps_list_response',
120
176
  apps: apps.map((a) => {
@@ -19,7 +19,6 @@ import {
19
19
  startOutbound,
20
20
  } from '../../runtime/guardian-outbound-actions.js';
21
21
  import type {
22
- ChannelReadinessRequest,
23
22
  GuardianVerificationRequest,
24
23
  GuardianVerificationResponse,
25
24
  } from '../ipc-protocol.js';
@@ -60,7 +59,6 @@ export function getReadinessService(): ChannelReadinessService {
60
59
 
61
60
  export function createGuardianChallenge(
62
61
  channel?: ChannelId,
63
- assistantId?: string,
64
62
  rebind?: boolean,
65
63
  sessionId?: string,
66
64
  ): GuardianVerificationResult {
@@ -89,7 +87,6 @@ export function createGuardianChallenge(
89
87
 
90
88
  export function getGuardianStatus(
91
89
  channel?: ChannelId,
92
- _assistantId?: string,
93
90
  ): GuardianVerificationResult {
94
91
  const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
95
92
  const resolvedChannel = channel ?? 'telegram';
@@ -166,10 +163,10 @@ export function handleGuardianVerification(
166
163
 
167
164
  try {
168
165
  if (msg.action === 'create_challenge') {
169
- const result = createGuardianChallenge(channel, assistantId, msg.rebind, msg.sessionId);
166
+ const result = createGuardianChallenge(channel, msg.rebind, msg.sessionId);
170
167
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
171
168
  } else if (msg.action === 'status') {
172
- const result = getGuardianStatus(channel, assistantId);
169
+ const result = getGuardianStatus(channel);
173
170
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
174
171
  } else if (msg.action === 'revoke') {
175
172
  // Capture binding before revoking so we can revoke the guardian's
@@ -198,13 +195,13 @@ export function handleGuardianVerification(
198
195
  channel,
199
196
  });
200
197
  } else if (msg.action === 'start_outbound') {
201
- const result = startOutbound({ channel, assistantId, destination: msg.destination, rebind: msg.rebind, originConversationId: msg.originConversationId });
198
+ const result = startOutbound({ channel, destination: msg.destination, rebind: msg.rebind, originConversationId: msg.originConversationId });
202
199
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
203
200
  } else if (msg.action === 'resend_outbound') {
204
- const result = resendOutbound({ channel, assistantId, originConversationId: msg.originConversationId });
201
+ const result = resendOutbound({ channel, originConversationId: msg.originConversationId });
205
202
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
206
203
  } else if (msg.action === 'cancel_outbound') {
207
- const result = cancelOutbound({ channel, assistantId });
204
+ const result = cancelOutbound({ channel });
208
205
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
209
206
  } else {
210
207
  ctx.send(socket, {
@@ -227,53 +224,6 @@ export function handleGuardianVerification(
227
224
  }
228
225
 
229
226
 
230
- // ---------------------------------------------------------------------------
231
- // Channel readiness handler
232
- // ---------------------------------------------------------------------------
233
-
234
- export async function handleChannelReadiness(
235
- msg: ChannelReadinessRequest,
236
- socket: net.Socket,
237
- ctx: HandlerContext,
238
- ): Promise<void> {
239
- try {
240
- const service = getReadinessService();
241
-
242
- if (msg.action === 'refresh') {
243
- if (msg.channel) {
244
- service.invalidateChannel(msg.channel, msg.assistantId);
245
- } else {
246
- service.invalidateAll();
247
- }
248
- }
249
-
250
- const snapshots = await service.getReadiness(msg.channel, msg.includeRemote, msg.assistantId);
251
-
252
- ctx.send(socket, {
253
- type: 'channel_readiness_response',
254
- success: true,
255
- snapshots: snapshots.map((s) => ({
256
- channel: s.channel,
257
- ready: s.ready,
258
- checkedAt: s.checkedAt,
259
- stale: s.stale,
260
- reasons: s.reasons,
261
- localChecks: s.localChecks,
262
- remoteChecks: s.remoteChecks,
263
- })),
264
- });
265
- } catch (err) {
266
- const message = err instanceof Error ? err.message : String(err);
267
- log.error({ err }, 'Failed to handle channel readiness');
268
- ctx.send(socket, {
269
- type: 'channel_readiness_response',
270
- success: false,
271
- error: message,
272
- });
273
- }
274
- }
275
-
276
227
  export const channelHandlers = defineHandlers({
277
- channel_readiness: handleChannelReadiness,
278
228
  guardian_verification: handleGuardianVerification,
279
229
  });
@@ -305,9 +305,15 @@ async function executeApprove(
305
305
  }
306
306
  }
307
307
  session.setAssistantId(assistantId);
308
- // The guardian approved this escalation, so tag as guardian trust to avoid
309
- // unknown-provenance memory gating.
310
- session.setGuardianContext({ trustClass: 'guardian', sourceChannel: sourceChannel ?? 'vellum' });
308
+ // The guardian already approved this escalation via the inbox, so we
309
+ // directly set guardian trust. Going through resolveLocalIpcGuardianContext
310
+ // would look up the vellum binding's guardian ID and compare it against
311
+ // a different channel's binding (e.g. telegram/sms), misclassifying the
312
+ // actor as 'unknown'.
313
+ session.setGuardianContext({
314
+ sourceChannel: sourceChannel ?? 'vellum',
315
+ trustClass: 'guardian',
316
+ });
311
317
  session.setCommandIntent(null);
312
318
 
313
319
  // Process the message through the agent loop (no IPC event callback
@@ -10,6 +10,7 @@ import {
10
10
  setIngressPublicBaseUrl,
11
11
  } from '../../config/env.js';
12
12
  import { loadRawConfig, saveRawConfig } from '../../config/loader.js';
13
+ import { registerCallbackRoute, resolveCallbackUrl, shouldUsePlatformCallbacks } from '../../inbound/platform-callback-registration.js';
13
14
  import {
14
15
  getTwilioSmsWebhookUrl,
15
16
  getTwilioStatusCallbackUrl,
@@ -92,9 +93,21 @@ export async function syncTwilioWebhooks(
92
93
  ingressConfig: IngressConfig,
93
94
  ): Promise<{ success: boolean; warning?: string }> {
94
95
  try {
95
- const voiceUrl = getTwilioVoiceWebhookUrl(ingressConfig);
96
- const statusCallbackUrl = getTwilioStatusCallbackUrl(ingressConfig);
97
- const smsUrl = getTwilioSmsWebhookUrl(ingressConfig);
96
+ const voiceUrl = await resolveCallbackUrl(
97
+ () => getTwilioVoiceWebhookUrl(ingressConfig),
98
+ 'webhooks/twilio/voice',
99
+ 'twilio_voice',
100
+ );
101
+ const statusCallbackUrl = await resolveCallbackUrl(
102
+ () => getTwilioStatusCallbackUrl(ingressConfig),
103
+ 'webhooks/twilio/status',
104
+ 'twilio_status',
105
+ );
106
+ const smsUrl = await resolveCallbackUrl(
107
+ () => getTwilioSmsWebhookUrl(ingressConfig),
108
+ 'webhooks/twilio/sms',
109
+ 'twilio_sms',
110
+ );
98
111
  await updatePhoneNumberWebhooks(accountSid, authToken, phoneNumber, {
99
112
  voiceUrl,
100
113
  statusCallbackUrl,
@@ -183,6 +196,18 @@ export async function handleIngressConfig(
183
196
  // Use the effective URL from process.env (which accounts for the
184
197
  // fallback branch above) rather than the raw `value` from the UI.
185
198
  const effectiveUrl = isEnabled ? getIngressPublicBaseUrl() : undefined;
199
+
200
+ // When containerized with a platform, register the Telegram callback
201
+ // route so the platform knows how to forward Telegram webhooks.
202
+ // This must happen independently of effectiveUrl — in containerized
203
+ // deployments without ingress.publicBaseUrl, platform callbacks are the
204
+ // only way to receive Telegram webhooks.
205
+ if (shouldUsePlatformCallbacks()) {
206
+ registerCallbackRoute('webhooks/telegram', 'telegram').catch((err) => {
207
+ log.warn({ err }, 'Failed to register Telegram platform callback route');
208
+ });
209
+ }
210
+
186
211
  triggerGatewayReconcile(effectiveUrl);
187
212
 
188
213
  // Best-effort Twilio webhook reconciliation: when ingress is being
@@ -1,6 +1,7 @@
1
1
  import * as net from 'node:net';
2
2
 
3
3
  import { getIngressPublicBaseUrl } from '../../config/env.js';
4
+ import { registerCallbackRoute, shouldUsePlatformCallbacks } from '../../inbound/platform-callback-registration.js';
4
5
  import { deleteSecureKey,getSecureKey, setSecureKey } from '../../security/secure-keys.js';
5
6
  import { deleteCredentialMetadata, getCredentialMetadata,upsertCredentialMetadata } from '../../tools/credentials/metadata-store.js';
6
7
  import type { TelegramConfigRequest, TelegramConfigResponse } from '../ipc-protocol.js';
@@ -161,6 +162,17 @@ export async function setTelegramConfig(botToken?: string): Promise<TelegramConf
161
162
  hasWebhookSecret,
162
163
  };
163
164
 
165
+ // When containerized with a platform, register the Telegram callback
166
+ // route so the platform knows how to forward Telegram webhooks.
167
+ // This must happen independently of effectiveUrl — in containerized
168
+ // deployments without ingress.publicBaseUrl, platform callbacks are the
169
+ // only way to receive Telegram webhooks.
170
+ if (shouldUsePlatformCallbacks()) {
171
+ registerCallbackRoute('webhooks/telegram', 'telegram').catch((err) => {
172
+ log.warn({ err }, 'Failed to register Telegram platform callback route');
173
+ });
174
+ }
175
+
164
176
  // Trigger gateway reconcile so the webhook registration updates immediately
165
177
  const effectiveUrl = getIngressPublicBaseUrl();
166
178
  if (effectiveUrl) {
@@ -11,13 +11,12 @@
11
11
  * config-platform.ts — Platform base URL configuration
12
12
  * config-integrations.ts — Vercel API & Twitter integration
13
13
  * config-telegram.ts — Telegram bot configuration
14
- * config-twilio.ts Twilio SMS/voice configuration
15
- * config-channels.ts — Channel guardian & readiness
14
+ * config-channels.ts Channel guardian verification
16
15
  * config-tools.ts — Env vars, tool permission simulation, tool names
17
16
  */
18
17
 
19
18
  // Re-export individual handlers for direct import by tests and other modules
20
- export { getReadinessService,handleChannelReadiness, handleGuardianVerification } from './config-channels.js';
19
+ export { getReadinessService, handleGuardianVerification } from './config-channels.js';
21
20
  export { handleHeartbeatChecklistRead, handleHeartbeatChecklistWrite,handleHeartbeatConfig, handleHeartbeatRunNow, handleHeartbeatRunsList } from './config-heartbeat.js';
22
21
  export { computeGatewayTarget, handleIngressConfig, syncTwilioWebhooks,triggerGatewayReconcile } from './config-ingress.js';
23
22
  export { handleTwitterIntegrationConfig,handleVercelApiConfig } from './config-integrations.js';
@@ -28,7 +27,6 @@ export { handleShareToSlack, handleSlackWebhookConfig } from './config-slack.js'
28
27
  export { handleTelegramConfig, summarizeTelegramError } from './config-telegram.js';
29
28
  export { handleEnvVarsRequest, handleToolNamesList,handleToolPermissionSimulate } from './config-tools.js';
30
29
  export { handleAcceptStarterBundle,handleAddTrustRule, handleRemoveTrustRule, handleTrustRulesList, handleUpdateTrustRule } from './config-trust.js';
31
- export { handleTwilioConfig } from './config-twilio.js';
32
30
  export { broadcastClientSettingsUpdate, handleVoiceConfigUpdate, normalizeActivationKey } from './config-voice.js';
33
31
 
34
32
  // Assemble the combined dispatch map from domain-specific handler groups
@@ -43,7 +41,6 @@ import { slackHandlers } from './config-slack.js';
43
41
  import { telegramHandlers } from './config-telegram.js';
44
42
  import { toolHandlers } from './config-tools.js';
45
43
  import { trustHandlers } from './config-trust.js';
46
- import { twilioHandlers } from './config-twilio.js';
47
44
  import { voiceHandlers } from './config-voice.js';
48
45
 
49
46
  export const configHandlers = {
@@ -55,7 +52,6 @@ export const configHandlers = {
55
52
  ...platformHandlers,
56
53
  ...integrationHandlers,
57
54
  ...telegramHandlers,
58
- ...twilioHandlers,
59
55
  ...channelHandlers,
60
56
  ...toolHandlers,
61
57
  ...heartbeatHandlers,
@@ -1,5 +1,6 @@
1
1
  import * as net from 'node:net';
2
2
 
3
+ import { cleanupPairingState } from '../../runtime/routes/pairing-routes.js';
3
4
  import {
4
5
  approveDevice,
5
6
  clearAllDevices,
@@ -46,6 +47,7 @@ function handlePairingApprovalResponse(
46
47
 
47
48
  if (msg.decision === 'deny') {
48
49
  pairingStoreRef.deny(msg.pairingRequestId);
50
+ cleanupPairingState(msg.pairingRequestId);
49
51
  log.info({ pairingRequestId: msg.pairingRequestId }, 'Pairing request denied');
50
52
  return;
51
53
  }
@@ -19,6 +19,7 @@ import { GENERATING_TITLE, queueGenerateConversationTitle, UNTITLED_FALLBACK } f
19
19
  import * as externalConversationStore from '../../memory/external-conversation-store.js';
20
20
  import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
21
21
  import { routeGuardianReply } from '../../runtime/guardian-reply-router.js';
22
+ import { resolveLocalIpcGuardianContext } from '../../runtime/local-actor-identity.js';
22
23
  import * as pendingInteractions from '../../runtime/pending-interactions.js';
23
24
  import { checkIngressForSecrets } from '../../security/secret-ingress.js';
24
25
  import { compileCustomPatterns, redactSecrets } from '../../security/secret-scanner.js';
@@ -176,6 +177,10 @@ export async function handleUserMessage(
176
177
  conversationId: msg.sessionId,
177
178
  sourceChannel: ipcChannel,
178
179
  });
180
+ // Route prompter-originated events (confirmation_request/secret_request)
181
+ // through the IPC wrapper so pending-interactions + canonical tracking
182
+ // are updated before the message is sent to the client.
183
+ session.updateClient(sendEvent, false);
179
184
  const ipcInterface = parseInterfaceId(msg.interface);
180
185
  if (!ipcInterface) {
181
186
  ctx.send(socket, {
@@ -264,6 +269,7 @@ export async function handleUserMessage(
264
269
  }
265
270
 
266
271
  rlog.info({ source }, 'Processing user message');
272
+ session.emitActivityState('thinking', 'message_dequeued', 'assistant_turn', dispatchRequestId);
267
273
  session.setTurnChannelContext({
268
274
  userMessageChannel: ipcChannel,
269
275
  assistantMessageChannel: ipcChannel,
@@ -273,9 +279,11 @@ export async function handleUserMessage(
273
279
  assistantMessageInterface: ipcInterface,
274
280
  });
275
281
  session.setAssistantId(DAEMON_INTERNAL_ASSISTANT_ID);
276
- // IPC/desktop user IS the guardian default to guardian trust so
277
- // messages are not tagged as unknown provenance.
278
- session.setGuardianContext({ trustClass: 'guardian', sourceChannel: ipcChannel });
282
+ // Resolve local IPC actor identity through the same trust pipeline
283
+ // used by HTTP channel ingress. The vellum guardian binding provides
284
+ // the guardianPrincipalId, and resolveGuardianContext classifies the
285
+ // local user as 'guardian' via binding match.
286
+ session.setGuardianContext(resolveLocalIpcGuardianContext(ipcChannel));
279
287
  session.setCommandIntent(null);
280
288
  // Fire-and-forget: don't block the IPC handler so the connection can
281
289
  // continue receiving messages (e.g. cancel, confirmations, or
@@ -600,9 +608,29 @@ export async function handleUserMessage(
600
608
  conversationId: msg.sessionId,
601
609
  pendingRequestIds: pendingRequestIdsForConversation,
602
610
  approvalConversationGenerator: desktopApprovalConversationGenerator,
611
+ emissionContext: {
612
+ source: 'inline_nl',
613
+ causedByRequestId: requestId,
614
+ decisionText: messageText.trim(),
615
+ },
603
616
  });
604
617
 
605
618
  if (routerResult.consumed && routerResult.type !== 'nl_keep_pending') {
619
+ // Success-path emissions (approved/denied) are handled centrally
620
+ // by handleConfirmationResponse (called via the resolver chain).
621
+ // However, stale/failed paths never reach handleConfirmationResponse,
622
+ // so we emit resolved_stale here for those cases.
623
+ if (routerResult.requestId && !routerResult.decisionApplied) {
624
+ session.emitConfirmationStateChanged({
625
+ sessionId: msg.sessionId,
626
+ requestId: routerResult.requestId,
627
+ state: 'resolved_stale',
628
+ source: 'inline_nl',
629
+ causedByRequestId: requestId,
630
+ decisionText: messageText.trim(),
631
+ });
632
+ }
633
+
606
634
  const consumedChannelMeta = {
607
635
  userMessageChannel: ipcChannel,
608
636
  assistantMessageChannel: ipcChannel,
@@ -686,6 +714,19 @@ export async function handleUserMessage(
686
714
  // will see the denial and can re-request the tool if still needed.
687
715
  if (session.hasAnyPendingConfirmation()) {
688
716
  rlog.info('Auto-denying pending confirmation(s) due to new user message');
717
+ // Emit authoritative confirmation state for each auto-denied request
718
+ // before the prompter clears them.
719
+ for (const interaction of pendingInteractions.getByConversation(msg.sessionId)) {
720
+ if (interaction.session === session && interaction.kind === 'confirmation') {
721
+ session.emitConfirmationStateChanged({
722
+ sessionId: msg.sessionId,
723
+ requestId: interaction.requestId,
724
+ state: 'denied',
725
+ source: 'auto_deny',
726
+ causedByRequestId: requestId,
727
+ });
728
+ }
729
+ }
689
730
  session.denyAllPendingConfirmations();
690
731
  // Keep the pending-interaction tracker aligned with the prompter so
691
732
  // stale request IDs are not reused as routing candidates.
@@ -731,6 +772,8 @@ export function handleConfirmationResponse(
731
772
  msg.decision,
732
773
  msg.selectedPattern,
733
774
  msg.selectedScope,
775
+ undefined,
776
+ { source: 'button' },
734
777
  );
735
778
  syncCanonicalStatusFromIpcConfirmationDecision(msg.requestId, msg.decision);
736
779
  pendingInteractions.resolve(msg.requestId);
@@ -910,6 +953,7 @@ export async function handleSessionCreate(
910
953
  conversationId: conversation.id,
911
954
  sourceChannel: transportChannel,
912
955
  });
956
+ session.updateClient(sendEvent, false);
913
957
  session.setTurnChannelContext({
914
958
  userMessageChannel: transportChannel,
915
959
  assistantMessageChannel: transportChannel,
@@ -1260,6 +1304,7 @@ export async function handleRegenerate(
1260
1304
  conversationId: msg.sessionId,
1261
1305
  sourceChannel: regenerateChannel,
1262
1306
  });
1307
+ session.updateClient(sendEvent, false);
1263
1308
  const requestId = uuid();
1264
1309
  session.traceEmitter.emit('request_received', 'Regenerate requested', {
1265
1310
  requestId,
@@ -338,9 +338,24 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
338
338
  let currentSegmentParts: string[] = [];
339
339
  let hasOpenSegment = false;
340
340
 
341
+ function joinWithSpacing(parts: string[]): string {
342
+ let result = parts[0] ?? '';
343
+ for (let i = 1; i < parts.length; i++) {
344
+ const prev = result[result.length - 1];
345
+ const next = parts[i][0];
346
+ // Only insert a space when neither side already has whitespace
347
+ if (prev && next && prev !== ' ' && prev !== '\n' && prev !== '\t' &&
348
+ next !== ' ' && next !== '\n' && next !== '\t') {
349
+ result += ' ';
350
+ }
351
+ result += parts[i];
352
+ }
353
+ return result;
354
+ }
355
+
341
356
  function finalizeSegment(): void {
342
357
  if (hasOpenSegment) {
343
- textSegments[textSegments.length - 1] = currentSegmentParts.join('');
358
+ textSegments[textSegments.length - 1] = joinWithSpacing(currentSegmentParts);
344
359
  currentSegmentParts = [];
345
360
  hasOpenSegment = false;
346
361
  }
@@ -445,7 +460,7 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
445
460
 
446
461
  finalizeSegment();
447
462
 
448
- const text = textParts.join('');
463
+ const text = joinWithSpacing(textParts);
449
464
  let rendered: string;
450
465
  if (attachmentParts.length === 0) {
451
466
  rendered = text;