@vellumai/assistant 0.6.4 → 0.6.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 (1008) hide show
  1. package/.prettierignore +5 -0
  2. package/AGENTS.md +9 -1
  3. package/ARCHITECTURE.md +43 -49
  4. package/Dockerfile +17 -3
  5. package/README.md +3 -4
  6. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  7. package/bun.lock +8 -3
  8. package/docs/architecture/integrations.md +33 -59
  9. package/docs/architecture/memory.md +25 -30
  10. package/docs/architecture/security.md +19 -18
  11. package/docs/browser-use-architecture-phase2.md +63 -20
  12. package/docs/error-handling.md +111 -0
  13. package/docs/plugins.md +761 -0
  14. package/docs/skills.md +10 -10
  15. package/docs/stt-provider-onboarding.md +2 -1
  16. package/examples/plugins/echo/README.md +132 -0
  17. package/examples/plugins/echo/package.json +17 -0
  18. package/examples/plugins/echo/register.ts +187 -0
  19. package/knip.json +9 -2
  20. package/node_modules/@vellumai/ces-contracts/package.json +2 -1
  21. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
  22. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
  23. package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
  24. package/node_modules/@vellumai/credential-storage/package.json +2 -2
  25. package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
  26. package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
  27. package/node_modules/@vellumai/egress-proxy/package.json +2 -2
  28. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  29. package/openapi.yaml +334 -78
  30. package/package.json +6 -3
  31. package/scripts/generate-openapi.ts +50 -11
  32. package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
  33. package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
  34. package/src/__tests__/agent-loop.test.ts +112 -1
  35. package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
  36. package/src/__tests__/anthropic-provider.test.ts +171 -2
  37. package/src/__tests__/app-compiler.test.ts +57 -0
  38. package/src/__tests__/approval-cascade.test.ts +36 -10
  39. package/src/__tests__/approval-routes-http.test.ts +134 -10
  40. package/src/__tests__/assistant-attachments.test.ts +44 -0
  41. package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
  42. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  43. package/src/__tests__/avatar-generator.test.ts +4 -2
  44. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  45. package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
  46. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
  47. package/src/__tests__/browser-skill-endstate.test.ts +51 -182
  48. package/src/__tests__/btw-routes.test.ts +47 -1
  49. package/src/__tests__/bundled-asset.test.ts +6 -6
  50. package/src/__tests__/call-controller.test.ts +1 -2
  51. package/src/__tests__/call-site-routing-provider.test.ts +214 -0
  52. package/src/__tests__/catalog-cache.test.ts +96 -4
  53. package/src/__tests__/channel-approval-routes.test.ts +4 -4
  54. package/src/__tests__/channel-reply-delivery.test.ts +300 -2
  55. package/src/__tests__/checker.test.ts +870 -655
  56. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  57. package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
  58. package/src/__tests__/compaction-events.test.ts +501 -0
  59. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  60. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  61. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  62. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  63. package/src/__tests__/config-analysis.test.ts +11 -28
  64. package/src/__tests__/config-loader-backfill.test.ts +174 -0
  65. package/src/__tests__/config-loader-corrupt.test.ts +183 -0
  66. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
  67. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  68. package/src/__tests__/config-schema-cmd.test.ts +11 -5
  69. package/src/__tests__/config-schema.test.ts +440 -114
  70. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  71. package/src/__tests__/config-watcher.test.ts +2 -2
  72. package/src/__tests__/contact-store-user-file.test.ts +72 -73
  73. package/src/__tests__/contacts-tools.test.ts +26 -0
  74. package/src/__tests__/contacts-write.test.ts +4 -4
  75. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  76. package/src/__tests__/context-token-estimator.test.ts +191 -1
  77. package/src/__tests__/context-window-manager.test.ts +883 -4
  78. package/src/__tests__/conversation-abort-tool-results.test.ts +32 -15
  79. package/src/__tests__/conversation-agent-loop-overflow.test.ts +86 -46
  80. package/src/__tests__/conversation-agent-loop.test.ts +435 -216
  81. package/src/__tests__/conversation-attachments.test.ts +1 -1
  82. package/src/__tests__/conversation-confirmation-signals.test.ts +36 -10
  83. package/src/__tests__/conversation-error.test.ts +37 -6
  84. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  85. package/src/__tests__/conversation-init.benchmark.test.ts +34 -12
  86. package/src/__tests__/conversation-lifecycle.test.ts +336 -0
  87. package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
  88. package/src/__tests__/conversation-pairing.test.ts +174 -10
  89. package/src/__tests__/conversation-pre-run-repair.test.ts +32 -15
  90. package/src/__tests__/conversation-process-callsite.test.ts +309 -0
  91. package/src/__tests__/conversation-provider-retry-repair.test.ts +44 -21
  92. package/src/__tests__/conversation-queue.test.ts +68 -38
  93. package/src/__tests__/conversation-routes-disk-view.test.ts +36 -7
  94. package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
  95. package/src/__tests__/conversation-runtime-assembly.test.ts +2877 -152
  96. package/src/__tests__/conversation-runtime-workspace.test.ts +35 -50
  97. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  98. package/src/__tests__/conversation-skill-tools.test.ts +12 -146
  99. package/src/__tests__/conversation-slash-queue.test.ts +39 -19
  100. package/src/__tests__/conversation-slash-unknown.test.ts +53 -16
  101. package/src/__tests__/conversation-speed-override.test.ts +36 -12
  102. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
  103. package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
  104. package/src/__tests__/conversation-title-service.test.ts +118 -2
  105. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  106. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
  107. package/src/__tests__/conversation-unread-route.test.ts +2 -2
  108. package/src/__tests__/conversation-usage.test.ts +4 -2
  109. package/src/__tests__/conversation-workspace-cache-state.test.ts +33 -9
  110. package/src/__tests__/conversation-workspace-injection.test.ts +46 -15
  111. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -15
  112. package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
  113. package/src/__tests__/credential-health-service.test.ts +78 -9
  114. package/src/__tests__/credential-security-invariants.test.ts +5 -2
  115. package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
  116. package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
  117. package/src/__tests__/credential-vault-unit.test.ts +135 -19
  118. package/src/__tests__/credentials-cli.test.ts +1 -9
  119. package/src/__tests__/cross-provider-web-search.test.ts +84 -0
  120. package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
  121. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  122. package/src/__tests__/delete-propagation.test.ts +437 -0
  123. package/src/__tests__/dm-backfill.test.ts +417 -0
  124. package/src/__tests__/dm-persistence.test.ts +227 -0
  125. package/src/__tests__/edit-propagation.test.ts +280 -0
  126. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  127. package/src/__tests__/ephemeral-permissions.test.ts +93 -3
  128. package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
  129. package/src/__tests__/estimator-calibration.test.ts +213 -0
  130. package/src/__tests__/extension-id-sync-guard.test.ts +29 -10
  131. package/src/__tests__/file-write-tool.test.ts +151 -1
  132. package/src/__tests__/filing-service.test.ts +255 -0
  133. package/src/__tests__/first-greeting.test.ts +247 -5
  134. package/src/__tests__/gemini-provider.test.ts +0 -3
  135. package/src/__tests__/guardian-grant-minting.test.ts +8 -0
  136. package/src/__tests__/headless-browser-interactions.test.ts +1 -1
  137. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  138. package/src/__tests__/heartbeat-service.test.ts +96 -15
  139. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  140. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  141. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  142. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  143. package/src/__tests__/host-shell-tool.test.ts +124 -18
  144. package/src/__tests__/http-user-message-parity.test.ts +29 -1
  145. package/src/__tests__/image-credentials.test.ts +137 -0
  146. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  147. package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
  148. package/src/__tests__/injector-chain.test.ts +526 -0
  149. package/src/__tests__/intent-routing.test.ts +1 -66
  150. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  151. package/src/__tests__/llm-catalog-parity.test.ts +174 -0
  152. package/src/__tests__/llm-context-normalization.test.ts +121 -0
  153. package/src/__tests__/llm-resolver.test.ts +214 -0
  154. package/src/__tests__/llm-schema.test.ts +223 -0
  155. package/src/__tests__/managed-proxy-context.test.ts +6 -2
  156. package/src/__tests__/media-generate-image.test.ts +119 -13
  157. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  158. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  159. package/src/__tests__/messaging-skill-split.test.ts +3 -34
  160. package/src/__tests__/migration-import-from-url.test.ts +621 -0
  161. package/src/__tests__/model-intents.test.ts +11 -83
  162. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  163. package/src/__tests__/notification-decision-fallback.test.ts +0 -10
  164. package/src/__tests__/notification-decision-identity.test.ts +0 -9
  165. package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
  166. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  167. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  168. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  169. package/src/__tests__/oauth-cli.test.ts +14 -12
  170. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  171. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  172. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  173. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  174. package/src/__tests__/oauth-store.test.ts +46 -78
  175. package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
  176. package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
  177. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  178. package/src/__tests__/openai-image-service.test.ts +368 -0
  179. package/src/__tests__/openai-provider.test.ts +7 -0
  180. package/src/__tests__/openai-responses-provider.test.ts +396 -0
  181. package/src/__tests__/openrouter-provider-only.test.ts +135 -0
  182. package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
  183. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  184. package/src/__tests__/permission-checker-host-gate.test.ts +1 -25
  185. package/src/__tests__/permission-mode.test.ts +16 -0
  186. package/src/__tests__/permission-types.test.ts +0 -1
  187. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  188. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  189. package/src/__tests__/persona-resolver.test.ts +13 -13
  190. package/src/__tests__/pipeline-runner.test.ts +565 -0
  191. package/src/__tests__/pkb-autoinject.test.ts +37 -1
  192. package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
  193. package/src/__tests__/platform.test.ts +5 -2
  194. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  195. package/src/__tests__/plugin-registry.test.ts +273 -0
  196. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  197. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  198. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  199. package/src/__tests__/plugin-types.test.ts +320 -0
  200. package/src/__tests__/pricing.test.ts +93 -14
  201. package/src/__tests__/profiler-routes.test.ts +1 -1
  202. package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
  203. package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
  204. package/src/__tests__/provider-error-scenarios.test.ts +135 -6
  205. package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
  206. package/src/__tests__/provider-registry-ollama.test.ts +1 -2
  207. package/src/__tests__/proxy-approval-callback.test.ts +69 -9
  208. package/src/__tests__/reaction-persistence.test.ts +561 -0
  209. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  210. package/src/__tests__/registry.test.ts +0 -2
  211. package/src/__tests__/relay-server.test.ts +1 -1
  212. package/src/__tests__/require-fresh-approval.test.ts +1 -1
  213. package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
  214. package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
  215. package/src/__tests__/risk-classifier-parity.test.ts +230 -0
  216. package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
  217. package/src/__tests__/schedule-routes.test.ts +131 -1
  218. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  219. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  220. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  221. package/src/__tests__/secret-ingress-http.test.ts +28 -0
  222. package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
  223. package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
  224. package/src/__tests__/secret-scanner-executor.test.ts +1 -1
  225. package/src/__tests__/send-endpoint-busy.test.ts +29 -1
  226. package/src/__tests__/server-history-render.test.ts +31 -0
  227. package/src/__tests__/shell-identity.test.ts +0 -134
  228. package/src/__tests__/shell-parser-property.test.ts +13 -13
  229. package/src/__tests__/skill-cache-store.test.ts +182 -0
  230. package/src/__tests__/skills.test.ts +19 -33
  231. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  232. package/src/__tests__/slack-skill.test.ts +3 -8
  233. package/src/__tests__/starter-bundle.test.ts +35 -0
  234. package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
  235. package/src/__tests__/suggestion-routes.test.ts +259 -3
  236. package/src/__tests__/system-prompt.test.ts +22 -35
  237. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  238. package/src/__tests__/task-runner.test.ts +3 -1
  239. package/src/__tests__/task-scheduler.test.ts +3 -15
  240. package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
  241. package/src/__tests__/terminal-tools.test.ts +8 -0
  242. package/src/__tests__/test-preload.ts +11 -0
  243. package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
  244. package/src/__tests__/thread-backfill.test.ts +941 -0
  245. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  246. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  247. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  248. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  249. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -8
  250. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
  251. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  252. package/src/__tests__/tool-executor.test.ts +201 -94
  253. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  254. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  255. package/src/__tests__/trust-store.test.ts +442 -109
  256. package/src/__tests__/update-bulletin-job.test.ts +389 -0
  257. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
  258. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  259. package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
  260. package/src/__tests__/voice-session-bridge.test.ts +39 -0
  261. package/src/__tests__/volume-security-guard.test.ts +3 -2
  262. package/src/__tests__/web-search-history.test.ts +337 -0
  263. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
  264. package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
  265. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
  266. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  267. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  268. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  269. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  270. package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
  271. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  272. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
  273. package/src/__tests__/workspace-policy.test.ts +22 -16
  274. package/src/acp/client-handler.ts +1 -2
  275. package/src/agent/loop.ts +545 -115
  276. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  277. package/src/approvals/guardian-request-resolvers.ts +80 -0
  278. package/src/avatar/resvg-lazy.test.ts +136 -0
  279. package/src/avatar/resvg-lazy.ts +82 -9
  280. package/src/avatar/traits-png-sync.ts +21 -1
  281. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  282. package/src/backup/backup-worker.ts +3 -15
  283. package/src/browser/__tests__/operations.test.ts +163 -0
  284. package/src/browser/identifiers.ts +51 -0
  285. package/src/browser/operations.ts +660 -0
  286. package/src/browser/types.ts +81 -0
  287. package/src/bundler/app-compiler.ts +84 -1
  288. package/src/calls/call-state.ts +2 -2
  289. package/src/calls/guardian-question-copy.ts +2 -2
  290. package/src/calls/telephony-stt-routing.ts +1 -1
  291. package/src/calls/voice-session-bridge.ts +1 -0
  292. package/src/channels/__tests__/types.test.ts +3 -3
  293. package/src/channels/types.ts +6 -4
  294. package/src/cli/AGENTS.md +1 -1
  295. package/src/cli/__tests__/notifications.test.ts +87 -211
  296. package/src/cli/commands/__tests__/attachment.test.ts +438 -0
  297. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  298. package/src/cli/commands/__tests__/browser.test.ts +554 -0
  299. package/src/cli/commands/__tests__/cache.test.ts +623 -0
  300. package/src/cli/commands/__tests__/email-list.test.ts +6 -0
  301. package/src/cli/commands/__tests__/email-send.test.ts +93 -1
  302. package/src/cli/commands/__tests__/image-generation.test.ts +886 -0
  303. package/src/cli/commands/__tests__/inference-send.test.ts +463 -0
  304. package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
  305. package/src/cli/commands/__tests__/task.test.ts +913 -0
  306. package/src/cli/commands/__tests__/tts-synthesize.test.ts +606 -0
  307. package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
  308. package/src/cli/commands/__tests__/ui.test.ts +1215 -0
  309. package/src/cli/commands/__tests__/watchers.test.ts +716 -0
  310. package/src/cli/commands/attachment.ts +182 -0
  311. package/src/cli/commands/backup.ts +2 -2
  312. package/src/cli/commands/browser.ts +350 -0
  313. package/src/cli/commands/cache.ts +341 -0
  314. package/src/cli/commands/clients.ts +138 -0
  315. package/src/cli/commands/completions.ts +2 -12
  316. package/src/cli/commands/config.ts +6 -6
  317. package/src/cli/commands/conversations-import.ts +347 -0
  318. package/src/cli/commands/conversations.ts +69 -8
  319. package/src/cli/commands/email.ts +234 -194
  320. package/src/cli/commands/image-generation.ts +299 -0
  321. package/src/cli/commands/inference.ts +200 -0
  322. package/src/cli/commands/memory.ts +127 -17
  323. package/src/cli/commands/notifications.ts +68 -103
  324. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  325. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  326. package/src/cli/commands/oauth/connect.ts +2 -2
  327. package/src/cli/commands/oauth/providers.ts +176 -8
  328. package/src/cli/commands/oauth/status.ts +46 -36
  329. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  330. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
  331. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
  332. package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
  333. package/src/cli/commands/skills.ts +3 -4
  334. package/src/cli/commands/stt.ts +339 -0
  335. package/src/cli/commands/task.ts +795 -0
  336. package/src/cli/commands/trust.ts +50 -19
  337. package/src/cli/commands/tts.ts +273 -0
  338. package/src/cli/commands/ui.ts +670 -0
  339. package/src/cli/commands/watchers.ts +509 -0
  340. package/src/cli/lib/daemon-credential-client.ts +0 -19
  341. package/src/cli/program.ts +39 -24
  342. package/src/cli.ts +0 -37
  343. package/src/config/__tests__/backup-schema.test.ts +7 -2
  344. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  345. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  346. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  347. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  348. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  349. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  350. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  351. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  352. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  353. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  354. package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
  355. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  356. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +20 -1
  357. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
  358. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
  359. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +69 -12
  360. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
  361. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  362. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  363. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  364. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  365. package/src/config/bundled-tool-registry.ts +0 -190
  366. package/src/config/env.ts +7 -2
  367. package/src/config/feature-flag-registry.json +42 -10
  368. package/src/config/llm-resolver.ts +128 -0
  369. package/src/config/loader.ts +194 -10
  370. package/src/config/raw-config-utils.ts +30 -2
  371. package/src/config/sanitize-for-transfer.ts +35 -0
  372. package/src/config/schema.ts +49 -41
  373. package/src/config/schemas/analysis.ts +3 -22
  374. package/src/config/schemas/backup.ts +1 -1
  375. package/src/config/schemas/calls.ts +0 -4
  376. package/src/config/schemas/conversations.ts +16 -0
  377. package/src/config/schemas/filing.ts +2 -7
  378. package/src/config/schemas/heartbeat.ts +0 -5
  379. package/src/config/schemas/inference.ts +3 -23
  380. package/src/config/schemas/llm.ts +317 -0
  381. package/src/config/schemas/memory-processing.ts +1 -9
  382. package/src/config/schemas/notifications.ts +4 -11
  383. package/src/config/schemas/platform.ts +3 -9
  384. package/src/config/schemas/security.ts +33 -0
  385. package/src/config/schemas/services.ts +9 -4
  386. package/src/config/schemas/stt.ts +1 -0
  387. package/src/config/schemas/tts.ts +64 -0
  388. package/src/config/schemas/updates.ts +1 -1
  389. package/src/config/schemas/workspace-git.ts +3 -40
  390. package/src/config/skill-state.ts +6 -2
  391. package/src/config/skills.ts +96 -7
  392. package/src/context/__tests__/compact-prompt.test.ts +63 -0
  393. package/src/context/__tests__/microcompact.test.ts +805 -0
  394. package/src/context/estimator-calibration.ts +136 -0
  395. package/src/context/microcompact.ts +443 -0
  396. package/src/context/prompts/compact.md +26 -0
  397. package/src/context/token-estimator.ts +61 -3
  398. package/src/context/tool-result-truncation.ts +3 -63
  399. package/src/context/window-manager.ts +417 -39
  400. package/src/credential-execution/approval-bridge.ts +0 -1
  401. package/src/credential-execution/executable-discovery.ts +19 -8
  402. package/src/credential-execution/process-manager.test.ts +109 -0
  403. package/src/credential-execution/process-manager.ts +65 -2
  404. package/src/credential-health/credential-health-service.ts +19 -6
  405. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  406. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  407. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  408. package/src/daemon/approval-generators.ts +29 -4
  409. package/src/daemon/assistant-attachments.ts +24 -13
  410. package/src/daemon/classifier.ts +2 -2
  411. package/src/daemon/config-watcher.ts +0 -3
  412. package/src/daemon/context-overflow-policy.ts +4 -13
  413. package/src/daemon/context-overflow-reducer.ts +4 -1
  414. package/src/daemon/conversation-agent-loop-handlers.ts +162 -34
  415. package/src/daemon/conversation-agent-loop.ts +1282 -599
  416. package/src/daemon/conversation-attachments.ts +2 -6
  417. package/src/daemon/conversation-error.ts +36 -1
  418. package/src/daemon/conversation-history.ts +10 -19
  419. package/src/daemon/conversation-lifecycle.ts +59 -17
  420. package/src/daemon/conversation-messaging.ts +73 -4
  421. package/src/daemon/conversation-notifiers.ts +2 -110
  422. package/src/daemon/conversation-process.ts +24 -11
  423. package/src/daemon/conversation-queue-manager.ts +3 -0
  424. package/src/daemon/conversation-runtime-assembly.ts +1063 -211
  425. package/src/daemon/conversation-slash.ts +2 -2
  426. package/src/daemon/conversation-surfaces.ts +389 -1
  427. package/src/daemon/conversation-tool-setup.ts +51 -9
  428. package/src/daemon/conversation-usage.ts +1 -1
  429. package/src/daemon/conversation.ts +197 -64
  430. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  431. package/src/daemon/external-skills-bootstrap.ts +41 -0
  432. package/src/daemon/first-greeting.ts +191 -14
  433. package/src/daemon/guardian-action-generators.ts +34 -14
  434. package/src/daemon/handlers/config-model.test.ts +86 -0
  435. package/src/daemon/handlers/config-model.ts +65 -12
  436. package/src/daemon/handlers/conversations.ts +9 -2
  437. package/src/daemon/handlers/shared.ts +39 -11
  438. package/src/daemon/handlers/skills.ts +7 -3
  439. package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
  440. package/src/daemon/lifecycle.ts +109 -82
  441. package/src/daemon/message-types/computer-use.ts +2 -34
  442. package/src/daemon/message-types/conversations.ts +63 -0
  443. package/src/daemon/message-types/messages.ts +21 -1
  444. package/src/daemon/message-types/trust.ts +0 -2
  445. package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
  446. package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
  447. package/src/daemon/pkb-context-tracker.test.ts +169 -0
  448. package/src/daemon/pkb-context-tracker.ts +125 -0
  449. package/src/daemon/pkb-reminder-builder.test.ts +70 -0
  450. package/src/daemon/pkb-reminder-builder.ts +31 -0
  451. package/src/daemon/providers-setup.ts +6 -0
  452. package/src/daemon/server.ts +122 -12
  453. package/src/daemon/shutdown-handlers.ts +2 -12
  454. package/src/daemon/tool-side-effects.ts +14 -65
  455. package/src/daemon/web-search-history.ts +126 -0
  456. package/src/events/domain-events.ts +0 -1
  457. package/src/filing/filing-service.ts +9 -10
  458. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  459. package/src/heartbeat/heartbeat-service.ts +99 -28
  460. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  461. package/src/home/__tests__/feed-scheduler.test.ts +39 -11
  462. package/src/home/__tests__/rollup-producer.test.ts +44 -0
  463. package/src/home/assistant-feed-authoring.ts +4 -0
  464. package/src/home/emit-feed-event.ts +11 -0
  465. package/src/home/feed-scheduler.ts +20 -4
  466. package/src/home/feed-types.ts +97 -4
  467. package/src/home/relationship-state-writer.ts +2 -2
  468. package/src/home/rewrite-command-preview.ts +66 -0
  469. package/src/home/rollup-producer.ts +34 -5
  470. package/src/home/suggested-prompts.ts +101 -0
  471. package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
  472. package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
  473. package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
  474. package/src/ipc/__tests__/socket-path.test.ts +34 -0
  475. package/src/ipc/__tests__/task-ipc.test.ts +577 -0
  476. package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
  477. package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
  478. package/src/ipc/cli-client.ts +2 -1
  479. package/src/ipc/cli-server.ts +26 -8
  480. package/src/ipc/gateway-client.ts +6 -3
  481. package/src/ipc/routes/attachment.ts +114 -0
  482. package/src/ipc/routes/browser-context.ts +63 -0
  483. package/src/ipc/routes/browser.ts +97 -0
  484. package/src/ipc/routes/cache.ts +96 -0
  485. package/src/ipc/routes/get-contact.ts +16 -0
  486. package/src/ipc/routes/index.ts +31 -1
  487. package/src/ipc/routes/list-clients.ts +31 -0
  488. package/src/ipc/routes/merge-contacts.ts +17 -0
  489. package/src/ipc/routes/notification.ts +133 -0
  490. package/src/ipc/routes/rename-conversation.ts +59 -0
  491. package/src/ipc/routes/search-contacts.ts +19 -0
  492. package/src/ipc/routes/task-queue.ts +226 -0
  493. package/src/ipc/routes/task.ts +173 -0
  494. package/src/ipc/routes/ui-request.ts +50 -0
  495. package/src/ipc/routes/upsert-contact.ts +25 -0
  496. package/src/ipc/routes/watcher.ts +203 -0
  497. package/src/ipc/socket-path.ts +76 -0
  498. package/src/media/app-icon-generator.ts +23 -46
  499. package/src/media/avatar-router.ts +26 -41
  500. package/src/media/gemini-image-service.ts +8 -41
  501. package/src/media/image-credentials.ts +73 -0
  502. package/src/media/image-service.ts +85 -0
  503. package/src/media/openai-image-service.ts +131 -0
  504. package/src/media/types.ts +46 -0
  505. package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
  506. package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
  507. package/src/memory/admin.ts +18 -0
  508. package/src/memory/conversation-analyze-job.ts +14 -13
  509. package/src/memory/conversation-attention-store.ts +13 -6
  510. package/src/memory/conversation-crud.ts +133 -3
  511. package/src/memory/conversation-group-migration.ts +38 -6
  512. package/src/memory/conversation-queries.ts +57 -4
  513. package/src/memory/conversation-title-service.ts +32 -4
  514. package/src/memory/db-init.ts +10 -0
  515. package/src/memory/embedding-backend.ts +1 -1
  516. package/src/memory/embedding-gemini.test.ts +41 -2
  517. package/src/memory/embedding-gemini.ts +6 -1
  518. package/src/memory/graph/bootstrap.test.ts +282 -0
  519. package/src/memory/graph/bootstrap.ts +8 -5
  520. package/src/memory/graph/compaction.ts +299 -0
  521. package/src/memory/graph/consolidation.ts +4 -4
  522. package/src/memory/graph/conversation-graph-memory.ts +89 -29
  523. package/src/memory/graph/extraction.test.ts +272 -2
  524. package/src/memory/graph/extraction.ts +183 -53
  525. package/src/memory/graph/graph-search.test.ts +93 -0
  526. package/src/memory/graph/graph-search.ts +4 -1
  527. package/src/memory/graph/inspect.ts +2 -2
  528. package/src/memory/graph/narrative.ts +2 -2
  529. package/src/memory/graph/pattern-scan.ts +2 -2
  530. package/src/memory/graph/retriever.test.ts +459 -0
  531. package/src/memory/graph/retriever.ts +237 -48
  532. package/src/memory/graph/store.ts +41 -0
  533. package/src/memory/graph/tool-handlers.ts +27 -0
  534. package/src/memory/graph/tools.ts +6 -1
  535. package/src/memory/indexer.ts +5 -5
  536. package/src/memory/job-handlers/conversation-starters.ts +23 -20
  537. package/src/memory/job-handlers/summarization.ts +2 -2
  538. package/src/memory/job-utils.ts +7 -1
  539. package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
  540. package/src/memory/jobs/embed-pkb-file.ts +54 -0
  541. package/src/memory/jobs-store.ts +44 -3
  542. package/src/memory/jobs-worker.ts +4 -0
  543. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  544. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
  545. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  546. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
  547. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
  548. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  549. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  550. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  551. package/src/memory/migrations/index.ts +5 -0
  552. package/src/memory/pkb/pkb-index.test.ts +369 -0
  553. package/src/memory/pkb/pkb-index.ts +255 -0
  554. package/src/memory/pkb/pkb-reconcile.test.ts +252 -0
  555. package/src/memory/pkb/pkb-reconcile.ts +148 -0
  556. package/src/memory/pkb/pkb-search.test.ts +499 -0
  557. package/src/memory/pkb/pkb-search.ts +159 -0
  558. package/src/memory/pkb/types.ts +53 -0
  559. package/src/memory/qdrant-client.test.ts +60 -0
  560. package/src/memory/qdrant-client.ts +147 -1
  561. package/src/memory/schema/infrastructure.ts +1 -0
  562. package/src/memory/schema/oauth.ts +4 -1
  563. package/src/memory/slack-thread-store.ts +37 -0
  564. package/src/messaging/providers/gmail/adapter.ts +6 -16
  565. package/src/messaging/providers/gmail/client.ts +22 -0
  566. package/src/messaging/providers/gmail/types.ts +7 -0
  567. package/src/messaging/providers/slack/adapter.ts +14 -2
  568. package/src/messaging/providers/slack/backfill.test.ts +257 -0
  569. package/src/messaging/providers/slack/backfill.ts +101 -0
  570. package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
  571. package/src/messaging/providers/slack/message-metadata.ts +123 -0
  572. package/src/messaging/providers/slack/render-transcript.test.ts +1421 -0
  573. package/src/messaging/providers/slack/render-transcript.ts +501 -0
  574. package/src/messaging/style-analyzer.ts +5 -2
  575. package/src/notifications/README.md +9 -5
  576. package/src/notifications/conversation-pairing.ts +78 -19
  577. package/src/notifications/copy-composer.ts +0 -5
  578. package/src/notifications/decision-engine.ts +3 -9
  579. package/src/notifications/emit-signal.ts +1 -1
  580. package/src/notifications/preference-extractor.ts +2 -6
  581. package/src/notifications/signal.ts +1 -2
  582. package/src/oauth/AGENTS.md +1 -1
  583. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  584. package/src/oauth/connect-orchestrator.ts +8 -34
  585. package/src/oauth/connect-types.ts +6 -10
  586. package/src/oauth/manual-token-connection.ts +23 -0
  587. package/src/oauth/oauth-store.ts +31 -14
  588. package/src/oauth/platform-connection.test.ts +47 -0
  589. package/src/oauth/platform-connection.ts +15 -5
  590. package/src/oauth/provider-serializer.ts +6 -1
  591. package/src/oauth/seed-providers.ts +56 -106
  592. package/src/outbound-proxy/http-forwarder.ts +9 -0
  593. package/src/permissions/approval-policy.test.ts +1223 -0
  594. package/src/permissions/approval-policy.ts +309 -0
  595. package/src/permissions/arg-parser.test.ts +161 -0
  596. package/src/permissions/arg-parser.ts +141 -0
  597. package/src/permissions/bash-risk-classifier.test.ts +1620 -0
  598. package/src/permissions/bash-risk-classifier.ts +950 -0
  599. package/src/permissions/checker.ts +348 -711
  600. package/src/permissions/command-registry.test.ts +774 -0
  601. package/src/permissions/command-registry.ts +1005 -0
  602. package/src/permissions/defaults.ts +28 -79
  603. package/src/permissions/file-risk-classifier.test.ts +535 -0
  604. package/src/permissions/file-risk-classifier.ts +274 -0
  605. package/src/permissions/gateway-threshold-reader.ts +196 -0
  606. package/src/permissions/prompter.ts +4 -0
  607. package/src/permissions/risk-types.ts +262 -0
  608. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  609. package/src/permissions/schedule-risk-classifier.ts +85 -0
  610. package/src/permissions/secret-prompter.ts +53 -2
  611. package/src/permissions/shell-identity.ts +2 -42
  612. package/src/permissions/skill-risk-classifier.test.ts +311 -0
  613. package/src/permissions/skill-risk-classifier.ts +214 -0
  614. package/src/permissions/trust-client.ts +52 -25
  615. package/src/permissions/trust-store-interface.ts +1 -6
  616. package/src/permissions/trust-store.ts +161 -62
  617. package/src/permissions/types.ts +25 -14
  618. package/src/permissions/web-risk-classifier.test.ts +170 -0
  619. package/src/permissions/web-risk-classifier.ts +89 -0
  620. package/src/permissions/workspace-policy.ts +9 -19
  621. package/src/platform/client.ts +19 -1
  622. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  623. package/src/plugins/defaults/compaction.ts +145 -0
  624. package/src/plugins/defaults/empty-response.ts +126 -0
  625. package/src/plugins/defaults/history-repair.ts +85 -0
  626. package/src/plugins/defaults/index.ts +116 -0
  627. package/src/plugins/defaults/injectors.ts +491 -0
  628. package/src/plugins/defaults/llm-call.ts +82 -0
  629. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  630. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  631. package/src/plugins/defaults/persistence.ts +129 -0
  632. package/src/plugins/defaults/title-generate.ts +95 -0
  633. package/src/plugins/defaults/token-estimate.ts +104 -0
  634. package/src/plugins/defaults/tool-error.ts +126 -0
  635. package/src/plugins/defaults/tool-execute.ts +89 -0
  636. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  637. package/src/plugins/pipeline.ts +316 -0
  638. package/src/plugins/plugin-skill-contributions.ts +292 -0
  639. package/src/plugins/registry.ts +241 -0
  640. package/src/plugins/types.ts +1134 -0
  641. package/src/plugins/user-loader.ts +177 -0
  642. package/src/prompts/persona-resolver.ts +3 -3
  643. package/src/prompts/system-prompt.ts +19 -20
  644. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  645. package/src/prompts/templates/SOUL.md +2 -2
  646. package/src/prompts/update-bulletin-job.ts +190 -0
  647. package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
  648. package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
  649. package/src/providers/__tests__/retry-callsite.test.ts +424 -0
  650. package/src/providers/anthropic/client.ts +183 -14
  651. package/src/providers/call-site-routing.ts +71 -0
  652. package/src/providers/gemini/client.ts +65 -2
  653. package/src/providers/managed-proxy/constants.ts +2 -1
  654. package/src/providers/model-catalog.ts +524 -33
  655. package/src/providers/model-intents.ts +4 -4
  656. package/src/providers/openai/chat-completions-provider.ts +57 -1
  657. package/src/providers/openai/responses-provider.ts +86 -9
  658. package/src/providers/openrouter/client.ts +80 -9
  659. package/src/providers/provider-env-vars.ts +56 -0
  660. package/src/providers/provider-send-message.ts +22 -5
  661. package/src/providers/ratelimit.ts +4 -0
  662. package/src/providers/registry.ts +19 -8
  663. package/src/providers/retry.ts +174 -39
  664. package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
  665. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  666. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  667. package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
  668. package/src/providers/speech-to-text/provider-catalog.ts +17 -0
  669. package/src/providers/speech-to-text/resolve.ts +7 -0
  670. package/src/providers/speech-to-text/xai-realtime.test.ts +646 -0
  671. package/src/providers/speech-to-text/xai-realtime.ts +821 -0
  672. package/src/providers/speech-to-text/xai.test.ts +155 -0
  673. package/src/providers/speech-to-text/xai.ts +97 -0
  674. package/src/providers/types.ts +93 -3
  675. package/src/runtime/AGENTS.md +27 -18
  676. package/src/runtime/__tests__/agent-wake.test.ts +43 -2
  677. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  678. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  679. package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
  680. package/src/runtime/agent-wake.ts +63 -22
  681. package/src/runtime/auth/route-policy.ts +4 -0
  682. package/src/runtime/btw-sidechain.ts +13 -3
  683. package/src/runtime/channel-reply-delivery.ts +106 -2
  684. package/src/runtime/client-registry.ts +261 -0
  685. package/src/runtime/decision-token.ts +116 -0
  686. package/src/runtime/gateway-client.ts +2 -2
  687. package/src/runtime/http-router.ts +32 -0
  688. package/src/runtime/http-server.ts +129 -9
  689. package/src/runtime/http-types.ts +23 -3
  690. package/src/runtime/interactive-ui.ts +362 -0
  691. package/src/runtime/invite-instruction-generator.ts +2 -2
  692. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
  693. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
  694. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
  695. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
  696. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
  697. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
  698. package/src/runtime/migrations/gcs-signed-url.ts +162 -0
  699. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  700. package/src/runtime/migrations/vbundle-importer.ts +154 -9
  701. package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
  702. package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
  703. package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
  704. package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
  705. package/src/runtime/migrations/vbundle-validator.ts +15 -6
  706. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
  707. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
  708. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
  709. package/src/runtime/routes/approval-prompt-ts-tracker.ts +78 -0
  710. package/src/runtime/routes/approval-routes.ts +29 -17
  711. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
  712. package/src/runtime/routes/avatar-routes.ts +20 -4
  713. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  714. package/src/runtime/routes/btw-routes.ts +1 -4
  715. package/src/runtime/routes/conversation-management-routes.ts +20 -2
  716. package/src/runtime/routes/conversation-routes.ts +351 -138
  717. package/src/runtime/routes/debug-routes.ts +1 -1
  718. package/src/runtime/routes/diagnostics-routes.ts +6 -4
  719. package/src/runtime/routes/events-routes.ts +16 -0
  720. package/src/runtime/routes/guardian-approval-interception.ts +33 -3
  721. package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
  722. package/src/runtime/routes/home-feed-routes.ts +120 -2
  723. package/src/runtime/routes/inbound-message-handler.ts +987 -2
  724. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
  725. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
  726. package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
  727. package/src/runtime/routes/integrations/slack/channel.ts +25 -3
  728. package/src/runtime/routes/llm-context-normalization.ts +23 -1
  729. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  730. package/src/runtime/routes/migration-routes.ts +720 -127
  731. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  732. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  733. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  734. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  735. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  736. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  737. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  738. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  739. package/src/runtime/routes/playground/deps.ts +56 -0
  740. package/src/runtime/routes/playground/force-compact.ts +73 -0
  741. package/src/runtime/routes/playground/guard.ts +37 -0
  742. package/src/runtime/routes/playground/index.ts +28 -0
  743. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  744. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  745. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  746. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  747. package/src/runtime/routes/playground/state.ts +78 -0
  748. package/src/runtime/routes/schedule-routes.ts +89 -8
  749. package/src/runtime/routes/settings-routes.ts +4 -2
  750. package/src/runtime/routes/trust-rules-routes.ts +30 -14
  751. package/src/runtime/routes/work-items-routes.test.ts +1 -1
  752. package/src/runtime/routes/work-items-routes.ts +3 -2
  753. package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
  754. package/src/runtime/services/analyze-conversation.ts +12 -16
  755. package/src/runtime/skill-route-registry.ts +97 -15
  756. package/src/schedule/run-script.ts +68 -0
  757. package/src/schedule/schedule-store.ts +7 -1
  758. package/src/schedule/scheduler.ts +56 -8
  759. package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
  760. package/src/security/__tests__/untrusted-content.test.ts +109 -0
  761. package/src/security/oauth2.ts +98 -35
  762. package/src/security/secure-keys.ts +7 -8
  763. package/src/security/token-manager.ts +27 -13
  764. package/src/security/untrusted-content.ts +102 -0
  765. package/src/skills/catalog-cache.ts +35 -9
  766. package/src/skills/catalog-install.ts +31 -3
  767. package/src/skills/skill-cache-store.ts +97 -0
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
  769. package/src/stt/daemon-batch-transcriber.ts +33 -0
  770. package/src/stt/stt-stream-session.ts +8 -1
  771. package/src/stt/types.ts +5 -1
  772. package/src/subagent/manager.ts +41 -13
  773. package/src/tasks/ephemeral-permissions.ts +9 -4
  774. package/src/telemetry/usage-telemetry-reporter.ts +27 -5
  775. package/src/tools/browser/__tests__/browser-status.test.ts +234 -2
  776. package/src/tools/browser/browser-execution.ts +150 -54
  777. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  778. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  779. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
  780. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  781. package/src/tools/browser/cdp-client/factory.ts +15 -4
  782. package/src/tools/credentials/tool-policy.ts +39 -5
  783. package/src/tools/credentials/vault.ts +9 -4
  784. package/src/tools/executor.ts +129 -73
  785. package/src/tools/filesystem/write.ts +52 -0
  786. package/src/tools/host-terminal/host-shell.ts +45 -5
  787. package/src/tools/memory/register.test.ts +185 -0
  788. package/src/tools/memory/register.ts +3 -1
  789. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  790. package/src/tools/network/web-fetch.ts +20 -10
  791. package/src/tools/network/web-search.ts +19 -4
  792. package/src/tools/permission-checker.ts +116 -46
  793. package/src/tools/policy-context.ts +29 -8
  794. package/src/tools/registry.ts +195 -6
  795. package/src/tools/schedule/create.ts +23 -8
  796. package/src/tools/schedule/update.ts +3 -1
  797. package/src/tools/secret-detection-handler.ts +0 -51
  798. package/src/tools/side-effects.ts +0 -11
  799. package/src/tools/skills/execute.ts +2 -2
  800. package/src/tools/skills/sandbox-runner.ts +5 -2
  801. package/src/tools/system/avatar-generator.ts +6 -2
  802. package/src/tools/terminal/backends/native.ts +51 -2
  803. package/src/tools/terminal/safe-env.ts +3 -2
  804. package/src/tools/terminal/shell.ts +1 -0
  805. package/src/tools/tool-manifest.ts +6 -21
  806. package/src/tools/types.ts +40 -5
  807. package/src/tools/verification-control-plane-policy.ts +1 -1
  808. package/src/tts/__tests__/provider-adapters.test.ts +240 -13
  809. package/src/tts/provider-catalog.ts +18 -0
  810. package/src/tts/providers/index.ts +2 -0
  811. package/src/tts/providers/xai-provider.ts +224 -0
  812. package/src/tts/types.ts +46 -0
  813. package/src/types/tar-stream.d.ts +66 -0
  814. package/src/util/json.ts +17 -0
  815. package/src/util/platform.ts +9 -4
  816. package/src/util/pricing.ts +41 -8
  817. package/src/watcher/engine.ts +1 -1
  818. package/src/watcher/providers/google-calendar.ts +134 -8
  819. package/src/watcher/providers/outlook-calendar.ts +42 -2
  820. package/src/workspace/git-service.ts +23 -4
  821. package/src/workspace/migrations/006-services-config.ts +2 -4
  822. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  823. package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
  824. package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
  825. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
  826. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +56 -0
  827. package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
  828. package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
  829. package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
  830. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
  831. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  832. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  833. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  834. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  835. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  836. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  837. package/src/workspace/migrations/AGENTS.md +1 -1
  838. package/src/workspace/migrations/registry.ts +28 -0
  839. package/src/workspace/provider-commit-message-generator.ts +19 -38
  840. package/tsconfig.json +1 -1
  841. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  842. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  843. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  844. package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
  845. package/src/__tests__/gmail-archive-gate.test.ts +0 -246
  846. package/src/__tests__/gmail-preferences.test.ts +0 -117
  847. package/src/__tests__/hooks-blocking.test.ts +0 -178
  848. package/src/__tests__/hooks-cli.test.ts +0 -182
  849. package/src/__tests__/hooks-config.test.ts +0 -108
  850. package/src/__tests__/hooks-discovery.test.ts +0 -211
  851. package/src/__tests__/hooks-integration.test.ts +0 -196
  852. package/src/__tests__/hooks-manager.test.ts +0 -226
  853. package/src/__tests__/hooks-runner.test.ts +0 -175
  854. package/src/__tests__/hooks-settings.test.ts +0 -160
  855. package/src/__tests__/hooks-templates.test.ts +0 -169
  856. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  857. package/src/__tests__/hooks-watch.test.ts +0 -112
  858. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  859. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  860. package/src/__tests__/outlook-attachments.test.ts +0 -301
  861. package/src/__tests__/outlook-automation-tools.test.ts +0 -425
  862. package/src/__tests__/outlook-categories.test.ts +0 -212
  863. package/src/__tests__/outlook-compose-tools.test.ts +0 -325
  864. package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
  865. package/src/__tests__/outlook-follow-up.test.ts +0 -196
  866. package/src/__tests__/outlook-trash.test.ts +0 -77
  867. package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
  868. package/src/__tests__/send-notification-tool.test.ts +0 -83
  869. package/src/__tests__/update-bulletin-format.test.ts +0 -181
  870. package/src/__tests__/update-bulletin-state.test.ts +0 -135
  871. package/src/__tests__/update-bulletin.test.ts +0 -478
  872. package/src/__tests__/update-template-contract.test.ts +0 -29
  873. package/src/cli/commands/doctor.ts +0 -341
  874. package/src/cli/commands/shotgun.ts +0 -266
  875. package/src/config/bundled-skills/browser/SKILL.md +0 -88
  876. package/src/config/bundled-skills/browser/TOOLS.json +0 -516
  877. package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
  878. package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
  879. package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
  880. package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
  881. package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
  882. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
  883. package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
  884. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
  885. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
  886. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
  887. package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
  888. package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
  889. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
  890. package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
  891. package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
  892. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
  893. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
  894. package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
  895. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
  896. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
  897. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  898. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  899. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -66
  900. package/src/config/bundled-skills/gmail/SKILL.md +0 -221
  901. package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
  902. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
  903. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
  904. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
  905. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
  906. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
  907. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
  908. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
  909. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
  910. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
  911. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
  912. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
  913. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
  914. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
  915. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
  916. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
  917. package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
  918. package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
  919. package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
  920. package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
  921. package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
  922. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
  923. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
  924. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
  925. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
  926. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
  927. package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
  928. package/src/config/bundled-skills/google-calendar/types.ts +0 -97
  929. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  930. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  931. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  932. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  933. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  934. package/src/config/bundled-skills/outlook/SKILL.md +0 -196
  935. package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
  936. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
  937. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
  938. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
  939. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
  940. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
  941. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
  942. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
  943. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
  944. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
  945. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
  946. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
  947. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
  948. package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
  949. package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
  950. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
  951. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
  952. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
  953. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
  954. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
  955. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
  956. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
  957. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
  958. package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
  959. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  960. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  961. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  962. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  963. package/src/config/bundled-skills/slack/SKILL.md +0 -108
  964. package/src/config/bundled-skills/tasks/SKILL.md +0 -37
  965. package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
  966. package/src/config/bundled-skills/tasks/icon.svg +0 -34
  967. package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
  968. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
  969. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
  970. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
  971. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
  972. package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
  973. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
  974. package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
  975. package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
  976. package/src/config/bundled-skills/watcher/SKILL.md +0 -31
  977. package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
  978. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
  979. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
  980. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
  981. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
  982. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
  983. package/src/daemon/context-overflow-approval.ts +0 -52
  984. package/src/daemon/watch-handler.ts +0 -399
  985. package/src/hooks/cli.ts +0 -253
  986. package/src/hooks/config.ts +0 -100
  987. package/src/hooks/discovery.ts +0 -135
  988. package/src/hooks/manager.ts +0 -179
  989. package/src/hooks/runner.ts +0 -117
  990. package/src/hooks/templates.ts +0 -77
  991. package/src/hooks/types.ts +0 -75
  992. package/src/oauth/scope-policy.ts +0 -89
  993. package/src/prompts/templates/UPDATES.md +0 -50
  994. package/src/prompts/update-bulletin-format.ts +0 -85
  995. package/src/prompts/update-bulletin-state.ts +0 -58
  996. package/src/prompts/update-bulletin-template-path.ts +0 -13
  997. package/src/prompts/update-bulletin.ts +0 -139
  998. package/src/runtime/gateway-internal-client.ts +0 -94
  999. package/src/runtime/routes/watch-routes.ts +0 -156
  1000. package/src/shared/provider-env-vars.ts +0 -19
  1001. package/src/signals/shotgun.ts +0 -203
  1002. package/src/tools/watch/screen-watch.ts +0 -144
  1003. package/src/tools/watch/watch-state.ts +0 -142
  1004. package/src/tools/watcher/create.ts +0 -86
  1005. package/src/tools/watcher/delete.ts +0 -36
  1006. package/src/tools/watcher/digest.ts +0 -54
  1007. package/src/tools/watcher/list.ts +0 -83
  1008. package/src/tools/watcher/update.ts +0 -71
@@ -0,0 +1,1421 @@
1
+ /**
2
+ * Unit tests for the pure chronological Slack transcript renderer.
3
+ *
4
+ * Covers tag variants (top-level, reply, edit, delete, reaction add/remove),
5
+ * stable parent aliases, reaction cap, sort stability under identical ts,
6
+ * the four scenarios from the design brief, and mixed legacy/post-upgrade
7
+ * fixtures.
8
+ */
9
+
10
+ import { describe, expect, test } from "bun:test";
11
+
12
+ import type { Message } from "../../../providers/types.js";
13
+ import {
14
+ extractTagLineTexts,
15
+ isReactionTagLine,
16
+ parentAlias,
17
+ type RenderableSlackMessage,
18
+ renderSlackTranscript,
19
+ } from "./render-transcript.js";
20
+
21
+ // ── helpers ──────────────────────────────────────────────────────────────────
22
+
23
+ // Anchor times: 14:25:00 UTC on 2023-11-14 = 1699971900 (Slack ts seconds).
24
+ // We work entirely in UTC because the renderer formats UTC MM/DD/YY HH:MM.
25
+ const TS_14_24 = "1699971840.000050"; // 14:24 UTC
26
+ const TS_14_25 = "1699971900.000100"; // 14:25 UTC
27
+ const TS_14_26 = "1699971960.000200"; // 14:26 UTC
28
+ const TS_14_28 = "1699972080.000300"; // 14:28 UTC
29
+ const TS_14_30 = "1699972200.000400"; // 14:30 UTC
30
+
31
+ const MS_14_25 = 1699971900_000;
32
+ const MS_14_30 = 1699972200_000;
33
+ const MS_14_32 = 1699972320_000;
34
+
35
+ const CHANNEL = "C0001";
36
+
37
+ function userMsg(
38
+ ts: string,
39
+ sender: string | null,
40
+ content: string,
41
+ opts: {
42
+ threadTs?: string;
43
+ editedAt?: number;
44
+ deletedAt?: number;
45
+ role?: "user" | "assistant";
46
+ createdAt?: number;
47
+ } = {},
48
+ ): RenderableSlackMessage {
49
+ return {
50
+ role: opts.role ?? "user",
51
+ content,
52
+ senderLabel: sender,
53
+ createdAt: opts.createdAt ?? Number.parseFloat(ts) * 1000,
54
+ metadata: {
55
+ source: "slack",
56
+ channelId: CHANNEL,
57
+ channelTs: ts,
58
+ threadTs: opts.threadTs,
59
+ eventKind: "message",
60
+ editedAt: opts.editedAt,
61
+ deletedAt: opts.deletedAt,
62
+ },
63
+ };
64
+ }
65
+
66
+ function reactionMsg(
67
+ ts: string,
68
+ actor: string | null,
69
+ emoji: string,
70
+ targetTs: string,
71
+ op: "added" | "removed" = "added",
72
+ role: "user" | "assistant" = "user",
73
+ ): RenderableSlackMessage {
74
+ return {
75
+ role,
76
+ content: "",
77
+ senderLabel: actor,
78
+ createdAt: Number.parseFloat(ts) * 1000,
79
+ metadata: {
80
+ source: "slack",
81
+ channelId: CHANNEL,
82
+ channelTs: ts,
83
+ eventKind: "reaction",
84
+ reaction: {
85
+ emoji,
86
+ targetChannelTs: targetTs,
87
+ op,
88
+ },
89
+ },
90
+ };
91
+ }
92
+
93
+ function legacyMsg(
94
+ createdAt: number,
95
+ sender: string | null,
96
+ content: string,
97
+ role: "user" | "assistant" = "user",
98
+ ): RenderableSlackMessage {
99
+ return { role, content, senderLabel: sender, createdAt, metadata: null };
100
+ }
101
+
102
+ /** Build an expected `Message` fixture with a single text content block. */
103
+ function textMsg(role: "user" | "assistant", text: string): Message {
104
+ return { role, content: [{ type: "text", text }] };
105
+ }
106
+
107
+ // ── basics ───────────────────────────────────────────────────────────────────
108
+
109
+ describe("renderSlackTranscript — basics", () => {
110
+ test("empty array yields empty array", () => {
111
+ expect(renderSlackTranscript([])).toEqual([]);
112
+ });
113
+
114
+ test("renders top-level message with MM/DD/YY HH:MM tag", () => {
115
+ const out = renderSlackTranscript([userMsg(TS_14_25, "@alice", "hi")]);
116
+ expect(out).toEqual([textMsg("user", "[11/14/23 14:25 @alice]: hi")]);
117
+ });
118
+
119
+ test("renders thread reply with parent alias arrow", () => {
120
+ const out = renderSlackTranscript([
121
+ userMsg(TS_14_28, "@bob", "got it", { threadTs: TS_14_25 }),
122
+ ]);
123
+ const alias = parentAlias(TS_14_25);
124
+ expect(out).toEqual([
125
+ textMsg("user", `[11/14/23 14:28 @bob → ${alias}]: got it`),
126
+ ]);
127
+ });
128
+
129
+ test("renders edited message with editedAt suffix", () => {
130
+ const out = renderSlackTranscript([
131
+ userMsg(TS_14_25, "@alice", "hi (revised)", { editedAt: MS_14_30 }),
132
+ ]);
133
+ expect(out).toEqual([
134
+ textMsg(
135
+ "user",
136
+ "[11/14/23 14:25 @alice, edited 11/14/23 14:30]: hi (revised)",
137
+ ),
138
+ ]);
139
+ });
140
+
141
+ test("edited marker uses editedAt time, not channelTs", () => {
142
+ // channelTs at 14:25 (original send time), edited later at 14:32.
143
+ // The opening time bracket must reflect 14:25 and the suffix must
144
+ // reflect 14:32 — derived from editedAt, not from channelTs.
145
+ const out = renderSlackTranscript([
146
+ userMsg(TS_14_25, "@alice", "v2", { editedAt: MS_14_32 }),
147
+ ]);
148
+ expect(out).toEqual([
149
+ textMsg("user", "[11/14/23 14:25 @alice, edited 11/14/23 14:32]: v2"),
150
+ ]);
151
+ });
152
+
153
+ test("edited message in a thread renders both arrow and edit suffix", () => {
154
+ const out = renderSlackTranscript([
155
+ userMsg(TS_14_28, "@bob", "got it (edit)", {
156
+ threadTs: TS_14_25,
157
+ editedAt: MS_14_30,
158
+ }),
159
+ ]);
160
+ const alias = parentAlias(TS_14_25);
161
+ expect(out).toEqual([
162
+ textMsg(
163
+ "user",
164
+ `[11/14/23 14:28 @bob → ${alias}, edited 11/14/23 14:30]: got it (edit)`,
165
+ ),
166
+ ]);
167
+ });
168
+
169
+ test("renders deleted message with deletedAt — content elided", () => {
170
+ const out = renderSlackTranscript([
171
+ userMsg(TS_14_25, "@alice", "(removed)", { deletedAt: MS_14_32 }),
172
+ ]);
173
+ expect(out).toEqual([
174
+ textMsg("user", "[11/14/23 14:25 @alice — deleted 11/14/23 14:32]"),
175
+ ]);
176
+ });
177
+
178
+ test("delete takes precedence over edit (delete wins)", () => {
179
+ // A message that was edited at 14:30 and then deleted at 14:32
180
+ // should render as deleted, not edited — and content must be elided.
181
+ const out = renderSlackTranscript([
182
+ userMsg(TS_14_25, "@alice", "edited body", {
183
+ editedAt: MS_14_30,
184
+ deletedAt: MS_14_32,
185
+ }),
186
+ ]);
187
+ expect(out).toEqual([
188
+ textMsg("user", "[11/14/23 14:25 @alice — deleted 11/14/23 14:32]"),
189
+ ]);
190
+ const text0 = extractTagLineTexts(out)[0];
191
+ // No "edited" suffix should leak through.
192
+ expect(text0.includes("edited")).toBe(false);
193
+ // Content body must not appear.
194
+ expect(text0.includes("edited body")).toBe(false);
195
+ });
196
+
197
+ test("deleted message preserves chronological ordering", () => {
198
+ // A deleted message in the middle of a transcript should still occupy
199
+ // its chronological slot — only the body is elided.
200
+ const out = renderSlackTranscript([
201
+ userMsg(TS_14_25, "@alice", "first"),
202
+ userMsg(TS_14_28, "@bob", "(removed)", { deletedAt: MS_14_30 }),
203
+ userMsg(TS_14_30, "@carol", "third"),
204
+ ]);
205
+ expect(extractTagLineTexts(out)).toEqual([
206
+ "[11/14/23 14:25 @alice]: first",
207
+ "[11/14/23 14:28 @bob — deleted 11/14/23 14:30]",
208
+ "[11/14/23 14:30 @carol]: third",
209
+ ]);
210
+ });
211
+
212
+ test("renders reaction added", () => {
213
+ const alias = parentAlias(TS_14_25);
214
+ const out = renderSlackTranscript([
215
+ reactionMsg(TS_14_28, "@bob", "👍", TS_14_25, "added"),
216
+ ]);
217
+ expect(out).toEqual([
218
+ textMsg("user", `[11/14/23 14:28 @bob reacted 👍 to ${alias}]`),
219
+ ]);
220
+ });
221
+
222
+ test("renders reaction removed", () => {
223
+ const alias = parentAlias(TS_14_25);
224
+ const out = renderSlackTranscript([
225
+ reactionMsg(TS_14_28, "@bob", "👍", TS_14_25, "removed"),
226
+ ]);
227
+ expect(out).toEqual([
228
+ textMsg("user", `[11/14/23 14:28 @bob removed 👍 from ${alias}]`),
229
+ ]);
230
+ });
231
+
232
+ test("assistant-role message emits content with no tag-line wrapper", () => {
233
+ // Rationale: the `role` slot already conveys identity, and the
234
+ // assistant responds ~immediately after the triggering user message
235
+ // so the timestamp would add little beyond chronological adjacency.
236
+ // Keeping a bracketed tag on assistant rows caused the model to
237
+ // mimic the `[MM/DD/YY HH:MM]:` format as a literal prefix in new
238
+ // outbound Slack replies.
239
+ const out = renderSlackTranscript([
240
+ userMsg(TS_14_25, null, "yo 👋", { role: "assistant" }),
241
+ ]);
242
+ expect(out).toEqual([textMsg("assistant", "yo 👋")]);
243
+ });
244
+
245
+ test("omits sender label for user-role message with null senderLabel (no displayName)", () => {
246
+ const out = renderSlackTranscript([userMsg(TS_14_25, null, "yo")]);
247
+ expect(out).toEqual([textMsg("user", "[11/14/23 14:25]: yo")]);
248
+ });
249
+
250
+ test("omits sender label on legacy user row with null senderLabel", () => {
251
+ const out = renderSlackTranscript([legacyMsg(MS_14_25, null, "hi")]);
252
+ expect(out).toEqual([textMsg("user", "[11/14/23 14:25]: hi")]);
253
+ });
254
+
255
+ test("thread-reply assistant row emits content-only — no tag wrapper, no thread arrow", () => {
256
+ const out = renderSlackTranscript([
257
+ userMsg(TS_14_28, null, "got it", {
258
+ threadTs: TS_14_25,
259
+ role: "assistant",
260
+ }),
261
+ ]);
262
+ expect(out).toEqual([textMsg("assistant", "got it")]);
263
+ });
264
+
265
+ test("deleted assistant row collapses to the `[deleted]` sentinel", () => {
266
+ // Chronology must still be preserved — we emit a stable short sentinel
267
+ // rather than eliding the row entirely. The sentinel is intentionally
268
+ // different from the user-row `[MM/DD/YY — deleted MM/DD/YY]` form so
269
+ // the model has no timestamp pattern to mimic in new outbound replies.
270
+ const out = renderSlackTranscript([
271
+ userMsg(TS_14_25, null, "(removed)", {
272
+ deletedAt: MS_14_32,
273
+ role: "assistant",
274
+ }),
275
+ ]);
276
+ expect(out).toEqual([textMsg("assistant", "[deleted]")]);
277
+ });
278
+
279
+ test("edited assistant row emits the latest content verbatim — no edit suffix", () => {
280
+ const out = renderSlackTranscript([
281
+ userMsg(TS_14_25, null, "v2", {
282
+ editedAt: MS_14_30,
283
+ role: "assistant",
284
+ }),
285
+ ]);
286
+ expect(out).toEqual([textMsg("assistant", "v2")]);
287
+ });
288
+
289
+ test("reaction with null senderLabel falls back on role-derived subject", () => {
290
+ // Defensive: reactions always need a grammatical subject. If a caller
291
+ // accidentally passes null, the renderer falls back on a role-derived
292
+ // label so the tag line still parses.
293
+ const alias = parentAlias(TS_14_25);
294
+ const out = renderSlackTranscript([
295
+ reactionMsg(TS_14_28, null, "👍", TS_14_25, "added", "assistant"),
296
+ ]);
297
+ expect(out).toEqual([
298
+ textMsg(
299
+ "assistant",
300
+ `[11/14/23 14:28 @assistant reacted 👍 to ${alias}]`,
301
+ ),
302
+ ]);
303
+ });
304
+ });
305
+
306
+ // ── edited marker ────────────────────────────────────────────────────────────
307
+
308
+ describe("renderSlackTranscript — edited marker", () => {
309
+ test("deleted takes precedence over edited (no edit suffix on deleted line)", () => {
310
+ // A row may carry both editedAt and deletedAt if it was edited before
311
+ // being deleted. The deleted form takes precedence and the edited
312
+ // suffix must not appear.
313
+ const out = renderSlackTranscript([
314
+ userMsg(TS_14_25, "@alice", "(removed)", {
315
+ editedAt: MS_14_30,
316
+ deletedAt: MS_14_32,
317
+ }),
318
+ ]);
319
+ expect(out).toEqual([
320
+ textMsg("user", "[11/14/23 14:25 @alice — deleted 11/14/23 14:32]"),
321
+ ]);
322
+ expect(extractTagLineTexts(out)[0].includes("edited")).toBe(false);
323
+ });
324
+
325
+ test("reaction rows do not render the edited marker even if metadata has editedAt", () => {
326
+ // The renderer must never apply the edited suffix to a reaction-kind row.
327
+ // We construct a reaction with an editedAt field set in metadata to
328
+ // confirm the reaction code path ignores it.
329
+ const reaction: RenderableSlackMessage = {
330
+ role: "user",
331
+ content: "",
332
+ senderLabel: "@bob",
333
+ createdAt: Number.parseFloat(TS_14_28) * 1000,
334
+ metadata: {
335
+ source: "slack",
336
+ channelId: CHANNEL,
337
+ channelTs: TS_14_28,
338
+ eventKind: "reaction",
339
+ reaction: {
340
+ emoji: "👍",
341
+ targetChannelTs: TS_14_25,
342
+ op: "added",
343
+ },
344
+ editedAt: MS_14_30,
345
+ },
346
+ };
347
+ const out = renderSlackTranscript([reaction]);
348
+ const alias = parentAlias(TS_14_25);
349
+ expect(out).toEqual([
350
+ textMsg("user", `[11/14/23 14:28 @bob reacted 👍 to ${alias}]`),
351
+ ]);
352
+ expect(extractTagLineTexts(out)[0].includes("edited")).toBe(false);
353
+ });
354
+
355
+ test("editedAt of 0 (epoch) still renders as 00:00 marker", () => {
356
+ // Defensive: 0 is a valid (if unusual) timestamp and must not be
357
+ // skipped by a truthy check.
358
+ const out = renderSlackTranscript([
359
+ userMsg(TS_14_25, "@alice", "v2", { editedAt: 0 }),
360
+ ]);
361
+ expect(out).toEqual([
362
+ textMsg("user", "[11/14/23 14:25 @alice, edited 01/01/70 00:00]: v2"),
363
+ ]);
364
+ });
365
+ });
366
+
367
+ // ── parent alias stability ───────────────────────────────────────────────────
368
+
369
+ describe("parentAlias", () => {
370
+ test("is stable across calls with the same ts", () => {
371
+ const a = parentAlias("1700000000.000100");
372
+ const b = parentAlias("1700000000.000100");
373
+ expect(a).toEqual(b);
374
+ });
375
+
376
+ test("differs across distinct ts values", () => {
377
+ const a = parentAlias("1700000000.000100");
378
+ const b = parentAlias("1700000000.000200");
379
+ expect(a).not.toEqual(b);
380
+ });
381
+
382
+ test("starts with M and is 7 chars long (M + 6 hex)", () => {
383
+ const a = parentAlias("1700000000.000100");
384
+ expect(a).toMatch(/^M[0-9a-f]{6}$/);
385
+ });
386
+ });
387
+
388
+ // ── isReactionTagLine ────────────────────────────────────────────────────────
389
+
390
+ describe("isReactionTagLine", () => {
391
+ // Pinned to the exact shapes `renderReaction` and the overflow trailer
392
+ // produce. The helper is the public contract that lets consumers
393
+ // re-label the transcript without double-attributing reaction lines,
394
+ // so drift here silently breaks `buildActiveThreadBlockFromRenderable`.
395
+ const alias = parentAlias("1700000000.000100");
396
+
397
+ test("matches reaction-add line", () => {
398
+ expect(
399
+ isReactionTagLine(`[11/14/23 14:28 @bob reacted 👍 to ${alias}]`),
400
+ ).toBe(true);
401
+ });
402
+
403
+ test("matches reaction-remove line", () => {
404
+ expect(
405
+ isReactionTagLine(`[11/14/23 14:28 @bob removed 👍 from ${alias}]`),
406
+ ).toBe(true);
407
+ });
408
+
409
+ test("matches overflow trailer line", () => {
410
+ expect(isReactionTagLine(`[…and 2 more reactions to ${alias}]`)).toBe(true);
411
+ });
412
+
413
+ test("does not match a regular message tag line", () => {
414
+ expect(isReactionTagLine("[11/14/23 14:25 @alice]: hi")).toBe(false);
415
+ });
416
+
417
+ test("does not match content-only assistant output", () => {
418
+ expect(isReactionTagLine("on it. here's the answer")).toBe(false);
419
+ });
420
+
421
+ test("does not match the `[deleted]` sentinel", () => {
422
+ expect(isReactionTagLine("[deleted]")).toBe(false);
423
+ });
424
+
425
+ test("does not match a user-deleted marker", () => {
426
+ expect(
427
+ isReactionTagLine("[11/14/23 14:25 @alice — deleted 11/14/23 14:32]"),
428
+ ).toBe(false);
429
+ });
430
+ });
431
+
432
+ // ── reaction cap ─────────────────────────────────────────────────────────────
433
+
434
+ describe("renderSlackTranscript — reaction cap", () => {
435
+ test("renders all reactions when below the default cap (5)", () => {
436
+ const messages: RenderableSlackMessage[] = [
437
+ userMsg(TS_14_25, "@alice", "hi"),
438
+ reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
439
+ reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
440
+ reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_25),
441
+ ];
442
+ const out = renderSlackTranscript(messages);
443
+ expect(out.length).toBe(4);
444
+ expect(
445
+ extractTagLineTexts(out).some((t) => t.includes("more reactions")),
446
+ ).toBe(false);
447
+ });
448
+
449
+ test("collapses excess reactions into a trailer line", () => {
450
+ const messages: RenderableSlackMessage[] = [
451
+ userMsg(TS_14_25, "@alice", "hi"),
452
+ reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
453
+ reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
454
+ reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_25),
455
+ reactionMsg("1700000800.000004", "@u4", "💯", TS_14_25),
456
+ reactionMsg("1700000800.000005", "@u5", "👏", TS_14_25),
457
+ reactionMsg("1700000800.000006", "@u6", "👀", TS_14_25),
458
+ reactionMsg("1700000800.000007", "@u7", "🚀", TS_14_25),
459
+ ];
460
+ const out = renderSlackTranscript(messages);
461
+ // 1 message + 5 rendered reactions + 1 trailer.
462
+ expect(out.length).toBe(7);
463
+ const trailer = extractTagLineTexts(out)[out.length - 1];
464
+ expect(trailer).toMatch(/…and 2 more reactions to M[0-9a-f]{6}\]/);
465
+ });
466
+
467
+ test("respects custom maxReactionsPerMessage", () => {
468
+ const messages: RenderableSlackMessage[] = [
469
+ userMsg(TS_14_25, "@alice", "hi"),
470
+ reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
471
+ reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
472
+ reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_25),
473
+ ];
474
+ const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 2 });
475
+ // 1 msg + 2 reactions + 1 trailer for 1 excess.
476
+ expect(out.length).toBe(4);
477
+ // Singular "reaction" when excess is exactly 1.
478
+ expect(extractTagLineTexts(out)[out.length - 1]).toMatch(
479
+ /…and 1 more reaction to M[0-9a-f]{6}\]/,
480
+ );
481
+ });
482
+
483
+ test("overflow trailer uses plural 'reactions' when excess > 1", () => {
484
+ const messages: RenderableSlackMessage[] = [
485
+ userMsg(TS_14_25, "@alice", "hi"),
486
+ reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
487
+ reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
488
+ reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_25),
489
+ reactionMsg("1700000800.000004", "@u4", "💯", TS_14_25),
490
+ ];
491
+ const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 2 });
492
+ // 1 msg + 2 reactions + 1 trailer for 2 excess.
493
+ expect(out.length).toBe(4);
494
+ expect(extractTagLineTexts(out)[out.length - 1]).toMatch(
495
+ /…and 2 more reactions to M[0-9a-f]{6}\]/,
496
+ );
497
+ });
498
+
499
+ test("overflow trailer lands in chronological position, before later non-reaction messages", () => {
500
+ // Reactions overflow the cap, then a later message arrives. The trailer
501
+ // must be emitted at the point the overflow window closes — immediately
502
+ // before the later message — so chronology is preserved.
503
+ const alias = parentAlias(TS_14_25);
504
+ const messages: RenderableSlackMessage[] = [
505
+ userMsg(TS_14_25, "@alice", "hi"),
506
+ // Cap is 2 — first two reactions render inline.
507
+ reactionMsg("1699971950.000001", "@u1", "👍", TS_14_25), // 14:25:50
508
+ reactionMsg("1699971955.000002", "@u2", "🎉", TS_14_25), // 14:25:55
509
+ // Next two reactions overflow.
510
+ reactionMsg("1699971960.000003", "@u3", "🔥", TS_14_25), // 14:26
511
+ reactionMsg("1699971965.000004", "@u4", "💯", TS_14_25), // 14:26:05
512
+ // A later top-level message — trailer must land BEFORE this line.
513
+ userMsg(TS_14_30, "@bob", "later"),
514
+ ];
515
+ const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 2 });
516
+ expect(extractTagLineTexts(out)).toEqual([
517
+ "[11/14/23 14:25 @alice]: hi",
518
+ `[11/14/23 14:25 @u1 reacted 👍 to ${alias}]`,
519
+ `[11/14/23 14:25 @u2 reacted 🎉 to ${alias}]`,
520
+ `[…and 2 more reactions to ${alias}]`,
521
+ "[11/14/23 14:30 @bob]: later",
522
+ ]);
523
+ });
524
+
525
+ test("overflow trailer for one target flushes before reaction event on a different target", () => {
526
+ // Two independent reaction streams. The first target overflows, then a
527
+ // reaction arrives for a second target. The first target's trailer must
528
+ // close its window before the second target's reaction is emitted.
529
+ const parentA_ts = "1700000000.000001";
530
+ const parentB_ts = "1700000000.000002";
531
+ const aliasA = parentAlias(parentA_ts);
532
+ const aliasB = parentAlias(parentB_ts);
533
+ const messages: RenderableSlackMessage[] = [
534
+ userMsg(parentA_ts, "@alice", "A"),
535
+ userMsg(parentB_ts, "@bob", "B"),
536
+ // Overflow the cap on A.
537
+ reactionMsg("1700000100.000001", "@u1", "👍", parentA_ts),
538
+ reactionMsg("1700000100.000002", "@u2", "🎉", parentA_ts),
539
+ reactionMsg("1700000100.000003", "@u3", "🔥", parentA_ts), // excess 1
540
+ reactionMsg("1700000100.000004", "@u4", "💯", parentA_ts), // excess 2
541
+ // Reaction on B arrives chronologically after the overflow — A's
542
+ // trailer should flush here, before B's reaction renders.
543
+ reactionMsg("1700000100.000005", "@u5", "👏", parentB_ts),
544
+ ];
545
+ const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 2 });
546
+ expect(extractTagLineTexts(out)).toEqual([
547
+ "[11/14/23 22:13 @alice]: A",
548
+ "[11/14/23 22:13 @bob]: B",
549
+ `[11/14/23 22:15 @u1 reacted 👍 to ${aliasA}]`,
550
+ `[11/14/23 22:15 @u2 reacted 🎉 to ${aliasA}]`,
551
+ `[…and 2 more reactions to ${aliasA}]`,
552
+ `[11/14/23 22:15 @u5 reacted 👏 to ${aliasB}]`,
553
+ ]);
554
+ });
555
+
556
+ test("caps are tracked per-target message independently", () => {
557
+ const messages: RenderableSlackMessage[] = [
558
+ userMsg(TS_14_25, "@alice", "first"),
559
+ userMsg(TS_14_26, "@alice", "second"),
560
+ // 2 reactions on first
561
+ reactionMsg("1700000800.000001", "@u1", "👍", TS_14_25),
562
+ reactionMsg("1700000800.000002", "@u2", "🎉", TS_14_25),
563
+ // 2 reactions on second
564
+ reactionMsg("1700000800.000003", "@u3", "🔥", TS_14_26),
565
+ reactionMsg("1700000800.000004", "@u4", "💯", TS_14_26),
566
+ ];
567
+ const out = renderSlackTranscript(messages, { maxReactionsPerMessage: 5 });
568
+ // 2 messages + 4 reactions, no trailers.
569
+ expect(out.length).toBe(6);
570
+ expect(
571
+ extractTagLineTexts(out).some((t) => t.includes("more reactions")),
572
+ ).toBe(false);
573
+ });
574
+ });
575
+
576
+ // ── mixed message + reaction chronology ─────────────────────────────────────
577
+
578
+ describe("renderSlackTranscript — mixed message + reaction chronology", () => {
579
+ test("reaction renders inline at correct chronological position", () => {
580
+ // Order of events as they happened in time:
581
+ // 14:25 — alice posts the parent
582
+ // 14:26 — bob posts a follow-up message
583
+ // 14:28 — carol reacts to alice's parent
584
+ // 14:30 — dan posts another message
585
+ // Inputs are intentionally shuffled so the renderer must sort.
586
+ const aliasParent = parentAlias(TS_14_25);
587
+ const out = renderSlackTranscript([
588
+ reactionMsg(TS_14_28, "@carol", "👍", TS_14_25, "added"),
589
+ userMsg(TS_14_30, "@dan", "later"),
590
+ userMsg(TS_14_25, "@alice", "lunch?"),
591
+ userMsg(TS_14_26, "@bob", "yes"),
592
+ ]);
593
+ expect(extractTagLineTexts(out)).toEqual([
594
+ "[11/14/23 14:25 @alice]: lunch?",
595
+ "[11/14/23 14:26 @bob]: yes",
596
+ `[11/14/23 14:28 @carol reacted 👍 to ${aliasParent}]`,
597
+ "[11/14/23 14:30 @dan]: later",
598
+ ]);
599
+ });
600
+
601
+ test("removed reactions interleave with messages by their own ts", () => {
602
+ // A reaction is added at 14:26 then removed at 14:30; bob posts a message
603
+ // at 14:28 in between. The "removed" line must land after bob's message,
604
+ // not collapsed beside the "added" line.
605
+ const aliasParent = parentAlias(TS_14_25);
606
+ const out = renderSlackTranscript([
607
+ userMsg(TS_14_25, "@alice", "lunch?"),
608
+ reactionMsg("1699971960.000010", "@carol", "👍", TS_14_25, "added"),
609
+ userMsg(TS_14_28, "@bob", "yes"),
610
+ reactionMsg(TS_14_30, "@carol", "👍", TS_14_25, "removed"),
611
+ ]);
612
+ expect(extractTagLineTexts(out)).toEqual([
613
+ "[11/14/23 14:25 @alice]: lunch?",
614
+ `[11/14/23 14:26 @carol reacted 👍 to ${aliasParent}]`,
615
+ "[11/14/23 14:28 @bob]: yes",
616
+ `[11/14/23 14:30 @carol removed 👍 from ${aliasParent}]`,
617
+ ]);
618
+ });
619
+ });
620
+
621
+ // ── sort stability ───────────────────────────────────────────────────────────
622
+
623
+ describe("renderSlackTranscript — sort", () => {
624
+ test("orders chronologically by channelTs", () => {
625
+ const out = renderSlackTranscript([
626
+ userMsg(TS_14_30, "@late", "later"),
627
+ userMsg(TS_14_25, "@early", "earlier"),
628
+ userMsg(TS_14_28, "@mid", "middle"),
629
+ ]);
630
+ expect(extractTagLineTexts(out)).toEqual([
631
+ "[11/14/23 14:25 @early]: earlier",
632
+ "[11/14/23 14:28 @mid]: middle",
633
+ "[11/14/23 14:30 @late]: later",
634
+ ]);
635
+ });
636
+
637
+ test("preserves input order when sort keys are identical (stable sort)", () => {
638
+ const sameTs = TS_14_25;
639
+ const out = renderSlackTranscript([
640
+ userMsg(sameTs, "@first", "1"),
641
+ userMsg(sameTs, "@second", "2"),
642
+ userMsg(sameTs, "@third", "3"),
643
+ ]);
644
+ expect(extractTagLineTexts(out)).toEqual([
645
+ "[11/14/23 14:25 @first]: 1",
646
+ "[11/14/23 14:25 @second]: 2",
647
+ "[11/14/23 14:25 @third]: 3",
648
+ ]);
649
+ });
650
+ });
651
+
652
+ // ── design brief scenarios ───────────────────────────────────────────────────
653
+
654
+ describe("renderSlackTranscript — four design-brief scenarios", () => {
655
+ // Setup: a top-level @alice message at 14:25; a sibling @carol top-level
656
+ // at 14:28; two replies in @alice's thread.
657
+ const aliceTopTs = TS_14_25;
658
+ const carolTopTs = TS_14_28;
659
+ const bobReply1Ts = "1699971960.000300"; // 14:26
660
+ const aliceReply2Ts = "1699972020.000400"; // 14:27
661
+
662
+ function baseFixture(): RenderableSlackMessage[] {
663
+ return [
664
+ userMsg(aliceTopTs, "@alice", "lunch?"),
665
+ userMsg(bobReply1Ts, "@bob", "yes!", { threadTs: aliceTopTs }),
666
+ userMsg(aliceReply2Ts, "@alice", "12:30 ok?", { threadTs: aliceTopTs }),
667
+ userMsg(carolTopTs, "@carol", "standup soon"),
668
+ ];
669
+ }
670
+
671
+ test("scenario: reply in an existing thread", () => {
672
+ const replyTs = "1699972100.000500"; // 14:28:20 — after carol's top
673
+ const messages = [
674
+ ...baseFixture(),
675
+ userMsg(replyTs, "@dan", "I'll join", { threadTs: aliceTopTs }),
676
+ ];
677
+ const out = renderSlackTranscript(messages);
678
+ const aliceAlias = parentAlias(aliceTopTs);
679
+ expect(extractTagLineTexts(out)).toEqual([
680
+ "[11/14/23 14:25 @alice]: lunch?",
681
+ `[11/14/23 14:26 @bob → ${aliceAlias}]: yes!`,
682
+ `[11/14/23 14:27 @alice → ${aliceAlias}]: 12:30 ok?`,
683
+ "[11/14/23 14:28 @carol]: standup soon",
684
+ `[11/14/23 14:28 @dan → ${aliceAlias}]: I'll join`,
685
+ ]);
686
+ });
687
+
688
+ test("scenario: reply to a top-level message (creating a new thread)", () => {
689
+ // @ed replies to @carol's top-level message; carol's top becomes a thread.
690
+ const replyTs = "1699972100.000600"; // 14:28:20
691
+ const messages = [
692
+ ...baseFixture(),
693
+ userMsg(replyTs, "@ed", "joining now", { threadTs: carolTopTs }),
694
+ ];
695
+ const out = renderSlackTranscript(messages);
696
+ const carolAlias = parentAlias(carolTopTs);
697
+ const texts = extractTagLineTexts(out);
698
+ // The reply tag points at carol's alias; carol's top stays untagged.
699
+ expect(texts[texts.length - 1]).toBe(
700
+ `[11/14/23 14:28 @ed → ${carolAlias}]: joining now`,
701
+ );
702
+ expect(texts[3]).toBe("[11/14/23 14:28 @carol]: standup soon");
703
+ });
704
+
705
+ test("scenario: reply to the most recent top-level message", () => {
706
+ // Same as above but emphasises the "last message" case.
707
+ const replyTs = "1699972110.000700"; // 14:28:30
708
+ const messages = [
709
+ ...baseFixture(),
710
+ userMsg(replyTs, "@frank", "+1", { threadTs: carolTopTs }),
711
+ ];
712
+ const out = renderSlackTranscript(messages);
713
+ const carolAlias = parentAlias(carolTopTs);
714
+ const texts = extractTagLineTexts(out);
715
+ expect(texts[texts.length - 1]).toBe(
716
+ `[11/14/23 14:28 @frank → ${carolAlias}]: +1`,
717
+ );
718
+ });
719
+
720
+ test("scenario: new top-level message (no threadTs)", () => {
721
+ const messages = [
722
+ ...baseFixture(),
723
+ userMsg("1699972260.000800", "@gina", "anyone in office?"), // 14:31
724
+ ];
725
+ const out = renderSlackTranscript(messages);
726
+ const texts = extractTagLineTexts(out);
727
+ // No arrow on the new top-level row.
728
+ expect(texts[texts.length - 1]).toBe(
729
+ "[11/14/23 14:31 @gina]: anyone in office?",
730
+ );
731
+ });
732
+ });
733
+
734
+ // ── mixed legacy + post-upgrade fixture ──────────────────────────────────────
735
+
736
+ describe("renderSlackTranscript — mixed legacy + post-upgrade", () => {
737
+ test("legacy rows render flat with no thread tag and intermix chronologically", () => {
738
+ const messages: RenderableSlackMessage[] = [
739
+ // Post-upgrade: 14:28 reply in alice's thread
740
+ userMsg("1699972080.000900", "@bob", "yes!", { threadTs: TS_14_25 }),
741
+ // Legacy row at 14:26 — should sort BETWEEN the 14:25 post-upgrade
742
+ // top-level and the 14:28 post-upgrade reply.
743
+ legacyMsg(1699971960_000, "@dana", "drive-by note"),
744
+ // Post-upgrade: 14:25 alice top-level
745
+ userMsg(TS_14_25, "@alice", "lunch?"),
746
+ ];
747
+ const out = renderSlackTranscript(messages);
748
+ const alias = parentAlias(TS_14_25);
749
+
750
+ const texts = extractTagLineTexts(out);
751
+ expect(texts).toEqual([
752
+ "[11/14/23 14:25 @alice]: lunch?",
753
+ "[11/14/23 14:26 @dana]: drive-by note",
754
+ `[11/14/23 14:28 @bob → ${alias}]: yes!`,
755
+ ]);
756
+ // Ensure the legacy row has no arrow.
757
+ expect(texts[1].includes("→")).toBe(false);
758
+ });
759
+
760
+ test("legacy assistant row carries assistant role and emits content verbatim", () => {
761
+ const out = renderSlackTranscript([
762
+ legacyMsg(MS_14_25, "@bot", "ack", "assistant"),
763
+ ]);
764
+ expect(out).toEqual([textMsg("assistant", "ack")]);
765
+ });
766
+
767
+ test("preserves message role faithfully across mixed inputs", () => {
768
+ const out = renderSlackTranscript([
769
+ userMsg(TS_14_25, "@alice", "q?"),
770
+ userMsg(TS_14_26, "@bot", "a", { role: "assistant" }),
771
+ legacyMsg(MS_14_30, "@bot", "later legacy", "assistant"),
772
+ ]);
773
+ expect(out.map((r) => r.role)).toEqual(["user", "assistant", "assistant"]);
774
+ });
775
+ });
776
+
777
+ // ── purity ────────────────────────────────────────────────────────────────────
778
+
779
+ describe("renderSlackTranscript — purity", () => {
780
+ test("does not mutate the input array or its elements", () => {
781
+ const original: RenderableSlackMessage[] = [
782
+ userMsg(TS_14_30, "@late", "later"),
783
+ userMsg(TS_14_25, "@early", "earlier"),
784
+ ];
785
+ const snapshot = original.map((m) => ({ ...m, metadata: m.metadata }));
786
+ renderSlackTranscript(original);
787
+ expect(original.length).toBe(snapshot.length);
788
+ for (let i = 0; i < original.length; i++) {
789
+ expect(original[i].content).toBe(snapshot[i].content);
790
+ expect(original[i].senderLabel).toBe(snapshot[i].senderLabel);
791
+ expect(original[i].metadata).toBe(snapshot[i].metadata);
792
+ }
793
+ });
794
+
795
+ test("identical inputs produce identical outputs (deterministic)", () => {
796
+ const fixture: RenderableSlackMessage[] = [
797
+ userMsg(TS_14_25, "@alice", "hi"),
798
+ userMsg(TS_14_28, "@bob", "yo", { threadTs: TS_14_25 }),
799
+ reactionMsg(TS_14_30, "@carol", "👍", TS_14_25),
800
+ ];
801
+ const a = renderSlackTranscript(fixture);
802
+ const b = renderSlackTranscript(fixture);
803
+ expect(a).toEqual(b);
804
+ });
805
+ });
806
+
807
+ // ── shape: Message[] / content-block structure ───────────────────────────────
808
+
809
+ describe("renderSlackTranscript — Message[] shape", () => {
810
+ test("empty input returns an empty array", () => {
811
+ expect(renderSlackTranscript([])).toEqual([]);
812
+ });
813
+
814
+ test("single text message returns one Message with one text content block", () => {
815
+ const out = renderSlackTranscript([userMsg(TS_14_25, "@alice", "hi")]);
816
+ expect(out).toEqual([
817
+ {
818
+ role: "user",
819
+ content: [{ type: "text", text: "[11/14/23 14:25 @alice]: hi" }],
820
+ },
821
+ ]);
822
+ });
823
+
824
+ test("stable sort: messages with identical channelTs preserve input order", () => {
825
+ const sameTs = TS_14_25;
826
+ const out = renderSlackTranscript([
827
+ userMsg(sameTs, "@first", "1"),
828
+ userMsg(sameTs, "@second", "2"),
829
+ ]);
830
+ expect(out).toEqual([
831
+ {
832
+ role: "user",
833
+ content: [{ type: "text", text: "[11/14/23 14:25 @first]: 1" }],
834
+ },
835
+ {
836
+ role: "user",
837
+ content: [{ type: "text", text: "[11/14/23 14:25 @second]: 2" }],
838
+ },
839
+ ]);
840
+ });
841
+ });
842
+
843
+ // ── extractTagLineTexts helper ───────────────────────────────────────────────
844
+
845
+ describe("extractTagLineTexts", () => {
846
+ test("returns first text block text per message", () => {
847
+ const rendered: Message[] = [
848
+ { role: "user", content: [{ type: "text", text: "line-a" }] },
849
+ { role: "assistant", content: [{ type: "text", text: "line-b" }] },
850
+ ];
851
+ expect(extractTagLineTexts(rendered)).toEqual(["line-a", "line-b"]);
852
+ });
853
+
854
+ test("returns empty string for a message with no text block", () => {
855
+ const rendered: Message[] = [
856
+ { role: "user", content: [{ type: "text", text: "only text" }] },
857
+ // A message whose content has no text block at all (e.g. solely a
858
+ // tool_use/tool_result). The helper must emit "" rather than throw.
859
+ {
860
+ role: "assistant",
861
+ content: [
862
+ {
863
+ type: "tool_use",
864
+ id: "t1",
865
+ name: "noop",
866
+ input: {},
867
+ },
868
+ ],
869
+ },
870
+ ];
871
+ expect(extractTagLineTexts(rendered)).toEqual(["only text", ""]);
872
+ });
873
+
874
+ test("picks the first text block when multiple text blocks are present", () => {
875
+ const rendered: Message[] = [
876
+ {
877
+ role: "user",
878
+ content: [
879
+ { type: "text", text: "first" },
880
+ { type: "text", text: "second" },
881
+ ],
882
+ },
883
+ ];
884
+ expect(extractTagLineTexts(rendered)).toEqual(["first"]);
885
+ });
886
+
887
+ test("returns empty array for empty input", () => {
888
+ expect(extractTagLineTexts([])).toEqual([]);
889
+ });
890
+ });
891
+
892
+ // ── contentBlocks preservation ───────────────────────────────────────────────
893
+
894
+ describe("renderSlackTranscript — replayable content-block preservation", () => {
895
+ // When `contentBlocks` is populated, the renderer preserves replayable
896
+ // Anthropic blocks (tool_use, tool_result, thinking, redacted_thinking,
897
+ // image, file) verbatim alongside the tag line. Non-replayable blocks
898
+ // (ui_surface, server_tool_use, web_search_tool_result, unknown types) are
899
+ // stripped. Legacy rows (no contentBlocks field) render as a single text
900
+ // block.
901
+
902
+ test("[text, tool_use] assistant row preserves tool_use after tag line", () => {
903
+ // Assistant tool_use is paired with a follow-up user tool_result so the
904
+ // orphan-pair filter leaves both blocks intact.
905
+ const assistantRow: RenderableSlackMessage = {
906
+ ...userMsg(TS_14_25, null, "looking it up", {
907
+ role: "assistant",
908
+ }),
909
+ contentBlocks: [
910
+ { type: "text", text: "looking it up" },
911
+ { type: "tool_use", id: "tu_1", name: "search", input: { q: "x" } },
912
+ ],
913
+ };
914
+ const userRow: RenderableSlackMessage = {
915
+ ...userMsg(TS_14_26, "@alice", ""),
916
+ contentBlocks: [
917
+ { type: "tool_result", tool_use_id: "tu_1", content: "result text" },
918
+ ],
919
+ };
920
+ const out = renderSlackTranscript([assistantRow, userRow]);
921
+ expect(out[0]).toEqual({
922
+ role: "assistant",
923
+ content: [
924
+ { type: "text", text: "looking it up" },
925
+ { type: "tool_use", id: "tu_1", name: "search", input: { q: "x" } },
926
+ ],
927
+ });
928
+ });
929
+
930
+ test("[tool_result] user row emits only tool_result — no tag line", () => {
931
+ // Pair the user tool_result with a preceding assistant tool_use so the
932
+ // orphan-pair filter leaves the row intact; the assertion still pins
933
+ // the shape of the user row specifically (no tag line, single block).
934
+ const assistantRow: RenderableSlackMessage = {
935
+ ...userMsg(TS_14_24, null, "", { role: "assistant" }),
936
+ contentBlocks: [
937
+ { type: "tool_use", id: "tu_1", name: "search", input: {} },
938
+ ],
939
+ };
940
+ const userRow: RenderableSlackMessage = {
941
+ ...userMsg(TS_14_25, "@alice", ""),
942
+ contentBlocks: [
943
+ { type: "tool_result", tool_use_id: "tu_1", content: "result text" },
944
+ ],
945
+ };
946
+ const out = renderSlackTranscript([assistantRow, userRow]);
947
+ // Pin the second (user) row's shape — this is what the test is about.
948
+ expect(out[1]).toEqual({
949
+ role: "user",
950
+ content: [
951
+ { type: "tool_result", tool_use_id: "tu_1", content: "result text" },
952
+ ],
953
+ });
954
+ });
955
+
956
+ test("[thinking, text] assistant row preserves thinking before tag line (order preserved)", () => {
957
+ const base: RenderableSlackMessage = {
958
+ ...userMsg(TS_14_25, null, "here's the answer", {
959
+ role: "assistant",
960
+ }),
961
+ contentBlocks: [
962
+ { type: "thinking", thinking: "let me think", signature: "sig-abc" },
963
+ { type: "text", text: "here's the answer" },
964
+ ],
965
+ };
966
+ const out = renderSlackTranscript([base]);
967
+ expect(out).toEqual([
968
+ {
969
+ role: "assistant",
970
+ content: [
971
+ { type: "thinking", thinking: "let me think", signature: "sig-abc" },
972
+ { type: "text", text: "here's the answer" },
973
+ ],
974
+ },
975
+ ]);
976
+ });
977
+
978
+ test("[text, tool_use, tool_result] assistant row (degenerate) preserves order", () => {
979
+ // Degenerate but possible via the cleanAssistantContent path — rows
980
+ // that carry both tool_use and tool_result in the same message. The
981
+ // renderer passes them through in order.
982
+ const base: RenderableSlackMessage = {
983
+ ...userMsg(TS_14_25, null, "doing a thing", {
984
+ role: "assistant",
985
+ }),
986
+ contentBlocks: [
987
+ { type: "text", text: "doing a thing" },
988
+ { type: "tool_use", id: "tu_A", name: "op", input: {} },
989
+ { type: "tool_result", tool_use_id: "tu_A", content: "ok" },
990
+ ],
991
+ };
992
+ const out = renderSlackTranscript([base]);
993
+ expect(out).toEqual([
994
+ {
995
+ role: "assistant",
996
+ content: [
997
+ { type: "text", text: "doing a thing" },
998
+ { type: "tool_use", id: "tu_A", name: "op", input: {} },
999
+ { type: "tool_result", tool_use_id: "tu_A", content: "ok" },
1000
+ ],
1001
+ },
1002
+ ]);
1003
+ });
1004
+
1005
+ test("[text, ui_surface] assistant row strips ui_surface — only content remains", () => {
1006
+ const base: RenderableSlackMessage = {
1007
+ ...userMsg(TS_14_25, null, "reply body", { role: "assistant" }),
1008
+ contentBlocks: [
1009
+ { type: "text", text: "reply body" },
1010
+ // ui_surface is local-only UI scaffolding and must not leak into
1011
+ // the replayable output. Typed as a generic shape here because
1012
+ // ui_surface is not part of the Anthropic ContentBlock union.
1013
+ { type: "ui_surface", foo: "bar" } as unknown as never,
1014
+ ] as never,
1015
+ };
1016
+ const out = renderSlackTranscript([base]);
1017
+ expect(out).toEqual([
1018
+ {
1019
+ role: "assistant",
1020
+ content: [{ type: "text", text: "reply body" }],
1021
+ },
1022
+ ]);
1023
+ });
1024
+
1025
+ test("[text, server_tool_use] assistant row strips server_tool_use (unknown to replay)", () => {
1026
+ const base: RenderableSlackMessage = {
1027
+ ...userMsg(TS_14_25, null, "web search", { role: "assistant" }),
1028
+ contentBlocks: [
1029
+ { type: "text", text: "web search" },
1030
+ {
1031
+ type: "server_tool_use",
1032
+ id: "st_1",
1033
+ name: "web_search",
1034
+ input: { q: "x" },
1035
+ },
1036
+ ],
1037
+ };
1038
+ const out = renderSlackTranscript([base]);
1039
+ expect(out).toEqual([
1040
+ {
1041
+ role: "assistant",
1042
+ content: [{ type: "text", text: "web search" }],
1043
+ },
1044
+ ]);
1045
+ });
1046
+
1047
+ test("[image, text] user row preserves image before tag line (order preserved)", () => {
1048
+ const base: RenderableSlackMessage = {
1049
+ ...userMsg(TS_14_25, "@alice", "check this out"),
1050
+ contentBlocks: [
1051
+ {
1052
+ type: "image",
1053
+ source: {
1054
+ type: "base64",
1055
+ media_type: "image/png",
1056
+ data: "base64data==",
1057
+ },
1058
+ },
1059
+ { type: "text", text: "check this out" },
1060
+ ],
1061
+ };
1062
+ const out = renderSlackTranscript([base]);
1063
+ expect(out).toEqual([
1064
+ {
1065
+ role: "user",
1066
+ content: [
1067
+ {
1068
+ type: "image",
1069
+ source: {
1070
+ type: "base64",
1071
+ media_type: "image/png",
1072
+ data: "base64data==",
1073
+ },
1074
+ },
1075
+ { type: "text", text: "[11/14/23 14:25 @alice]: check this out" },
1076
+ ],
1077
+ },
1078
+ ]);
1079
+ });
1080
+
1081
+ test("deleted row with [text, tool_use] contentBlocks emits only the deleted tag line", () => {
1082
+ const base: RenderableSlackMessage = {
1083
+ ...userMsg(TS_14_25, "@alice", "(removed)", { deletedAt: MS_14_32 }),
1084
+ contentBlocks: [
1085
+ { type: "text", text: "old body" },
1086
+ { type: "tool_use", id: "tu_zombie", name: "op", input: {} },
1087
+ ],
1088
+ };
1089
+ const out = renderSlackTranscript([base]);
1090
+ expect(out).toEqual([
1091
+ {
1092
+ role: "user",
1093
+ content: [
1094
+ {
1095
+ type: "text",
1096
+ text: "[11/14/23 14:25 @alice — deleted 11/14/23 14:32]",
1097
+ },
1098
+ ],
1099
+ },
1100
+ ]);
1101
+ });
1102
+
1103
+ test("row with only non-replayable blocks emits fallback tag line annotated with what was stripped", () => {
1104
+ // Rows whose only content blocks are non-replayable (e.g. `server_tool_use`,
1105
+ // `ui_surface`) must still produce a turn so chronology and adjacent
1106
+ // tool_result context are preserved. `buildMessageContentBlocks` emits a
1107
+ // single fallback text block whose tag line names each stripped block's
1108
+ // type (and tool name, when available).
1109
+ const base: RenderableSlackMessage = {
1110
+ ...userMsg(TS_14_25, null, "ran a web search", { role: "assistant" }),
1111
+ contentBlocks: [
1112
+ {
1113
+ type: "server_tool_use",
1114
+ id: "st_1",
1115
+ name: "web_search",
1116
+ input: { q: "x" },
1117
+ },
1118
+ { type: "ui_surface", foo: "bar" } as unknown as never,
1119
+ ] as never,
1120
+ };
1121
+ const out = renderSlackTranscript([base]);
1122
+ expect(out).toEqual([
1123
+ {
1124
+ role: "assistant",
1125
+ content: [
1126
+ {
1127
+ type: "text",
1128
+ text: "ran a web search [stripped non-replayable: server_tool_use(web_search), ui_surface]",
1129
+ },
1130
+ ],
1131
+ },
1132
+ ]);
1133
+ });
1134
+
1135
+ test("legacy row (contentBlocks undefined) renders as single tag line — unchanged", () => {
1136
+ const base = userMsg(TS_14_25, "@alice", "legacy plain");
1137
+ // No `contentBlocks` field assigned — emulates a row whose JSON content
1138
+ // failed to parse or predates the plumbing.
1139
+ expect(base.contentBlocks).toBeUndefined();
1140
+ const out = renderSlackTranscript([base]);
1141
+ expect(out).toEqual([
1142
+ textMsg("user", "[11/14/23 14:25 @alice]: legacy plain"),
1143
+ ]);
1144
+ });
1145
+
1146
+ test("legacy row with empty contentBlocks array also falls back to single tag line", () => {
1147
+ const base: RenderableSlackMessage = {
1148
+ ...userMsg(TS_14_25, "@alice", "empty blocks"),
1149
+ contentBlocks: [],
1150
+ };
1151
+ const out = renderSlackTranscript([base]);
1152
+ expect(out).toEqual([
1153
+ textMsg("user", "[11/14/23 14:25 @alice]: empty blocks"),
1154
+ ]);
1155
+ });
1156
+
1157
+ test("reaction row ignores contentBlocks and renders the single reaction tag line", () => {
1158
+ // Reactions go through the reaction path and never touch the
1159
+ // replayable-block preservation. Even if contentBlocks were somehow
1160
+ // populated on a reaction row, the tool blocks must not leak through.
1161
+ const reactionBase = reactionMsg(TS_14_28, "@bob", "👍", TS_14_25, "added");
1162
+ const withBlocks: RenderableSlackMessage = {
1163
+ ...reactionBase,
1164
+ contentBlocks: [
1165
+ { type: "tool_use", id: "tu_stray", name: "op", input: {} },
1166
+ ],
1167
+ };
1168
+ const out = renderSlackTranscript([withBlocks]);
1169
+ const alias = parentAlias(TS_14_25);
1170
+ expect(out).toEqual([
1171
+ textMsg("user", `[11/14/23 14:28 @bob reacted 👍 to ${alias}]`),
1172
+ ]);
1173
+ });
1174
+ });
1175
+
1176
+ // ── orphan tool_use / tool_result filter ─────────────────────────────────────
1177
+
1178
+ describe("renderSlackTranscript — orphan tool_use / tool_result filter", () => {
1179
+ // A final safety pass strips any tool_use without a matching tool_result
1180
+ // (and vice versa) before returning. Messages that become empty after
1181
+ // filtering are dropped entirely so the caller never sees
1182
+ // `{role, content: []}`.
1183
+
1184
+ test("orphan tool_use is dropped; surrounding tag line survives", () => {
1185
+ // Assistant row has [text, tool_use] but no follower tool_result exists
1186
+ // anywhere in the transcript. The tool_use must be stripped; the tag
1187
+ // line (derived from the text block) stays.
1188
+ const base: RenderableSlackMessage = {
1189
+ ...userMsg(TS_14_25, null, "looking it up", {
1190
+ role: "assistant",
1191
+ }),
1192
+ contentBlocks: [
1193
+ { type: "text", text: "looking it up" },
1194
+ {
1195
+ type: "tool_use",
1196
+ id: "tu_orphan",
1197
+ name: "search",
1198
+ input: { q: "x" },
1199
+ },
1200
+ ],
1201
+ };
1202
+ const out = renderSlackTranscript([base]);
1203
+ expect(out).toEqual([
1204
+ {
1205
+ role: "assistant",
1206
+ content: [{ type: "text", text: "looking it up" }],
1207
+ },
1208
+ ]);
1209
+ });
1210
+
1211
+ test("orphan tool_result is dropped; other content on the user row survives", () => {
1212
+ // User row with [tool_result (orphan), text]. The orphan tool_result is
1213
+ // stripped and the tag line derived from the text block survives.
1214
+ const base: RenderableSlackMessage = {
1215
+ ...userMsg(TS_14_25, "@alice", "follow up"),
1216
+ contentBlocks: [
1217
+ {
1218
+ type: "tool_result",
1219
+ tool_use_id: "tu_missing",
1220
+ content: "stale result",
1221
+ },
1222
+ { type: "text", text: "follow up" },
1223
+ ],
1224
+ };
1225
+ const out = renderSlackTranscript([base]);
1226
+ expect(out).toEqual([
1227
+ {
1228
+ role: "user",
1229
+ content: [{ type: "text", text: "[11/14/23 14:25 @alice]: follow up" }],
1230
+ },
1231
+ ]);
1232
+ });
1233
+
1234
+ test("fully-paired tool_use/tool_result — both preserved", () => {
1235
+ const assistantRow: RenderableSlackMessage = {
1236
+ ...userMsg(TS_14_25, null, "running op", { role: "assistant" }),
1237
+ contentBlocks: [
1238
+ { type: "text", text: "running op" },
1239
+ { type: "tool_use", id: "tu_paired", name: "op", input: { a: 1 } },
1240
+ ],
1241
+ };
1242
+ const userRow: RenderableSlackMessage = {
1243
+ ...userMsg(TS_14_26, "@alice", ""),
1244
+ contentBlocks: [
1245
+ {
1246
+ type: "tool_result",
1247
+ tool_use_id: "tu_paired",
1248
+ content: "ok",
1249
+ },
1250
+ ],
1251
+ };
1252
+ const out = renderSlackTranscript([assistantRow, userRow]);
1253
+ expect(out).toEqual([
1254
+ {
1255
+ role: "assistant",
1256
+ content: [
1257
+ { type: "text", text: "running op" },
1258
+ { type: "tool_use", id: "tu_paired", name: "op", input: { a: 1 } },
1259
+ ],
1260
+ },
1261
+ {
1262
+ role: "user",
1263
+ content: [
1264
+ { type: "tool_result", tool_use_id: "tu_paired", content: "ok" },
1265
+ ],
1266
+ },
1267
+ ]);
1268
+ });
1269
+
1270
+ test("message that becomes empty after filtering is dropped entirely", () => {
1271
+ // Pure tool-only user row whose tool_result has no matching tool_use.
1272
+ // After filtering the row is empty and must NOT be emitted as
1273
+ // `{role, content: []}` — it must be dropped so downstream consumers
1274
+ // never see an empty-content message.
1275
+ const orphanResultRow: RenderableSlackMessage = {
1276
+ ...userMsg(TS_14_25, "@alice", ""),
1277
+ contentBlocks: [
1278
+ {
1279
+ type: "tool_result",
1280
+ tool_use_id: "tu_missing",
1281
+ content: "stale",
1282
+ },
1283
+ ],
1284
+ };
1285
+ // A normal neighbour row to confirm we don't accidentally drop it too.
1286
+ const neighbour: RenderableSlackMessage = userMsg(TS_14_26, "@bob", "hi");
1287
+ const out = renderSlackTranscript([orphanResultRow, neighbour]);
1288
+ expect(out).toEqual([textMsg("user", "[11/14/23 14:26 @bob]: hi")]);
1289
+ // Sanity: the output contains no {role, content: []} placeholder.
1290
+ for (const m of out) {
1291
+ expect(m.content.length).toBeGreaterThan(0);
1292
+ }
1293
+ });
1294
+
1295
+ test("filter is idempotent: re-rendering the same input yields the same output", () => {
1296
+ // The function signature is `renderSlackTranscript(RenderableSlackMessage[])
1297
+ // -> Message[]`. Idempotence here means: rendering the same input twice
1298
+ // produces the same output. A mixed fixture exercises the paired path,
1299
+ // the orphan-tool_use drop path, and the orphan-tool_result drop path
1300
+ // in a single run.
1301
+ const fixture: RenderableSlackMessage[] = [
1302
+ // Paired tool call.
1303
+ {
1304
+ ...userMsg(TS_14_25, null, "running op", { role: "assistant" }),
1305
+ contentBlocks: [
1306
+ { type: "text", text: "running op" },
1307
+ { type: "tool_use", id: "tu_paired", name: "op", input: {} },
1308
+ ],
1309
+ },
1310
+ {
1311
+ ...userMsg(TS_14_26, "@alice", ""),
1312
+ contentBlocks: [
1313
+ { type: "tool_result", tool_use_id: "tu_paired", content: "ok" },
1314
+ ],
1315
+ },
1316
+ // Orphan tool_use on the assistant side.
1317
+ {
1318
+ ...userMsg(TS_14_28, null, "looking", { role: "assistant" }),
1319
+ contentBlocks: [
1320
+ { type: "text", text: "looking" },
1321
+ { type: "tool_use", id: "tu_orphan", name: "op", input: {} },
1322
+ ],
1323
+ },
1324
+ // Orphan tool_result on the user side.
1325
+ {
1326
+ ...userMsg(TS_14_30, "@alice", "stray"),
1327
+ contentBlocks: [
1328
+ {
1329
+ type: "tool_result",
1330
+ tool_use_id: "tu_missing",
1331
+ content: "stale",
1332
+ },
1333
+ { type: "text", text: "stray" },
1334
+ ],
1335
+ },
1336
+ ];
1337
+ const a = renderSlackTranscript(fixture);
1338
+ const b = renderSlackTranscript(fixture);
1339
+ expect(a).toEqual(b);
1340
+
1341
+ // And confirm the expected shape explicitly so the idempotence claim is
1342
+ // grounded in the actual filter behaviour (paired kept, orphans stripped).
1343
+ expect(a).toEqual([
1344
+ {
1345
+ role: "assistant",
1346
+ content: [
1347
+ { type: "text", text: "running op" },
1348
+ { type: "tool_use", id: "tu_paired", name: "op", input: {} },
1349
+ ],
1350
+ },
1351
+ {
1352
+ role: "user",
1353
+ content: [
1354
+ { type: "tool_result", tool_use_id: "tu_paired", content: "ok" },
1355
+ ],
1356
+ },
1357
+ {
1358
+ role: "assistant",
1359
+ content: [{ type: "text", text: "looking" }],
1360
+ },
1361
+ {
1362
+ role: "user",
1363
+ content: [{ type: "text", text: "[11/14/23 14:30 @alice]: stray" }],
1364
+ },
1365
+ ]);
1366
+ });
1367
+
1368
+ test("filter does not touch thinking, image, file, or text blocks", () => {
1369
+ const base: RenderableSlackMessage = {
1370
+ ...userMsg(TS_14_25, null, "here you go", { role: "assistant" }),
1371
+ contentBlocks: [
1372
+ { type: "thinking", thinking: "ponder", signature: "sig" },
1373
+ {
1374
+ type: "image",
1375
+ source: {
1376
+ type: "base64",
1377
+ media_type: "image/png",
1378
+ data: "b64==",
1379
+ },
1380
+ },
1381
+ {
1382
+ type: "file",
1383
+ source: {
1384
+ type: "base64",
1385
+ media_type: "application/pdf",
1386
+ data: "pdfbase64==",
1387
+ filename: "doc.pdf",
1388
+ },
1389
+ },
1390
+ { type: "text", text: "here you go" },
1391
+ ],
1392
+ };
1393
+ const out = renderSlackTranscript([base]);
1394
+ expect(out).toEqual([
1395
+ {
1396
+ role: "assistant",
1397
+ content: [
1398
+ { type: "thinking", thinking: "ponder", signature: "sig" },
1399
+ {
1400
+ type: "image",
1401
+ source: {
1402
+ type: "base64",
1403
+ media_type: "image/png",
1404
+ data: "b64==",
1405
+ },
1406
+ },
1407
+ {
1408
+ type: "file",
1409
+ source: {
1410
+ type: "base64",
1411
+ media_type: "application/pdf",
1412
+ data: "pdfbase64==",
1413
+ filename: "doc.pdf",
1414
+ },
1415
+ },
1416
+ { type: "text", text: "here you go" },
1417
+ ],
1418
+ },
1419
+ ]);
1420
+ });
1421
+ });