@vellumai/assistant 0.4.43 → 0.4.45

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 (713) hide show
  1. package/.prettierignore +4 -0
  2. package/ARCHITECTURE.md +46 -44
  3. package/README.md +15 -16
  4. package/bun.lock +10 -35
  5. package/docs/architecture/integrations.md +102 -215
  6. package/docs/architecture/keychain-broker.md +1 -1
  7. package/docs/architecture/memory.md +2 -2
  8. package/docs/architecture/scheduling.md +1 -1
  9. package/docs/architecture/security.md +11 -11
  10. package/docs/error-handling.md +1 -1
  11. package/docs/trusted-contact-access.md +3 -3
  12. package/drizzle/meta/0000_snapshot.json +34 -100
  13. package/drizzle/meta/_journal.json +1 -1
  14. package/drizzle.config.ts +4 -4
  15. package/package.json +3 -2
  16. package/scripts/capture-x-graphql.ts +237 -141
  17. package/scripts/generate-bundled-tool-registry.ts +223 -0
  18. package/src/__tests__/access-request-decision.test.ts +0 -1
  19. package/src/__tests__/actor-token-service.test.ts +23 -24
  20. package/src/__tests__/agent-loop.test.ts +0 -131
  21. package/src/__tests__/always-loaded-tools-guard.test.ts +71 -0
  22. package/src/__tests__/amazon-cdp-integration.test.ts +11 -9
  23. package/src/__tests__/approval-primitive.test.ts +0 -1
  24. package/src/__tests__/approval-routes-http.test.ts +11 -3
  25. package/src/__tests__/asset-materialize-tool.test.ts +0 -1
  26. package/src/__tests__/asset-search-tool.test.ts +0 -1
  27. package/src/__tests__/assistant-attachment-directive.test.ts +1 -1
  28. package/src/__tests__/assistant-events-sse-hardening.test.ts +0 -1
  29. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  30. package/src/__tests__/assistant-feature-flags-integration.test.ts +70 -18
  31. package/src/__tests__/assistant-id-boundary-guard.test.ts +6 -6
  32. package/src/__tests__/attachments-store.test.ts +0 -1
  33. package/src/__tests__/avatar-e2e.test.ts +74 -115
  34. package/src/__tests__/avatar-router.test.ts +25 -62
  35. package/src/__tests__/browser-manager.test.ts +24 -0
  36. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +4 -3
  37. package/src/__tests__/browser-skill-endstate.test.ts +8 -11
  38. package/src/__tests__/btw-routes.test.ts +326 -0
  39. package/src/__tests__/bundled-asset.test.ts +1 -1
  40. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +23 -9
  41. package/src/__tests__/call-controller.test.ts +0 -1
  42. package/src/__tests__/call-conversation-messages.test.ts +0 -1
  43. package/src/__tests__/call-domain.test.ts +0 -1
  44. package/src/__tests__/call-pointer-messages.test.ts +0 -1
  45. package/src/__tests__/call-recovery.test.ts +0 -1
  46. package/src/__tests__/call-routes-http.test.ts +0 -1
  47. package/src/__tests__/call-store.test.ts +0 -1
  48. package/src/__tests__/canonical-guardian-store.test.ts +0 -1
  49. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  50. package/src/__tests__/channel-approvals.test.ts +1 -1
  51. package/src/__tests__/channel-delivery-store.test.ts +0 -1
  52. package/src/__tests__/channel-guardian.test.ts +5 -7
  53. package/src/__tests__/channel-retry-sweep.test.ts +0 -1
  54. package/src/__tests__/checker.test.ts +32 -36
  55. package/src/__tests__/compaction.benchmark.test.ts +16 -14
  56. package/src/__tests__/computer-use-session-lifecycle.test.ts +10 -11
  57. package/src/__tests__/computer-use-session-working-dir.test.ts +2 -6
  58. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -5
  59. package/src/__tests__/computer-use-tools.test.ts +35 -31
  60. package/src/__tests__/config-schema.test.ts +11 -15
  61. package/src/__tests__/config-watcher.test.ts +0 -1
  62. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  63. package/src/__tests__/conflict-store.test.ts +0 -1
  64. package/src/__tests__/connection-policy.test.ts +4 -7
  65. package/src/__tests__/contacts-tools.test.ts +0 -1
  66. package/src/__tests__/context-memory-e2e.test.ts +2 -4
  67. package/src/__tests__/context-overflow-reducer.test.ts +2 -4
  68. package/src/__tests__/context-window-manager.test.ts +147 -60
  69. package/src/__tests__/contradiction-checker.test.ts +0 -1
  70. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  71. package/src/__tests__/conversation-attention-telegram.test.ts +1 -1
  72. package/src/__tests__/conversation-pairing.test.ts +2 -2
  73. package/src/__tests__/conversation-routes-guardian-reply.test.ts +31 -7
  74. package/src/__tests__/conversation-routes-slash-commands.test.ts +381 -0
  75. package/src/__tests__/conversation-store.test.ts +0 -1
  76. package/src/__tests__/conversation-unread-route.test.ts +1 -2
  77. package/src/__tests__/credential-security-invariants.test.ts +8 -8
  78. package/src/__tests__/cross-provider-web-search.test.ts +353 -0
  79. package/src/__tests__/daemon-assistant-events.test.ts +6 -7
  80. package/src/__tests__/db-schedule-syntax-migration.test.ts +15 -3
  81. package/src/__tests__/delete-managed-skill-tool.test.ts +5 -9
  82. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  83. package/src/__tests__/diagnostics-export.test.ts +189 -0
  84. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  85. package/src/__tests__/emit-signal-routing-intent.test.ts +3 -3
  86. package/src/__tests__/entity-extractor.test.ts +0 -1
  87. package/src/__tests__/entity-search.test.ts +0 -1
  88. package/src/__tests__/ephemeral-permissions.test.ts +2 -4
  89. package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
  90. package/src/__tests__/file-read-tool.test.ts +86 -0
  91. package/src/__tests__/followup-tools.test.ts +0 -1
  92. package/src/__tests__/frontmatter.test.ts +77 -34
  93. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  94. package/src/__tests__/gateway-only-guard.test.ts +1 -1
  95. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -1
  96. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
  97. package/src/__tests__/guardian-action-followup-store.test.ts +0 -1
  98. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  99. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1
  100. package/src/__tests__/guardian-action-store.test.ts +0 -1
  101. package/src/__tests__/guardian-action-sweep.test.ts +0 -1
  102. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  103. package/src/__tests__/guardian-dispatch.test.ts +1 -2
  104. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  105. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  106. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +0 -1
  107. package/src/__tests__/guardian-routing-invariants.test.ts +1 -1
  108. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  109. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  110. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +3 -5
  111. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +28 -426
  112. package/src/__tests__/host-bash-proxy.test.ts +335 -0
  113. package/src/__tests__/host-file-proxy.test.ts +374 -0
  114. package/src/__tests__/host-shell-tool.test.ts +147 -1
  115. package/src/__tests__/http-user-message-parity.test.ts +361 -0
  116. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  117. package/src/__tests__/integration-status.test.ts +3 -8
  118. package/src/__tests__/intent-routing.test.ts +7 -46
  119. package/src/__tests__/invite-redemption-service.test.ts +0 -1
  120. package/src/__tests__/invite-routes-http.test.ts +0 -1
  121. package/src/__tests__/llm-usage-store.test.ts +0 -1
  122. package/src/__tests__/managed-avatar-client.test.ts +101 -55
  123. package/src/__tests__/managed-skill-lifecycle.test.ts +9 -18
  124. package/src/__tests__/managed-store.test.ts +94 -21
  125. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -1
  126. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +2 -4
  127. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -1
  128. package/src/__tests__/memory-recall-quality.test.ts +0 -1
  129. package/src/__tests__/memory-regressions.experimental.test.ts +0 -1
  130. package/src/__tests__/memory-regressions.test.ts +0 -1
  131. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -1
  132. package/src/__tests__/memory-upsert-concurrency.test.ts +0 -1
  133. package/src/__tests__/messaging-send-tool.test.ts +35 -0
  134. package/src/__tests__/messaging-skill-split.test.ts +138 -0
  135. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  136. package/src/__tests__/migration-export-http.test.ts +2 -3
  137. package/src/__tests__/migration-import-commit-http.test.ts +1 -2
  138. package/src/__tests__/migration-import-preflight-http.test.ts +1 -2
  139. package/src/__tests__/migration-validate-http.test.ts +1 -2
  140. package/src/__tests__/native-web-search.test.ts +475 -0
  141. package/src/__tests__/navigate-settings-tab.test.ts +84 -0
  142. package/src/__tests__/non-member-access-request.test.ts +0 -1
  143. package/src/__tests__/notification-broadcaster.test.ts +15 -15
  144. package/src/__tests__/notification-decision-strategy.test.ts +6 -6
  145. package/src/__tests__/notification-deep-link.test.ts +7 -7
  146. package/src/__tests__/notification-guardian-path.test.ts +2 -3
  147. package/src/__tests__/notification-telegram-adapter.test.ts +1 -1
  148. package/src/__tests__/notification-thread-candidates.test.ts +4 -4
  149. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  150. package/src/__tests__/onboarding-template-contract.test.ts +0 -10
  151. package/src/__tests__/playbook-execution.test.ts +0 -1
  152. package/src/__tests__/playbook-tools.test.ts +0 -1
  153. package/src/__tests__/profile-compiler.test.ts +0 -1
  154. package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
  155. package/src/__tests__/provider-managed-proxy-integration.test.ts +25 -0
  156. package/src/__tests__/qdrant-collection-migration.test.ts +223 -0
  157. package/src/__tests__/recording-handler.test.ts +30 -94
  158. package/src/__tests__/registry.test.ts +28 -35
  159. package/src/__tests__/relay-server.test.ts +0 -1
  160. package/src/__tests__/ride-shotgun-handler.test.ts +4 -20
  161. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  162. package/src/__tests__/runtime-events-sse-parity.test.ts +3 -4
  163. package/src/__tests__/runtime-events-sse.test.ts +0 -1
  164. package/src/__tests__/sandbox-diagnostics.test.ts +0 -1
  165. package/src/__tests__/scaffold-managed-skill-tool.test.ts +30 -28
  166. package/src/__tests__/schedule-store.test.ts +441 -1
  167. package/src/__tests__/schedule-tools.test.ts +468 -7
  168. package/src/__tests__/scheduler-recurrence.test.ts +196 -23
  169. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  170. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  171. package/src/__tests__/secret-prompt-log-hygiene.test.ts +6 -3
  172. package/src/__tests__/secret-response-routing.test.ts +4 -1
  173. package/src/__tests__/send-endpoint-busy.test.ts +14 -5
  174. package/src/__tests__/send-notification-tool.test.ts +0 -7
  175. package/src/__tests__/sequence-store.test.ts +0 -1
  176. package/src/__tests__/server-history-render.test.ts +1 -2
  177. package/src/__tests__/session-abort-tool-results.test.ts +0 -1
  178. package/src/__tests__/session-agent-loop.test.ts +46 -6
  179. package/src/__tests__/session-confirmation-signals.test.ts +7 -46
  180. package/src/__tests__/session-conflict-gate.test.ts +2 -6
  181. package/src/__tests__/session-error.test.ts +5 -14
  182. package/src/__tests__/session-init.benchmark.test.ts +3 -5
  183. package/src/__tests__/session-load-history-repair.test.ts +0 -1
  184. package/src/__tests__/session-media-retry.test.ts +12 -74
  185. package/src/__tests__/session-pre-run-repair.test.ts +0 -1
  186. package/src/__tests__/session-profile-injection.test.ts +2 -6
  187. package/src/__tests__/session-provider-retry-repair.test.ts +2 -6
  188. package/src/__tests__/session-queue.test.ts +94 -139
  189. package/src/__tests__/session-skill-tools.test.ts +115 -115
  190. package/src/__tests__/session-slash-known.test.ts +0 -1
  191. package/src/__tests__/session-slash-queue.test.ts +0 -1
  192. package/src/__tests__/session-slash-unknown.test.ts +0 -1
  193. package/src/__tests__/session-surfaces-task-progress.test.ts +34 -0
  194. package/src/__tests__/session-usage.test.ts +0 -1
  195. package/src/__tests__/session-workspace-cache-state.test.ts +2 -6
  196. package/src/__tests__/session-workspace-injection.test.ts +2 -6
  197. package/src/__tests__/session-workspace-tool-tracking.test.ts +2 -6
  198. package/src/__tests__/skill-feature-flags-integration.test.ts +180 -184
  199. package/src/__tests__/skill-feature-flags.test.ts +125 -18
  200. package/src/__tests__/skill-load-feature-flag.test.ts +1 -2
  201. package/src/__tests__/skill-load-tool.test.ts +194 -2
  202. package/src/__tests__/skill-projection-feature-flag.test.ts +27 -16
  203. package/src/__tests__/skill-projection.benchmark.test.ts +15 -14
  204. package/src/__tests__/skills.test.ts +14 -53
  205. package/src/__tests__/slack-channel-config.test.ts +0 -1
  206. package/src/__tests__/slack-inbound-verification.test.ts +0 -1
  207. package/src/__tests__/slack-skill.test.ts +1 -1
  208. package/src/__tests__/starter-task-flow.test.ts +9 -19
  209. package/src/__tests__/subagent-tools.test.ts +2 -2
  210. package/src/__tests__/system-prompt.test.ts +7 -7
  211. package/src/__tests__/task-compiler.test.ts +0 -1
  212. package/src/__tests__/task-management-tools.test.ts +0 -1
  213. package/src/__tests__/task-memory-cleanup.test.ts +0 -1
  214. package/src/__tests__/task-runner.test.ts +0 -1
  215. package/src/__tests__/task-scheduler.test.ts +0 -1
  216. package/src/__tests__/terminal-tools.test.ts +0 -1
  217. package/src/__tests__/test-support/computer-use-skill-harness.ts +2 -4
  218. package/src/__tests__/thread-seed-composer.test.ts +5 -5
  219. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  220. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  221. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  222. package/src/__tests__/tool-executor.test.ts +8 -86
  223. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  224. package/src/__tests__/tool-notification-listener.test.ts +1 -1
  225. package/src/__tests__/tool-preview-lifecycle.test.ts +416 -0
  226. package/src/__tests__/trust-store.test.ts +84 -8
  227. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  228. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  229. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -1
  230. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  231. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  232. package/src/__tests__/twilio-provider.test.ts +0 -1
  233. package/src/__tests__/twilio-routes.test.ts +0 -1
  234. package/src/__tests__/{request-file-tool.test.ts → ui-file-upload-surface.test.ts} +11 -72
  235. package/src/__tests__/update-bulletin.test.ts +0 -1
  236. package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -1
  237. package/src/__tests__/usage-routes.test.ts +0 -1
  238. package/src/__tests__/verification-control-plane-policy.test.ts +4 -4
  239. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  240. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  241. package/src/__tests__/voice-session-bridge.test.ts +9 -1
  242. package/src/__tests__/web-fetch.test.ts +57 -0
  243. package/src/__tests__/workspace-git-service.test.ts +5 -14
  244. package/src/__tests__/workspace-policy.test.ts +0 -1
  245. package/src/agent/loop.ts +22 -34
  246. package/src/bundler/bundle-signer.ts +4 -4
  247. package/src/calls/call-controller.ts +1 -1
  248. package/src/calls/relay-server.ts +1 -1
  249. package/src/calls/twilio-rest.ts +1 -1
  250. package/src/calls/voice-session-bridge.ts +3 -1
  251. package/src/cli/__tests__/notifications.test.ts +3 -4
  252. package/src/cli/commands/map.ts +2 -6
  253. package/src/cli/commands/mcp.ts +73 -15
  254. package/src/cli/commands/notifications.ts +4 -4
  255. package/src/cli/commands/sessions.ts +9 -1
  256. package/src/cli/commands/skills.ts +6 -10
  257. package/src/cli/http-client.ts +2 -3
  258. package/src/cli/main-screen.tsx +10 -10
  259. package/src/cli/program.ts +0 -4
  260. package/src/cli/reference.ts +0 -2
  261. package/src/cli.ts +15 -9
  262. package/src/config/__tests__/bundled-tool-registry-guard.test.ts +120 -0
  263. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +11 -0
  264. package/src/config/bundled-skills/app-builder/SKILL.md +6 -7
  265. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
  266. package/src/config/bundled-skills/browser/SKILL.md +6 -1
  267. package/src/config/bundled-skills/chatgpt-import/SKILL.md +5 -1
  268. package/src/config/bundled-skills/claude-code/SKILL.md +5 -1
  269. package/src/config/bundled-skills/computer-use/SKILL.md +6 -1
  270. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -69
  271. package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +10 -1
  272. package/src/config/bundled-skills/contacts/SKILL.md +10 -1
  273. package/src/config/bundled-skills/contacts/TOOLS.json +35 -0
  274. package/src/config/bundled-skills/{messaging → contacts}/tools/google-contacts.ts +9 -2
  275. package/src/config/bundled-skills/document/SKILL.md +4 -1
  276. package/src/config/bundled-skills/doordash/SKILL.md +8 -2
  277. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
  278. package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
  279. package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
  280. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +4 -1
  281. package/src/config/bundled-skills/followups/SKILL.md +4 -1
  282. package/src/config/bundled-skills/gmail/SKILL.md +180 -0
  283. package/src/config/bundled-skills/gmail/TOOLS.json +506 -0
  284. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +149 -0
  285. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +110 -0
  286. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-draft.ts +1 -1
  287. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-filters.ts +1 -1
  288. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-follow-up.ts +1 -1
  289. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-forward.ts +1 -1
  290. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +50 -0
  291. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-outreach-scan.ts +8 -90
  292. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-send-draft.ts +1 -1
  293. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-sender-digest.ts +2 -2
  294. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-trash.ts +1 -1
  295. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-unsubscribe.ts +1 -1
  296. package/src/config/bundled-skills/{messaging → gmail}/tools/gmail-vacation.ts +1 -1
  297. package/src/config/bundled-skills/gmail/tools/shared.ts +47 -0
  298. package/src/config/bundled-skills/google-calendar/SKILL.md +5 -1
  299. package/src/config/bundled-skills/image-studio/SKILL.md +5 -1
  300. package/src/config/bundled-skills/knowledge-graph/SKILL.md +4 -1
  301. package/src/config/bundled-skills/media-processing/SKILL.md +7 -13
  302. package/src/config/bundled-skills/media-processing/TOOLS.json +0 -22
  303. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +12 -1
  304. package/src/config/bundled-skills/messaging/SKILL.md +23 -139
  305. package/src/config/bundled-skills/messaging/TOOLS.json +33 -1215
  306. package/src/config/bundled-skills/messaging/tools/gmail-mime-helpers.ts +42 -0
  307. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +165 -2
  308. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +1 -13
  309. package/src/config/bundled-skills/messaging/tools/shared.ts +81 -34
  310. package/src/config/bundled-skills/notifications/SKILL.md +5 -1
  311. package/src/config/bundled-skills/orchestration/SKILL.md +30 -0
  312. package/src/config/bundled-skills/orchestration/TOOLS.json +35 -0
  313. package/src/config/bundled-skills/{reminder/tools/reminder-cancel.ts → orchestration/tools/swarm-delegate.ts} +3 -3
  314. package/src/config/bundled-skills/phone-calls/SKILL.md +9 -1
  315. package/src/config/bundled-skills/playbooks/SKILL.md +4 -1
  316. package/src/config/bundled-skills/schedule/SKILL.md +70 -9
  317. package/src/config/bundled-skills/schedule/TOOLS.json +38 -6
  318. package/src/config/bundled-skills/screen-watch/SKILL.md +28 -0
  319. package/src/config/bundled-skills/screen-watch/TOOLS.json +35 -0
  320. package/src/config/bundled-skills/{reminder/tools/reminder-create.ts → screen-watch/tools/start-screen-watch.ts} +3 -3
  321. package/src/config/bundled-skills/sequences/SKILL.md +47 -0
  322. package/src/config/bundled-skills/sequences/TOOLS.json +340 -0
  323. package/src/config/bundled-skills/sequences/tools/sequence-update.ts +128 -0
  324. package/src/config/bundled-skills/sequences/tools/shared.ts +9 -0
  325. package/src/config/bundled-skills/settings/SKILL.md +12 -0
  326. package/src/config/bundled-skills/settings/TOOLS.json +112 -0
  327. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +43 -0
  328. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +52 -0
  329. package/src/config/bundled-skills/{computer-use/tools/computer-use-right-click.ts → settings/tools/set-avatar.ts} +2 -6
  330. package/src/{tools/system/voice-config.ts → config/bundled-skills/settings/tools/voice-config-update.ts} +59 -96
  331. package/src/config/bundled-skills/skill-management/SKILL.md +18 -0
  332. package/src/config/bundled-skills/skill-management/TOOLS.json +90 -0
  333. package/src/config/bundled-skills/{computer-use/tools/computer-use-double-click.ts → skill-management/tools/delete-managed.ts} +2 -6
  334. package/src/config/bundled-skills/skill-management/tools/scaffold-managed.ts +12 -0
  335. package/src/config/bundled-skills/slack/SKILL.md +5 -1
  336. package/src/config/bundled-skills/subagent/SKILL.md +4 -1
  337. package/src/config/bundled-skills/tasks/SKILL.md +5 -2
  338. package/src/config/bundled-skills/transcribe/SKILL.md +4 -1
  339. package/src/config/bundled-skills/watcher/SKILL.md +4 -1
  340. package/src/config/bundled-tool-registry.ts +118 -107
  341. package/src/config/env.ts +5 -2
  342. package/src/config/feature-flag-registry.json +33 -9
  343. package/src/config/loader.ts +10 -2
  344. package/src/config/schema.ts +19 -16
  345. package/src/config/schemas/inference.ts +12 -22
  346. package/src/config/schemas/memory-storage.ts +19 -1
  347. package/src/config/schemas/platform.ts +0 -16
  348. package/src/config/skill-state.ts +11 -8
  349. package/src/config/skills.ts +83 -32
  350. package/src/context/token-estimator.ts +11 -0
  351. package/src/context/window-manager.ts +180 -151
  352. package/src/daemon/computer-use-session.ts +11 -43
  353. package/src/daemon/daemon-control.ts +4 -1
  354. package/src/daemon/handlers/config-channels.ts +5 -9
  355. package/src/daemon/handlers/config-ingress.ts +0 -4
  356. package/src/daemon/handlers/config-model.ts +7 -13
  357. package/src/daemon/handlers/config-telegram.ts +4 -8
  358. package/src/daemon/handlers/config-voice.ts +2 -5
  359. package/src/daemon/handlers/dictation.ts +2 -12
  360. package/src/daemon/handlers/identity.ts +0 -105
  361. package/src/daemon/handlers/recording.ts +3 -23
  362. package/src/daemon/handlers/session-history.ts +42 -10
  363. package/src/daemon/handlers/sessions.ts +53 -72
  364. package/src/daemon/handlers/shared.ts +7 -28
  365. package/src/daemon/handlers/skills.ts +31 -27
  366. package/src/daemon/host-bash-proxy.ts +148 -0
  367. package/src/daemon/host-file-proxy.ts +135 -0
  368. package/src/daemon/lifecycle.ts +53 -41
  369. package/src/daemon/mcp-reload-service.ts +123 -0
  370. package/src/daemon/message-protocol.ts +6 -0
  371. package/src/daemon/message-types/apps.ts +0 -25
  372. package/src/daemon/message-types/browser.ts +1 -1
  373. package/src/daemon/message-types/computer-use.ts +1 -4
  374. package/src/daemon/message-types/guardian-actions.ts +1 -1
  375. package/src/daemon/message-types/host-bash.ts +18 -0
  376. package/src/daemon/message-types/host-file.ts +44 -0
  377. package/src/daemon/message-types/integrations.ts +1 -73
  378. package/src/daemon/message-types/messages.ts +15 -0
  379. package/src/daemon/message-types/schedules.ts +11 -27
  380. package/src/daemon/message-types/sessions.ts +8 -2
  381. package/src/daemon/message-types/settings.ts +1 -1
  382. package/src/daemon/message-types/shared.ts +1 -1
  383. package/src/daemon/message-types/surfaces.ts +2 -0
  384. package/src/daemon/ride-shotgun-handler.ts +35 -43
  385. package/src/daemon/seed-files.ts +3 -27
  386. package/src/daemon/server.ts +45 -28
  387. package/src/daemon/session-agent-loop-handlers.ts +72 -9
  388. package/src/daemon/session-agent-loop.ts +97 -66
  389. package/src/daemon/session-attachments.ts +1 -1
  390. package/src/daemon/session-error.ts +17 -16
  391. package/src/daemon/session-lifecycle.ts +20 -1
  392. package/src/daemon/session-media-retry.ts +1 -15
  393. package/src/daemon/session-messaging.ts +14 -6
  394. package/src/daemon/session-process.ts +36 -7
  395. package/src/daemon/session-queue-manager.ts +62 -103
  396. package/src/daemon/session-runtime-assembly.ts +27 -7
  397. package/src/daemon/session-skill-tools.ts +12 -11
  398. package/src/daemon/session-slash.ts +7 -0
  399. package/src/daemon/session-surfaces.ts +192 -118
  400. package/src/daemon/session-tool-setup.ts +146 -6
  401. package/src/daemon/session.ts +75 -37
  402. package/src/errors.ts +0 -2
  403. package/src/export/formatter.ts +6 -0
  404. package/src/mcp/mcp-oauth-provider.ts +1 -3
  405. package/src/media/avatar-router.ts +20 -28
  406. package/src/media/avatar-types.ts +7 -14
  407. package/src/media/managed-avatar-client.ts +70 -34
  408. package/src/memory/app-store.ts +0 -18
  409. package/src/memory/conversation-title-service.ts +1 -2
  410. package/src/memory/db-init.ts +16 -0
  411. package/src/memory/embedding-backend.ts +129 -27
  412. package/src/memory/embedding-gemini.test.ts +256 -0
  413. package/src/memory/embedding-gemini.ts +47 -13
  414. package/src/memory/embedding-local.ts +14 -2
  415. package/src/memory/embedding-ollama.ts +15 -2
  416. package/src/memory/embedding-openai.ts +15 -2
  417. package/src/memory/embedding-types.test.ts +116 -0
  418. package/src/memory/embedding-types.ts +61 -0
  419. package/src/memory/fingerprint.ts +1 -1
  420. package/src/memory/indexer.ts +25 -1
  421. package/src/memory/job-handlers/embedding.test.ts +258 -0
  422. package/src/memory/job-handlers/embedding.ts +81 -1
  423. package/src/memory/job-handlers/index-maintenance.ts +35 -1
  424. package/src/memory/job-handlers/media-processing.ts +11 -1
  425. package/src/memory/job-utils.ts +21 -6
  426. package/src/memory/jobs-store.ts +5 -1
  427. package/src/memory/jobs-worker.ts +8 -0
  428. package/src/memory/message-content.ts +66 -0
  429. package/src/memory/migrations/100-core-tables.ts +1 -31
  430. package/src/memory/migrations/104-core-indexes.ts +0 -11
  431. package/src/memory/migrations/145-drop-accounts-table.ts +19 -0
  432. package/src/memory/migrations/146-schedule-oneshot-routing.ts +94 -0
  433. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +129 -0
  434. package/src/memory/migrations/148-drop-reminders-table.ts +18 -0
  435. package/src/memory/migrations/index.ts +4 -0
  436. package/src/memory/migrations/registry.ts +19 -0
  437. package/src/memory/qdrant-client.ts +158 -43
  438. package/src/memory/retriever.test.ts +0 -1
  439. package/src/memory/retriever.ts +12 -2
  440. package/src/memory/schema/infrastructure.ts +5 -37
  441. package/src/memory/search/formatting.ts +34 -9
  442. package/src/memory/search/semantic.ts +57 -2
  443. package/src/memory/search/types.ts +2 -1
  444. package/src/notifications/AGENTS.md +2 -2
  445. package/src/notifications/README.md +59 -58
  446. package/src/notifications/adapters/macos.ts +1 -1
  447. package/src/notifications/broadcaster.ts +5 -5
  448. package/src/notifications/copy-composer.ts +1 -1
  449. package/src/notifications/decision-engine.ts +2 -2
  450. package/src/notifications/destination-resolver.ts +2 -2
  451. package/src/notifications/emit-signal.ts +8 -8
  452. package/src/notifications/signal.ts +1 -1
  453. package/src/notifications/thread-seed-composer.ts +1 -1
  454. package/src/oauth/connect-orchestrator.ts +1 -1
  455. package/src/oauth/token-persistence.ts +1 -1
  456. package/src/permissions/checker.ts +12 -1
  457. package/src/permissions/defaults.ts +13 -17
  458. package/src/permissions/trust-store.ts +37 -0
  459. package/src/permissions/workspace-policy.ts +0 -1
  460. package/src/prompts/__tests__/build-cli-reference-section.test.ts +11 -0
  461. package/src/prompts/computer-use-prompt.ts +1 -1
  462. package/src/prompts/system-prompt.ts +33 -35
  463. package/src/prompts/templates/BOOTSTRAP.md +0 -3
  464. package/src/prompts/templates/SOUL.md +1 -2
  465. package/src/prompts/templates/UPDATES.md +16 -7
  466. package/src/providers/anthropic/client.ts +87 -33
  467. package/src/providers/gemini/client.ts +6 -0
  468. package/src/providers/managed-proxy/constants.ts +5 -0
  469. package/src/providers/openai/client.ts +15 -0
  470. package/src/providers/registry.ts +4 -6
  471. package/src/providers/types.ts +24 -2
  472. package/src/runtime/AGENTS.md +18 -0
  473. package/src/runtime/assistant-event-hub.ts +2 -3
  474. package/src/runtime/assistant-event.ts +4 -4
  475. package/src/runtime/auth/__tests__/context.test.ts +5 -5
  476. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  477. package/src/runtime/auth/__tests__/guard-tests.test.ts +3 -2
  478. package/src/runtime/auth/__tests__/{ipc-auth-context.test.ts → local-auth-context.test.ts} +21 -21
  479. package/src/runtime/auth/__tests__/route-policy.test.ts +2 -2
  480. package/src/runtime/auth/__tests__/scopes.test.ts +9 -8
  481. package/src/runtime/auth/__tests__/subject.test.ts +8 -8
  482. package/src/runtime/auth/__tests__/token-service.test.ts +0 -1
  483. package/src/runtime/auth/route-policy.ts +8 -8
  484. package/src/runtime/auth/scopes.ts +2 -1
  485. package/src/runtime/auth/subject.ts +4 -4
  486. package/src/runtime/auth/token-service.ts +1 -24
  487. package/src/runtime/auth/types.ts +3 -3
  488. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  489. package/src/runtime/guardian-action-grant-minter.ts +1 -1
  490. package/src/runtime/guardian-action-service.ts +3 -3
  491. package/src/runtime/http-server.ts +15 -2
  492. package/src/runtime/http-types.ts +10 -0
  493. package/src/runtime/invite-service.ts +3 -3
  494. package/src/runtime/local-actor-identity.ts +17 -22
  495. package/src/runtime/middleware/error-handler.ts +14 -1
  496. package/src/runtime/pending-interactions.ts +21 -9
  497. package/src/runtime/routes/app-management-routes.ts +63 -67
  498. package/src/runtime/routes/approval-routes.ts +1 -3
  499. package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
  500. package/src/runtime/routes/brain-graph-routes.ts +4 -42
  501. package/src/runtime/routes/btw-routes.ts +155 -0
  502. package/src/runtime/routes/computer-use-routes.ts +77 -31
  503. package/src/runtime/routes/conversation-routes.ts +234 -47
  504. package/src/runtime/routes/diagnostics-routes.ts +154 -43
  505. package/src/runtime/routes/documents-routes.ts +2 -2
  506. package/src/runtime/routes/global-search-routes.ts +1 -1
  507. package/src/runtime/routes/host-bash-routes.ts +83 -0
  508. package/src/runtime/routes/host-file-routes.ts +79 -0
  509. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  510. package/src/runtime/routes/log-export-routes.ts +120 -0
  511. package/src/runtime/routes/mcp-routes.ts +20 -0
  512. package/src/runtime/routes/migration-routes.ts +3 -3
  513. package/src/runtime/routes/pairing-routes.ts +1 -1
  514. package/src/runtime/routes/recording-routes.ts +6 -4
  515. package/src/runtime/routes/schedule-routes.ts +31 -5
  516. package/src/runtime/routes/session-management-routes.ts +2 -6
  517. package/src/runtime/routes/session-query-routes.ts +18 -15
  518. package/src/runtime/routes/settings-routes.ts +7 -351
  519. package/src/runtime/routes/skills-routes.ts +7 -6
  520. package/src/runtime/routes/subagents-routes.ts +4 -10
  521. package/src/runtime/routes/surface-action-routes.ts +3 -14
  522. package/src/runtime/routes/surface-content-routes.ts +22 -5
  523. package/src/runtime/routes/work-items-routes.ts +21 -25
  524. package/src/runtime/routes/workspace-routes.test.ts +3 -3
  525. package/src/runtime/routes/workspace-utils.ts +1 -1
  526. package/src/runtime/telegram-streaming-delivery.ts +3 -0
  527. package/src/runtime/verification-outbound-actions.ts +2 -2
  528. package/src/schedule/integration-status.ts +0 -6
  529. package/src/schedule/schedule-store.ts +234 -43
  530. package/src/schedule/scheduler.ts +73 -74
  531. package/src/security/oauth2.ts +1 -1
  532. package/src/sequence/store.ts +12 -2
  533. package/src/skills/frontmatter.ts +19 -77
  534. package/src/skills/managed-store.ts +11 -2
  535. package/src/subagent/manager.ts +5 -3
  536. package/src/tasks/ephemeral-permissions.ts +3 -5
  537. package/src/tools/AGENTS.md +37 -0
  538. package/src/tools/apps/executors.ts +0 -6
  539. package/src/tools/browser/browser-manager.ts +17 -11
  540. package/src/tools/browser/jit-auth.ts +4 -1
  541. package/src/tools/claude-code/claude-code.ts +1 -1
  542. package/src/tools/computer-use/definitions.ts +48 -60
  543. package/src/tools/document/document-tool.ts +6 -6
  544. package/src/tools/document/editor-template.ts +10 -8
  545. package/src/tools/filesystem/edit.ts +2 -1
  546. package/src/tools/filesystem/read.ts +20 -2
  547. package/src/tools/filesystem/write.ts +2 -1
  548. package/src/tools/host-filesystem/edit.ts +17 -1
  549. package/src/tools/host-filesystem/read.ts +16 -1
  550. package/src/tools/host-filesystem/write.ts +15 -1
  551. package/src/tools/host-terminal/host-shell.ts +24 -0
  552. package/src/tools/memory/definitions.ts +45 -81
  553. package/src/tools/memory/handlers.test.ts +0 -1
  554. package/src/tools/memory/handlers.ts +1 -1
  555. package/src/tools/memory/register.ts +26 -60
  556. package/src/tools/network/script-proxy/session-manager.ts +6 -8
  557. package/src/tools/network/web-fetch.ts +7 -1
  558. package/src/tools/network/web-search.ts +2 -1
  559. package/src/tools/registry.ts +23 -0
  560. package/src/tools/schedule/create.ts +113 -5
  561. package/src/tools/schedule/list.ts +57 -15
  562. package/src/tools/schedule/update.ts +73 -3
  563. package/src/tools/shared/filesystem/image-read.ts +192 -0
  564. package/src/tools/side-effects.ts +1 -7
  565. package/src/tools/skills/delete-managed.ts +27 -64
  566. package/src/tools/skills/execute.ts +54 -0
  567. package/src/tools/skills/load.ts +127 -5
  568. package/src/tools/skills/scaffold-managed.ts +93 -172
  569. package/src/tools/subagent/message.ts +0 -7
  570. package/src/tools/subagent/spawn.ts +1 -1
  571. package/src/tools/swarm/delegate.ts +0 -3
  572. package/src/tools/system/avatar-generator.ts +13 -19
  573. package/src/tools/system/request-permission.ts +2 -1
  574. package/src/tools/terminal/safe-env.ts +1 -0
  575. package/src/tools/tool-manifest.ts +41 -47
  576. package/src/tools/types.ts +6 -2
  577. package/src/tools/ui-surface/definitions.ts +0 -55
  578. package/src/util/errors.ts +12 -10
  579. package/src/workspace/git-service.ts +0 -2
  580. package/src/__tests__/account-registry.test.ts +0 -258
  581. package/src/__tests__/email-classifier.test.ts +0 -25
  582. package/src/__tests__/gmail-integration.test.ts +0 -97
  583. package/src/__tests__/handle-user-message-secret-resume.test.ts +0 -172
  584. package/src/__tests__/home-base-bootstrap.test.ts +0 -84
  585. package/src/__tests__/managed-twitter-guardrails.test.ts +0 -353
  586. package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
  587. package/src/__tests__/recording-intent-fallback.test.ts +0 -199
  588. package/src/__tests__/recording-intent.test.ts +0 -985
  589. package/src/__tests__/recording-state-machine.test.ts +0 -1574
  590. package/src/__tests__/reminder-store.test.ts +0 -350
  591. package/src/__tests__/reminder.test.ts +0 -337
  592. package/src/__tests__/scan-result-store.test.ts +0 -121
  593. package/src/__tests__/twitter-platform-proxy-client.test.ts +0 -450
  594. package/src/__tests__/view-image-tool.test.ts +0 -241
  595. package/src/cli/commands/amazon/cart.ts +0 -513
  596. package/src/cli/commands/amazon/checkout.ts +0 -394
  597. package/src/cli/commands/amazon/client.ts +0 -513
  598. package/src/cli/commands/amazon/index.ts +0 -920
  599. package/src/cli/commands/amazon/product-details.ts +0 -145
  600. package/src/cli/commands/amazon/request-extractor.ts +0 -187
  601. package/src/cli/commands/amazon/search.ts +0 -76
  602. package/src/cli/commands/amazon/session.ts +0 -116
  603. package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
  604. package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +0 -483
  605. package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +0 -412
  606. package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +0 -197
  607. package/src/cli/commands/twitter/client.ts +0 -989
  608. package/src/cli/commands/twitter/index.ts +0 -1160
  609. package/src/cli/commands/twitter/oauth-client.ts +0 -94
  610. package/src/cli/commands/twitter/router.ts +0 -396
  611. package/src/cli/commands/twitter/session.ts +0 -121
  612. package/src/config/bundled-skills/agentmail/SKILL.md +0 -132
  613. package/src/config/bundled-skills/agentmail/icon.svg +0 -21
  614. package/src/config/bundled-skills/amazon/SKILL.md +0 -137
  615. package/src/config/bundled-skills/amazon/icon.svg +0 -13
  616. package/src/config/bundled-skills/api-mapping/SKILL.md +0 -78
  617. package/src/config/bundled-skills/api-mapping/icon.svg +0 -18
  618. package/src/config/bundled-skills/cli-discover/SKILL.md +0 -68
  619. package/src/config/bundled-skills/deploy-fullstack-vercel/SKILL.md +0 -179
  620. package/src/config/bundled-skills/document-writer/SKILL.md +0 -195
  621. package/src/config/bundled-skills/elevenlabs-voice/SKILL.md +0 -140
  622. package/src/config/bundled-skills/email-setup/SKILL.md +0 -68
  623. package/src/config/bundled-skills/frontend-design/SKILL.md +0 -44
  624. package/src/config/bundled-skills/frontend-design/icon.svg +0 -16
  625. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +0 -452
  626. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +0 -203
  627. package/src/config/bundled-skills/influencer/SKILL.md +0 -144
  628. package/src/config/bundled-skills/influencer/scripts/client.ts +0 -1269
  629. package/src/config/bundled-skills/influencer/scripts/influencer.ts +0 -267
  630. package/src/config/bundled-skills/macos-automation/SKILL.md +0 -65
  631. package/src/config/bundled-skills/macos-automation/icon.svg +0 -12
  632. package/src/config/bundled-skills/mcp-setup/SKILL.md +0 -75
  633. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +0 -184
  634. package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +0 -80
  635. package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +0 -29
  636. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +0 -56
  637. package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +0 -34
  638. package/src/config/bundled-skills/messaging/tools/gmail-download-attachment.ts +0 -47
  639. package/src/config/bundled-skills/messaging/tools/gmail-label.ts +0 -31
  640. package/src/config/bundled-skills/messaging/tools/gmail-list-attachments.ts +0 -67
  641. package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +0 -97
  642. package/src/config/bundled-skills/messaging/tools/gmail-summarize-thread.ts +0 -87
  643. package/src/config/bundled-skills/messaging/tools/gmail-triage.ts +0 -135
  644. package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +0 -24
  645. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +0 -201
  646. package/src/config/bundled-skills/messaging/tools/send-notification.ts +0 -1
  647. package/src/config/bundled-skills/messaging/tools/sequence-cancel.ts +0 -27
  648. package/src/config/bundled-skills/messaging/tools/sequence-pause.ts +0 -48
  649. package/src/config/bundled-skills/messaging/tools/sequence-resume.ts +0 -27
  650. package/src/config/bundled-skills/messaging/tools/sequence-update.ts +0 -56
  651. package/src/config/bundled-skills/notion/SKILL.md +0 -240
  652. package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +0 -126
  653. package/src/config/bundled-skills/oauth-setup/SKILL.md +0 -143
  654. package/src/config/bundled-skills/public-ingress/SKILL.md +0 -258
  655. package/src/config/bundled-skills/reminder/SKILL.md +0 -79
  656. package/src/config/bundled-skills/reminder/TOOLS.json +0 -89
  657. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +0 -12
  658. package/src/config/bundled-skills/restaurant-reservation/SKILL.md +0 -141
  659. package/src/config/bundled-skills/screen-recording/SKILL.md +0 -148
  660. package/src/config/bundled-skills/self-upgrade/SKILL.md +0 -69
  661. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -78
  662. package/src/config/bundled-skills/slack-app-setup/SKILL.md +0 -178
  663. package/src/config/bundled-skills/slack-digest-setup/SKILL.md +0 -163
  664. package/src/config/bundled-skills/slack-oauth-setup/SKILL.md +0 -157
  665. package/src/config/bundled-skills/start-the-day/SKILL.md +0 -70
  666. package/src/config/bundled-skills/start-the-day/icon.svg +0 -13
  667. package/src/config/bundled-skills/telegram-setup/SKILL.md +0 -105
  668. package/src/config/bundled-skills/time-based-actions/SKILL.md +0 -142
  669. package/src/config/bundled-skills/twilio-setup/SKILL.md +0 -232
  670. package/src/config/bundled-skills/twitter/SKILL.md +0 -319
  671. package/src/config/bundled-skills/twitter/icon.svg +0 -14
  672. package/src/config/bundled-skills/typescript-eval/SKILL.md +0 -60
  673. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +0 -214
  674. package/src/config/bundled-skills/voice-setup/SKILL.md +0 -131
  675. package/src/config/bundled-skills/voice-setup/icon.svg +0 -20
  676. package/src/daemon/handlers/pairing.ts +0 -119
  677. package/src/daemon/handlers/session-user-message.ts +0 -961
  678. package/src/daemon/recording-executor.ts +0 -180
  679. package/src/daemon/recording-intent-fallback.ts +0 -162
  680. package/src/daemon/recording-intent.ts +0 -493
  681. package/src/home-base/app-link-store.ts +0 -78
  682. package/src/home-base/bootstrap.ts +0 -74
  683. package/src/home-base/prebuilt/brain-graph.html +0 -1483
  684. package/src/home-base/prebuilt/index.html +0 -702
  685. package/src/home-base/prebuilt/seed-metadata.json +0 -21
  686. package/src/home-base/prebuilt/seed.ts +0 -122
  687. package/src/home-base/prebuilt-home-base-updater.ts +0 -36
  688. package/src/memory/account-store.ts +0 -117
  689. package/src/messaging/activity-analyzer.ts +0 -76
  690. package/src/messaging/email-classifier.ts +0 -208
  691. package/src/messaging/index.ts +0 -2
  692. package/src/messaging/outreach-classifier.ts +0 -185
  693. package/src/messaging/thread-summarizer.ts +0 -346
  694. package/src/messaging/types.ts +0 -17
  695. package/src/tools/browser/x-auto-navigate.ts +0 -254
  696. package/src/tools/credentials/account-registry.ts +0 -144
  697. package/src/tools/filesystem/view-image.ts +0 -244
  698. package/src/tools/reminder/reminder-store.ts +0 -194
  699. package/src/tools/reminder/reminder.ts +0 -158
  700. package/src/tools/system/navigate-settings.ts +0 -74
  701. package/src/tools/system/open-system-settings.ts +0 -85
  702. package/src/tools/system/version.ts +0 -54
  703. package/src/twitter/platform-proxy-client.ts +0 -405
  704. package/src/util/cookie-session.ts +0 -98
  705. /package/src/config/bundled-skills/{messaging → gmail}/tools/scan-result-store.ts +0 -0
  706. /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-analytics.ts +0 -0
  707. /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-create.ts +0 -0
  708. /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-delete.ts +0 -0
  709. /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-enroll.ts +0 -0
  710. /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-enrollment-list.ts +0 -0
  711. /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-get.ts +0 -0
  712. /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-import.ts +0 -0
  713. /package/src/config/bundled-skills/{messaging → sequences}/tools/sequence-list.ts +0 -0
@@ -1,1574 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- // ─── Mocks (must be before any imports that depend on them) ─────────────────
4
-
5
- const noop = () => {};
6
- const noopLogger = {
7
- info: noop,
8
- warn: noop,
9
- error: noop,
10
- debug: noop,
11
- trace: noop,
12
- fatal: noop,
13
- child: () => noopLogger,
14
- };
15
-
16
- mock.module("../util/logger.js", () => ({
17
- getLogger: () => noopLogger,
18
- }));
19
-
20
- mock.module("../config/loader.js", () => ({
21
- getConfig: () => ({
22
- ui: {},
23
-
24
- daemon: { standaloneRecording: true },
25
- provider: "mock-provider",
26
- permissions: { mode: "workspace" },
27
- apiKeys: {},
28
- sandbox: { enabled: false },
29
- timeouts: { toolExecutionTimeoutSec: 30, permissionTimeoutSec: 5 },
30
- skills: { load: { extraDirs: [] } },
31
- secretDetection: { enabled: false, allowOneTimeSend: false },
32
- contextWindow: {
33
- enabled: true,
34
- maxInputTokens: 180000,
35
- targetInputTokens: 110000,
36
- compactThreshold: 0.8,
37
- preserveRecentUserTurns: 8,
38
- summaryMaxTokens: 1200,
39
- chunkTokens: 12000,
40
- },
41
- }),
42
- invalidateConfigCache: noop,
43
- loadConfig: noop,
44
- saveConfig: noop,
45
- loadRawConfig: () => ({}),
46
- saveRawConfig: noop,
47
- getNestedValue: () => undefined,
48
- setNestedValue: noop,
49
- }));
50
-
51
- // Conversation store mock
52
- const mockMessages: Array<{ id: string; role: string; content: string }> = [];
53
- let mockMessageIdCounter = 0;
54
-
55
- mock.module("../memory/conversation-crud.js", () => ({
56
- getConversationThreadType: () => "default",
57
- setConversationOriginChannelIfUnset: () => {},
58
- updateConversationContextWindow: () => {},
59
- deleteMessageById: () => {},
60
- updateConversationTitle: () => {},
61
- updateConversationUsage: () => {},
62
- provenanceFromTrustContext: () => ({
63
- source: "user",
64
- trustContext: undefined,
65
- }),
66
- getConversationOriginInterface: () => null,
67
- getConversationOriginChannel: () => null,
68
- getMessages: () => mockMessages,
69
- addMessage: (_convId: string, role: string, content: string) => {
70
- const msg = { id: `msg-${++mockMessageIdCounter}`, role, content };
71
- mockMessages.push(msg);
72
- return msg;
73
- },
74
- createConversation: () => ({ id: "conv-mock" }),
75
- getConversation: () => ({ id: "conv-mock" }),
76
- }));
77
-
78
- // Attachments store mock
79
- mock.module("../memory/attachments-store.js", () => ({
80
- uploadFileBackedAttachment: () => ({
81
- id: "att-mock",
82
- originalFilename: "test.mov",
83
- mimeType: "video/quicktime",
84
- sizeBytes: 1024,
85
- }),
86
- linkAttachmentToMessage: noop,
87
- setAttachmentThumbnail: noop,
88
- }));
89
-
90
- // Capture real modules BEFORE mocking to avoid circular resolution
91
- // (mock.module('node:fs') + require('fs') inside factory = deadlock)
92
- // eslint-disable-next-line @typescript-eslint/no-require-imports
93
- const realFs = require("fs");
94
- // eslint-disable-next-line @typescript-eslint/no-require-imports
95
- const realPath = require("path");
96
-
97
- // Mock node:fs
98
- mock.module("node:fs", () => ({
99
- ...realFs,
100
- existsSync: (p: string) => {
101
- if (p.includes("recording") || p.includes("/tmp/")) return true;
102
- return realFs.existsSync(p);
103
- },
104
- statSync: (p: string, opts?: any) => {
105
- if (p.includes("recording") || p.includes("/tmp/")) return { size: 1024 };
106
- return realFs.statSync(p, opts);
107
- },
108
- realpathSync: (p: string) => {
109
- // Use path.resolve() to canonicalize `..` segments so traversal
110
- // attacks like `${ALLOWED_DIR}/../outside.mov` are normalized,
111
- // preserving the same semantics as the real realpathSync without
112
- // hitting the filesystem (which would throw ENOENT for test paths).
113
- return realPath.resolve(p);
114
- },
115
- }));
116
-
117
- // Mock video thumbnail
118
- mock.module("../daemon/video-thumbnail.js", () => ({
119
- generateVideoThumbnailFromPath: async () => null,
120
- }));
121
-
122
- // ─── Imports (after mocks) ──────────────────────────────────────────────────
123
-
124
- import {
125
- __injectRecordingOwner,
126
- __resetRecordingState,
127
- getActiveRestartToken,
128
- handleRecordingPause,
129
- handleRecordingRestart,
130
- handleRecordingResume,
131
- handleRecordingStart,
132
- handleRecordingStop,
133
- isRecordingIdle,
134
- recordingHandlers,
135
- } from "../daemon/handlers/recording.js";
136
- import type { HandlerContext } from "../daemon/handlers/shared.js";
137
- import type { RecordingStatus } from "../daemon/message-types/computer-use.js";
138
- import { executeRecordingIntent } from "../daemon/recording-executor.js";
139
- import { DebouncerMap } from "../util/debounce.js";
140
-
141
- // The allowed recordings directory used by the recording handler
142
- const ALLOWED_RECORDINGS_DIR = `${process.env.HOME}/Library/Application Support/vellum-assistant/recordings`;
143
-
144
- // ─── Test helpers ───────────────────────────────────────────────────────────
145
-
146
- function createCtx(): {
147
- ctx: HandlerContext;
148
- sent: Array<{ type: string; [k: string]: unknown }>;
149
- } {
150
- const sent: Array<{ type: string; [k: string]: unknown }> = [];
151
-
152
- const ctx: HandlerContext = {
153
- sessions: new Map(),
154
- cuSessions: new Map(),
155
- cuObservationParseSequence: new Map(),
156
- sharedRequestTimestamps: [],
157
- debounceTimers: new DebouncerMap({ defaultDelayMs: 200 }),
158
- suppressConfigReload: false,
159
- setSuppressConfigReload: noop,
160
- updateConfigFingerprint: noop,
161
- send: (msg) => {
162
- sent.push(msg as { type: string; [k: string]: unknown });
163
- },
164
- broadcast: (msg) => {
165
- sent.push(msg as { type: string; [k: string]: unknown });
166
- },
167
- clearAllSessions: () => 0,
168
- getOrCreateSession: () => {
169
- throw new Error("not implemented");
170
- },
171
- touchSession: noop,
172
- };
173
-
174
- return { ctx, sent };
175
- }
176
-
177
- // ─── Restart state machine tests ────────────────────────────────────────────
178
-
179
- describe("handleRecordingRestart", () => {
180
- beforeEach(() => {
181
- __resetRecordingState();
182
- mockMessages.length = 0;
183
- mockMessageIdCounter = 0;
184
- });
185
-
186
- test("sends recording_stop and defers start until stop-ack", () => {
187
- const { ctx, sent } = createCtx();
188
- const conversationId = "conv-restart-1";
189
-
190
- // Start a recording first
191
- const originalId = handleRecordingStart(
192
- conversationId,
193
- undefined,
194
- ctx,
195
- );
196
- expect(originalId).not.toBeNull();
197
- sent.length = 0;
198
-
199
- const result = handleRecordingRestart(conversationId, ctx);
200
-
201
- expect(result.initiated).toBe(true);
202
- expect(result.operationToken).toBeTruthy();
203
- expect(result.responseText).toBe("Restarting screen recording.");
204
-
205
- // Should have sent only recording_stop (start is deferred)
206
- const stopMsgs = sent.filter((m) => m.type === "recording_stop");
207
- const startMsgs = sent.filter((m) => m.type === "recording_start");
208
- expect(stopMsgs).toHaveLength(1);
209
- expect(startMsgs).toHaveLength(0);
210
-
211
- // Simulate the client acknowledging the stop
212
- const stoppedStatus: RecordingStatus = {
213
- type: "recording_status",
214
- sessionId: originalId!,
215
- status: "stopped",
216
- attachToConversationId: conversationId,
217
- };
218
- recordingHandlers.recording_status(stoppedStatus, ctx);
219
-
220
- // NOW the deferred recording_start should have been sent
221
- const startMsgsAfterAck = sent.filter((m) => m.type === "recording_start");
222
- expect(startMsgsAfterAck).toHaveLength(1);
223
- expect(startMsgsAfterAck[0].operationToken).toBe(result.operationToken);
224
- });
225
-
226
- test('returns "no active recording" with reason when nothing is recording', () => {
227
- const { ctx } = createCtx();
228
-
229
- const result = handleRecordingRestart("conv-no-rec", ctx);
230
-
231
- expect(result.initiated).toBe(false);
232
- expect(result.reason).toBe("no_active_recording");
233
- expect(result.responseText).toBe("No active recording to restart.");
234
- });
235
-
236
- test("generates unique operation token for each restart", () => {
237
- const { ctx, sent } = createCtx();
238
- const conversationId = "conv-restart-unique";
239
-
240
- // First restart cycle
241
- const originalId = handleRecordingStart(
242
- conversationId,
243
- undefined,
244
- ctx,
245
- );
246
- const result1 = handleRecordingRestart(conversationId, ctx);
247
-
248
- // Simulate the stop-ack to trigger the deferred start
249
- const stoppedStatus1: RecordingStatus = {
250
- type: "recording_status",
251
- sessionId: originalId!,
252
- status: "stopped",
253
- attachToConversationId: conversationId,
254
- };
255
- recordingHandlers.recording_status(stoppedStatus1, ctx);
256
-
257
- // Simulate the first restart completing (started status)
258
- const startMsg1 = sent.filter((m) => m.type === "recording_start").pop();
259
- const status1: RecordingStatus = {
260
- type: "recording_status",
261
- sessionId: startMsg1!.recordingId as string,
262
- status: "started",
263
- operationToken: result1.operationToken,
264
- };
265
- recordingHandlers.recording_status(status1, ctx);
266
-
267
- // Second restart cycle
268
- sent.length = 0;
269
- const result2 = handleRecordingRestart(conversationId, ctx);
270
-
271
- expect(result1.operationToken).not.toBe(result2.operationToken);
272
- });
273
- });
274
-
275
- // ─── Restart cancel tests ───────────────────────────────────────────────────
276
-
277
- describe("restart_cancelled status", () => {
278
- beforeEach(() => {
279
- __resetRecordingState();
280
- mockMessages.length = 0;
281
- mockMessageIdCounter = 0;
282
- });
283
-
284
- test('emits restart_cancelled response, never "new recording started"', () => {
285
- const { ctx, sent } = createCtx();
286
- const conversationId = "conv-cancel-1";
287
-
288
- // Start -> restart
289
- const originalId = handleRecordingStart(
290
- conversationId,
291
- undefined,
292
- ctx,
293
- );
294
- const restartResult = handleRecordingRestart(
295
- conversationId,
296
- ctx,
297
- );
298
- expect(restartResult.initiated).toBe(true);
299
-
300
- // Simulate the stop-ack to trigger the deferred start
301
- const stoppedStatus: RecordingStatus = {
302
- type: "recording_status",
303
- sessionId: originalId!,
304
- status: "stopped",
305
- attachToConversationId: conversationId,
306
- };
307
- recordingHandlers.recording_status(stoppedStatus, ctx);
308
-
309
- // Get the new recording ID from the deferred recording_start message
310
- const startMsg = sent.filter((m) => m.type === "recording_start").pop();
311
- sent.length = 0;
312
-
313
- // Client sends restart_cancelled (picker was closed) with the correct operation token
314
- const cancelStatus: RecordingStatus = {
315
- type: "recording_status",
316
- sessionId: startMsg!.recordingId as string,
317
- status: "restart_cancelled",
318
- attachToConversationId: conversationId,
319
- operationToken: restartResult.operationToken,
320
- };
321
- recordingHandlers.recording_status(cancelStatus, ctx);
322
-
323
- // Should have emitted the cancellation message
324
- const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
325
- expect(textDeltas).toHaveLength(1);
326
- expect(textDeltas[0].text).toBe("Recording restart cancelled.");
327
-
328
- // Should NOT have "new recording started" anywhere
329
- const startedMsgs = sent.filter(
330
- (m) =>
331
- m.type === "assistant_text_delta" &&
332
- typeof m.text === "string" &&
333
- m.text.includes("new recording started"),
334
- );
335
- expect(startedMsgs).toHaveLength(0);
336
-
337
- // Recording should be truly idle after cancel
338
- expect(isRecordingIdle()).toBe(true);
339
- });
340
-
341
- test("cleans up restart state on cancel", () => {
342
- const { ctx, sent } = createCtx();
343
- const conversationId = "conv-cancel-cleanup";
344
-
345
- const originalId = handleRecordingStart(
346
- conversationId,
347
- undefined,
348
- ctx,
349
- );
350
- const restartResult = handleRecordingRestart(
351
- conversationId,
352
- ctx,
353
- );
354
-
355
- // Before stop-ack: not idle (mid-restart)
356
- expect(isRecordingIdle()).toBe(false);
357
-
358
- // Simulate the stop-ack to trigger the deferred start
359
- const stoppedStatus: RecordingStatus = {
360
- type: "recording_status",
361
- sessionId: originalId!,
362
- status: "stopped",
363
- attachToConversationId: conversationId,
364
- };
365
- recordingHandlers.recording_status(stoppedStatus, ctx);
366
-
367
- // Still not idle — the new recording has started
368
- expect(isRecordingIdle()).toBe(false);
369
-
370
- const startMsg = sent.filter((m) => m.type === "recording_start").pop();
371
- const cancelStatus: RecordingStatus = {
372
- type: "recording_status",
373
- sessionId: startMsg!.recordingId as string,
374
- status: "restart_cancelled",
375
- attachToConversationId: conversationId,
376
- operationToken: restartResult.operationToken,
377
- };
378
- recordingHandlers.recording_status(cancelStatus, ctx);
379
-
380
- // After cancel: truly idle
381
- expect(isRecordingIdle()).toBe(true);
382
- expect(getActiveRestartToken()).toBeNull();
383
- });
384
- });
385
-
386
- // ─── Stale completion guard tests ───────────────────────────────────────────
387
-
388
- describe("stale completion guard (operation token)", () => {
389
- beforeEach(() => {
390
- __resetRecordingState();
391
- mockMessages.length = 0;
392
- mockMessageIdCounter = 0;
393
- });
394
-
395
- test("rejects recording_status with stale operation token", () => {
396
- const { ctx, sent } = createCtx();
397
- const conversationId = "conv-stale-1";
398
-
399
- // Start recording -> restart (creates operation token)
400
- const originalId = handleRecordingStart(
401
- conversationId,
402
- undefined,
403
- ctx,
404
- );
405
- const restartResult = handleRecordingRestart(
406
- conversationId,
407
- ctx,
408
- );
409
- expect(restartResult.initiated).toBe(true);
410
-
411
- // Simulate the stop-ack to trigger the deferred start
412
- const stoppedStatus: RecordingStatus = {
413
- type: "recording_status",
414
- sessionId: originalId!,
415
- status: "stopped",
416
- attachToConversationId: conversationId,
417
- };
418
- recordingHandlers.recording_status(stoppedStatus, ctx);
419
-
420
- const startMsg = sent.filter((m) => m.type === "recording_start").pop();
421
- sent.length = 0;
422
-
423
- // Simulate a stale "started" status from a PREVIOUS restart cycle
424
- const staleStatus: RecordingStatus = {
425
- type: "recording_status",
426
- sessionId: startMsg!.recordingId as string,
427
- status: "started",
428
- operationToken: "old-stale-token-from-previous-cycle",
429
- };
430
- recordingHandlers.recording_status(staleStatus, ctx);
431
-
432
- // Should have been rejected — no "started" confirmation messages
433
- const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
434
- expect(textDeltas).toHaveLength(0);
435
-
436
- // Active restart token should still be set (not cleared by stale completion)
437
- expect(getActiveRestartToken()).toBe(restartResult.operationToken!);
438
- });
439
-
440
- test("accepts recording_status with matching operation token", () => {
441
- const { ctx, sent } = createCtx();
442
- const conversationId = "conv-matching-1";
443
-
444
- const originalId = handleRecordingStart(
445
- conversationId,
446
- undefined,
447
- ctx,
448
- );
449
- const restartResult = handleRecordingRestart(
450
- conversationId,
451
- ctx,
452
- );
453
-
454
- // Simulate the stop-ack to trigger the deferred start
455
- const stoppedStatus: RecordingStatus = {
456
- type: "recording_status",
457
- sessionId: originalId!,
458
- status: "stopped",
459
- attachToConversationId: conversationId,
460
- };
461
- recordingHandlers.recording_status(stoppedStatus, ctx);
462
-
463
- const startMsg = sent.filter((m) => m.type === "recording_start").pop();
464
-
465
- // Send status with the CORRECT token
466
- const validStatus: RecordingStatus = {
467
- type: "recording_status",
468
- sessionId: startMsg!.recordingId as string,
469
- status: "started",
470
- operationToken: restartResult.operationToken,
471
- };
472
- recordingHandlers.recording_status(validStatus, ctx);
473
-
474
- // Should have been accepted — restart token cleared
475
- expect(getActiveRestartToken()).toBeNull();
476
- });
477
-
478
- test("allows tokenless recording_status during active restart (old recording ack)", async () => {
479
- const { ctx, sent } = createCtx();
480
- const conversationId = "conv-tokenless-1";
481
-
482
- // Start recording -> restart (creates operation token)
483
- handleRecordingStart(conversationId, undefined, ctx);
484
- const restartResult = handleRecordingRestart(
485
- conversationId,
486
- ctx,
487
- );
488
- expect(restartResult.initiated).toBe(true);
489
-
490
- const startMsgs = sent.filter((m) => m.type === "recording_start");
491
- const oldStartMsg = startMsgs[0]; // first recording_start = original/old recording
492
- sent.length = 0;
493
-
494
- // Simulate a tokenless "stopped" status arriving during the restart.
495
- // This represents the OLD recording's stopped ack — it was started before
496
- // the restart was initiated, so it has no operationToken. This MUST be
497
- // allowed through for the deferred restart pattern to work.
498
- const tokenlessStatus: RecordingStatus = {
499
- type: "recording_status",
500
- sessionId: oldStartMsg!.recordingId as string,
501
- status: "stopped",
502
- attachToConversationId: conversationId,
503
- // No operationToken — from old recording, should be allowed
504
- };
505
- await recordingHandlers.recording_status(tokenlessStatus, ctx);
506
-
507
- // Should have triggered the deferred restart start
508
- const newStartMsgs = sent.filter((m) => m.type === "recording_start");
509
- expect(newStartMsgs).toHaveLength(1);
510
-
511
- // The old recording finalization runs (no filePath → "no file was produced"
512
- // text delta). This is expected after M2: the stopped handler finalizes the
513
- // old recording before starting the new one.
514
- const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
515
- expect(textDeltas.length).toBeGreaterThanOrEqual(1);
516
- });
517
-
518
- test("no ghost state after restart stop/start handoff", () => {
519
- const { ctx, sent } = createCtx();
520
- const conversationId = "conv-ghost-1";
521
-
522
- const originalId = handleRecordingStart(
523
- conversationId,
524
- undefined,
525
- ctx,
526
- );
527
-
528
- // Restart sends stop and defers start until stop-ack
529
- const restartResult = handleRecordingRestart(
530
- conversationId,
531
- ctx,
532
- );
533
- expect(restartResult.initiated).toBe(true);
534
-
535
- // Simulate the stop-ack to trigger the deferred start
536
- const stoppedStatus: RecordingStatus = {
537
- type: "recording_status",
538
- sessionId: originalId!,
539
- status: "stopped",
540
- attachToConversationId: conversationId,
541
- };
542
- recordingHandlers.recording_status(stoppedStatus, ctx);
543
-
544
- // The new recording should be active (not the old one)
545
- const startMsgs = sent.filter((m) => m.type === "recording_start");
546
- expect(startMsgs.length).toBeGreaterThanOrEqual(2); // original + deferred restart
547
-
548
- // The last recording_start should have the operation token
549
- const lastStart = startMsgs[startMsgs.length - 1];
550
- expect(lastStart.operationToken).toBe(restartResult.operationToken);
551
- });
552
- });
553
-
554
- // ─── Pause/resume state transition tests ────────────────────────────────────
555
-
556
- describe("handleRecordingPause", () => {
557
- beforeEach(() => {
558
- __resetRecordingState();
559
- });
560
-
561
- test("sends recording_pause for active recording", () => {
562
- const { ctx, sent } = createCtx();
563
- const conversationId = "conv-pause-1";
564
-
565
- const recordingId = handleRecordingStart(
566
- conversationId,
567
- undefined,
568
- ctx,
569
- );
570
- expect(recordingId).not.toBeNull();
571
- sent.length = 0;
572
-
573
- const result = handleRecordingPause(conversationId, ctx);
574
-
575
- expect(result).toBe(recordingId!);
576
- expect(sent).toHaveLength(1);
577
- expect(sent[0].type).toBe("recording_pause");
578
- expect(sent[0].recordingId).toBe(recordingId);
579
- });
580
-
581
- test("returns undefined when no active recording", () => {
582
- const { ctx } = createCtx();
583
-
584
- const result = handleRecordingPause("conv-no-rec", ctx);
585
- expect(result).toBeUndefined();
586
- });
587
-
588
- test("resolves to globally active recording from different conversation", () => {
589
- const { ctx, sent } = createCtx();
590
- const convA = "conv-owner-pause";
591
-
592
- const recordingId = handleRecordingStart(convA, undefined, ctx);
593
- sent.length = 0;
594
-
595
- const result = handleRecordingPause("conv-other-pause", ctx);
596
- expect(result).toBe(recordingId!);
597
- });
598
- });
599
-
600
- describe("handleRecordingResume", () => {
601
- beforeEach(() => {
602
- __resetRecordingState();
603
- });
604
-
605
- test("sends recording_resume for active recording", () => {
606
- const { ctx, sent } = createCtx();
607
- const conversationId = "conv-resume-1";
608
-
609
- const recordingId = handleRecordingStart(
610
- conversationId,
611
- undefined,
612
- ctx,
613
- );
614
- expect(recordingId).not.toBeNull();
615
- sent.length = 0;
616
-
617
- const result = handleRecordingResume(conversationId, ctx);
618
-
619
- expect(result).toBe(recordingId!);
620
- expect(sent).toHaveLength(1);
621
- expect(sent[0].type).toBe("recording_resume");
622
- expect(sent[0].recordingId).toBe(recordingId);
623
- });
624
-
625
- test("returns undefined when no active recording", () => {
626
- const { ctx } = createCtx();
627
-
628
- const result = handleRecordingResume("conv-no-rec", ctx);
629
- expect(result).toBeUndefined();
630
- });
631
- });
632
-
633
- // ─── isRecordingIdle tests ──────────────────────────────────────────────────
634
-
635
- describe("isRecordingIdle", () => {
636
- beforeEach(() => {
637
- __resetRecordingState();
638
- });
639
-
640
- test("returns true when no recording and no pending restart", () => {
641
- expect(isRecordingIdle()).toBe(true);
642
- });
643
-
644
- test("returns false when recording is active", () => {
645
- const { ctx } = createCtx();
646
- handleRecordingStart("conv-idle-1", undefined, ctx);
647
- expect(isRecordingIdle()).toBe(false);
648
- });
649
-
650
- test("returns false when mid-restart (between stop-ack and start confirmation)", () => {
651
- const { ctx } = createCtx();
652
- const conversationId = "conv-idle-restart";
653
-
654
- handleRecordingStart(conversationId, undefined, ctx);
655
- handleRecordingRestart(conversationId, ctx);
656
-
657
- // Mid-restart: the old recording maps are still present AND there's a
658
- // pending restart, so the system is not idle
659
- expect(isRecordingIdle()).toBe(false);
660
- });
661
-
662
- test("returns true after restart completes", () => {
663
- const { ctx, sent } = createCtx();
664
- const conversationId = "conv-idle-complete";
665
-
666
- const originalId = handleRecordingStart(
667
- conversationId,
668
- undefined,
669
- ctx,
670
- );
671
- const restartResult = handleRecordingRestart(
672
- conversationId,
673
- ctx,
674
- );
675
-
676
- // Simulate the stop-ack to trigger the deferred start
677
- const stoppedStatus: RecordingStatus = {
678
- type: "recording_status",
679
- sessionId: originalId!,
680
- status: "stopped",
681
- attachToConversationId: conversationId,
682
- };
683
- recordingHandlers.recording_status(stoppedStatus, ctx);
684
-
685
- // Simulate the new recording starting
686
- const startMsg = sent.filter((m) => m.type === "recording_start").pop();
687
- const startedStatus: RecordingStatus = {
688
- type: "recording_status",
689
- sessionId: startMsg!.recordingId as string,
690
- status: "started",
691
- operationToken: restartResult.operationToken,
692
- };
693
- recordingHandlers.recording_status(startedStatus, ctx);
694
-
695
- // Restart is complete, but recording is still active
696
- expect(getActiveRestartToken()).toBeNull();
697
- // Not idle because the new recording is still running
698
- expect(isRecordingIdle()).toBe(false);
699
- });
700
- });
701
-
702
- // ─── Recording executor integration tests ───────────────────────────────────
703
-
704
- describe("executeRecordingIntent — restart/pause/resume", () => {
705
- beforeEach(() => {
706
- __resetRecordingState();
707
- });
708
-
709
- test("restart_only executes actual restart (deferred start)", () => {
710
- const { ctx, sent } = createCtx();
711
- const conversationId = "conv-exec-restart";
712
-
713
- // Start a recording first
714
- const originalId = handleRecordingStart(
715
- conversationId,
716
- undefined,
717
- ctx,
718
- );
719
- sent.length = 0;
720
-
721
- const result = executeRecordingIntent(
722
- { kind: "restart_only" },
723
- { conversationId, ctx },
724
- );
725
-
726
- expect(result.handled).toBe(true);
727
- expect(result.responseText).toBe("Restarting screen recording.");
728
-
729
- // Should have sent only stop (start is deferred until stop-ack)
730
- const stopMsgs = sent.filter((m) => m.type === "recording_stop");
731
- const startMsgs = sent.filter((m) => m.type === "recording_start");
732
- expect(stopMsgs).toHaveLength(1);
733
- expect(startMsgs).toHaveLength(0);
734
-
735
- // Simulate the stop-ack to trigger the deferred start
736
- const stoppedStatus: RecordingStatus = {
737
- type: "recording_status",
738
- sessionId: originalId!,
739
- status: "stopped",
740
- attachToConversationId: conversationId,
741
- };
742
- recordingHandlers.recording_status(stoppedStatus, ctx);
743
-
744
- // NOW the deferred start should have been sent
745
- const startMsgsAfterAck = sent.filter((m) => m.type === "recording_start");
746
- expect(startMsgsAfterAck).toHaveLength(1);
747
- });
748
-
749
- test('restart_only returns "no active recording" when idle', () => {
750
- const { ctx } = createCtx();
751
-
752
- const result = executeRecordingIntent(
753
- { kind: "restart_only" },
754
- { conversationId: "conv-no-rec", ctx },
755
- );
756
-
757
- expect(result.handled).toBe(true);
758
- expect(result.responseText).toBe("No active recording to restart.");
759
- });
760
-
761
- test("restart_with_remainder returns deferred restart", () => {
762
- const { ctx } = createCtx();
763
-
764
- const result = executeRecordingIntent(
765
- { kind: "restart_with_remainder", remainder: "do something else" },
766
- { conversationId: "conv-rem", ctx },
767
- );
768
-
769
- expect(result.handled).toBe(false);
770
- expect(result.pendingRestart).toBe(true);
771
- expect(result.remainderText).toBe("do something else");
772
- });
773
-
774
- test("pause_only executes actual pause", () => {
775
- const { ctx, sent } = createCtx();
776
- const conversationId = "conv-exec-pause";
777
-
778
- handleRecordingStart(conversationId, undefined, ctx);
779
- sent.length = 0;
780
-
781
- const result = executeRecordingIntent(
782
- { kind: "pause_only" },
783
- { conversationId, ctx },
784
- );
785
-
786
- expect(result.handled).toBe(true);
787
- expect(result.responseText).toBe("Pausing the recording.");
788
-
789
- const pauseMsgs = sent.filter((m) => m.type === "recording_pause");
790
- expect(pauseMsgs).toHaveLength(1);
791
- });
792
-
793
- test('pause_only returns "no active recording" when idle', () => {
794
- const { ctx } = createCtx();
795
-
796
- const result = executeRecordingIntent(
797
- { kind: "pause_only" },
798
- { conversationId: "conv-no-rec", ctx },
799
- );
800
-
801
- expect(result.handled).toBe(true);
802
- expect(result.responseText).toBe("No active recording to pause.");
803
- });
804
-
805
- test("resume_only executes actual resume", () => {
806
- const { ctx, sent } = createCtx();
807
- const conversationId = "conv-exec-resume";
808
-
809
- handleRecordingStart(conversationId, undefined, ctx);
810
- sent.length = 0;
811
-
812
- const result = executeRecordingIntent(
813
- { kind: "resume_only" },
814
- { conversationId, ctx },
815
- );
816
-
817
- expect(result.handled).toBe(true);
818
- expect(result.responseText).toBe("Resuming the recording.");
819
-
820
- const resumeMsgs = sent.filter((m) => m.type === "recording_resume");
821
- expect(resumeMsgs).toHaveLength(1);
822
- });
823
-
824
- test('resume_only returns "no active recording" when idle', () => {
825
- const { ctx } = createCtx();
826
-
827
- const result = executeRecordingIntent(
828
- { kind: "resume_only" },
829
- { conversationId: "conv-no-rec", ctx },
830
- );
831
-
832
- expect(result.handled).toBe(true);
833
- expect(result.responseText).toBe("No active recording to resume.");
834
- });
835
- });
836
-
837
- // ─── Recording status paused/resumed acknowledgement tests ──────────────────
838
-
839
- describe("recording_status paused/resumed", () => {
840
- beforeEach(() => {
841
- __resetRecordingState();
842
- });
843
-
844
- test("handles paused status without error", () => {
845
- const { ctx } = createCtx();
846
- const conversationId = "conv-status-paused";
847
-
848
- const recordingId = handleRecordingStart(
849
- conversationId,
850
- undefined,
851
- ctx,
852
- );
853
- expect(recordingId).not.toBeNull();
854
-
855
- const statusMsg: RecordingStatus = {
856
- type: "recording_status",
857
- sessionId: recordingId!,
858
- status: "paused",
859
- };
860
-
861
- expect(() => {
862
- recordingHandlers.recording_status(statusMsg, ctx);
863
- }).not.toThrow();
864
- });
865
-
866
- test("handles resumed status without error", () => {
867
- const { ctx } = createCtx();
868
- const conversationId = "conv-status-resumed";
869
-
870
- const recordingId = handleRecordingStart(
871
- conversationId,
872
- undefined,
873
- ctx,
874
- );
875
- expect(recordingId).not.toBeNull();
876
-
877
- const statusMsg: RecordingStatus = {
878
- type: "recording_status",
879
- sessionId: recordingId!,
880
- status: "resumed",
881
- };
882
-
883
- expect(() => {
884
- recordingHandlers.recording_status(statusMsg, ctx);
885
- }).not.toThrow();
886
- });
887
- });
888
-
889
- // ─── Failed during restart cleans up restart state ──────────────────────────
890
-
891
- describe("failure during restart", () => {
892
- beforeEach(() => {
893
- __resetRecordingState();
894
- mockMessages.length = 0;
895
- mockMessageIdCounter = 0;
896
- });
897
-
898
- test("failed status during restart clears pending restart state (old recording fails)", () => {
899
- const { ctx, sent } = createCtx();
900
- const conversationId = "conv-fail-restart";
901
-
902
- const originalId = handleRecordingStart(
903
- conversationId,
904
- undefined,
905
- ctx,
906
- );
907
- handleRecordingRestart(conversationId, ctx);
908
- sent.length = 0;
909
-
910
- // Simulate the old recording failing to stop (before stop-ack)
911
- const failedStatus: RecordingStatus = {
912
- type: "recording_status",
913
- sessionId: originalId!,
914
- status: "failed",
915
- error: "Permission denied",
916
- attachToConversationId: conversationId,
917
- };
918
- recordingHandlers.recording_status(failedStatus, ctx);
919
-
920
- // Restart state and deferred restart should be cleaned up
921
- expect(getActiveRestartToken()).toBeNull();
922
- expect(isRecordingIdle()).toBe(true);
923
- });
924
-
925
- test("failed status during restart clears state (new recording fails after deferred start)", () => {
926
- const { ctx, sent } = createCtx();
927
- const conversationId = "conv-fail-restart-new";
928
-
929
- const originalId = handleRecordingStart(
930
- conversationId,
931
- undefined,
932
- ctx,
933
- );
934
- const restartResult = handleRecordingRestart(
935
- conversationId,
936
- ctx,
937
- );
938
-
939
- // Simulate the stop-ack to trigger the deferred start
940
- const stoppedStatus: RecordingStatus = {
941
- type: "recording_status",
942
- sessionId: originalId!,
943
- status: "stopped",
944
- attachToConversationId: conversationId,
945
- };
946
- recordingHandlers.recording_status(stoppedStatus, ctx);
947
-
948
- const startMsg = sent.filter((m) => m.type === "recording_start").pop();
949
- sent.length = 0;
950
-
951
- // Simulate new recording failing (with the correct operation token)
952
- const failedStatus: RecordingStatus = {
953
- type: "recording_status",
954
- sessionId: startMsg!.recordingId as string,
955
- status: "failed",
956
- error: "Permission denied",
957
- attachToConversationId: conversationId,
958
- operationToken: restartResult.operationToken,
959
- };
960
- recordingHandlers.recording_status(failedStatus, ctx);
961
-
962
- // Restart state should be cleaned up
963
- expect(getActiveRestartToken()).toBeNull();
964
- expect(isRecordingIdle()).toBe(true);
965
- });
966
- });
967
-
968
- // ─── start_and_stop_only from idle state ─────────────────────────────────────
969
-
970
- describe("start_and_stop_only fallback to plain start when idle", () => {
971
- beforeEach(() => {
972
- __resetRecordingState();
973
- });
974
-
975
- test("falls back to handleRecordingStart when no active recording", () => {
976
- const { ctx, sent } = createCtx();
977
- const conversationId = "conv-stop-start-idle";
978
-
979
- // No recording is active — start_and_stop_only should fall back to a
980
- // plain start rather than returning "No active recording to restart."
981
- const result = executeRecordingIntent(
982
- { kind: "start_and_stop_only" },
983
- { conversationId, ctx },
984
- );
985
-
986
- expect(result.handled).toBe(true);
987
- expect(result.recordingStarted).toBe(true);
988
- expect(result.responseText).toBe("Starting screen recording.");
989
-
990
- // Should have sent only a recording_start (no stop since nothing was active)
991
- const stopMsgs = sent.filter((m) => m.type === "recording_stop");
992
- const startMsgs = sent.filter((m) => m.type === "recording_start");
993
- expect(stopMsgs).toHaveLength(0);
994
- expect(startMsgs).toHaveLength(1);
995
- });
996
-
997
- test("goes through restart when a recording is active (deferred start)", () => {
998
- const { ctx, sent } = createCtx();
999
- const conversationId = "conv-stop-start-active";
1000
-
1001
- // Start a recording first
1002
- const originalId = handleRecordingStart(
1003
- conversationId,
1004
- undefined,
1005
- ctx,
1006
- );
1007
- expect(originalId).not.toBeNull();
1008
- sent.length = 0;
1009
-
1010
- // Now start_and_stop_only should go through handleRecordingRestart
1011
- const result = executeRecordingIntent(
1012
- { kind: "start_and_stop_only" },
1013
- { conversationId, ctx },
1014
- );
1015
-
1016
- expect(result.handled).toBe(true);
1017
- expect(result.recordingStarted).toBe(true);
1018
- expect(result.responseText).toBe(
1019
- "Stopping current recording and starting a new one.",
1020
- );
1021
-
1022
- // Should have sent only stop (start is deferred until stop-ack)
1023
- const stopMsgs = sent.filter((m) => m.type === "recording_stop");
1024
- const startMsgs = sent.filter((m) => m.type === "recording_start");
1025
- expect(stopMsgs).toHaveLength(1);
1026
- expect(startMsgs).toHaveLength(0);
1027
-
1028
- // Simulate the stop-ack to trigger the deferred start
1029
- const stoppedStatus: RecordingStatus = {
1030
- type: "recording_status",
1031
- sessionId: originalId!,
1032
- status: "stopped",
1033
- attachToConversationId: conversationId,
1034
- };
1035
- recordingHandlers.recording_status(stoppedStatus, ctx);
1036
-
1037
- // NOW the deferred start should have been sent
1038
- const startMsgsAfterAck = sent.filter((m) => m.type === "recording_start");
1039
- expect(startMsgsAfterAck).toHaveLength(1);
1040
- });
1041
- });
1042
-
1043
- // ─── start_and_stop_with_remainder from idle state ───────────────────────────
1044
-
1045
- describe("start_and_stop_with_remainder fallback to plain start when idle", () => {
1046
- beforeEach(() => {
1047
- __resetRecordingState();
1048
- });
1049
-
1050
- test("sets pendingStart (not pendingRestart) when no active recording", () => {
1051
- const { ctx } = createCtx();
1052
- const conversationId = "conv-rem-idle";
1053
-
1054
- const result = executeRecordingIntent(
1055
- { kind: "start_and_stop_with_remainder", remainder: "do something" },
1056
- { conversationId, ctx },
1057
- );
1058
-
1059
- expect(result.handled).toBe(false);
1060
- expect(result.pendingStart).toBe(true);
1061
- expect(result.pendingRestart).toBeUndefined();
1062
- expect(result.remainderText).toBe("do something");
1063
- });
1064
-
1065
- test("sets pendingRestart when a recording is active", () => {
1066
- const { ctx } = createCtx();
1067
- const conversationId = "conv-rem-active";
1068
-
1069
- // Start a recording first
1070
- handleRecordingStart(conversationId, undefined, ctx);
1071
-
1072
- const result = executeRecordingIntent(
1073
- { kind: "start_and_stop_with_remainder", remainder: "do something" },
1074
- { conversationId, ctx },
1075
- );
1076
-
1077
- expect(result.handled).toBe(false);
1078
- expect(result.pendingRestart).toBe(true);
1079
- expect(result.pendingStart).toBeUndefined();
1080
- expect(result.remainderText).toBe("do something");
1081
- });
1082
- });
1083
-
1084
- // ─── Deferred restart race condition tests ───────────────────────────────────
1085
-
1086
- describe("deferred restart prevents race condition", () => {
1087
- beforeEach(() => {
1088
- __resetRecordingState();
1089
- mockMessages.length = 0;
1090
- mockMessageIdCounter = 0;
1091
- });
1092
-
1093
- test("recording_start is NOT sent until client acks the stop", () => {
1094
- const { ctx, sent } = createCtx();
1095
- const conversationId = "conv-deferred-race";
1096
-
1097
- handleRecordingStart(conversationId, undefined, ctx);
1098
- sent.length = 0;
1099
-
1100
- handleRecordingRestart(conversationId, ctx);
1101
-
1102
- // Only recording_stop should have been sent — no recording_start yet
1103
- expect(sent.filter((m) => m.type === "recording_stop")).toHaveLength(1);
1104
- expect(sent.filter((m) => m.type === "recording_start")).toHaveLength(0);
1105
-
1106
- // System is mid-restart — not idle
1107
- expect(isRecordingIdle()).toBe(false);
1108
- });
1109
-
1110
- test("stop-ack timeout cleans up deferred restart state", () => {
1111
- // This test uses a real timer via bun's jest-compatible API
1112
- const { ctx } = createCtx();
1113
- const conversationId = "conv-deferred-timeout";
1114
-
1115
- handleRecordingStart(conversationId, undefined, ctx);
1116
- handleRecordingRestart(conversationId, ctx);
1117
-
1118
- // Mid-restart: not idle
1119
- expect(isRecordingIdle()).toBe(false);
1120
-
1121
- // We cannot easily test the setTimeout firing here without mocking timers,
1122
- // but we can verify the state is correctly set up for the timeout to clean up.
1123
- expect(getActiveRestartToken()).not.toBeNull();
1124
- });
1125
-
1126
- test("cross-conversation restart: conversation B restarts recording owned by A", () => {
1127
- const { ctx, sent } = createCtx();
1128
- const convA = "conv-owner-A";
1129
- const convB = "conv-requester-B";
1130
-
1131
- // Conversation A starts a recording
1132
- const originalId = handleRecordingStart(convA, undefined, ctx);
1133
- expect(originalId).not.toBeNull();
1134
- sent.length = 0;
1135
-
1136
- // Conversation B requests a restart (cross-conversation via global fallback)
1137
- const result = handleRecordingRestart(convB, ctx);
1138
- expect(result.initiated).toBe(true);
1139
- expect(result.operationToken).toBeTruthy();
1140
-
1141
- // Should have sent recording_stop (start is deferred)
1142
- expect(sent.filter((m) => m.type === "recording_stop")).toHaveLength(1);
1143
- expect(sent.filter((m) => m.type === "recording_start")).toHaveLength(0);
1144
-
1145
- // Simulate the client acknowledging the stop. The stopped status resolves
1146
- // conversationId from standaloneRecordingConversationId which maps to A.
1147
- const stoppedStatus: RecordingStatus = {
1148
- type: "recording_status",
1149
- sessionId: originalId!,
1150
- status: "stopped",
1151
- attachToConversationId: convA,
1152
- };
1153
- recordingHandlers.recording_status(stoppedStatus, ctx);
1154
-
1155
- // The deferred recording_start MUST have been triggered even though the
1156
- // stopped callback resolved to conversation A (owner), not B (requester).
1157
- const startMsgs = sent.filter((m) => m.type === "recording_start");
1158
- expect(startMsgs).toHaveLength(1);
1159
- expect(startMsgs[0].operationToken).toBe(result.operationToken);
1160
-
1161
- // The new recording is owned by B (the requester). Simulate the client
1162
- // confirming the new recording started. The 'started' status resolves
1163
- // conversationId to B, so pendingRestartByConversation must have been
1164
- // migrated from A to B for the restart cycle to complete.
1165
- const newRecordingId = startMsgs[0].recordingId as string;
1166
- const startedStatus: RecordingStatus = {
1167
- type: "recording_status",
1168
- sessionId: newRecordingId,
1169
- status: "started",
1170
- operationToken: result.operationToken,
1171
- attachToConversationId: convB,
1172
- };
1173
- recordingHandlers.recording_status(startedStatus, ctx);
1174
-
1175
- // Restart cycle must be fully complete: activeRestartToken cleared
1176
- expect(getActiveRestartToken()).toBeNull();
1177
-
1178
- // Not idle yet because the new recording is still running
1179
- expect(isRecordingIdle()).toBe(false);
1180
-
1181
- // Stop the new recording and verify system returns to idle
1182
- handleRecordingStop(convB, ctx);
1183
- const newStoppedStatus: RecordingStatus = {
1184
- type: "recording_status",
1185
- sessionId: newRecordingId,
1186
- status: "stopped",
1187
- attachToConversationId: convB,
1188
- };
1189
- recordingHandlers.recording_status(newStoppedStatus, ctx);
1190
-
1191
- expect(isRecordingIdle()).toBe(true);
1192
- });
1193
-
1194
- test("normal stop (non-restart) does not trigger deferred start", () => {
1195
- const { ctx, sent } = createCtx();
1196
- const conversationId = "conv-normal-stop";
1197
-
1198
- const recordingId = handleRecordingStart(
1199
- conversationId,
1200
- undefined,
1201
- ctx,
1202
- );
1203
- expect(recordingId).not.toBeNull();
1204
-
1205
- // Manually stop (not via restart)
1206
- handleRecordingStop(conversationId, ctx);
1207
- sent.length = 0;
1208
-
1209
- // Simulate stop-ack without file path (e.g. very short recording)
1210
- const stoppedStatus: RecordingStatus = {
1211
- type: "recording_status",
1212
- sessionId: recordingId!,
1213
- status: "stopped",
1214
- attachToConversationId: conversationId,
1215
- };
1216
- recordingHandlers.recording_status(stoppedStatus, ctx);
1217
-
1218
- // Should NOT have sent a recording_start (no deferred restart pending)
1219
- const startMsgs = sent.filter((m) => m.type === "recording_start");
1220
- expect(startMsgs).toHaveLength(0);
1221
- expect(isRecordingIdle()).toBe(true);
1222
- });
1223
- });
1224
-
1225
- // ─── Restart finalization tests ──────────────────────────────────────────────
1226
-
1227
- describe("restart finalization", () => {
1228
- beforeEach(() => {
1229
- __resetRecordingState();
1230
- mockMessages.length = 0;
1231
- mockMessageIdCounter = 0;
1232
- });
1233
-
1234
- test("publishes previous recording attachment on restart", async () => {
1235
- const { ctx, sent } = createCtx();
1236
- const conversationId = "conv-fin-publish";
1237
-
1238
- // Start a recording
1239
- const originalId = handleRecordingStart(
1240
- conversationId,
1241
- undefined,
1242
- ctx,
1243
- );
1244
- expect(originalId).not.toBeNull();
1245
-
1246
- // Trigger restart
1247
- const restartResult = handleRecordingRestart(
1248
- conversationId,
1249
- ctx,
1250
- );
1251
- expect(restartResult.initiated).toBe(true);
1252
- sent.length = 0;
1253
-
1254
- // Simulate stopped for old recording with filePath and durationMs
1255
- const stoppedStatus: RecordingStatus = {
1256
- type: "recording_status",
1257
- sessionId: originalId!,
1258
- status: "stopped",
1259
- filePath: `${ALLOWED_RECORDINGS_DIR}/recording-old.mov`,
1260
- durationMs: 5000,
1261
- attachToConversationId: conversationId,
1262
- };
1263
- await recordingHandlers.recording_status(stoppedStatus, ctx);
1264
-
1265
- // Verify: a new recording_start IPC was sent (deferred start triggered)
1266
- const startMsgs = sent.filter((m) => m.type === "recording_start");
1267
- expect(startMsgs).toHaveLength(1);
1268
- expect(startMsgs[0].operationToken).toBe(restartResult.operationToken);
1269
-
1270
- // Verify: finalizeAndPublishRecording was called — check that sent
1271
- // contains messages with attachment data (assistant_text_delta + message_complete)
1272
- const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
1273
- expect(textDeltas.length).toBeGreaterThanOrEqual(1);
1274
- const successMsg = textDeltas.find(
1275
- (m) =>
1276
- typeof m.text === "string" &&
1277
- m.text.includes("Screen recording complete"),
1278
- );
1279
- expect(successMsg).toBeTruthy();
1280
-
1281
- const completes = sent.filter((m) => m.type === "message_complete");
1282
- const attachmentComplete = completes.find((m) => m.attachments != null);
1283
- expect(attachmentComplete).toBeTruthy();
1284
-
1285
- // Verify: a message was added to the conversation store
1286
- const assistantMsg = mockMessages.find((m) => m.role === "assistant");
1287
- expect(assistantMsg).toBeTruthy();
1288
- });
1289
-
1290
- test("restart + picker cancel preserves previous publish", async () => {
1291
- const { ctx, sent } = createCtx();
1292
- const conversationId = "conv-fin-cancel-preserve";
1293
-
1294
- // Start a recording
1295
- const originalId = handleRecordingStart(
1296
- conversationId,
1297
- undefined,
1298
- ctx,
1299
- );
1300
-
1301
- // Restart
1302
- const restartResult = handleRecordingRestart(
1303
- conversationId,
1304
- ctx,
1305
- );
1306
- expect(restartResult.initiated).toBe(true);
1307
-
1308
- // Simulate stopped for old recording with filePath
1309
- const stoppedStatus: RecordingStatus = {
1310
- type: "recording_status",
1311
- sessionId: originalId!,
1312
- status: "stopped",
1313
- filePath: `${ALLOWED_RECORDINGS_DIR}/recording-preserved.mov`,
1314
- durationMs: 3000,
1315
- attachToConversationId: conversationId,
1316
- };
1317
- await recordingHandlers.recording_status(stoppedStatus, ctx);
1318
-
1319
- // Capture sent messages so far (should include old recording's attachment)
1320
- const preCancelTextDeltas = sent.filter(
1321
- (m) => m.type === "assistant_text_delta",
1322
- );
1323
- const oldAttachmentMsg = preCancelTextDeltas.find(
1324
- (m) =>
1325
- typeof m.text === "string" &&
1326
- m.text.includes("Screen recording complete"),
1327
- );
1328
- expect(oldAttachmentMsg).toBeTruthy();
1329
-
1330
- // Get the new recording ID from the deferred recording_start message
1331
- const startMsg = sent.filter((m) => m.type === "recording_start").pop();
1332
- expect(startMsg).toBeTruthy();
1333
-
1334
- // Client sends restart_cancelled (picker was closed)
1335
- const cancelStatus: RecordingStatus = {
1336
- type: "recording_status",
1337
- sessionId: startMsg!.recordingId as string,
1338
- status: "restart_cancelled",
1339
- attachToConversationId: conversationId,
1340
- operationToken: restartResult.operationToken,
1341
- };
1342
- await recordingHandlers.recording_status(cancelStatus, ctx);
1343
-
1344
- // Verify: the old recording's attachment messages are still in sent
1345
- const postCancelTextDeltas = sent.filter(
1346
- (m) => m.type === "assistant_text_delta",
1347
- );
1348
- const oldMsgStillPresent = postCancelTextDeltas.find(
1349
- (m) =>
1350
- typeof m.text === "string" &&
1351
- m.text.includes("Screen recording complete"),
1352
- );
1353
- expect(oldMsgStillPresent).toBeTruthy();
1354
-
1355
- // Verify: restart_cancelled adds "Recording restart cancelled." text
1356
- const cancelMsg = postCancelTextDeltas.find(
1357
- (m) =>
1358
- typeof m.text === "string" && m.text === "Recording restart cancelled.",
1359
- );
1360
- expect(cancelMsg).toBeTruthy();
1361
-
1362
- // Verify: the old recording's message was not removed from conversation store
1363
- const assistantMsg = mockMessages.find((m) => m.role === "assistant");
1364
- expect(assistantMsg).toBeTruthy();
1365
- });
1366
-
1367
- test("emits truthful failure text when previous finalize fails", async () => {
1368
- const { ctx, sent } = createCtx();
1369
- const conversationId = "conv-fin-fail-truth";
1370
-
1371
- // Start a recording
1372
- const originalId = handleRecordingStart(
1373
- conversationId,
1374
- undefined,
1375
- ctx,
1376
- );
1377
-
1378
- // Restart
1379
- const restartResult = handleRecordingRestart(
1380
- conversationId,
1381
- ctx,
1382
- );
1383
- expect(restartResult.initiated).toBe(true);
1384
- sent.length = 0;
1385
-
1386
- // Simulate stopped with missing filePath (no file produced)
1387
- const stoppedStatus: RecordingStatus = {
1388
- type: "recording_status",
1389
- sessionId: originalId!,
1390
- status: "stopped",
1391
- // No filePath — recording stopped without producing a file
1392
- attachToConversationId: conversationId,
1393
- };
1394
- await recordingHandlers.recording_status(stoppedStatus, ctx);
1395
-
1396
- // Verify: error message text is sent (not "Screen recording complete")
1397
- const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
1398
- const hasSuccessMsg = textDeltas.some(
1399
- (m) =>
1400
- typeof m.text === "string" &&
1401
- m.text.includes("Screen recording complete"),
1402
- );
1403
- expect(hasSuccessMsg).toBe(false);
1404
-
1405
- const hasErrorMsg = textDeltas.some(
1406
- (m) =>
1407
- typeof m.text === "string" && m.text.includes("no file was produced"),
1408
- );
1409
- expect(hasErrorMsg).toBe(true);
1410
-
1411
- // Verify: new recording_start still triggers (deferred start)
1412
- const startMsgs = sent.filter((m) => m.type === "recording_start");
1413
- expect(startMsgs).toHaveLength(1);
1414
- expect(startMsgs[0].operationToken).toBe(restartResult.operationToken);
1415
- });
1416
-
1417
- test("preserves previous attachment when new start fails", async () => {
1418
- const { ctx, sent } = createCtx();
1419
- const conversationId = "conv-fin-new-fail";
1420
-
1421
- // Start a recording
1422
- const originalId = handleRecordingStart(
1423
- conversationId,
1424
- undefined,
1425
- ctx,
1426
- );
1427
-
1428
- // Restart
1429
- const restartResult = handleRecordingRestart(
1430
- conversationId,
1431
- ctx,
1432
- );
1433
- expect(restartResult.initiated).toBe(true);
1434
-
1435
- // Inject a blocker recording on a different conversation so that
1436
- // when the stopped handler calls cleanupMaps (removing conv-A's entry)
1437
- // and then tries handleRecordingStart, the global single-active guard
1438
- // sees the blocker entry and returns null — exercising the start-failure
1439
- // code path.
1440
- __injectRecordingOwner("conv-blocker", "rec-blocker");
1441
-
1442
- sent.length = 0;
1443
-
1444
- const stoppedStatus: RecordingStatus = {
1445
- type: "recording_status",
1446
- sessionId: originalId!,
1447
- status: "stopped",
1448
- filePath: `${ALLOWED_RECORDINGS_DIR}/recording-old-success.mov`,
1449
- durationMs: 4000,
1450
- attachToConversationId: conversationId,
1451
- };
1452
- await recordingHandlers.recording_status(stoppedStatus, ctx);
1453
-
1454
- // Verify: old recording attachment is published (finalization succeeded)
1455
- const textDeltas = sent.filter((m) => m.type === "assistant_text_delta");
1456
- const successMsg = textDeltas.find(
1457
- (m) =>
1458
- typeof m.text === "string" &&
1459
- m.text.includes("Screen recording complete"),
1460
- );
1461
- expect(successMsg).toBeTruthy();
1462
-
1463
- const completes = sent.filter((m) => m.type === "message_complete");
1464
- const attachmentComplete = completes.find((m) => m.attachments != null);
1465
- expect(attachmentComplete).toBeTruthy();
1466
-
1467
- // Verify: no recording_start was sent (deferred start was blocked)
1468
- const startMsgs = sent.filter((m) => m.type === "recording_start");
1469
- expect(startMsgs).toHaveLength(0);
1470
-
1471
- // Verify: follow-up message about the start failure was sent
1472
- const failureMsg = textDeltas.find(
1473
- (m) =>
1474
- typeof m.text === "string" &&
1475
- m.text.includes(
1476
- "Previous recording saved. New recording failed to start.",
1477
- ),
1478
- );
1479
- expect(failureMsg).toBeTruthy();
1480
-
1481
- // Verify: old recording message exists in conversation store
1482
- const assistantMsg = mockMessages.find((m) => m.role === "assistant");
1483
- expect(assistantMsg).toBeTruthy();
1484
- });
1485
-
1486
- test("duplicate stopped callback does not double-attach", async () => {
1487
- const { ctx, sent } = createCtx();
1488
- const conversationId = "conv-fin-dup-stop";
1489
-
1490
- // Start a recording
1491
- const originalId = handleRecordingStart(
1492
- conversationId,
1493
- undefined,
1494
- ctx,
1495
- );
1496
-
1497
- // Restart
1498
- const restartResult = handleRecordingRestart(
1499
- conversationId,
1500
- ctx,
1501
- );
1502
- expect(restartResult.initiated).toBe(true);
1503
- sent.length = 0;
1504
-
1505
- // First stopped callback with filePath
1506
- const stoppedStatus: RecordingStatus = {
1507
- type: "recording_status",
1508
- sessionId: originalId!,
1509
- status: "stopped",
1510
- filePath: `${ALLOWED_RECORDINGS_DIR}/recording-dup.mov`,
1511
- durationMs: 2000,
1512
- attachToConversationId: conversationId,
1513
- };
1514
- await recordingHandlers.recording_status(stoppedStatus, ctx);
1515
-
1516
- // Count attachment-related messages after first callback
1517
- const firstCallAttachmentMsgs = sent.filter(
1518
- (m) =>
1519
- m.type === "assistant_text_delta" &&
1520
- typeof m.text === "string" &&
1521
- m.text.includes("Screen recording complete"),
1522
- );
1523
- expect(firstCallAttachmentMsgs).toHaveLength(1);
1524
-
1525
- const firstCallMsgCount = mockMessages.filter(
1526
- (m) => m.role === "assistant",
1527
- ).length;
1528
- expect(firstCallMsgCount).toBe(1);
1529
-
1530
- // Send stopped again with same recordingId (duplicate)
1531
- await recordingHandlers.recording_status(stoppedStatus, ctx);
1532
-
1533
- // Verify: only one attachment message exists in sent — the duplicate was
1534
- // rejected by the idempotency guard in finalizeAndPublishRecording
1535
- const allAttachmentMsgs = sent.filter(
1536
- (m) =>
1537
- m.type === "assistant_text_delta" &&
1538
- typeof m.text === "string" &&
1539
- m.text.includes("Screen recording complete"),
1540
- );
1541
- expect(allAttachmentMsgs).toHaveLength(1);
1542
-
1543
- // Verify: only one assistant message in conversation store
1544
- const assistantMsgs = mockMessages.filter((m) => m.role === "assistant");
1545
- expect(assistantMsgs).toHaveLength(1);
1546
- });
1547
-
1548
- test("existing stale-token and restart-timeout protections still pass", () => {
1549
- // This test verifies that the module-level state functions are
1550
- // consistent and that __resetRecordingState properly clears all state.
1551
- // The actual stale-token and restart-timeout protection tests are in
1552
- // the 'stale completion guard' and 'deferred restart' describe blocks
1553
- // above — this test simply validates that state is clean after reset.
1554
- __resetRecordingState();
1555
- expect(isRecordingIdle()).toBe(true);
1556
- expect(getActiveRestartToken()).toBeNull();
1557
-
1558
- // Start a recording, verify not idle
1559
- const { ctx } = createCtx();
1560
- const conversationId = "conv-fin-sanity";
1561
-
1562
- handleRecordingStart(conversationId, undefined, ctx);
1563
- expect(isRecordingIdle()).toBe(false);
1564
-
1565
- // Restart, verify token is set
1566
- handleRecordingRestart(conversationId, ctx);
1567
- expect(getActiveRestartToken()).not.toBeNull();
1568
-
1569
- // Reset everything, verify clean state
1570
- __resetRecordingState();
1571
- expect(isRecordingIdle()).toBe(true);
1572
- expect(getActiveRestartToken()).toBeNull();
1573
- });
1574
- });