@vellumai/assistant 0.4.35 → 0.4.37

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 (239) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +5 -2
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +814 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +494 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +134 -245
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +175 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +11 -0
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  175. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  176. package/src/memory/migrations/index.ts +2 -0
  177. package/src/memory/schema-migration.ts +1 -0
  178. package/src/memory/shared-app-links-store.ts +1 -1
  179. package/src/messaging/registry.ts +27 -0
  180. package/src/notifications/README.md +79 -70
  181. package/src/notifications/broadcaster.ts +2 -1
  182. package/src/notifications/conversation-pairing.ts +147 -13
  183. package/src/notifications/copy-composer.ts +7 -3
  184. package/src/notifications/destination-resolver.ts +14 -1
  185. package/src/notifications/emit-signal.ts +3 -2
  186. package/src/notifications/signal.ts +105 -1
  187. package/src/notifications/types.ts +16 -0
  188. package/src/permissions/checker.ts +29 -3
  189. package/src/permissions/prompter.ts +11 -3
  190. package/src/runtime/access-request-helper.ts +2 -1
  191. package/src/runtime/auth/route-policy.ts +7 -1
  192. package/src/runtime/channel-invite-transport.ts +40 -63
  193. package/src/runtime/channel-invite-transports/email.ts +13 -39
  194. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  195. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  196. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  197. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  198. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  199. package/src/runtime/channel-readiness-service.ts +202 -45
  200. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  201. package/src/runtime/guardian-outbound-actions.ts +8 -5
  202. package/src/runtime/http-server.ts +2 -0
  203. package/src/runtime/invite-instruction-generator.ts +178 -0
  204. package/src/runtime/invite-service.ts +22 -25
  205. package/src/runtime/migrations/migration-transport.ts +13 -0
  206. package/src/runtime/routes/app-routes.ts +1 -1
  207. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  208. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  209. package/src/runtime/routes/contact-routes.ts +54 -26
  210. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  211. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  212. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  213. package/src/runtime/routes/integration-routes.ts +1 -1
  214. package/src/runtime/routes/invite-routes.ts +1 -1
  215. package/src/runtime/routes/secret-routes.ts +31 -7
  216. package/src/runtime/routes/twilio-routes.ts +32 -1
  217. package/src/runtime/routes/usage-routes.ts +114 -0
  218. package/src/runtime/tool-grant-request-helper.ts +2 -1
  219. package/src/security/encrypted-store.ts +9 -5
  220. package/src/security/keychain-broker-client.ts +393 -0
  221. package/src/security/secure-keys.ts +106 -321
  222. package/src/tools/apps/executors.ts +73 -0
  223. package/src/tools/browser/auto-navigate.ts +15 -6
  224. package/src/tools/browser/chrome-cdp.ts +211 -0
  225. package/src/tools/browser/network-recorder.test.ts +83 -0
  226. package/src/tools/browser/network-recorder.ts +8 -7
  227. package/src/tools/browser/x-auto-navigate.ts +12 -6
  228. package/src/tools/credentials/policy-types.ts +24 -0
  229. package/src/tools/credentials/vault.ts +22 -27
  230. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  231. package/src/tools/permission-checker.ts +1 -0
  232. package/src/tools/types.ts +2 -0
  233. package/src/tools/ui-surface/definitions.ts +1 -2
  234. package/src/tools/watch/watch-state.ts +2 -0
  235. package/src/__tests__/key-migration.test.ts +0 -240
  236. package/src/__tests__/keychain.test.ts +0 -286
  237. package/src/cli/core-commands.ts +0 -899
  238. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  239. package/src/security/keychain.ts +0 -490
@@ -218,11 +218,11 @@ export function revokeGuardianForChannel(
218
218
  // Guardian verification handler
219
219
  // ---------------------------------------------------------------------------
220
220
 
221
- export function handleGuardianVerification(
221
+ export async function handleGuardianVerification(
222
222
  msg: GuardianVerificationRequest,
223
223
  socket: net.Socket,
224
224
  ctx: HandlerContext,
225
- ): void {
225
+ ): Promise<void> {
226
226
  const channel = msg.channel ?? "telegram";
227
227
 
228
228
  try {
@@ -240,7 +240,7 @@ export function handleGuardianVerification(
240
240
  const result = revokeGuardianForChannel(channel);
241
241
  ctx.send(socket, { type: "guardian_verification_response", ...result });
242
242
  } else if (msg.action === "start_outbound") {
243
- const result = startOutbound({
243
+ const result = await startOutbound({
244
244
  channel,
245
245
  destination: msg.destination,
246
246
  rebind: msg.rebind,
@@ -0,0 +1,29 @@
1
+ import { channelHandlers } from "./config-channels.js";
2
+ import { heartbeatHandlers } from "./config-heartbeat.js";
3
+ import { ingressHandlers } from "./config-ingress.js";
4
+ import { integrationHandlers } from "./config-integrations.js";
5
+ import { modelHandlers } from "./config-model.js";
6
+ import { platformHandlers } from "./config-platform.js";
7
+ import { schedulingHandlers } from "./config-scheduling.js";
8
+ import { slackHandlers } from "./config-slack.js";
9
+ import { telegramHandlers } from "./config-telegram.js";
10
+ import { toolHandlers } from "./config-tools.js";
11
+ import { trustHandlers } from "./config-trust.js";
12
+ import { voiceHandlers } from "./config-voice.js";
13
+
14
+ // Keep the dispatch map isolated from the public config barrel so direct
15
+ // handler imports do not race with index.ts when Bun evaluates modules eagerly.
16
+ export const configHandlers = {
17
+ ...modelHandlers,
18
+ ...trustHandlers,
19
+ ...schedulingHandlers,
20
+ ...slackHandlers,
21
+ ...ingressHandlers,
22
+ ...platformHandlers,
23
+ ...integrationHandlers,
24
+ ...telegramHandlers,
25
+ ...channelHandlers,
26
+ ...toolHandlers,
27
+ ...heartbeatHandlers,
28
+ ...voiceHandlers,
29
+ };
@@ -32,19 +32,20 @@ import {
32
32
  renderHistoryContent,
33
33
  } from "./shared.js";
34
34
 
35
- export function handleContactsInvite(
35
+ export async function handleContactsInvite(
36
36
  msg: ContactsInviteRequest,
37
37
  socket: net.Socket,
38
38
  ctx: HandlerContext,
39
- ): void {
39
+ ): Promise<void> {
40
40
  try {
41
41
  switch (msg.action) {
42
42
  case "create": {
43
- const result = createIngressInvite({
43
+ const result = await createIngressInvite({
44
44
  sourceChannel: msg.sourceChannel,
45
45
  note: msg.note,
46
46
  maxUses: msg.maxUses,
47
47
  expiresInMs: msg.expiresInMs,
48
+ contactName: msg.contactName,
48
49
  friendName: msg.friendName,
49
50
  guardianName: msg.guardianName,
50
51
  });
@@ -1,18 +1,7 @@
1
1
  /**
2
- * Config handler barrel — re-exports all config domain handlers and assembles
3
- * the combined `configHandlers` dispatch map.
4
- *
5
- * Individual handlers live in domain-specific files:
6
- * config-model.ts — Model selection (LLM + image gen)
7
- * config-trust.ts — Trust rules (permissions allowlist)
8
- * config-scheduling.ts — Schedules & reminders
9
- * config-slack.ts — Slack webhook sharing
10
- * config-ingress.ts — Public ingress URL & gateway reconciliation
11
- * config-platform.ts — Platform base URL configuration
12
- * config-integrations.ts — Vercel API & Twitter integration
13
- * config-telegram.ts — Telegram bot configuration
14
- * config-channels.ts — Channel guardian verification
15
- * config-tools.ts — Env vars, tool permission simulation, tool names
2
+ * Config handler barrel — re-exports direct handler entry points for tests and
3
+ * feature code. The aggregate dispatch map now lives in config-dispatch.ts so
4
+ * direct imports do not participate in the runtime handler graph.
16
5
  */
17
6
 
18
7
  // Re-export individual handlers for direct import by tests and other modules
@@ -76,32 +65,3 @@ export {
76
65
  handleVoiceConfigUpdate,
77
66
  normalizeActivationKey,
78
67
  } from "./config-voice.js";
79
-
80
- // Assemble the combined dispatch map from domain-specific handler groups
81
- import { channelHandlers } from "./config-channels.js";
82
- import { heartbeatHandlers } from "./config-heartbeat.js";
83
- import { ingressHandlers } from "./config-ingress.js";
84
- import { integrationHandlers } from "./config-integrations.js";
85
- import { modelHandlers } from "./config-model.js";
86
- import { platformHandlers } from "./config-platform.js";
87
- import { schedulingHandlers } from "./config-scheduling.js";
88
- import { slackHandlers } from "./config-slack.js";
89
- import { telegramHandlers } from "./config-telegram.js";
90
- import { toolHandlers } from "./config-tools.js";
91
- import { trustHandlers } from "./config-trust.js";
92
- import { voiceHandlers } from "./config-voice.js";
93
-
94
- export const configHandlers = {
95
- ...modelHandlers,
96
- ...trustHandlers,
97
- ...schedulingHandlers,
98
- ...slackHandlers,
99
- ...ingressHandlers,
100
- ...platformHandlers,
101
- ...integrationHandlers,
102
- ...telegramHandlers,
103
- ...channelHandlers,
104
- ...toolHandlers,
105
- ...heartbeatHandlers,
106
- ...voiceHandlers,
107
- };
@@ -2,6 +2,7 @@ import * as net from "node:net";
2
2
 
3
3
  import { resolveGuardianName } from "../../config/user-reference.js";
4
4
  import {
5
+ deleteContact,
5
6
  getContact,
6
7
  listContacts,
7
8
  updateChannelStatus,
@@ -135,6 +136,39 @@ export function handleContacts(
135
136
  return;
136
137
  }
137
138
 
139
+ case "delete": {
140
+ if (!msg.contactId) {
141
+ ctx.send(socket, {
142
+ type: "contacts_response",
143
+ success: false,
144
+ error: "contactId is required for delete",
145
+ });
146
+ return;
147
+ }
148
+ const result = deleteContact(msg.contactId);
149
+ if (result === "not_found") {
150
+ ctx.send(socket, {
151
+ type: "contacts_response",
152
+ success: false,
153
+ error: `Contact "${msg.contactId}" not found`,
154
+ });
155
+ return;
156
+ }
157
+ if (result === "is_guardian") {
158
+ ctx.send(socket, {
159
+ type: "contacts_response",
160
+ success: false,
161
+ error: "Cannot delete a guardian contact",
162
+ });
163
+ return;
164
+ }
165
+ ctx.send(socket, {
166
+ type: "contacts_response",
167
+ success: true,
168
+ });
169
+ return;
170
+ }
171
+
138
172
  default: {
139
173
  ctx.send(socket, {
140
174
  type: "contacts_response",
@@ -16,7 +16,7 @@ import { appHandlers } from "./apps.js";
16
16
  import { avatarHandlers } from "./avatar.js";
17
17
  import { browserHandlers } from "./browser.js";
18
18
  import { computerUseHandlers } from "./computer-use.js";
19
- import { configHandlers } from "./config.js";
19
+ import { configHandlers } from "./config-dispatch.js";
20
20
  import { inboxInviteHandlers } from "./config-inbox.js";
21
21
  import { contactsHandlers } from "./contacts.js";
22
22
  import { diagnosticsHandlers } from "./diagnostics.js";
@@ -198,11 +198,25 @@ export function handleMessage(
198
198
  if (msg.type === "auth") return;
199
199
 
200
200
  const handler = handlers[msg.type] as
201
- | ((msg: ClientMessage, socket: net.Socket, ctx: HandlerContext) => void)
201
+ | ((
202
+ msg: ClientMessage,
203
+ socket: net.Socket,
204
+ ctx: HandlerContext,
205
+ ) => void | Promise<void>)
202
206
  | undefined;
203
207
  if (!handler) {
204
208
  log.warn({ type: msg.type }, "Unknown message type, ignoring");
205
209
  return;
206
210
  }
207
- handler(msg, socket, ctx);
211
+ // Handlers may be async — catch rejected promises so they don't become
212
+ // unhandled rejections at the process level.
213
+ const result = handler(msg, socket, ctx);
214
+ if (result && typeof result.catch === "function") {
215
+ result.catch((err: unknown) => {
216
+ log.error(
217
+ { err, type: msg.type },
218
+ "Unhandled error in async message handler",
219
+ );
220
+ });
221
+ }
208
222
  }
@@ -539,6 +539,11 @@ async function handlePendingConfirmationReply(
539
539
  causedByRequestId: requestId,
540
540
  decisionText: messageText.trim(),
541
541
  });
542
+ // Notify agent loop so the outcome is persisted on the tool_use block
543
+ session.onConfirmationOutcome?.(
544
+ routerResult.requestId,
545
+ "resolved_stale",
546
+ );
542
547
  }
543
548
 
544
549
  const consumedChannelMeta = {
@@ -646,6 +651,8 @@ function autoDenyPendingConfirmations(
646
651
  source: "auto_deny",
647
652
  causedByRequestId: requestId,
648
653
  });
654
+ // Notify agent loop so the outcome is persisted on the tool_use block
655
+ session.onConfirmationOutcome?.(interaction.requestId, "denied");
649
656
  }
650
657
  }
651
658
  session.denyAllPendingConfirmations();
@@ -61,6 +61,23 @@ import {
61
61
  export { handleUserMessage } from "./session-user-message.js";
62
62
  import { handleUserMessage } from "./session-user-message.js";
63
63
 
64
+ /**
65
+ * Extract a valid ChannelId from a binding's sourceChannel, which may carry a
66
+ * `notification:` namespace prefix (e.g. `"notification:telegram"` -> `"telegram"`).
67
+ * Returns the ChannelId if valid, or null otherwise.
68
+ */
69
+ function parseBindingSourceChannel(
70
+ sourceChannel: string,
71
+ ): import("../../channels/types.js").ChannelId | null {
72
+ if (isChannelId(sourceChannel)) return sourceChannel;
73
+ const NOTIFICATION_PREFIX = "notification:";
74
+ if (sourceChannel.startsWith(NOTIFICATION_PREFIX)) {
75
+ const inner = sourceChannel.slice(NOTIFICATION_PREFIX.length);
76
+ if (isChannelId(inner)) return inner;
77
+ }
78
+ return null;
79
+ }
80
+
64
81
  export function syncCanonicalStatusFromIpcConfirmationDecision(
65
82
  requestId: string,
66
83
  decision: ConfirmationResponse["decision"],
@@ -283,10 +300,12 @@ export function handleSessionList(
283
300
  updatedAt: c.updatedAt,
284
301
  threadType: normalizeThreadType(c.threadType),
285
302
  source: c.source ?? "user",
286
- ...(binding && isChannelId(binding.sourceChannel)
303
+ ...(binding && parseBindingSourceChannel(binding.sourceChannel)
287
304
  ? {
288
305
  channelBinding: {
289
- sourceChannel: binding.sourceChannel,
306
+ sourceChannel: parseBindingSourceChannel(
307
+ binding.sourceChannel,
308
+ )!,
290
309
  externalChatId: binding.externalChatId,
291
310
  externalUserId: binding.externalUserId,
292
311
  displayName: binding.displayName,
@@ -70,6 +70,14 @@ export interface HistoryToolCall {
70
70
  isError?: boolean;
71
71
  /** Base64-encoded image data from tool contentBlocks (e.g. browser_screenshot). */
72
72
  imageData?: string;
73
+ /** Unix ms when the tool started executing. */
74
+ startedAt?: number;
75
+ /** Unix ms when the tool completed. */
76
+ completedAt?: number;
77
+ /** Confirmation decision for this tool call: "approved" | "denied" | "timed_out". */
78
+ confirmationDecision?: string;
79
+ /** Friendly label for the confirmation (e.g. "Edit File", "Run Command"). */
80
+ confirmationLabel?: string;
73
81
  }
74
82
 
75
83
  export interface HistorySurface {
@@ -468,6 +476,15 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
468
476
  : {};
469
477
  const id = typeof block.id === "string" ? block.id : "";
470
478
  const entry: HistoryToolCall = { name, input };
479
+ // Extract persisted timing/confirmation metadata
480
+ if (typeof block._startedAt === "number")
481
+ entry.startedAt = block._startedAt;
482
+ if (typeof block._completedAt === "number")
483
+ entry.completedAt = block._completedAt;
484
+ if (typeof block._confirmationDecision === "string")
485
+ entry.confirmationDecision = block._confirmationDecision;
486
+ if (typeof block._confirmationLabel === "string")
487
+ entry.confirmationLabel = block._confirmationLabel;
471
488
  toolCalls.push(entry);
472
489
  if (id) pendingToolUses.set(id, entry);
473
490
  contentOrder.push(`tool:${toolCalls.length - 1}`);
@@ -238,6 +238,8 @@ export interface ForkSharedAppResponse {
238
238
  export interface BundleAppResponse {
239
239
  type: "bundle_app_response";
240
240
  bundlePath: string;
241
+ /** Base64-encoded PNG of the generated app icon, if available. */
242
+ iconImageBase64?: string;
241
243
  manifest: {
242
244
  format_version: number;
243
245
  name: string;
@@ -217,6 +217,14 @@ export interface WatchCompleteRequest {
217
217
  watchId: string;
218
218
  }
219
219
 
220
+ /** Server → Client: bootstrap failure during learn-mode recording setup. */
221
+ export interface RideShotgunError {
222
+ type: "ride_shotgun_error";
223
+ watchId: string;
224
+ sessionId: string;
225
+ message: string;
226
+ }
227
+
220
228
  // --- Domain-level union aliases (consumed by the barrel file) ---
221
229
 
222
230
  export type _ComputerUseClientMessages =
@@ -236,6 +244,7 @@ export type _ComputerUseServerMessages =
236
244
  | TaskRouted
237
245
  | RideShotgunProgress
238
246
  | RideShotgunResult
247
+ | RideShotgunError
239
248
  | WatchStarted
240
249
  | WatchCompleteRequest
241
250
  | RecordingStart
@@ -1,11 +1,11 @@
1
- // Contact management: list, get, and update channel status.
1
+ // Contact management: list, get, update channel status, and delete.
2
2
 
3
3
  // === Client → Server ===
4
4
 
5
5
  export interface ContactsRequest {
6
6
  type: "contacts";
7
- action: "list" | "get" | "update_channel";
8
- /** Contact ID (get only). */
7
+ action: "list" | "get" | "update_channel" | "delete";
8
+ /** Contact ID (get and delete). */
9
9
  contactId?: string;
10
10
  /** Channel ID (update_channel only). */
11
11
  channelId?: string;
@@ -25,6 +25,8 @@ export interface ContactsInviteRequest {
25
25
  status?: string;
26
26
  /** Invitee's first name (voice invite create only). */
27
27
  friendName?: string;
28
+ /** Contact display name for personalizing invite instructions (create only). */
29
+ contactName?: string;
28
30
  /** Guardian's first name (voice invite create only). */
29
31
  guardianName?: string;
30
32
  }
@@ -80,6 +80,8 @@ export interface ToolUseStart {
80
80
  toolName: string;
81
81
  input: Record<string, unknown>;
82
82
  sessionId?: string;
83
+ /** The tool_use block ID for client-side correlation. */
84
+ toolUseId?: string;
83
85
  }
84
86
 
85
87
  export interface ToolOutputChunk {
@@ -240,6 +242,8 @@ export interface ConfirmationStateChanged {
240
242
  causedByRequestId?: string;
241
243
  /** Normalized user text for analytics/debug (e.g. "approve", "deny"). */
242
244
  decisionText?: string;
245
+ /** The tool_use block ID this confirmation applies to, for disambiguating parallel tool calls. */
246
+ toolUseId?: string;
243
247
  }
244
248
 
245
249
  /**
@@ -288,6 +288,14 @@ export interface HistoryResponseToolCall {
288
288
  isError?: boolean;
289
289
  /** Base64-encoded image data from tool contentBlocks (e.g. browser_screenshot). */
290
290
  imageData?: string;
291
+ /** Unix ms when the tool started executing. */
292
+ startedAt?: number;
293
+ /** Unix ms when the tool completed. */
294
+ completedAt?: number;
295
+ /** Confirmation decision for this tool call: "approved" | "denied" | "timed_out". */
296
+ confirmationDecision?: string;
297
+ /** Friendly label for the confirmation (e.g. "Edit File", "Run Command"). */
298
+ confirmationLabel?: string;
291
299
  }
292
300
 
293
301
  export interface HistoryResponseSurface {
@@ -278,6 +278,7 @@
278
278
  "recording_start",
279
279
  "recording_stop",
280
280
  "reminders_list_response",
281
+ "ride_shotgun_error",
281
282
  "ride_shotgun_progress",
282
283
  "ride_shotgun_result",
283
284
  "schedule_thread_created",
@@ -47,7 +47,6 @@ import {
47
47
  import { ensureVellumGuardianBinding } from "../runtime/guardian-vellum-migration.js";
48
48
  import { RuntimeHttpServer } from "../runtime/http-server.js";
49
49
  import { startScheduler } from "../schedule/scheduler.js";
50
- import { migrateKeychainToEncrypted } from "../security/keychain-to-encrypted-migration.js";
51
50
  import { getLogger, initLogger } from "../util/logger.js";
52
51
  import {
53
52
  ensureDataDir,
@@ -143,10 +142,6 @@ export async function runDaemon(): Promise<void> {
143
142
  migrateToWorkspaceLayout();
144
143
  ensureDataDir();
145
144
 
146
- // Copy any existing macOS keychain secrets into the encrypted file store
147
- // before config loads, so the new encrypted-store-first read path sees them.
148
- migrateKeychainToEncrypted();
149
-
150
145
  // Load (or generate + persist) the auth signing key so tokens survive
151
146
  // daemon restarts. Must happen after ensureDataDir() creates the
152
147
  // protected directory.
@@ -2,6 +2,11 @@ import { randomUUID } from "node:crypto";
2
2
  import type * as net from "node:net";
3
3
 
4
4
  import { autoNavigate } from "../tools/browser/auto-navigate.js";
5
+ import {
6
+ type CdpSession,
7
+ ensureChromeWithCdp,
8
+ minimizeChromeWindow,
9
+ } from "../tools/browser/chrome-cdp.js";
5
10
  import { NetworkRecorder } from "../tools/browser/network-recorder.js";
6
11
  import type { SessionRecording } from "../tools/browser/network-recording-types.js";
7
12
  import { saveRecording } from "../tools/browser/recording-store.js";
@@ -24,6 +29,9 @@ const log = getLogger("ride-shotgun-handler");
24
29
  /** Active network recorders keyed by watchId. */
25
30
  const activeRecorders = new Map<string, NetworkRecorder>();
26
31
 
32
+ /** Active CDP sessions keyed by watchId — tracks browser ownership for cleanup. */
33
+ const activeCdpSessions = new Map<string, CdpSession>();
34
+
27
35
  /** Active progress interval timers keyed by watchId, cleared on session completion. */
28
36
  const activeProgressIntervals = new Map<string, NodeJS.Timeout>();
29
37
 
@@ -71,20 +79,48 @@ async function completeSession(session: WatchSession): Promise<void> {
71
79
 
72
80
  // In learn mode, stop recording and save — skip the LLM summary (not needed)
73
81
  if (session.isLearnMode && session.recordingId) {
74
- session.savedRecordingPath = await finalizeLearnRecording(
75
- watchId,
76
- session,
77
- session.recordingId,
78
- );
79
- lastSummaryBySession.set(
80
- sessionId,
81
- session.savedRecordingPath
82
+ const hasRecorder = activeRecorders.has(watchId);
83
+
84
+ if (hasRecorder) {
85
+ session.savedRecordingPath = await finalizeLearnRecording(
86
+ watchId,
87
+ session,
88
+ session.recordingId,
89
+ );
90
+ }
91
+
92
+ // Clean up the CDP session — minimize if we launched Chrome, leave it alone otherwise
93
+ const cdpSession = activeCdpSessions.get(watchId);
94
+ if (cdpSession) {
95
+ activeCdpSessions.delete(watchId);
96
+ if (cdpSession.launchedByUs) {
97
+ try {
98
+ await minimizeChromeWindow(cdpSession.baseUrl);
99
+ log.info({ watchId }, "Minimized assistant-launched Chrome window");
100
+ } catch (err) {
101
+ log.debug({ err, watchId }, "Failed to minimize Chrome window");
102
+ }
103
+ }
104
+ }
105
+
106
+ // Use bootstrapFailureReason as the primary discriminator — hasRecorder
107
+ // alone can't distinguish "browser never launched" from "recorder failed
108
+ // after retries" since both leave activeRecorders empty.
109
+ const summary = session.bootstrapFailureReason
110
+ ? `Learn session failed — ${session.bootstrapFailureReason}`
111
+ : session.savedRecordingPath
82
112
  ? "Learn session completed — recording saved."
83
- : "Learn session completed — recording failed to save.",
84
- );
113
+ : "Learn session completed — recording failed to save.";
114
+
115
+ lastSummaryBySession.set(sessionId, summary);
85
116
  session.status = "completed";
86
117
  log.info(
87
- { watchId, sessionId },
118
+ {
119
+ watchId,
120
+ sessionId,
121
+ hasRecorder,
122
+ bootstrapFailureReason: session.bootstrapFailureReason,
123
+ },
88
124
  "Learn session complete — firing completion notifier",
89
125
  );
90
126
  fireWatchCompletionNotifier(sessionId, session);
@@ -142,10 +178,67 @@ export async function handleRideShotgunStart(
142
178
  "Session created and stored in watchSessions map",
143
179
  );
144
180
 
145
- // In learn mode, connect directly to Chrome's CDP endpoint for network recording.
146
- // Retry a few times since Chrome may still be starting up after the Swift client restarts it.
181
+ // In learn mode, ensure Chrome is available with CDP, then connect for network recording.
147
182
  if (isLearnMode) {
148
183
  const startRecording = async () => {
184
+ // Ensure Chrome is running with CDP — launches it if needed
185
+ let cdpSession: CdpSession;
186
+ try {
187
+ cdpSession = await ensureChromeWithCdp({
188
+ startUrl: targetDomain ? `https://${targetDomain}` : undefined,
189
+ });
190
+ // If session completed while we were awaiting Chrome, skip storing to avoid a stale map entry
191
+ if (session.status !== "active") {
192
+ log.info(
193
+ { watchId, status: session.status },
194
+ "Session no longer active after CDP launch — skipping recording",
195
+ );
196
+ // If we launched Chrome, minimize it since completeSession already ran and won't find it
197
+ if (cdpSession.launchedByUs) {
198
+ try {
199
+ await minimizeChromeWindow(cdpSession.baseUrl);
200
+ log.info(
201
+ { watchId },
202
+ "Minimized assistant-launched Chrome window (post-session)",
203
+ );
204
+ } catch (err) {
205
+ log.debug(
206
+ { err, watchId },
207
+ "Failed to minimize Chrome window (post-session)",
208
+ );
209
+ }
210
+ }
211
+ return;
212
+ }
213
+ activeCdpSessions.set(watchId, cdpSession);
214
+ log.info(
215
+ {
216
+ watchId,
217
+ launchedByUs: cdpSession.launchedByUs,
218
+ baseUrl: cdpSession.baseUrl,
219
+ },
220
+ "CDP session established",
221
+ );
222
+ } catch (err) {
223
+ log.warn(
224
+ { err, watchId },
225
+ "Failed to ensure Chrome with CDP — cannot start recording",
226
+ );
227
+ ctx.send(socket, {
228
+ type: "ride_shotgun_error",
229
+ watchId,
230
+ sessionId,
231
+ message:
232
+ "Failed to start browser — Chrome CDP could not be launched.",
233
+ });
234
+ // Fail-fast: complete the session immediately instead of waiting for timeout
235
+ session.bootstrapFailureReason = "browser could not be started.";
236
+ await completeSession(session);
237
+ return;
238
+ }
239
+
240
+ const cdpBaseUrl = cdpSession.baseUrl;
241
+
149
242
  for (let attempt = 0; attempt < 10; attempt++) {
150
243
  // Check if session is still active before each attempt
151
244
  if (session.status !== "active") {
@@ -156,7 +249,7 @@ export async function handleRideShotgunStart(
156
249
  return;
157
250
  }
158
251
  try {
159
- const recorder = new NetworkRecorder(targetDomain);
252
+ const recorder = new NetworkRecorder(targetDomain, cdpBaseUrl);
160
253
  recorder.loginSignals = getLoginSignals(targetDomain);
161
254
  await recorder.startDirect();
162
255
  // If session completed while we were connecting, stop immediately to avoid leak
@@ -248,7 +341,7 @@ export async function handleRideShotgunStart(
248
341
  clearInterval(checkInterval);
249
342
  }
250
343
  }, 1000);
251
- navigateXPages(abortSignal)
344
+ navigateXPages({ abortSignal, cdpBaseUrl })
252
345
  .then((completed) => {
253
346
  clearInterval(checkInterval);
254
347
  log.info(
@@ -275,16 +368,20 @@ export async function handleRideShotgunStart(
275
368
  clearInterval(checkInterval);
276
369
  }
277
370
  }, 1000);
278
- autoNavigate(navDomain, abortSignal, (progress) => {
279
- // Send progress to connected client
280
- if (progress.type === "visiting" && progress.url) {
281
- const shortUrl = progress.url.replace(/^https?:\/\//, "");
282
- ctx.send(socket, {
283
- type: "ride_shotgun_progress",
284
- watchId,
285
- message: `[${progress.pageNumber || "?"}] ${shortUrl}`,
286
- });
287
- }
371
+ autoNavigate(navDomain, {
372
+ abortSignal,
373
+ onProgress: (progress) => {
374
+ // Send progress to connected client
375
+ if (progress.type === "visiting" && progress.url) {
376
+ const shortUrl = progress.url.replace(/^https?:\/\//, "");
377
+ ctx.send(socket, {
378
+ type: "ride_shotgun_progress",
379
+ watchId,
380
+ message: `[${progress.pageNumber || "?"}] ${shortUrl}`,
381
+ });
382
+ }
383
+ },
384
+ cdpBaseUrl,
288
385
  })
289
386
  .then((visited) => {
290
387
  clearInterval(checkInterval);
@@ -326,6 +423,15 @@ export async function handleRideShotgunStart(
326
423
  { err, watchId },
327
424
  "Failed to start network recording after 10 attempts",
328
425
  );
426
+ ctx.send(socket, {
427
+ type: "ride_shotgun_error",
428
+ watchId,
429
+ sessionId,
430
+ message: "Failed to start network recording after 10 attempts.",
431
+ });
432
+ session.bootstrapFailureReason =
433
+ "network recording could not be started after 10 attempts.";
434
+ await completeSession(session);
329
435
  }
330
436
  }
331
437
  }
@@ -336,6 +442,14 @@ export async function handleRideShotgunStart(
336
442
 
337
443
  // Set timeout for duration expiry
338
444
  session.timeoutHandle = setTimeout(() => {
445
+ if (
446
+ session.isLearnMode &&
447
+ !activeRecorders.has(watchId) &&
448
+ !session.bootstrapFailureReason
449
+ ) {
450
+ session.bootstrapFailureReason =
451
+ "session timed out before recording could start.";
452
+ }
339
453
  completeSession(session);
340
454
  }, durationSeconds * 1000);
341
455