@vellumai/assistant 0.3.4 → 0.3.6

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 (506) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +88 -2
  3. package/eslint.config.mjs +31 -0
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  6. package/scripts/ipc/generate-swift.ts +31 -2
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
  8. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  9. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  10. package/src/__tests__/approval-message-composer.test.ts +253 -0
  11. package/src/__tests__/browser-manager.test.ts +1 -0
  12. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  13. package/src/__tests__/call-domain.test.ts +12 -2
  14. package/src/__tests__/call-orchestrator.test.ts +799 -249
  15. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  16. package/src/__tests__/call-recovery.test.ts +3 -0
  17. package/src/__tests__/call-routes-http.test.ts +32 -2
  18. package/src/__tests__/call-store.test.ts +3 -0
  19. package/src/__tests__/channel-approval-routes.test.ts +1277 -98
  20. package/src/__tests__/channel-approval.test.ts +37 -0
  21. package/src/__tests__/channel-approvals.test.ts +36 -50
  22. package/src/__tests__/channel-guardian.test.ts +630 -22
  23. package/src/__tests__/channel-readiness-service.test.ts +324 -0
  24. package/src/__tests__/checker.test.ts +14 -7
  25. package/src/__tests__/clarification-resolver.test.ts +44 -24
  26. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  27. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  28. package/src/__tests__/config-schema.test.ts +14 -8
  29. package/src/__tests__/context-window-manager.test.ts +30 -2
  30. package/src/__tests__/contradiction-checker.test.ts +20 -5
  31. package/src/__tests__/credential-security-invariants.test.ts +7 -2
  32. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  33. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  34. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  35. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  36. package/src/__tests__/entity-search.test.ts +615 -0
  37. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  38. package/src/__tests__/guardian-action-store.test.ts +123 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  40. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  41. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  43. package/src/__tests__/handlers-twilio-config.test.ts +533 -0
  44. package/src/__tests__/intent-routing.test.ts +2 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +291 -1
  46. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  47. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  48. package/src/__tests__/model-intents.test.ts +96 -0
  49. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  50. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  51. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  52. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  53. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  54. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  55. package/src/__tests__/qdrant-manager.test.ts +27 -20
  56. package/src/__tests__/relay-server.test.ts +779 -40
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
  58. package/src/__tests__/run-orchestrator.test.ts +42 -4
  59. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  60. package/src/__tests__/runtime-runs.test.ts +16 -0
  61. package/src/__tests__/schedule-store.test.ts +18 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  63. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  64. package/src/__tests__/session-agent-loop.test.ts +857 -0
  65. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  66. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  67. package/src/__tests__/session-profile-injection.test.ts +6 -0
  68. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  69. package/src/__tests__/session-queue.test.ts +6 -0
  70. package/src/__tests__/session-runtime-assembly.test.ts +321 -13
  71. package/src/__tests__/session-slash-known.test.ts +6 -0
  72. package/src/__tests__/session-slash-queue.test.ts +6 -0
  73. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  74. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  75. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  76. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  77. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  78. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  80. package/src/__tests__/skills.test.ts +2 -0
  81. package/src/__tests__/sms-messaging-provider.test.ts +126 -0
  82. package/src/__tests__/starter-task-flow.test.ts +2 -0
  83. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  84. package/src/__tests__/system-prompt.test.ts +2 -0
  85. package/src/__tests__/task-management-tools.test.ts +2 -2
  86. package/src/__tests__/task-runner.test.ts +14 -4
  87. package/src/__tests__/terminal-tools.test.ts +25 -19
  88. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  90. package/src/__tests__/tool-executor.test.ts +23 -24
  91. package/src/__tests__/trust-store.test.ts +3 -3
  92. package/src/__tests__/twilio-rest.test.ts +29 -0
  93. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  94. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  95. package/src/__tests__/twilio-routes.test.ts +167 -11
  96. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  97. package/src/__tests__/user-reference.test.ts +2 -0
  98. package/src/__tests__/voice-quality.test.ts +222 -0
  99. package/src/__tests__/web-search.test.ts +46 -30
  100. package/src/__tests__/work-item-output.test.ts +110 -0
  101. package/src/agent/loop.ts +1 -1
  102. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  103. package/src/amazon/client.ts +1418 -0
  104. package/src/amazon/request-extractor.ts +135 -0
  105. package/src/amazon/session.ts +109 -0
  106. package/src/autonomy/autonomy-store.ts +5 -5
  107. package/src/browser-extension-relay/client.ts +124 -0
  108. package/src/browser-extension-relay/protocol.ts +63 -0
  109. package/src/browser-extension-relay/server.ts +177 -0
  110. package/src/bundler/app-bundler.ts +3 -3
  111. package/src/bundler/bundle-signer.ts +1 -1
  112. package/src/bundler/signature-verifier.ts +1 -1
  113. package/src/calls/call-conversation-messages.ts +33 -0
  114. package/src/calls/call-domain.ts +114 -10
  115. package/src/calls/call-orchestrator.ts +268 -59
  116. package/src/calls/call-pointer-messages.ts +53 -0
  117. package/src/calls/call-recovery.ts +3 -8
  118. package/src/calls/call-store.ts +69 -87
  119. package/src/calls/elevenlabs-config.ts +3 -2
  120. package/src/calls/guardian-action-sweep.ts +105 -0
  121. package/src/calls/guardian-dispatch.ts +203 -0
  122. package/src/calls/guardian-question-copy.ts +133 -0
  123. package/src/calls/relay-server.ts +466 -8
  124. package/src/calls/speaker-identification.ts +1 -1
  125. package/src/calls/twilio-config.ts +22 -14
  126. package/src/calls/twilio-provider.ts +6 -4
  127. package/src/calls/twilio-rest.ts +308 -7
  128. package/src/calls/twilio-routes.ts +65 -12
  129. package/src/calls/types.ts +3 -1
  130. package/src/channels/types.ts +25 -0
  131. package/src/cli/amazon.ts +815 -0
  132. package/src/cli/config-commands.ts +2 -2
  133. package/src/cli/core-commands.ts +4 -3
  134. package/src/cli/influencer.ts +244 -0
  135. package/src/cli/map.ts +89 -6
  136. package/src/cli.ts +1 -1
  137. package/src/config/agent-schema.ts +171 -0
  138. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  139. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  140. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  141. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  142. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  143. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  144. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  145. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  146. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  147. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  148. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  149. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  150. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  151. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  152. package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
  153. package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
  154. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  155. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  156. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  157. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  158. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  159. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  160. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  161. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
  162. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  163. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
  164. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
  165. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  166. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  167. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
  168. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  169. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
  170. package/src/config/bundled-skills/messaging/SKILL.md +33 -8
  171. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  172. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  174. package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
  175. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  176. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  177. package/src/config/bundled-tool-registry.ts +310 -0
  178. package/src/config/calls-schema.ts +181 -0
  179. package/src/config/core-schema.ts +309 -0
  180. package/src/config/defaults.ts +28 -3
  181. package/src/config/env-registry.ts +162 -0
  182. package/src/config/env.ts +175 -0
  183. package/src/config/loader.ts +6 -6
  184. package/src/config/memory-schema.ts +528 -0
  185. package/src/config/sandbox-schema.ts +55 -0
  186. package/src/config/schema.ts +158 -1133
  187. package/src/config/skill-state.ts +1 -1
  188. package/src/config/skills-schema.ts +32 -0
  189. package/src/config/skills.ts +35 -24
  190. package/src/config/system-prompt.ts +131 -56
  191. package/src/config/templates/IDENTITY.md +2 -2
  192. package/src/config/templates/SOUL.md +1 -1
  193. package/src/config/types.ts +1 -0
  194. package/src/config/user-reference.ts +4 -9
  195. package/src/config/vellum-skills/catalog.json +6 -7
  196. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  197. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
  198. package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
  199. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  200. package/src/context/window-manager.ts +27 -7
  201. package/src/daemon/approval-generators.ts +186 -0
  202. package/src/daemon/approved-devices-store.ts +140 -0
  203. package/src/daemon/assistant-attachments.ts +1 -1
  204. package/src/daemon/classifier.ts +35 -32
  205. package/src/daemon/config-watcher.ts +1 -1
  206. package/src/daemon/daemon-control.ts +217 -0
  207. package/src/daemon/handlers/apps.ts +2 -3
  208. package/src/daemon/handlers/config-channels.ts +158 -0
  209. package/src/daemon/handlers/config-inbox.ts +540 -0
  210. package/src/daemon/handlers/config-ingress.ts +231 -0
  211. package/src/daemon/handlers/config-integrations.ts +258 -0
  212. package/src/daemon/handlers/config-model.ts +143 -0
  213. package/src/daemon/handlers/config-parental.ts +163 -0
  214. package/src/daemon/handlers/config-scheduling.ts +172 -0
  215. package/src/daemon/handlers/config-slack.ts +92 -0
  216. package/src/daemon/handlers/config-telegram.ts +301 -0
  217. package/src/daemon/handlers/config-tools.ts +177 -0
  218. package/src/daemon/handlers/config-trust.ts +104 -0
  219. package/src/daemon/handlers/config-twilio.ts +1080 -0
  220. package/src/daemon/handlers/config.ts +53 -1689
  221. package/src/daemon/handlers/diagnostics.ts +1 -1
  222. package/src/daemon/handlers/dictation.ts +180 -0
  223. package/src/daemon/handlers/documents.ts +18 -32
  224. package/src/daemon/handlers/identity.ts +14 -23
  225. package/src/daemon/handlers/index.ts +11 -0
  226. package/src/daemon/handlers/misc.ts +3 -5
  227. package/src/daemon/handlers/pairing.ts +98 -0
  228. package/src/daemon/handlers/sessions.ts +56 -5
  229. package/src/daemon/handlers/shared.ts +6 -1
  230. package/src/daemon/handlers/skills.ts +1 -1
  231. package/src/daemon/handlers/twitter-auth.ts +2 -0
  232. package/src/daemon/handlers/work-items.ts +17 -9
  233. package/src/daemon/handlers/workspace-files.ts +4 -3
  234. package/src/daemon/install-cli-launchers.ts +113 -0
  235. package/src/daemon/ipc-contract/apps.ts +356 -0
  236. package/src/daemon/ipc-contract/browser.ts +74 -0
  237. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  238. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  239. package/src/daemon/ipc-contract/documents.ts +74 -0
  240. package/src/daemon/ipc-contract/inbox.ts +209 -0
  241. package/src/daemon/ipc-contract/integrations.ts +284 -0
  242. package/src/daemon/ipc-contract/memory.ts +48 -0
  243. package/src/daemon/ipc-contract/messages.ts +211 -0
  244. package/src/daemon/ipc-contract/pairing.ts +45 -0
  245. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  246. package/src/daemon/ipc-contract/schedules.ts +97 -0
  247. package/src/daemon/ipc-contract/sessions.ts +315 -0
  248. package/src/daemon/ipc-contract/shared.ts +42 -0
  249. package/src/daemon/ipc-contract/skills.ts +120 -0
  250. package/src/daemon/ipc-contract/subagents.ts +58 -0
  251. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  252. package/src/daemon/ipc-contract/trust.ts +60 -0
  253. package/src/daemon/ipc-contract/work-items.ts +225 -0
  254. package/src/daemon/ipc-contract/workspace.ts +113 -0
  255. package/src/daemon/ipc-contract-inventory.json +70 -0
  256. package/src/daemon/ipc-contract-inventory.ts +55 -29
  257. package/src/daemon/ipc-contract.ts +229 -2426
  258. package/src/daemon/ipc-protocol.ts +1 -1
  259. package/src/daemon/ipc-validate.ts +7 -0
  260. package/src/daemon/lifecycle.ts +97 -377
  261. package/src/daemon/pairing-store.ts +177 -0
  262. package/src/daemon/providers-setup.ts +43 -0
  263. package/src/daemon/ride-shotgun-handler.ts +68 -3
  264. package/src/daemon/server.ts +66 -46
  265. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  266. package/src/daemon/session-agent-loop.ts +117 -275
  267. package/src/daemon/session-dynamic-profile.ts +1 -1
  268. package/src/daemon/session-history.ts +1 -1
  269. package/src/daemon/session-media-retry.ts +1 -1
  270. package/src/daemon/session-messaging.ts +37 -2
  271. package/src/daemon/session-notifiers.ts +5 -25
  272. package/src/daemon/session-process.ts +99 -59
  273. package/src/daemon/session-queue-manager.ts +96 -4
  274. package/src/daemon/session-runtime-assembly.ts +199 -10
  275. package/src/daemon/session-surfaces.ts +19 -4
  276. package/src/daemon/session-tool-setup.ts +30 -30
  277. package/src/daemon/session-workspace.ts +1 -1
  278. package/src/daemon/session.ts +35 -2
  279. package/src/daemon/shutdown-handlers.ts +122 -0
  280. package/src/daemon/trace-emitter.ts +1 -1
  281. package/src/daemon/watch-handler.ts +36 -33
  282. package/src/doordash/cart-queries.ts +787 -0
  283. package/src/doordash/client.ts +144 -127
  284. package/src/doordash/order-queries.ts +85 -0
  285. package/src/doordash/queries.ts +10 -1308
  286. package/src/doordash/search-queries.ts +203 -0
  287. package/src/doordash/session.ts +3 -2
  288. package/src/doordash/store-queries.ts +246 -0
  289. package/src/doordash/types.ts +367 -0
  290. package/src/email/providers/agentmail.ts +2 -1
  291. package/src/email/providers/index.ts +3 -2
  292. package/src/email/service.ts +3 -2
  293. package/src/errors.ts +43 -0
  294. package/src/home-base/prebuilt/seed.ts +1 -1
  295. package/src/hooks/cli.ts +6 -5
  296. package/src/hooks/config.ts +6 -8
  297. package/src/hooks/discovery.ts +6 -5
  298. package/src/hooks/manager.ts +4 -3
  299. package/src/hooks/runner.ts +2 -2
  300. package/src/hooks/templates.ts +5 -5
  301. package/src/inbound/public-ingress-urls.ts +6 -4
  302. package/src/index.ts +4 -2
  303. package/src/influencer/client.ts +1104 -0
  304. package/src/instrument.ts +4 -3
  305. package/src/logfire.ts +4 -3
  306. package/src/memory/admin.ts +25 -35
  307. package/src/memory/attachments-store.ts +4 -7
  308. package/src/memory/channel-delivery-store.ts +30 -1
  309. package/src/memory/channel-guardian-store.ts +202 -2
  310. package/src/memory/clarification-resolver.ts +37 -33
  311. package/src/memory/conflict-store.ts +67 -61
  312. package/src/memory/contradiction-checker.ts +141 -117
  313. package/src/memory/conversation-store.ts +335 -51
  314. package/src/memory/db-connection.ts +27 -4
  315. package/src/memory/db-init.ts +265 -4
  316. package/src/memory/db.ts +14 -1
  317. package/src/memory/embedding-backend.ts +27 -5
  318. package/src/memory/embedding-ollama.ts +2 -1
  319. package/src/memory/entity-extractor.ts +38 -35
  320. package/src/memory/guardian-action-store.ts +430 -0
  321. package/src/memory/inbox-escalation-projection.ts +59 -0
  322. package/src/memory/inbox-thread-store.ts +218 -0
  323. package/src/memory/ingress-invite-store.ts +338 -0
  324. package/src/memory/ingress-member-store.ts +350 -0
  325. package/src/memory/items-extractor.ts +91 -97
  326. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  327. package/src/memory/job-handlers/media-processing.ts +69 -0
  328. package/src/memory/job-handlers/summarization.ts +32 -26
  329. package/src/memory/job-utils.ts +3 -10
  330. package/src/memory/jobs-store.ts +8 -10
  331. package/src/memory/jobs-worker.ts +55 -36
  332. package/src/memory/media-store.ts +759 -0
  333. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  334. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  335. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  336. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  337. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  338. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  339. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  340. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  341. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  342. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  343. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  344. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  345. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  346. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  347. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  348. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  349. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  350. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  351. package/src/memory/migrations/index.ts +24 -0
  352. package/src/memory/migrations/registry.ts +79 -0
  353. package/src/memory/migrations/validate-migration-state.ts +69 -0
  354. package/src/memory/qdrant-manager.ts +49 -8
  355. package/src/memory/query-builder.ts +1 -1
  356. package/src/memory/raw-query.ts +119 -0
  357. package/src/memory/recall-cache.ts +4 -1
  358. package/src/memory/retriever.ts +165 -47
  359. package/src/memory/schema-migration.ts +25 -984
  360. package/src/memory/schema.ts +228 -7
  361. package/src/memory/search/entity.ts +205 -31
  362. package/src/memory/search/lexical.ts +81 -52
  363. package/src/memory/search/ranking.ts +27 -23
  364. package/src/memory/search/semantic.ts +157 -19
  365. package/src/memory/search/types.ts +24 -0
  366. package/src/memory/shared-app-links-store.ts +4 -5
  367. package/src/memory/validation.ts +19 -0
  368. package/src/messaging/draft-store.ts +5 -6
  369. package/src/messaging/provider-types.ts +2 -0
  370. package/src/messaging/providers/sms/adapter.ts +201 -0
  371. package/src/messaging/providers/sms/client.ts +93 -0
  372. package/src/messaging/providers/sms/types.ts +7 -0
  373. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  374. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  375. package/src/messaging/providers/whatsapp/client.ts +67 -0
  376. package/src/messaging/style-analyzer.ts +5 -4
  377. package/src/messaging/thread-summarizer.ts +61 -69
  378. package/src/messaging/triage-engine.ts +62 -71
  379. package/src/migrations/config-merge.ts +53 -0
  380. package/src/migrations/data-layout.ts +68 -0
  381. package/src/migrations/data-merge.ts +33 -0
  382. package/src/migrations/hooks-merge.ts +90 -0
  383. package/src/migrations/index.ts +6 -0
  384. package/src/migrations/log.ts +23 -0
  385. package/src/migrations/skills-merge.ts +33 -0
  386. package/src/migrations/workspace-layout.ts +79 -0
  387. package/src/permissions/checker.ts +133 -11
  388. package/src/permissions/prompter.ts +14 -0
  389. package/src/permissions/shell-identity.ts +31 -1
  390. package/src/permissions/trust-store.ts +21 -1
  391. package/src/providers/anthropic/client.ts +4 -4
  392. package/src/providers/failover.ts +2 -2
  393. package/src/providers/model-intents.ts +70 -0
  394. package/src/providers/ollama/client.ts +2 -1
  395. package/src/providers/provider-send-message.ts +176 -0
  396. package/src/providers/registry.ts +71 -30
  397. package/src/providers/retry.ts +35 -1
  398. package/src/providers/types.ts +12 -1
  399. package/src/runtime/approval-conversation-turn.ts +97 -0
  400. package/src/runtime/approval-message-composer.ts +253 -0
  401. package/src/runtime/channel-approval-parser.ts +36 -2
  402. package/src/runtime/channel-approvals.ts +11 -24
  403. package/src/runtime/channel-guardian-service.ts +88 -21
  404. package/src/runtime/channel-readiness-service.ts +418 -0
  405. package/src/runtime/channel-readiness-types.ts +35 -0
  406. package/src/runtime/channel-retry-sweep.ts +184 -0
  407. package/src/runtime/guardian-context-resolver.ts +108 -0
  408. package/src/runtime/http-server.ts +275 -717
  409. package/src/runtime/http-types.ts +59 -3
  410. package/src/runtime/middleware/auth.ts +116 -0
  411. package/src/runtime/middleware/error-handler.ts +33 -0
  412. package/src/runtime/middleware/twilio-validation.ts +127 -0
  413. package/src/runtime/routes/app-routes.ts +1 -1
  414. package/src/runtime/routes/call-routes.ts +51 -7
  415. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  416. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  417. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  418. package/src/runtime/routes/channel-route-shared.ts +144 -0
  419. package/src/runtime/routes/channel-routes.ts +32 -1588
  420. package/src/runtime/routes/conversation-routes.ts +50 -7
  421. package/src/runtime/routes/events-routes.ts +2 -2
  422. package/src/runtime/routes/identity-routes.ts +126 -0
  423. package/src/runtime/routes/pairing-routes.ts +143 -0
  424. package/src/runtime/routes/run-routes.ts +15 -1
  425. package/src/runtime/run-orchestrator.ts +86 -35
  426. package/src/schedule/schedule-store.ts +36 -32
  427. package/src/schedule/scheduler.ts +3 -3
  428. package/src/security/encrypted-store.ts +5 -7
  429. package/src/security/oauth2.ts +45 -15
  430. package/src/security/parental-control-store.ts +183 -0
  431. package/src/security/secret-allowlist.ts +4 -3
  432. package/src/security/secret-scanner.ts +5 -5
  433. package/src/security/secure-keys.ts +1 -1
  434. package/src/security/token-manager.ts +3 -2
  435. package/src/services/vercel-deploy.ts +6 -2
  436. package/src/skills/tool-manifest.ts +3 -3
  437. package/src/skills/vellum-catalog-remote.ts +75 -16
  438. package/src/slack/slack-webhook.ts +2 -1
  439. package/src/swarm/orchestrator.ts +92 -1
  440. package/src/swarm/router-planner.ts +6 -9
  441. package/src/swarm/worker-prompts.ts +9 -12
  442. package/src/tasks/task-compiler.ts +19 -28
  443. package/src/tasks/task-runner.ts +1 -1
  444. package/src/tools/assets/materialize.ts +2 -2
  445. package/src/tools/assets/search.ts +15 -14
  446. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  447. package/src/tools/browser/auto-navigate.ts +1 -0
  448. package/src/tools/browser/browser-execution.ts +10 -1
  449. package/src/tools/browser/browser-manager.ts +119 -4
  450. package/src/tools/browser/network-recorder.ts +5 -0
  451. package/src/tools/calls/call-start.ts +1 -0
  452. package/src/tools/credentials/broker.ts +11 -2
  453. package/src/tools/credentials/metadata-store.ts +18 -14
  454. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  455. package/src/tools/credentials/vault.ts +49 -23
  456. package/src/tools/execution-target.ts +11 -1
  457. package/src/tools/executor.ts +68 -9
  458. package/src/tools/host-terminal/cli-discover.ts +1 -1
  459. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  460. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  461. package/src/tools/network/script-proxy/server.ts +1 -1
  462. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  463. package/src/tools/network/web-fetch.ts +18 -2
  464. package/src/tools/network/web-search.ts +8 -4
  465. package/src/tools/reminder/reminder-store.ts +14 -15
  466. package/src/tools/schedule/create.ts +1 -0
  467. package/src/tools/schedule/list.ts +2 -1
  468. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  469. package/src/tools/skills/skill-script-runner.ts +24 -9
  470. package/src/tools/skills/skill-tool-factory.ts +1 -0
  471. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  472. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  473. package/src/tools/terminal/parser.ts +50 -0
  474. package/src/tools/types.ts +2 -0
  475. package/src/tools/watcher/delete.ts +6 -0
  476. package/src/tools/weather/service.ts +1 -1
  477. package/src/twitter/client.ts +190 -24
  478. package/src/twitter/router.ts +1 -1
  479. package/src/twitter/session.ts +4 -3
  480. package/src/util/clipboard.ts +1 -1
  481. package/src/util/errors.ts +65 -8
  482. package/src/util/fs.ts +40 -0
  483. package/src/util/json.ts +10 -0
  484. package/src/util/log-redact.ts +189 -0
  485. package/src/util/logger.ts +19 -17
  486. package/src/util/object.ts +3 -0
  487. package/src/util/platform.ts +105 -363
  488. package/src/util/pricing.ts +1 -1
  489. package/src/util/promise-guard.ts +1 -1
  490. package/src/util/retry.ts +19 -0
  491. package/src/util/row-mapper.ts +79 -0
  492. package/src/util/silently.ts +21 -0
  493. package/src/watcher/engine.ts +5 -1
  494. package/src/watcher/provider-types.ts +20 -0
  495. package/src/watcher/providers/github.ts +156 -0
  496. package/src/watcher/providers/gmail.ts +1 -0
  497. package/src/watcher/providers/google-calendar.ts +1 -0
  498. package/src/watcher/providers/linear.ts +460 -0
  499. package/src/watcher/providers/slack.ts +1 -0
  500. package/src/work-items/work-item-runner.ts +1 -1
  501. package/src/workspace/git-service.ts +1 -1
  502. package/src/workspace/provider-commit-message-generator.ts +51 -22
  503. package/src/__tests__/call-bridge.test.ts +0 -517
  504. package/src/__tests__/session-process-bridge.test.ts +0 -244
  505. package/src/calls/call-bridge.ts +0 -168
  506. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -0,0 +1,551 @@
1
+ import { join, dirname } from 'node:path';
2
+ import { mkdir, readdir, rename, rm, writeFile, readFile } from 'node:fs/promises';
3
+ import { randomUUID } from 'node:crypto';
4
+ import {
5
+ getMediaAssetById,
6
+ insertKeyframesBatch,
7
+ deleteKeyframesForAsset,
8
+ createProcessingStage,
9
+ updateProcessingStage,
10
+ getProcessingStagesForAsset,
11
+ type ProcessingStage,
12
+ } from '../../../../memory/media-store.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const FFMPEG_TIMEOUT_MS = 600_000;
19
+ const MIN_FRAMES_PER_SEGMENT = 4;
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface PreprocessConfig {
26
+ intervalSeconds: number;
27
+ shortEdge: number;
28
+ deadTimeThreshold: number;
29
+ segmentDuration: number;
30
+ }
31
+
32
+ export interface TimeRange {
33
+ start: number;
34
+ end: number;
35
+ }
36
+
37
+ export interface Segment {
38
+ id: string;
39
+ startSeconds: number;
40
+ endSeconds: number;
41
+ framePaths: string[];
42
+ frameTimestamps: number[];
43
+ }
44
+
45
+ export interface SubjectGroup {
46
+ label: string;
47
+ dominantColor: string;
48
+ identifiers: string[];
49
+ }
50
+
51
+ export interface SubjectRegistry {
52
+ groups: SubjectGroup[];
53
+ }
54
+
55
+ export interface SectionBoundary {
56
+ label: string;
57
+ startSeconds: number;
58
+ endSeconds: number;
59
+ }
60
+
61
+ export interface PreprocessManifest {
62
+ assetId: string;
63
+ videoPath: string;
64
+ durationSeconds: number;
65
+ segments: Segment[];
66
+ deadTimeRanges: TimeRange[];
67
+ subjectRegistry: SubjectRegistry;
68
+ sectionBoundaries: SectionBoundary[];
69
+ config: PreprocessConfig;
70
+ }
71
+
72
+ export interface PreprocessOptions {
73
+ intervalSeconds?: number;
74
+ segmentDuration?: number;
75
+ deadTimeThreshold?: number;
76
+ sectionConfigPath?: string;
77
+ skipDeadTime?: boolean;
78
+ shortEdge?: number;
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Spawn helper (same pattern as the existing extract-keyframes)
83
+ // ---------------------------------------------------------------------------
84
+
85
+ function spawnWithTimeout(
86
+ cmd: string[],
87
+ timeoutMs: number,
88
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
89
+ return new Promise((resolve, reject) => {
90
+ const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe' });
91
+ const timer = setTimeout(() => {
92
+ proc.kill();
93
+ reject(new Error(`Process timed out after ${timeoutMs}ms: ${cmd[0]}`));
94
+ }, timeoutMs);
95
+ proc.exited.then(async (exitCode) => {
96
+ clearTimeout(timer);
97
+ const stdout = await new Response(proc.stdout).text();
98
+ const stderr = await new Response(proc.stderr).text();
99
+ resolve({ exitCode, stdout, stderr });
100
+ });
101
+ });
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Pure functions (exported for testing)
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Parse mpdecimate filter stderr output to extract dropped frame timestamps.
110
+ * mpdecimate logs lines like:
111
+ * pts_time:12.34 ... drop
112
+ * We collect the pts_time values of dropped frames.
113
+ */
114
+ export function parseDroppedFrameTimestamps(stderr: string): number[] {
115
+ const timestamps: number[] = [];
116
+ const lines = stderr.split('\n');
117
+ for (const line of lines) {
118
+ // mpdecimate marks dropped frames with "drop" at the end of the line
119
+ if (!line.includes('drop')) continue;
120
+ const match = line.match(/pts_time:([\d.]+)/);
121
+ if (match) {
122
+ timestamps.push(parseFloat(match[1]));
123
+ }
124
+ }
125
+ return timestamps.sort((a, b) => a - b);
126
+ }
127
+
128
+ /**
129
+ * Build dead-time ranges from dropped frame timestamps.
130
+ * Groups consecutive dropped frames and merges ranges where the gap
131
+ * between dropped frames is small (within gapThresholdSeconds).
132
+ * Only ranges longer than minDurationSeconds are kept.
133
+ */
134
+ export function buildDeadTimeRanges(
135
+ droppedTimestamps: number[],
136
+ gapThresholdSeconds: number = 1.0,
137
+ minDurationSeconds: number = 5.0,
138
+ ): TimeRange[] {
139
+ if (droppedTimestamps.length === 0) return [];
140
+
141
+ const ranges: TimeRange[] = [];
142
+ let rangeStart = droppedTimestamps[0];
143
+ let rangeEnd = droppedTimestamps[0];
144
+
145
+ for (let i = 1; i < droppedTimestamps.length; i++) {
146
+ const ts = droppedTimestamps[i];
147
+ if (ts - rangeEnd <= gapThresholdSeconds) {
148
+ // Extend current range
149
+ rangeEnd = ts;
150
+ } else {
151
+ // Close current range if long enough
152
+ if (rangeEnd - rangeStart >= minDurationSeconds) {
153
+ ranges.push({ start: rangeStart, end: rangeEnd });
154
+ }
155
+ rangeStart = ts;
156
+ rangeEnd = ts;
157
+ }
158
+ }
159
+
160
+ // Close final range
161
+ if (rangeEnd - rangeStart >= minDurationSeconds) {
162
+ ranges.push({ start: rangeStart, end: rangeEnd });
163
+ }
164
+
165
+ return ranges;
166
+ }
167
+
168
+ /**
169
+ * Compute live time ranges by subtracting dead-time ranges from the total duration.
170
+ */
171
+ export function computeLiveRanges(
172
+ durationSeconds: number,
173
+ deadTimeRanges: TimeRange[],
174
+ ): TimeRange[] {
175
+ if (deadTimeRanges.length === 0) {
176
+ return [{ start: 0, end: durationSeconds }];
177
+ }
178
+
179
+ const sorted = [...deadTimeRanges].sort((a, b) => a.start - b.start);
180
+ const live: TimeRange[] = [];
181
+ let cursor = 0;
182
+
183
+ for (const dead of sorted) {
184
+ if (dead.start > cursor) {
185
+ live.push({ start: cursor, end: dead.start });
186
+ }
187
+ cursor = Math.max(cursor, dead.end);
188
+ }
189
+
190
+ if (cursor < durationSeconds) {
191
+ live.push({ start: cursor, end: durationSeconds });
192
+ }
193
+
194
+ return live;
195
+ }
196
+
197
+ /**
198
+ * Create non-overlapping segments of a given duration within live time ranges.
199
+ */
200
+ export function createSegments(
201
+ liveRanges: TimeRange[],
202
+ segmentDuration: number,
203
+ ): Array<{ id: string; startSeconds: number; endSeconds: number }> {
204
+ if (segmentDuration <= 0) {
205
+ throw new Error(`segmentDuration must be positive, got ${segmentDuration}`);
206
+ }
207
+
208
+ const segments: Array<{ id: string; startSeconds: number; endSeconds: number }> = [];
209
+ let segIndex = 0;
210
+
211
+ for (const range of liveRanges) {
212
+ let cursor = range.start;
213
+ while (cursor < range.end) {
214
+ const end = Math.min(cursor + segmentDuration, range.end);
215
+ segIndex++;
216
+ const id = `seg-${String(segIndex).padStart(3, '0')}`;
217
+ segments.push({ id, startSeconds: cursor, endSeconds: end });
218
+ cursor = end;
219
+ }
220
+ }
221
+
222
+ return segments;
223
+ }
224
+
225
+ /**
226
+ * Compute the effective frame extraction interval for a segment,
227
+ * ensuring at least MIN_FRAMES_PER_SEGMENT frames are produced.
228
+ */
229
+ export function computeEffectiveInterval(
230
+ segmentDurationSeconds: number,
231
+ requestedInterval: number,
232
+ ): number {
233
+ const framesAtRequestedInterval = Math.floor(segmentDurationSeconds / requestedInterval);
234
+ if (framesAtRequestedInterval >= MIN_FRAMES_PER_SEGMENT) {
235
+ return requestedInterval;
236
+ }
237
+ // Reduce interval to guarantee minimum frame count
238
+ return segmentDurationSeconds / MIN_FRAMES_PER_SEGMENT;
239
+ }
240
+
241
+ /**
242
+ * Generate frame timestamps for a segment based on effective interval.
243
+ */
244
+ export function generateFrameTimestamps(
245
+ segStart: number,
246
+ segEnd: number,
247
+ interval: number,
248
+ ): number[] {
249
+ const effectiveInterval = computeEffectiveInterval(segEnd - segStart, interval);
250
+ const timestamps: number[] = [];
251
+ let t = segStart;
252
+ while (t < segEnd) {
253
+ timestamps.push(parseFloat(t.toFixed(3)));
254
+ t += effectiveInterval;
255
+ }
256
+ return timestamps;
257
+ }
258
+
259
+ /**
260
+ * Create default section boundaries by splitting the duration into equal halves.
261
+ */
262
+ export function createDefaultSections(durationSeconds: number): SectionBoundary[] {
263
+ const mid = durationSeconds / 2;
264
+ return [
265
+ { label: 'section_1', startSeconds: 0, endSeconds: mid },
266
+ { label: 'section_2', startSeconds: mid, endSeconds: durationSeconds },
267
+ ];
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Subject registry (color sampling)
272
+ // ---------------------------------------------------------------------------
273
+
274
+ /**
275
+ * Sample ~10 frames evenly spread across the video for subject registry analysis.
276
+ * Returns indices into the total frame set.
277
+ */
278
+ export function sampleFrameIndices(totalFrames: number, sampleCount: number = 10): number[] {
279
+ if (totalFrames <= sampleCount) {
280
+ return Array.from({ length: totalFrames }, (_, i) => i);
281
+ }
282
+ const step = totalFrames / sampleCount;
283
+ const indices: number[] = [];
284
+ for (let i = 0; i < sampleCount; i++) {
285
+ indices.push(Math.min(Math.floor(i * step), totalFrames - 1));
286
+ }
287
+ return indices;
288
+ }
289
+
290
+ /**
291
+ * Extract dominant colors from a frame image using ffmpeg's thumbnail and showinfo filters.
292
+ * Returns hex color strings.
293
+ */
294
+ async function extractDominantColors(framePath: string): Promise<string[]> {
295
+ // Use ffmpeg to extract a palette from the frame
296
+ const result = await spawnWithTimeout([
297
+ 'ffmpeg', '-i', framePath,
298
+ '-vf', 'palettegen=max_colors=4:stats_mode=diff',
299
+ '-f', 'null', '-',
300
+ ], 30_000);
301
+
302
+ // Fallback: return empty if analysis fails
303
+ if (result.exitCode !== 0) return [];
304
+
305
+ // Parse palette info from stderr — look for color hex values
306
+ const colors: string[] = [];
307
+ const colorMatches = result.stderr.matchAll(/0x([0-9a-fA-F]{6})/g);
308
+ for (const m of colorMatches) {
309
+ colors.push(`#${m[1].toLowerCase()}`);
310
+ }
311
+ return colors.length > 0 ? colors.slice(0, 4) : [];
312
+ }
313
+
314
+ /**
315
+ * Build a subject registry by sampling frames and detecting dominant non-background colors.
316
+ */
317
+ async function buildSubjectRegistry(framePaths: string[]): Promise<SubjectRegistry> {
318
+ if (framePaths.length === 0) {
319
+ return { groups: [] };
320
+ }
321
+
322
+ const indices = sampleFrameIndices(framePaths.length);
323
+ const sampledPaths = indices.map((i) => framePaths[i]).filter(Boolean);
324
+
325
+ // Collect all dominant colors across sampled frames
326
+ const colorCounts = new Map<string, number>();
327
+ for (const path of sampledPaths) {
328
+ const colors = await extractDominantColors(path);
329
+ for (const c of colors) {
330
+ colorCounts.set(c, (colorCounts.get(c) || 0) + 1);
331
+ }
332
+ }
333
+
334
+ // Sort by frequency and pick top groups (skip very common colors likely to be court/background)
335
+ const sorted = [...colorCounts.entries()]
336
+ .sort((a, b) => b[1] - a[1])
337
+ .slice(0, 4);
338
+
339
+ const groups: SubjectGroup[] = sorted.map((entry, i) => ({
340
+ label: `group_${String.fromCharCode(97 + i)}`,
341
+ dominantColor: entry[0],
342
+ identifiers: [],
343
+ }));
344
+
345
+ return { groups };
346
+ }
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // Main preprocess function
350
+ // ---------------------------------------------------------------------------
351
+
352
+ export async function preprocessForAsset(
353
+ assetId: string,
354
+ options: PreprocessOptions = {},
355
+ onProgress?: (msg: string) => void,
356
+ ): Promise<PreprocessManifest> {
357
+ const config: PreprocessConfig = {
358
+ intervalSeconds: options.intervalSeconds ?? 3,
359
+ segmentDuration: options.segmentDuration ?? 20,
360
+ deadTimeThreshold: options.deadTimeThreshold ?? 0.02,
361
+ shortEdge: options.shortEdge ?? 480,
362
+ };
363
+
364
+ const skipDeadTime = options.skipDeadTime ?? true;
365
+
366
+ const asset = getMediaAssetById(assetId);
367
+ if (!asset) {
368
+ throw new Error(`Media asset not found: ${assetId}`);
369
+ }
370
+ if (asset.mediaType !== 'video') {
371
+ throw new Error(`Preprocess requires a video asset. Got: ${asset.mediaType}`);
372
+ }
373
+
374
+ const durationSeconds = asset.durationSeconds ?? 0;
375
+ if (durationSeconds <= 0) {
376
+ throw new Error('Video asset has no duration information.');
377
+ }
378
+
379
+ // Find or create processing stage
380
+ let stage: ProcessingStage | undefined;
381
+ const existingStages = getProcessingStagesForAsset(assetId);
382
+ stage = existingStages.find((s) => s.stage === 'preprocess');
383
+ if (!stage) {
384
+ stage = createProcessingStage({ assetId, stage: 'preprocess' });
385
+ }
386
+ updateProcessingStage(stage.id, { status: 'running', startedAt: Date.now() });
387
+
388
+ const pipelineDir = join(dirname(asset.filePath), 'pipeline', assetId);
389
+ const framesDir = join(pipelineDir, 'frames');
390
+ const tempDir = framesDir + '-tmp-' + randomUUID();
391
+ await mkdir(tempDir, { recursive: true });
392
+
393
+ try {
394
+ // Step 1: Dead-time detection
395
+ onProgress?.('Detecting dead time with mpdecimate filter...\n');
396
+ let deadTimeRanges: TimeRange[] = [];
397
+
398
+ if (skipDeadTime) {
399
+ const mpdecimateResult = await spawnWithTimeout([
400
+ 'ffmpeg', '-i', asset.filePath,
401
+ '-vf', `mpdecimate=hi=64*${config.deadTimeThreshold}:lo=64*${config.deadTimeThreshold}:frac=1`,
402
+ '-loglevel', 'debug',
403
+ '-f', 'null', '-',
404
+ ], FFMPEG_TIMEOUT_MS);
405
+
406
+ const droppedTimestamps = parseDroppedFrameTimestamps(mpdecimateResult.stderr);
407
+ deadTimeRanges = buildDeadTimeRanges(droppedTimestamps);
408
+ onProgress?.(`Found ${deadTimeRanges.length} dead-time range(s).\n`);
409
+ }
410
+
411
+ // Step 2: Segmentation
412
+ onProgress?.('Creating segments...\n');
413
+ const liveRanges = computeLiveRanges(durationSeconds, deadTimeRanges);
414
+ const rawSegments = createSegments(liveRanges, config.segmentDuration);
415
+ onProgress?.(`Created ${rawSegments.length} segment(s) from ${liveRanges.length} live range(s).\n`);
416
+
417
+ // Step 3: Frame extraction per segment
418
+ onProgress?.('Extracting frames per segment...\n');
419
+ const segments: Segment[] = [];
420
+ const allFramePaths: string[] = [];
421
+
422
+ const scaleFilter = `scale='if(gt(iw,ih),-1,${config.shortEdge})':'if(gt(iw,ih),${config.shortEdge},-1)'`;
423
+
424
+ for (const seg of rawSegments) {
425
+ const segDuration = seg.endSeconds - seg.startSeconds;
426
+ const effectiveInterval = computeEffectiveInterval(segDuration, config.intervalSeconds);
427
+ const _frameTimestamps = generateFrameTimestamps(seg.startSeconds, seg.endSeconds, config.intervalSeconds);
428
+
429
+ const segTempDir = join(tempDir, seg.id);
430
+ await mkdir(segTempDir, { recursive: true });
431
+
432
+ const vfFilter = `fps=1/${effectiveInterval},${scaleFilter}`;
433
+
434
+ const result = await spawnWithTimeout([
435
+ 'ffmpeg', '-y',
436
+ '-ss', String(seg.startSeconds),
437
+ '-t', String(segDuration),
438
+ '-i', asset.filePath,
439
+ '-vf', vfFilter,
440
+ '-q:v', '2',
441
+ join(segTempDir, 'frame-%06d.jpg'),
442
+ ], FFMPEG_TIMEOUT_MS);
443
+
444
+ if (result.exitCode !== 0) {
445
+ onProgress?.(`Warning: frame extraction failed for ${seg.id}: ${result.stderr.slice(0, 200)}\n`);
446
+ continue;
447
+ }
448
+
449
+ const files = await readdir(segTempDir);
450
+ const frameFiles = files
451
+ .filter((f) => f.startsWith('frame-') && f.endsWith('.jpg'))
452
+ .sort();
453
+
454
+ const framePaths = frameFiles.map((f) => join(framesDir, seg.id, f));
455
+ allFramePaths.push(...framePaths.map((_, i) => join(segTempDir, frameFiles[i])));
456
+
457
+ // Use actual extracted frame count to recalculate timestamps
458
+ const actualTimestamps = frameFiles.map((_, i) =>
459
+ parseFloat((seg.startSeconds + i * effectiveInterval).toFixed(3))
460
+ );
461
+
462
+ segments.push({
463
+ id: seg.id,
464
+ startSeconds: seg.startSeconds,
465
+ endSeconds: seg.endSeconds,
466
+ framePaths,
467
+ frameTimestamps: actualTimestamps,
468
+ });
469
+ }
470
+
471
+ const totalFrames = segments.reduce((sum, s) => sum + s.framePaths.length, 0);
472
+ if (rawSegments.length > 0 && totalFrames === 0) {
473
+ throw new Error(
474
+ `All ${rawSegments.length} segment(s) failed frame extraction — zero usable frames produced.`,
475
+ );
476
+ }
477
+ onProgress?.(`Extracted ${totalFrames} total frames across ${segments.length} segments.\n`);
478
+
479
+ // Atomically swap temp dir to durable path
480
+ await rm(framesDir, { recursive: true, force: true });
481
+ await mkdir(dirname(framesDir), { recursive: true });
482
+ await rename(tempDir, framesDir);
483
+
484
+ // Step 4: Subject registry
485
+ onProgress?.('Building subject registry...\n');
486
+ const allExtractedPaths = segments.flatMap((s) => s.framePaths);
487
+ const subjectRegistry = await buildSubjectRegistry(allExtractedPaths);
488
+ onProgress?.(`Identified ${subjectRegistry.groups.length} subject group(s).\n`);
489
+
490
+ // Step 5: Section boundaries
491
+ let sectionBoundaries: SectionBoundary[];
492
+ if (options.sectionConfigPath) {
493
+ const raw = await readFile(options.sectionConfigPath, 'utf-8');
494
+ sectionBoundaries = JSON.parse(raw) as SectionBoundary[];
495
+ } else {
496
+ sectionBoundaries = createDefaultSections(durationSeconds);
497
+ }
498
+
499
+ // Step 6: Register keyframes in DB
500
+ onProgress?.('Registering keyframes in database...\n');
501
+ deleteKeyframesForAsset(assetId);
502
+
503
+ const keyframeRows = segments.flatMap((seg) =>
504
+ seg.framePaths.map((fp, i) => ({
505
+ assetId,
506
+ timestamp: seg.frameTimestamps[i] ?? seg.startSeconds + i * config.intervalSeconds,
507
+ filePath: fp,
508
+ metadata: { segmentId: seg.id, frameIndex: i, intervalSeconds: config.intervalSeconds },
509
+ }))
510
+ );
511
+
512
+ if (keyframeRows.length > 0) {
513
+ insertKeyframesBatch(keyframeRows);
514
+ }
515
+
516
+ // Step 7: Write manifest
517
+ const manifest: PreprocessManifest = {
518
+ assetId,
519
+ videoPath: asset.filePath,
520
+ durationSeconds,
521
+ segments,
522
+ deadTimeRanges,
523
+ subjectRegistry,
524
+ sectionBoundaries,
525
+ config,
526
+ };
527
+
528
+ const manifestPath = join(pipelineDir, 'manifest.json');
529
+ await mkdir(pipelineDir, { recursive: true });
530
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
531
+
532
+ // Update stage
533
+ updateProcessingStage(stage.id, {
534
+ status: 'completed',
535
+ progress: 100,
536
+ completedAt: Date.now(),
537
+ });
538
+
539
+ onProgress?.(`Preprocess complete. Manifest written to ${manifestPath}\n`);
540
+
541
+ return manifest;
542
+ } catch (err) {
543
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {});
544
+ const msg = (err as Error).message;
545
+ updateProcessingStage(stage.id, {
546
+ status: 'failed',
547
+ lastError: msg.slice(0, 500),
548
+ });
549
+ throw err;
550
+ }
551
+ }