@vellumai/assistant 0.4.16 → 0.4.18

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 (536) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +1 -2
  3. package/eslint.config.mjs +2 -2
  4. package/package.json +1 -1
  5. package/src/__tests__/access-request-decision.test.ts +128 -120
  6. package/src/__tests__/account-registry.test.ts +121 -110
  7. package/src/__tests__/active-skill-tools.test.ts +200 -172
  8. package/src/__tests__/actor-token-service.test.ts +341 -274
  9. package/src/__tests__/agent-loop-thinking.test.ts +28 -19
  10. package/src/__tests__/agent-loop.test.ts +798 -378
  11. package/src/__tests__/anthropic-provider.test.ts +405 -247
  12. package/src/__tests__/app-builder-tool-scripts.test.ts +97 -97
  13. package/src/__tests__/app-bundler.test.ts +112 -79
  14. package/src/__tests__/app-executors.test.ts +205 -178
  15. package/src/__tests__/app-git-history.test.ts +90 -73
  16. package/src/__tests__/app-git-service.test.ts +67 -53
  17. package/src/__tests__/app-open-proxy.test.ts +29 -25
  18. package/src/__tests__/approval-conversation-turn.test.ts +100 -81
  19. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +45 -17
  20. package/src/__tests__/approval-message-composer.test.ts +119 -119
  21. package/src/__tests__/approval-primitive.test.ts +264 -233
  22. package/src/__tests__/approval-routes-http.test.ts +4 -3
  23. package/src/__tests__/asset-materialize-tool.test.ts +250 -178
  24. package/src/__tests__/asset-search-tool.test.ts +251 -191
  25. package/src/__tests__/assistant-attachment-directive.test.ts +187 -142
  26. package/src/__tests__/assistant-attachments.test.ts +254 -186
  27. package/src/__tests__/assistant-event-hub.test.ts +105 -63
  28. package/src/__tests__/assistant-event.test.ts +66 -58
  29. package/src/__tests__/assistant-events-sse-hardening.test.ts +113 -73
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +78 -52
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +48 -45
  32. package/src/__tests__/assistant-feature-flags-integration.test.ts +118 -77
  33. package/src/__tests__/assistant-id-boundary-guard.test.ts +158 -104
  34. package/src/__tests__/attachments-store.test.ts +240 -183
  35. package/src/__tests__/attachments.test.ts +70 -62
  36. package/src/__tests__/audit-log-rotation.test.ts +50 -35
  37. package/src/__tests__/browser-fill-credential.test.ts +169 -101
  38. package/src/__tests__/browser-manager.test.ts +97 -75
  39. package/src/__tests__/browser-runtime-check.test.ts +16 -15
  40. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +12 -10
  41. package/src/__tests__/browser-skill-endstate.test.ts +97 -72
  42. package/src/__tests__/bundle-scanner.test.ts +47 -22
  43. package/src/__tests__/bundled-asset.test.ts +74 -47
  44. package/src/__tests__/call-constants.test.ts +19 -19
  45. package/src/__tests__/call-controller.test.ts +1073 -751
  46. package/src/__tests__/call-conversation-messages.test.ts +90 -65
  47. package/src/__tests__/call-domain.test.ts +149 -121
  48. package/src/__tests__/call-pointer-message-composer.test.ts +113 -83
  49. package/src/__tests__/call-pointer-messages.test.ts +213 -154
  50. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +9 -10
  51. package/src/__tests__/call-recovery.test.ts +232 -212
  52. package/src/__tests__/call-routes-http.test.ts +328 -279
  53. package/src/__tests__/call-start-guardian-guard.test.ts +32 -30
  54. package/src/__tests__/call-state-machine.test.ts +62 -51
  55. package/src/__tests__/call-state.test.ts +89 -75
  56. package/src/__tests__/call-store.test.ts +387 -316
  57. package/src/__tests__/callback-handoff-copy.test.ts +84 -82
  58. package/src/__tests__/canonical-guardian-store.test.ts +331 -280
  59. package/src/__tests__/channel-approval-routes.test.ts +1643 -1126
  60. package/src/__tests__/channel-approval.test.ts +139 -137
  61. package/src/__tests__/channel-approvals.test.ts +226 -182
  62. package/src/__tests__/channel-delivery-store.test.ts +232 -194
  63. package/src/__tests__/channel-guardian.test.ts +6 -3
  64. package/src/__tests__/channel-invite-transport.test.ts +107 -92
  65. package/src/__tests__/channel-policy.test.ts +42 -38
  66. package/src/__tests__/channel-readiness-service.test.ts +119 -102
  67. package/src/__tests__/channel-reply-delivery.test.ts +147 -118
  68. package/src/__tests__/channel-retry-sweep.test.ts +153 -110
  69. package/src/__tests__/checker.test.ts +3309 -1850
  70. package/src/__tests__/clarification-resolver.test.ts +91 -79
  71. package/src/__tests__/classifier.test.ts +64 -54
  72. package/src/__tests__/claude-code-skill-regression.test.ts +42 -37
  73. package/src/__tests__/claude-code-tool-profiles.test.ts +31 -29
  74. package/src/__tests__/clawhub.test.ts +92 -82
  75. package/src/__tests__/cli.test.ts +30 -30
  76. package/src/__tests__/clipboard.test.ts +53 -46
  77. package/src/__tests__/commit-guarantee.test.ts +59 -52
  78. package/src/__tests__/commit-message-enrichment-service.test.ts +203 -75
  79. package/src/__tests__/compaction.benchmark.test.ts +33 -31
  80. package/src/__tests__/computer-use-session-compaction.test.ts +60 -50
  81. package/src/__tests__/computer-use-session-lifecycle.test.ts +145 -117
  82. package/src/__tests__/computer-use-session-working-dir.test.ts +62 -48
  83. package/src/__tests__/computer-use-skill-baseline.test.ts +22 -19
  84. package/src/__tests__/computer-use-skill-endstate.test.ts +45 -31
  85. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +121 -88
  86. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +65 -42
  87. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +33 -18
  88. package/src/__tests__/computer-use-tools.test.ts +121 -98
  89. package/src/__tests__/config-schema.test.ts +443 -347
  90. package/src/__tests__/config-watcher.test.ts +96 -81
  91. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +148 -133
  92. package/src/__tests__/conflict-intent-tokenization.test.ts +96 -78
  93. package/src/__tests__/conflict-policy.test.ts +151 -80
  94. package/src/__tests__/conflict-store.test.ts +203 -157
  95. package/src/__tests__/connection-policy.test.ts +89 -59
  96. package/src/__tests__/contacts-tools.test.ts +247 -178
  97. package/src/__tests__/context-memory-e2e.test.ts +306 -214
  98. package/src/__tests__/context-token-estimator.test.ts +114 -74
  99. package/src/__tests__/context-window-manager.test.ts +269 -167
  100. package/src/__tests__/contradiction-checker.test.ts +161 -135
  101. package/src/__tests__/conversation-attention-store.test.ts +350 -290
  102. package/src/__tests__/conversation-attention-telegram.test.ts +156 -114
  103. package/src/__tests__/conversation-pairing.test.ts +220 -113
  104. package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
  105. package/src/__tests__/conversation-routes.test.ts +71 -41
  106. package/src/__tests__/conversation-store.test.ts +390 -235
  107. package/src/__tests__/credential-broker-browser-fill.test.ts +325 -250
  108. package/src/__tests__/credential-broker-server-use.test.ts +283 -243
  109. package/src/__tests__/credential-broker.test.ts +128 -74
  110. package/src/__tests__/credential-host-pattern-match.test.ts +64 -44
  111. package/src/__tests__/credential-metadata-store.test.ts +360 -311
  112. package/src/__tests__/credential-policy-validate.test.ts +81 -65
  113. package/src/__tests__/credential-resolve.test.ts +212 -145
  114. package/src/__tests__/credential-security-e2e.test.ts +144 -103
  115. package/src/__tests__/credential-security-invariants.test.ts +253 -208
  116. package/src/__tests__/credential-selection.test.ts +254 -146
  117. package/src/__tests__/credential-vault-unit.test.ts +531 -341
  118. package/src/__tests__/credential-vault.test.ts +761 -484
  119. package/src/__tests__/daemon-assistant-events.test.ts +91 -66
  120. package/src/__tests__/daemon-lifecycle.test.ts +258 -190
  121. package/src/__tests__/daemon-server-session-init.test.ts +257 -191
  122. package/src/__tests__/date-context.test.ts +314 -249
  123. package/src/__tests__/db-migration-rollback.test.ts +259 -130
  124. package/src/__tests__/db-schedule-syntax-migration.test.ts +78 -41
  125. package/src/__tests__/delete-managed-skill-tool.test.ts +77 -53
  126. package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -135
  127. package/src/__tests__/dictation-mode-detection.test.ts +77 -55
  128. package/src/__tests__/dictation-profile-store.test.ts +70 -56
  129. package/src/__tests__/dictation-text-processing.test.ts +53 -35
  130. package/src/__tests__/diff.test.ts +102 -98
  131. package/src/__tests__/domain-normalize.test.ts +54 -54
  132. package/src/__tests__/domain-policy.test.ts +71 -55
  133. package/src/__tests__/dynamic-page-surface.test.ts +31 -33
  134. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +69 -69
  135. package/src/__tests__/edit-engine.test.ts +56 -56
  136. package/src/__tests__/elevenlabs-client.test.ts +117 -91
  137. package/src/__tests__/elevenlabs-config.test.ts +32 -31
  138. package/src/__tests__/email-classifier.test.ts +15 -12
  139. package/src/__tests__/email-cli.test.ts +121 -108
  140. package/src/__tests__/emit-signal-routing-intent.test.ts +76 -69
  141. package/src/__tests__/encrypted-store.test.ts +180 -154
  142. package/src/__tests__/entity-extractor.test.ts +108 -87
  143. package/src/__tests__/entity-search.test.ts +664 -258
  144. package/src/__tests__/ephemeral-permissions.test.ts +224 -188
  145. package/src/__tests__/event-bus.test.ts +81 -77
  146. package/src/__tests__/extract-email.test.ts +51 -0
  147. package/src/__tests__/file-edit-tool.test.ts +62 -44
  148. package/src/__tests__/file-ops-service.test.ts +131 -114
  149. package/src/__tests__/file-read-tool.test.ts +48 -31
  150. package/src/__tests__/file-write-tool.test.ts +43 -37
  151. package/src/__tests__/filesystem-tools.test.ts +238 -209
  152. package/src/__tests__/followup-tools.test.ts +237 -162
  153. package/src/__tests__/forbidden-legacy-symbols.test.ts +19 -20
  154. package/src/__tests__/frontmatter.test.ts +96 -81
  155. package/src/__tests__/fuzzy-match-property.test.ts +75 -81
  156. package/src/__tests__/fuzzy-match.test.ts +71 -65
  157. package/src/__tests__/gateway-client-managed-outbound.test.ts +76 -57
  158. package/src/__tests__/gateway-only-enforcement.test.ts +467 -369
  159. package/src/__tests__/gateway-only-guard.test.ts +54 -56
  160. package/src/__tests__/gemini-image-service.test.ts +113 -100
  161. package/src/__tests__/gemini-provider.test.ts +297 -220
  162. package/src/__tests__/get-weather.test.ts +188 -114
  163. package/src/__tests__/gmail-integration.test.ts +47 -46
  164. package/src/__tests__/guardian-action-conversation-turn.test.ts +226 -171
  165. package/src/__tests__/guardian-action-copy-generator.test.ts +111 -93
  166. package/src/__tests__/guardian-action-followup-executor.test.ts +215 -151
  167. package/src/__tests__/guardian-action-followup-store.test.ts +199 -167
  168. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +297 -250
  169. package/src/__tests__/guardian-action-late-reply.test.ts +462 -316
  170. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +23 -18
  171. package/src/__tests__/guardian-action-store.test.ts +158 -109
  172. package/src/__tests__/guardian-action-sweep.test.ts +114 -100
  173. package/src/__tests__/guardian-actions-endpoint.test.ts +440 -256
  174. package/src/__tests__/guardian-control-plane-policy.test.ts +497 -331
  175. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +217 -215
  176. package/src/__tests__/guardian-dispatch.test.ts +316 -256
  177. package/src/__tests__/guardian-grant-minting.test.ts +247 -178
  178. package/src/__tests__/guardian-outbound-http.test.ts +337 -209
  179. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +99 -96
  180. package/src/__tests__/guardian-question-copy.test.ts +17 -17
  181. package/src/__tests__/guardian-question-mode.test.ts +134 -100
  182. package/src/__tests__/guardian-routing-invariants.test.ts +679 -613
  183. package/src/__tests__/guardian-routing-state.test.ts +256 -209
  184. package/src/__tests__/guardian-verification-intent-routing.test.ts +94 -88
  185. package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -41
  186. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +0 -1
  187. package/src/__tests__/handle-user-message-secret-resume.test.ts +43 -21
  188. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +92 -76
  189. package/src/__tests__/handlers-cu-observation-blob.test.ts +103 -70
  190. package/src/__tests__/handlers-ipc-blob-probe.test.ts +77 -51
  191. package/src/__tests__/handlers-slack-config.test.ts +63 -54
  192. package/src/__tests__/handlers-task-submit-slash.test.ts +18 -18
  193. package/src/__tests__/handlers-telegram-config.test.ts +662 -329
  194. package/src/__tests__/handlers-twitter-config.test.ts +525 -298
  195. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +270 -195
  196. package/src/__tests__/headless-browser-interactions.test.ts +444 -280
  197. package/src/__tests__/headless-browser-navigate.test.ts +116 -79
  198. package/src/__tests__/headless-browser-read-tools.test.ts +123 -86
  199. package/src/__tests__/headless-browser-snapshot.test.ts +71 -56
  200. package/src/__tests__/heartbeat-service.test.ts +76 -58
  201. package/src/__tests__/history-repair-observability.test.ts +14 -14
  202. package/src/__tests__/history-repair.test.ts +171 -167
  203. package/src/__tests__/home-base-bootstrap.test.ts +30 -27
  204. package/src/__tests__/hooks-blocking.test.ts +86 -37
  205. package/src/__tests__/hooks-cli.test.ts +104 -68
  206. package/src/__tests__/hooks-config.test.ts +81 -43
  207. package/src/__tests__/hooks-discovery.test.ts +106 -96
  208. package/src/__tests__/hooks-integration.test.ts +78 -72
  209. package/src/__tests__/hooks-manager.test.ts +99 -61
  210. package/src/__tests__/hooks-runner.test.ts +94 -71
  211. package/src/__tests__/hooks-settings.test.ts +69 -64
  212. package/src/__tests__/hooks-templates.test.ts +85 -54
  213. package/src/__tests__/hooks-ts-runner.test.ts +82 -45
  214. package/src/__tests__/hooks-watch.test.ts +32 -22
  215. package/src/__tests__/host-file-edit-tool.test.ts +190 -148
  216. package/src/__tests__/host-file-read-tool.test.ts +86 -63
  217. package/src/__tests__/host-file-write-tool.test.ts +98 -64
  218. package/src/__tests__/host-shell-tool.test.ts +342 -233
  219. package/src/__tests__/inbound-invite-redemption.test.ts +194 -152
  220. package/src/__tests__/ingress-member-store.test.ts +163 -159
  221. package/src/__tests__/ingress-reconcile.test.ts +183 -142
  222. package/src/__tests__/ingress-routes-http.test.ts +441 -356
  223. package/src/__tests__/ingress-url-consistency.test.ts +125 -64
  224. package/src/__tests__/integration-status.test.ts +93 -73
  225. package/src/__tests__/intent-routing.test.ts +148 -118
  226. package/src/__tests__/invite-redemption-service.test.ts +163 -121
  227. package/src/__tests__/ipc-blob-store.test.ts +104 -91
  228. package/src/__tests__/ipc-contract-inventory.test.ts +27 -15
  229. package/src/__tests__/ipc-contract.test.ts +24 -23
  230. package/src/__tests__/ipc-protocol.test.ts +52 -46
  231. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +61 -50
  232. package/src/__tests__/ipc-snapshot.test.ts +1135 -1056
  233. package/src/__tests__/ipc-validate.test.ts +240 -179
  234. package/src/__tests__/key-migration.test.ts +123 -90
  235. package/src/__tests__/keychain.test.ts +150 -123
  236. package/src/__tests__/lifecycle-docs-guard.test.ts +65 -64
  237. package/src/__tests__/llm-usage-store.test.ts +112 -87
  238. package/src/__tests__/managed-skill-lifecycle.test.ts +147 -108
  239. package/src/__tests__/managed-store.test.ts +411 -360
  240. package/src/__tests__/mcp-cli.test.ts +189 -123
  241. package/src/__tests__/mcp-health-check.test.ts +26 -21
  242. package/src/__tests__/media-generate-image.test.ts +122 -99
  243. package/src/__tests__/media-reuse-story.e2e.test.ts +282 -214
  244. package/src/__tests__/media-visibility-policy.test.ts +86 -38
  245. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +146 -100
  246. package/src/__tests__/memory-lifecycle-e2e.test.ts +385 -297
  247. package/src/__tests__/memory-query-builder.test.ts +32 -33
  248. package/src/__tests__/memory-recall-quality.test.ts +761 -407
  249. package/src/__tests__/memory-regressions.experimental.test.ts +443 -380
  250. package/src/__tests__/memory-regressions.test.ts +3725 -2642
  251. package/src/__tests__/memory-retrieval-budget.test.ts +7 -8
  252. package/src/__tests__/memory-retrieval.benchmark.test.ts +144 -109
  253. package/src/__tests__/memory-upsert-concurrency.test.ts +292 -201
  254. package/src/__tests__/messaging-send-tool.test.ts +36 -29
  255. package/src/__tests__/migration-cli-flows.test.ts +69 -53
  256. package/src/__tests__/migration-ordering.test.ts +103 -86
  257. package/src/__tests__/mime-builder.test.ts +55 -32
  258. package/src/__tests__/mock-signup-server.test.ts +384 -246
  259. package/src/__tests__/model-intents.test.ts +61 -37
  260. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +9 -12
  261. package/src/__tests__/no-is-trusted-guard.test.ts +24 -21
  262. package/src/__tests__/non-member-access-request.test.ts +294 -249
  263. package/src/__tests__/notification-broadcaster.test.ts +99 -81
  264. package/src/__tests__/notification-decision-fallback.test.ts +223 -178
  265. package/src/__tests__/notification-decision-strategy.test.ts +375 -337
  266. package/src/__tests__/notification-deep-link.test.ts +67 -61
  267. package/src/__tests__/notification-guardian-path.test.ts +248 -206
  268. package/src/__tests__/notification-routing-intent.test.ts +166 -93
  269. package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
  270. package/src/__tests__/notification-thread-candidate-validation.test.ts +78 -75
  271. package/src/__tests__/notification-thread-candidates.test.ts +64 -61
  272. package/src/__tests__/oauth-callback-registry.test.ts +40 -30
  273. package/src/__tests__/oauth-connect-handler.test.ts +109 -89
  274. package/src/__tests__/oauth-scope-policy.test.ts +63 -55
  275. package/src/__tests__/oauth2-gateway-transport.test.ts +252 -174
  276. package/src/__tests__/onboarding-starter-tasks.test.ts +93 -89
  277. package/src/__tests__/onboarding-template-contract.test.ts +93 -94
  278. package/src/__tests__/openai-provider.test.ts +366 -274
  279. package/src/__tests__/pairing-concurrent.test.ts +18 -12
  280. package/src/__tests__/pairing-routes.test.ts +45 -41
  281. package/src/__tests__/parallel-tool.benchmark.test.ts +108 -58
  282. package/src/__tests__/parser.test.ts +316 -226
  283. package/src/__tests__/path-classifier.test.ts +24 -25
  284. package/src/__tests__/path-policy.test.ts +187 -147
  285. package/src/__tests__/phone.test.ts +36 -36
  286. package/src/__tests__/platform-move-helper.test.ts +48 -40
  287. package/src/__tests__/platform-socket-path.test.ts +23 -24
  288. package/src/__tests__/platform-workspace-migration.test.ts +464 -414
  289. package/src/__tests__/platform.test.ts +61 -53
  290. package/src/__tests__/playbook-execution.test.ts +397 -265
  291. package/src/__tests__/playbook-tools.test.ts +267 -196
  292. package/src/__tests__/prebuilt-home-base-seed.test.ts +30 -27
  293. package/src/__tests__/pricing.test.ts +316 -136
  294. package/src/__tests__/profile-compiler.test.ts +206 -188
  295. package/src/__tests__/provider-commit-message-generator.test.ts +114 -106
  296. package/src/__tests__/provider-error-scenarios.test.ts +212 -158
  297. package/src/__tests__/provider-fail-open-selection.test.ts +51 -44
  298. package/src/__tests__/provider-registry-ollama.test.ts +13 -9
  299. package/src/__tests__/provider-streaming.benchmark.test.ts +232 -183
  300. package/src/__tests__/proxy-approval-callback.test.ts +180 -119
  301. package/src/__tests__/public-ingress-urls.test.ts +112 -94
  302. package/src/__tests__/qdrant-manager.test.ts +147 -98
  303. package/src/__tests__/ratelimit.test.ts +152 -82
  304. package/src/__tests__/recording-handler.test.ts +273 -151
  305. package/src/__tests__/recording-intent-fallback.test.ts +94 -75
  306. package/src/__tests__/recording-intent-handler.test.ts +422 -292
  307. package/src/__tests__/recording-intent.test.ts +578 -379
  308. package/src/__tests__/recording-state-machine.test.ts +530 -316
  309. package/src/__tests__/recurrence-engine-rruleset.test.ts +150 -92
  310. package/src/__tests__/recurrence-engine.test.ts +81 -41
  311. package/src/__tests__/recurrence-types.test.ts +63 -44
  312. package/src/__tests__/relay-server.test.ts +2131 -1602
  313. package/src/__tests__/reminder-store.test.ts +158 -80
  314. package/src/__tests__/reminder.test.ts +113 -109
  315. package/src/__tests__/remote-skill-policy.test.ts +96 -72
  316. package/src/__tests__/request-file-tool.test.ts +74 -67
  317. package/src/__tests__/response-tier.test.ts +131 -74
  318. package/src/__tests__/runtime-attachment-metadata.test.ts +107 -70
  319. package/src/__tests__/runtime-events-sse-parity.test.ts +167 -145
  320. package/src/__tests__/runtime-events-sse.test.ts +67 -51
  321. package/src/__tests__/sandbox-diagnostics.test.ts +66 -56
  322. package/src/__tests__/sandbox-host-parity.test.ts +377 -301
  323. package/src/__tests__/scaffold-managed-skill-tool.test.ts +213 -161
  324. package/src/__tests__/schedule-store.test.ts +268 -205
  325. package/src/__tests__/schedule-tools.test.ts +702 -524
  326. package/src/__tests__/scheduler-recurrence.test.ts +240 -130
  327. package/src/__tests__/scoped-approval-grants.test.ts +258 -168
  328. package/src/__tests__/scoped-grant-security-matrix.test.ts +160 -146
  329. package/src/__tests__/script-proxy-certs.test.ts +38 -35
  330. package/src/__tests__/script-proxy-connect-tunnel.test.ts +71 -46
  331. package/src/__tests__/script-proxy-decision-trace.test.ts +161 -84
  332. package/src/__tests__/script-proxy-http-forwarder.test.ts +146 -129
  333. package/src/__tests__/script-proxy-injection-runtime.test.ts +139 -113
  334. package/src/__tests__/script-proxy-mitm-handler.test.ts +226 -142
  335. package/src/__tests__/script-proxy-policy-runtime.test.ts +126 -86
  336. package/src/__tests__/script-proxy-policy.test.ts +308 -153
  337. package/src/__tests__/script-proxy-rewrite-specificity.test.ts +74 -62
  338. package/src/__tests__/script-proxy-router.test.ts +111 -77
  339. package/src/__tests__/script-proxy-session-manager.test.ts +156 -113
  340. package/src/__tests__/script-proxy-session-runtime.test.ts +28 -24
  341. package/src/__tests__/secret-allowlist.test.ts +105 -90
  342. package/src/__tests__/secret-ingress-handler.test.ts +41 -30
  343. package/src/__tests__/secret-onetime-send.test.ts +67 -50
  344. package/src/__tests__/secret-prompt-log-hygiene.test.ts +35 -31
  345. package/src/__tests__/secret-response-routing.test.ts +50 -41
  346. package/src/__tests__/secret-scanner-executor.test.ts +152 -111
  347. package/src/__tests__/secret-scanner.test.ts +495 -413
  348. package/src/__tests__/secure-keys.test.ts +132 -121
  349. package/src/__tests__/send-endpoint-busy.test.ts +313 -232
  350. package/src/__tests__/send-notification-tool.test.ts +43 -42
  351. package/src/__tests__/sensitive-output-placeholders.test.ts +72 -64
  352. package/src/__tests__/sequence-store.test.ts +335 -167
  353. package/src/__tests__/server-history-render.test.ts +341 -202
  354. package/src/__tests__/session-abort-tool-results.test.ts +133 -70
  355. package/src/__tests__/session-approval-overrides.test.ts +93 -91
  356. package/src/__tests__/session-confirmation-signals.test.ts +252 -160
  357. package/src/__tests__/session-conflict-gate.test.ts +775 -585
  358. package/src/__tests__/session-error.test.ts +222 -191
  359. package/src/__tests__/session-evictor.test.ts +79 -62
  360. package/src/__tests__/session-init.benchmark.test.ts +170 -108
  361. package/src/__tests__/session-load-history-repair.test.ts +273 -139
  362. package/src/__tests__/session-messaging-secret-redirect.test.ts +130 -90
  363. package/src/__tests__/session-pre-run-repair.test.ts +106 -59
  364. package/src/__tests__/session-profile-injection.test.ts +198 -130
  365. package/src/__tests__/session-provider-retry-repair.test.ts +223 -141
  366. package/src/__tests__/session-queue.test.ts +624 -321
  367. package/src/__tests__/session-runtime-assembly.test.ts +425 -329
  368. package/src/__tests__/session-runtime-workspace.test.ts +69 -61
  369. package/src/__tests__/session-skill-tools.test.ts +973 -678
  370. package/src/__tests__/session-slash-known.test.ts +185 -133
  371. package/src/__tests__/session-slash-queue.test.ts +147 -81
  372. package/src/__tests__/session-slash-unknown.test.ts +135 -90
  373. package/src/__tests__/session-surfaces-task-progress.test.ts +122 -87
  374. package/src/__tests__/session-tool-setup-app-refresh.test.ts +338 -177
  375. package/src/__tests__/session-tool-setup-memory-scope.test.ts +63 -40
  376. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +60 -37
  377. package/src/__tests__/session-tool-setup-tools-disabled.test.ts +28 -26
  378. package/src/__tests__/session-undo.test.ts +43 -30
  379. package/src/__tests__/session-workspace-cache-state.test.ts +108 -67
  380. package/src/__tests__/session-workspace-injection.test.ts +245 -117
  381. package/src/__tests__/session-workspace-tool-tracking.test.ts +260 -93
  382. package/src/__tests__/shared-filesystem-errors.test.ts +47 -47
  383. package/src/__tests__/shell-credential-ref.test.ts +126 -90
  384. package/src/__tests__/shell-identity.test.ts +134 -111
  385. package/src/__tests__/shell-parser-fuzz.test.ts +263 -179
  386. package/src/__tests__/shell-parser-property.test.ts +435 -288
  387. package/src/__tests__/shell-tool-proxy-mode.test.ts +142 -70
  388. package/src/__tests__/size-guard.test.ts +42 -44
  389. package/src/__tests__/skill-feature-flags-integration.test.ts +79 -52
  390. package/src/__tests__/skill-feature-flags.test.ts +75 -47
  391. package/src/__tests__/skill-include-graph.test.ts +143 -148
  392. package/src/__tests__/skill-load-feature-flag.test.ts +94 -59
  393. package/src/__tests__/skill-load-tool.test.ts +371 -199
  394. package/src/__tests__/skill-projection-feature-flag.test.ts +131 -88
  395. package/src/__tests__/skill-projection.benchmark.test.ts +93 -65
  396. package/src/__tests__/skill-script-runner-host.test.ts +460 -250
  397. package/src/__tests__/skill-script-runner-sandbox.test.ts +168 -108
  398. package/src/__tests__/skill-script-runner.test.ts +115 -74
  399. package/src/__tests__/skill-tool-factory.test.ts +140 -96
  400. package/src/__tests__/skill-tool-manifest.test.ts +306 -210
  401. package/src/__tests__/skill-version-hash.test.ts +70 -56
  402. package/src/__tests__/skills.test.ts +0 -1
  403. package/src/__tests__/slack-channel-config.test.ts +127 -84
  404. package/src/__tests__/slack-skill.test.ts +60 -47
  405. package/src/__tests__/slash-commands-catalog.test.ts +37 -31
  406. package/src/__tests__/slash-commands-parser.test.ts +71 -64
  407. package/src/__tests__/slash-commands-resolver.test.ts +143 -107
  408. package/src/__tests__/slash-commands-rewrite.test.ts +22 -22
  409. package/src/__tests__/sms-messaging-provider.test.ts +74 -47
  410. package/src/__tests__/speaker-identification.test.ts +28 -25
  411. package/src/__tests__/starter-bundle.test.ts +27 -23
  412. package/src/__tests__/starter-task-flow.test.ts +67 -52
  413. package/src/__tests__/subagent-manager-notify.test.ts +154 -108
  414. package/src/__tests__/subagent-tools.test.ts +311 -270
  415. package/src/__tests__/subagent-types.test.ts +40 -40
  416. package/src/__tests__/surface-mutex-cleanup.test.ts +42 -30
  417. package/src/__tests__/swarm-dag-pathological.test.ts +122 -111
  418. package/src/__tests__/swarm-orchestrator.test.ts +135 -101
  419. package/src/__tests__/swarm-plan-validator.test.ts +125 -73
  420. package/src/__tests__/swarm-recursion.test.ts +58 -46
  421. package/src/__tests__/swarm-router-planner.test.ts +99 -74
  422. package/src/__tests__/swarm-session-integration.test.ts +148 -91
  423. package/src/__tests__/swarm-tool.test.ts +65 -45
  424. package/src/__tests__/swarm-worker-backend.test.ts +59 -45
  425. package/src/__tests__/swarm-worker-runner.test.ts +133 -118
  426. package/src/__tests__/system-prompt.test.ts +290 -256
  427. package/src/__tests__/task-compiler.test.ts +176 -120
  428. package/src/__tests__/task-management-tools.test.ts +561 -456
  429. package/src/__tests__/task-memory-cleanup.test.ts +627 -362
  430. package/src/__tests__/task-runner.test.ts +117 -94
  431. package/src/__tests__/task-scheduler.test.ts +113 -84
  432. package/src/__tests__/task-tools.test.ts +349 -264
  433. package/src/__tests__/terminal-sandbox.test.ts +138 -108
  434. package/src/__tests__/terminal-tools.test.ts +350 -305
  435. package/src/__tests__/thread-seed-composer.test.ts +307 -180
  436. package/src/__tests__/tool-approval-handler.test.ts +238 -137
  437. package/src/__tests__/tool-audit-listener.test.ts +69 -69
  438. package/src/__tests__/tool-domain-event-publisher.test.ts +142 -132
  439. package/src/__tests__/tool-execution-abort-cleanup.test.ts +153 -146
  440. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +136 -105
  441. package/src/__tests__/tool-executor-lifecycle-events.test.ts +355 -239
  442. package/src/__tests__/tool-executor-redaction.test.ts +112 -109
  443. package/src/__tests__/tool-executor-shell-integration.test.ts +130 -79
  444. package/src/__tests__/tool-executor.test.ts +1274 -674
  445. package/src/__tests__/tool-grant-request-escalation.test.ts +401 -283
  446. package/src/__tests__/tool-metrics-listener.test.ts +97 -85
  447. package/src/__tests__/tool-notification-listener.test.ts +42 -25
  448. package/src/__tests__/tool-permission-simulate-handler.test.ts +137 -113
  449. package/src/__tests__/tool-policy.test.ts +44 -25
  450. package/src/__tests__/tool-profiling-listener.test.ts +99 -93
  451. package/src/__tests__/tool-result-truncation.test.ts +5 -4
  452. package/src/__tests__/tool-trace-listener.test.ts +131 -111
  453. package/src/__tests__/top-level-renderer.test.ts +62 -58
  454. package/src/__tests__/top-level-scanner.test.ts +68 -64
  455. package/src/__tests__/trace-emitter.test.ts +56 -56
  456. package/src/__tests__/trust-context-guards.test.ts +65 -65
  457. package/src/__tests__/trust-store.test.ts +1239 -806
  458. package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -275
  459. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -373
  460. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +264 -241
  461. package/src/__tests__/trusted-contact-multichannel.test.ts +182 -142
  462. package/src/__tests__/trusted-contact-verification.test.ts +251 -231
  463. package/src/__tests__/turn-commit.test.ts +259 -200
  464. package/src/__tests__/twilio-config.test.ts +49 -41
  465. package/src/__tests__/twilio-provider.test.ts +140 -126
  466. package/src/__tests__/twilio-rest.test.ts +22 -18
  467. package/src/__tests__/twilio-routes-elevenlabs.test.ts +188 -162
  468. package/src/__tests__/twilio-routes-twiml.test.ts +55 -55
  469. package/src/__tests__/twilio-routes.test.ts +389 -281
  470. package/src/__tests__/twitter-auth-handler.test.ts +184 -139
  471. package/src/__tests__/twitter-cli-error-shaping.test.ts +88 -73
  472. package/src/__tests__/twitter-cli-routing.test.ts +146 -99
  473. package/src/__tests__/twitter-oauth-client.test.ts +82 -65
  474. package/src/__tests__/update-bulletin-format.test.ts +69 -66
  475. package/src/__tests__/update-bulletin-state.test.ts +66 -60
  476. package/src/__tests__/update-bulletin.test.ts +150 -114
  477. package/src/__tests__/update-template-contract.test.ts +15 -10
  478. package/src/__tests__/url-safety.test.ts +288 -265
  479. package/src/__tests__/user-reference.test.ts +32 -32
  480. package/src/__tests__/view-image-tool.test.ts +118 -96
  481. package/src/__tests__/voice-invite-redemption.test.ts +111 -106
  482. package/src/__tests__/voice-quality.test.ts +117 -102
  483. package/src/__tests__/voice-scoped-grant-consumer.test.ts +204 -146
  484. package/src/__tests__/voice-session-bridge.test.ts +351 -216
  485. package/src/__tests__/weather-skill-regression.test.ts +170 -120
  486. package/src/__tests__/web-fetch.test.ts +664 -526
  487. package/src/__tests__/web-search.test.ts +379 -213
  488. package/src/__tests__/work-item-output.test.ts +90 -53
  489. package/src/__tests__/workspace-git-service.test.ts +437 -356
  490. package/src/__tests__/workspace-heartbeat-service.test.ts +125 -91
  491. package/src/__tests__/workspace-lifecycle.test.ts +98 -64
  492. package/src/__tests__/workspace-policy.test.ts +139 -71
  493. package/src/commands/__tests__/cc-command-registry.test.ts +142 -134
  494. package/src/config/__tests__/feature-flag-registry-guard.test.ts +48 -39
  495. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +44 -4
  496. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -1
  497. package/src/config/bundled-skills/messaging/SKILL.md +9 -7
  498. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +15 -5
  499. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -5
  500. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +11 -7
  501. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +34 -32
  502. package/src/config/bundled-tool-registry.ts +2 -0
  503. package/src/config/env.ts +38 -29
  504. package/src/daemon/handlers/skills.ts +18 -10
  505. package/src/daemon/ipc-contract/messages.ts +1 -0
  506. package/src/daemon/ipc-contract/surfaces.ts +7 -1
  507. package/src/daemon/session-agent-loop-handlers.ts +5 -0
  508. package/src/daemon/session-agent-loop.ts +1 -1
  509. package/src/daemon/session-process.ts +1 -1
  510. package/src/daemon/session-surfaces.ts +42 -2
  511. package/src/memory/db-connection.ts +16 -10
  512. package/src/messaging/providers/gmail/adapter.ts +10 -3
  513. package/src/messaging/providers/gmail/client.ts +280 -72
  514. package/src/runtime/auth/__tests__/context.test.ts +75 -65
  515. package/src/runtime/auth/__tests__/credential-service.test.ts +137 -114
  516. package/src/runtime/auth/__tests__/guard-tests.test.ts +84 -90
  517. package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +40 -40
  518. package/src/runtime/auth/__tests__/middleware.test.ts +80 -74
  519. package/src/runtime/auth/__tests__/policy.test.ts +9 -9
  520. package/src/runtime/auth/__tests__/route-policy.test.ts +76 -65
  521. package/src/runtime/auth/__tests__/scopes.test.ts +68 -60
  522. package/src/runtime/auth/__tests__/subject.test.ts +54 -54
  523. package/src/runtime/auth/__tests__/token-service.test.ts +115 -108
  524. package/src/runtime/auth/scopes.ts +3 -0
  525. package/src/runtime/auth/token-service.ts +78 -48
  526. package/src/runtime/auth/types.ts +2 -1
  527. package/src/runtime/http-server.ts +2 -1
  528. package/src/security/secure-keys.ts +103 -53
  529. package/src/sequence/reply-matcher.ts +10 -6
  530. package/src/skills/frontmatter.ts +9 -6
  531. package/src/tools/browser/__tests__/auth-cache.test.ts +69 -63
  532. package/src/tools/browser/__tests__/auth-detector.test.ts +218 -157
  533. package/src/tools/browser/__tests__/jit-auth.test.ts +83 -99
  534. package/src/tools/ui-surface/definitions.ts +2 -1
  535. package/src/util/platform.ts +0 -12
  536. package/docs/architecture/http-token-refresh.md +0 -274
@@ -1,27 +1,27 @@
1
- import { mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
4
5
 
5
- import { afterAll, beforeEach, describe, expect, mock,test } from 'bun:test';
6
+ const testDir = mkdtempSync(join(tmpdir(), "conv-store-test-"));
6
7
 
7
- const testDir = mkdtempSync(join(tmpdir(), 'conv-store-test-'));
8
-
9
- mock.module('../util/platform.js', () => ({
8
+ mock.module("../util/platform.js", () => ({
10
9
  getDataDir: () => testDir,
11
- isMacOS: () => process.platform === 'darwin',
12
- isLinux: () => process.platform === 'linux',
13
- isWindows: () => process.platform === 'win32',
14
- getSocketPath: () => join(testDir, 'test.sock'),
15
- getPidPath: () => join(testDir, 'test.pid'),
16
- getDbPath: () => join(testDir, 'test.db'),
17
- getLogPath: () => join(testDir, 'test.log'),
10
+ isMacOS: () => process.platform === "darwin",
11
+ isLinux: () => process.platform === "linux",
12
+ isWindows: () => process.platform === "win32",
13
+ getSocketPath: () => join(testDir, "test.sock"),
14
+ getPidPath: () => join(testDir, "test.pid"),
15
+ getDbPath: () => join(testDir, "test.db"),
16
+ getLogPath: () => join(testDir, "test.log"),
18
17
  ensureDataDir: () => {},
19
18
  }));
20
19
 
21
- mock.module('../util/logger.js', () => ({
22
- getLogger: () => new Proxy({} as Record<string, unknown>, {
23
- get: () => () => {},
24
- }),
20
+ mock.module("../util/logger.js", () => ({
21
+ getLogger: () =>
22
+ new Proxy({} as Record<string, unknown>, {
23
+ get: () => () => {},
24
+ }),
25
25
  }));
26
26
 
27
27
  import {
@@ -29,7 +29,7 @@ import {
29
29
  getAttachmentsForMessage,
30
30
  linkAttachmentToMessage,
31
31
  uploadAttachment,
32
- } from '../memory/attachments-store.js';
32
+ } from "../memory/attachments-store.js";
33
33
  import {
34
34
  addMessage,
35
35
  clearAll,
@@ -40,18 +40,22 @@ import {
40
40
  getConversationThreadType,
41
41
  getMessages,
42
42
  isLastUserMessageToolResult,
43
- } from '../memory/conversation-store.js';
44
- import { getDb, initializeDb, resetDb } from '../memory/db.js';
43
+ } from "../memory/conversation-store.js";
44
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
45
45
 
46
46
  // Initialize db once before all tests
47
47
  initializeDb();
48
48
 
49
49
  afterAll(() => {
50
50
  resetDb();
51
- try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
51
+ try {
52
+ rmSync(testDir, { recursive: true });
53
+ } catch {
54
+ /* best effort */
55
+ }
52
56
  });
53
57
 
54
- describe('deleteLastExchange', () => {
58
+ describe("deleteLastExchange", () => {
55
59
  beforeEach(() => {
56
60
  // Reset database between tests by dropping and recreating tables
57
61
  const db = getDb();
@@ -59,38 +63,38 @@ describe('deleteLastExchange', () => {
59
63
  db.run(`DELETE FROM conversations`);
60
64
  });
61
65
 
62
- test('deletes last user message and subsequent assistant messages', () => {
63
- const conv = createConversation('test');
64
- addMessage(conv.id, 'user', 'first question');
65
- addMessage(conv.id, 'assistant', 'first answer');
66
- addMessage(conv.id, 'user', 'second question');
67
- addMessage(conv.id, 'assistant', 'second answer');
66
+ test("deletes last user message and subsequent assistant messages", () => {
67
+ const conv = createConversation("test");
68
+ addMessage(conv.id, "user", "first question");
69
+ addMessage(conv.id, "assistant", "first answer");
70
+ addMessage(conv.id, "user", "second question");
71
+ addMessage(conv.id, "assistant", "second answer");
68
72
 
69
73
  const deleted = deleteLastExchange(conv.id);
70
74
  expect(deleted).toBe(2);
71
75
 
72
76
  const remaining = getMessages(conv.id);
73
77
  expect(remaining).toHaveLength(2);
74
- expect(remaining[0].content).toBe('first question');
75
- expect(remaining[1].content).toBe('first answer');
78
+ expect(remaining[0].content).toBe("first question");
79
+ expect(remaining[1].content).toBe("first answer");
76
80
  });
77
81
 
78
- test('returns 0 when no user messages exist', () => {
79
- const conv = createConversation('test');
80
- addMessage(conv.id, 'assistant', 'hello');
82
+ test("returns 0 when no user messages exist", () => {
83
+ const conv = createConversation("test");
84
+ addMessage(conv.id, "assistant", "hello");
81
85
 
82
86
  const deleted = deleteLastExchange(conv.id);
83
87
  expect(deleted).toBe(0);
84
88
  });
85
89
 
86
- test('returns 0 for empty conversation', () => {
87
- const conv = createConversation('test');
90
+ test("returns 0 for empty conversation", () => {
91
+ const conv = createConversation("test");
88
92
  const deleted = deleteLastExchange(conv.id);
89
93
  expect(deleted).toBe(0);
90
94
  });
91
95
 
92
- test('uses rowid ordering so same-timestamp messages are handled correctly', () => {
93
- const conv = createConversation('test');
96
+ test("uses rowid ordering so same-timestamp messages are handled correctly", () => {
97
+ const conv = createConversation("test");
94
98
  const db = getDb();
95
99
  const now = Date.now();
96
100
 
@@ -116,64 +120,101 @@ describe('deleteLastExchange', () => {
116
120
 
117
121
  const remaining = getMessages(conv.id);
118
122
  expect(remaining).toHaveLength(2);
119
- expect(remaining[0].content).toBe('first');
120
- expect(remaining[1].content).toBe('reply1');
123
+ expect(remaining[0].content).toBe("first");
124
+ expect(remaining[1].content).toBe("reply1");
121
125
  });
122
126
  });
123
127
 
124
- describe('isLastUserMessageToolResult', () => {
128
+ describe("isLastUserMessageToolResult", () => {
125
129
  beforeEach(() => {
126
130
  const db = getDb();
127
131
  db.run(`DELETE FROM messages`);
128
132
  db.run(`DELETE FROM conversations`);
129
133
  });
130
134
 
131
- test('returns true when last user message is tool_result only', () => {
132
- const conv = createConversation('test');
133
- addMessage(conv.id, 'user', 'hello');
134
- addMessage(conv.id, 'assistant', JSON.stringify([{ type: 'tool_use', id: 'tu1', name: 'bash', input: {} }]));
135
- addMessage(conv.id, 'user', JSON.stringify([{ type: 'tool_result', tool_use_id: 'tu1', content: 'ok' }]));
135
+ test("returns true when last user message is tool_result only", () => {
136
+ const conv = createConversation("test");
137
+ addMessage(conv.id, "user", "hello");
138
+ addMessage(
139
+ conv.id,
140
+ "assistant",
141
+ JSON.stringify([
142
+ { type: "tool_use", id: "tu1", name: "bash", input: {} },
143
+ ]),
144
+ );
145
+ addMessage(
146
+ conv.id,
147
+ "user",
148
+ JSON.stringify([
149
+ { type: "tool_result", tool_use_id: "tu1", content: "ok" },
150
+ ]),
151
+ );
136
152
 
137
153
  expect(isLastUserMessageToolResult(conv.id)).toBe(true);
138
154
  });
139
155
 
140
- test('returns false when last user message is plain text', () => {
141
- const conv = createConversation('test');
142
- addMessage(conv.id, 'user', 'hello');
156
+ test("returns false when last user message is plain text", () => {
157
+ const conv = createConversation("test");
158
+ addMessage(conv.id, "user", "hello");
143
159
 
144
160
  expect(isLastUserMessageToolResult(conv.id)).toBe(false);
145
161
  });
146
162
 
147
- test('returns false when no user messages exist', () => {
148
- const conv = createConversation('test');
163
+ test("returns false when no user messages exist", () => {
164
+ const conv = createConversation("test");
149
165
  expect(isLastUserMessageToolResult(conv.id)).toBe(false);
150
166
  });
151
167
 
152
- test('returns false when last user message has mixed content types', () => {
153
- const conv = createConversation('test');
154
- addMessage(conv.id, 'user', JSON.stringify([
155
- { type: 'text', text: 'hello' },
156
- { type: 'tool_result', tool_use_id: 'tu1', content: 'ok' },
157
- ]));
168
+ test("returns false when last user message has mixed content types", () => {
169
+ const conv = createConversation("test");
170
+ addMessage(
171
+ conv.id,
172
+ "user",
173
+ JSON.stringify([
174
+ { type: "text", text: "hello" },
175
+ { type: "tool_result", tool_use_id: "tu1", content: "ok" },
176
+ ]),
177
+ );
158
178
 
159
179
  expect(isLastUserMessageToolResult(conv.id)).toBe(false);
160
180
  });
161
181
  });
162
182
 
163
- describe('deleteLastExchange with tool_result messages', () => {
183
+ describe("deleteLastExchange with tool_result messages", () => {
164
184
  beforeEach(() => {
165
185
  const db = getDb();
166
186
  db.run(`DELETE FROM messages`);
167
187
  db.run(`DELETE FROM conversations`);
168
188
  });
169
189
 
170
- test('looping deleteLastExchange cleans up tool_result user messages', () => {
171
- const conv = createConversation('test');
190
+ test("looping deleteLastExchange cleans up tool_result user messages", () => {
191
+ const conv = createConversation("test");
172
192
  // Simulate: user asks question -> assistant uses tool -> tool_result -> assistant responds
173
- addMessage(conv.id, 'user', 'What files are in /tmp?');
174
- addMessage(conv.id, 'assistant', JSON.stringify([{ type: 'tool_use', id: 'tu1', name: 'bash', input: { command: 'ls /tmp' } }]));
175
- addMessage(conv.id, 'user', JSON.stringify([{ type: 'tool_result', tool_use_id: 'tu1', content: 'file1.txt' }]));
176
- addMessage(conv.id, 'assistant', JSON.stringify([{ type: 'text', text: 'There is file1.txt in /tmp' }]));
193
+ addMessage(conv.id, "user", "What files are in /tmp?");
194
+ addMessage(
195
+ conv.id,
196
+ "assistant",
197
+ JSON.stringify([
198
+ {
199
+ type: "tool_use",
200
+ id: "tu1",
201
+ name: "bash",
202
+ input: { command: "ls /tmp" },
203
+ },
204
+ ]),
205
+ );
206
+ addMessage(
207
+ conv.id,
208
+ "user",
209
+ JSON.stringify([
210
+ { type: "tool_result", tool_use_id: "tu1", content: "file1.txt" },
211
+ ]),
212
+ );
213
+ addMessage(
214
+ conv.id,
215
+ "assistant",
216
+ JSON.stringify([{ type: "text", text: "There is file1.txt in /tmp" }]),
217
+ );
177
218
 
178
219
  // First deleteLastExchange removes the tool_result user msg + final assistant msg
179
220
  let deleted = deleteLastExchange(conv.id);
@@ -195,15 +236,43 @@ describe('deleteLastExchange with tool_result messages', () => {
195
236
  expect(remaining).toHaveLength(0);
196
237
  });
197
238
 
198
- test('looping pattern handles multiple tool uses in sequence', () => {
199
- const conv = createConversation('test');
239
+ test("looping pattern handles multiple tool uses in sequence", () => {
240
+ const conv = createConversation("test");
200
241
  // user -> assistant(tool_use) -> user(tool_result) -> assistant(tool_use) -> user(tool_result) -> assistant(text)
201
- addMessage(conv.id, 'user', 'Do two things');
202
- addMessage(conv.id, 'assistant', JSON.stringify([{ type: 'tool_use', id: 'tu1', name: 'bash', input: {} }]));
203
- addMessage(conv.id, 'user', JSON.stringify([{ type: 'tool_result', tool_use_id: 'tu1', content: 'result1' }]));
204
- addMessage(conv.id, 'assistant', JSON.stringify([{ type: 'tool_use', id: 'tu2', name: 'bash', input: {} }]));
205
- addMessage(conv.id, 'user', JSON.stringify([{ type: 'tool_result', tool_use_id: 'tu2', content: 'result2' }]));
206
- addMessage(conv.id, 'assistant', JSON.stringify([{ type: 'text', text: 'Done both' }]));
242
+ addMessage(conv.id, "user", "Do two things");
243
+ addMessage(
244
+ conv.id,
245
+ "assistant",
246
+ JSON.stringify([
247
+ { type: "tool_use", id: "tu1", name: "bash", input: {} },
248
+ ]),
249
+ );
250
+ addMessage(
251
+ conv.id,
252
+ "user",
253
+ JSON.stringify([
254
+ { type: "tool_result", tool_use_id: "tu1", content: "result1" },
255
+ ]),
256
+ );
257
+ addMessage(
258
+ conv.id,
259
+ "assistant",
260
+ JSON.stringify([
261
+ { type: "tool_use", id: "tu2", name: "bash", input: {} },
262
+ ]),
263
+ );
264
+ addMessage(
265
+ conv.id,
266
+ "user",
267
+ JSON.stringify([
268
+ { type: "tool_result", tool_use_id: "tu2", content: "result2" },
269
+ ]),
270
+ );
271
+ addMessage(
272
+ conv.id,
273
+ "assistant",
274
+ JSON.stringify([{ type: "text", text: "Done both" }]),
275
+ );
207
276
 
208
277
  // First delete: removes last tool_result user (row 5) + final assistant (row 6)
209
278
  deleteLastExchange(conv.id);
@@ -223,21 +292,25 @@ describe('deleteLastExchange with tool_result messages', () => {
223
292
  });
224
293
  });
225
294
 
226
- describe('attachment orphan cleanup', () => {
295
+ describe("attachment orphan cleanup", () => {
227
296
  beforeEach(() => {
228
297
  const db = getDb();
229
- db.run('DELETE FROM message_attachments');
230
- db.run('DELETE FROM attachments');
231
- db.run('DELETE FROM messages');
232
- db.run('DELETE FROM conversations');
233
- });
234
-
235
- test('deleteLastExchange cleans up orphaned attachments', async () => {
236
- const conv = createConversation('test');
237
- await addMessage(conv.id, 'user', 'hello');
238
- const assistantMsg = await addMessage(conv.id, 'assistant', 'Here is a file');
298
+ db.run("DELETE FROM message_attachments");
299
+ db.run("DELETE FROM attachments");
300
+ db.run("DELETE FROM messages");
301
+ db.run("DELETE FROM conversations");
302
+ });
303
+
304
+ test("deleteLastExchange cleans up orphaned attachments", async () => {
305
+ const conv = createConversation("test");
306
+ await addMessage(conv.id, "user", "hello");
307
+ const assistantMsg = await addMessage(
308
+ conv.id,
309
+ "assistant",
310
+ "Here is a file",
311
+ );
239
312
 
240
- const stored = uploadAttachment('chart.png', 'image/png', 'iVBOR');
313
+ const stored = uploadAttachment("chart.png", "image/png", "iVBOR");
241
314
  linkAttachmentToMessage(assistantMsg.id, stored.id, 0);
242
315
 
243
316
  // Verify attachment is linked
@@ -247,18 +320,24 @@ describe('attachment orphan cleanup', () => {
247
320
  deleteLastExchange(conv.id);
248
321
 
249
322
  // Attachment row should be gone
250
- const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
251
- const remaining = raw.query('SELECT COUNT(*) AS c FROM attachments').get() as { c: number };
323
+ const raw = (
324
+ getDb() as unknown as {
325
+ $client: import("bun:sqlite").Database;
326
+ }
327
+ ).$client;
328
+ const remaining = raw
329
+ .query("SELECT COUNT(*) AS c FROM attachments")
330
+ .get() as { c: number };
252
331
  expect(remaining.c).toBe(0);
253
332
  });
254
333
 
255
- test('deleteLastExchange preserves attachments still linked to other messages', async () => {
256
- const conv = createConversation('test');
257
- const msg1 = await addMessage(conv.id, 'assistant', 'first');
258
- await addMessage(conv.id, 'user', 'question');
259
- const msg2 = await addMessage(conv.id, 'assistant', 'second');
334
+ test("deleteLastExchange preserves attachments still linked to other messages", async () => {
335
+ const conv = createConversation("test");
336
+ const msg1 = await addMessage(conv.id, "assistant", "first");
337
+ await addMessage(conv.id, "user", "question");
338
+ const msg2 = await addMessage(conv.id, "assistant", "second");
260
339
 
261
- const shared = uploadAttachment('shared.png', 'image/png', 'AAAA');
340
+ const shared = uploadAttachment("shared.png", "image/png", "AAAA");
262
341
  linkAttachmentToMessage(msg1.id, shared.id, 0);
263
342
  linkAttachmentToMessage(msg2.id, shared.id, 0);
264
343
 
@@ -266,162 +345,186 @@ describe('attachment orphan cleanup', () => {
266
345
  deleteLastExchange(conv.id);
267
346
 
268
347
  // Attachment should survive because msg1 still links to it
269
- const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
270
- const remaining = raw.query('SELECT COUNT(*) AS c FROM attachments').get() as { c: number };
348
+ const raw = (
349
+ getDb() as unknown as {
350
+ $client: import("bun:sqlite").Database;
351
+ }
352
+ ).$client;
353
+ const remaining = raw
354
+ .query("SELECT COUNT(*) AS c FROM attachments")
355
+ .get() as { c: number };
271
356
  expect(remaining.c).toBe(1);
272
357
  });
273
358
 
274
- test('clearAll removes all attachments', async () => {
275
- const conv = createConversation('test');
276
- const msg = await addMessage(conv.id, 'assistant', 'file');
277
- const stored = uploadAttachment('doc.pdf', 'application/pdf', 'JVBER');
359
+ test("clearAll removes all attachments", async () => {
360
+ const conv = createConversation("test");
361
+ const msg = await addMessage(conv.id, "assistant", "file");
362
+ const stored = uploadAttachment("doc.pdf", "application/pdf", "JVBER");
278
363
  linkAttachmentToMessage(msg.id, stored.id, 0);
279
364
 
280
365
  clearAll();
281
366
 
282
- const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
283
- const attachmentCount = raw.query('SELECT COUNT(*) AS c FROM attachments').get() as { c: number };
284
- const linkCount = raw.query('SELECT COUNT(*) AS c FROM message_attachments').get() as { c: number };
367
+ const raw = (
368
+ getDb() as unknown as {
369
+ $client: import("bun:sqlite").Database;
370
+ }
371
+ ).$client;
372
+ const attachmentCount = raw
373
+ .query("SELECT COUNT(*) AS c FROM attachments")
374
+ .get() as { c: number };
375
+ const linkCount = raw
376
+ .query("SELECT COUNT(*) AS c FROM message_attachments")
377
+ .get() as { c: number };
285
378
  expect(attachmentCount.c).toBe(0);
286
379
  expect(linkCount.c).toBe(0);
287
380
  });
288
381
 
289
- test('deleteLastExchange does not delete unlinked user uploads', async () => {
290
- const conv = createConversation('test');
291
- await addMessage(conv.id, 'user', 'hello');
292
- const assistantMsg = await addMessage(conv.id, 'assistant', 'Here is a file');
382
+ test("deleteLastExchange does not delete unlinked user uploads", async () => {
383
+ const conv = createConversation("test");
384
+ await addMessage(conv.id, "user", "hello");
385
+ const assistantMsg = await addMessage(
386
+ conv.id,
387
+ "assistant",
388
+ "Here is a file",
389
+ );
293
390
 
294
391
  // An attachment linked to the assistant message (should be cleaned up)
295
- const linked = uploadAttachment('chart.png', 'image/png', 'iVBOR');
392
+ const linked = uploadAttachment("chart.png", "image/png", "iVBOR");
296
393
  linkAttachmentToMessage(assistantMsg.id, linked.id, 0);
297
394
 
298
395
  // A freshly uploaded attachment not linked to any message (should survive)
299
- uploadAttachment('pending.png', 'image/png', 'AAAA');
396
+ uploadAttachment("pending.png", "image/png", "AAAA");
300
397
 
301
398
  deleteLastExchange(conv.id);
302
399
 
303
- const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
304
- const remaining = raw.query('SELECT COUNT(*) AS c FROM attachments').get() as { c: number };
400
+ const raw = (
401
+ getDb() as unknown as {
402
+ $client: import("bun:sqlite").Database;
403
+ }
404
+ ).$client;
405
+ const remaining = raw
406
+ .query("SELECT COUNT(*) AS c FROM attachments")
407
+ .get() as { c: number };
305
408
  expect(remaining.c).toBe(1); // only the unlinked upload survives
306
409
  });
307
410
  });
308
411
 
309
- describe('conversation thread metadata defaults', () => {
412
+ describe("conversation thread metadata defaults", () => {
310
413
  beforeEach(() => {
311
414
  const db = getDb();
312
415
  db.run(`DELETE FROM messages`);
313
416
  db.run(`DELETE FROM conversations`);
314
417
  });
315
418
 
316
- test('new conversation has threadType defaulting to standard', () => {
317
- const conv = createConversation('test');
318
- expect(conv.threadType).toBe('standard');
419
+ test("new conversation has threadType defaulting to standard", () => {
420
+ const conv = createConversation("test");
421
+ expect(conv.threadType).toBe("standard");
319
422
  });
320
423
 
321
- test('new conversation has memoryScopeId defaulting to default', () => {
322
- const conv = createConversation('test');
323
- expect(conv.memoryScopeId).toBe('default');
424
+ test("new conversation has memoryScopeId defaulting to default", () => {
425
+ const conv = createConversation("test");
426
+ expect(conv.memoryScopeId).toBe("default");
324
427
  });
325
428
 
326
- test('defaults are persisted and retrievable from DB', () => {
327
- const conv = createConversation('test');
429
+ test("defaults are persisted and retrievable from DB", () => {
430
+ const conv = createConversation("test");
328
431
  const loaded = getConversation(conv.id);
329
432
  expect(loaded).not.toBeNull();
330
- expect(loaded!.threadType).toBe('standard');
331
- expect(loaded!.memoryScopeId).toBe('default');
433
+ expect(loaded!.threadType).toBe("standard");
434
+ expect(loaded!.memoryScopeId).toBe("default");
332
435
  });
333
436
 
334
- test('existing conversations without explicit values get defaults via migration', () => {
437
+ test("existing conversations without explicit values get defaults via migration", () => {
335
438
  // Insert a conversation row directly without the new columns
336
439
  // (simulates a pre-migration row — the ALTER TABLE DEFAULT handles it)
337
440
  const db = getDb();
338
441
  const now = Date.now();
339
- const id = 'legacy-conv-' + now;
442
+ const id = "legacy-conv-" + now;
340
443
  db.run(
341
444
  `INSERT INTO conversations (id, title, created_at, updated_at) VALUES ('${id}', 'legacy', ${now}, ${now})`,
342
445
  );
343
446
 
344
447
  const loaded = getConversation(id);
345
448
  expect(loaded).not.toBeNull();
346
- expect(loaded!.threadType).toBe('standard');
347
- expect(loaded!.memoryScopeId).toBe('default');
449
+ expect(loaded!.threadType).toBe("standard");
450
+ expect(loaded!.memoryScopeId).toBe("default");
348
451
  });
349
452
  });
350
453
 
351
- describe('createConversation with thread type option', () => {
454
+ describe("createConversation with thread type option", () => {
352
455
  beforeEach(() => {
353
456
  const db = getDb();
354
457
  db.run(`DELETE FROM messages`);
355
458
  db.run(`DELETE FROM conversations`);
356
459
  });
357
460
 
358
- test('standard create with string title uses defaults', () => {
359
- const conv = createConversation('hello');
360
- expect(conv.title).toBe('hello');
361
- expect(conv.threadType).toBe('standard');
362
- expect(conv.memoryScopeId).toBe('default');
461
+ test("standard create with string title uses defaults", () => {
462
+ const conv = createConversation("hello");
463
+ expect(conv.title).toBe("hello");
464
+ expect(conv.threadType).toBe("standard");
465
+ expect(conv.memoryScopeId).toBe("default");
363
466
  });
364
467
 
365
- test('standard create with options object uses defaults', () => {
366
- const conv = createConversation({ title: 'hello', threadType: 'standard' });
367
- expect(conv.threadType).toBe('standard');
368
- expect(conv.memoryScopeId).toBe('default');
468
+ test("standard create with options object uses defaults", () => {
469
+ const conv = createConversation({ title: "hello", threadType: "standard" });
470
+ expect(conv.threadType).toBe("standard");
471
+ expect(conv.memoryScopeId).toBe("default");
369
472
  });
370
473
 
371
- test('private create sets threadType and derives memoryScopeId', () => {
372
- const conv = createConversation({ title: 'secret', threadType: 'private' });
373
- expect(conv.threadType).toBe('private');
474
+ test("private create sets threadType and derives memoryScopeId", () => {
475
+ const conv = createConversation({ title: "secret", threadType: "private" });
476
+ expect(conv.threadType).toBe("private");
374
477
  expect(conv.memoryScopeId).toBe(`private:${conv.id}`);
375
478
  });
376
479
 
377
- test('private create memoryScopeId is persisted', () => {
378
- const conv = createConversation({ threadType: 'private' });
480
+ test("private create memoryScopeId is persisted", () => {
481
+ const conv = createConversation({ threadType: "private" });
379
482
  const loaded = getConversation(conv.id);
380
483
  expect(loaded).not.toBeNull();
381
- expect(loaded!.threadType).toBe('private');
484
+ expect(loaded!.threadType).toBe("private");
382
485
  expect(loaded!.memoryScopeId).toBe(`private:${conv.id}`);
383
486
  });
384
487
 
385
- test('no-arg create uses defaults', () => {
488
+ test("no-arg create uses defaults", () => {
386
489
  const conv = createConversation();
387
- expect(conv.threadType).toBe('standard');
388
- expect(conv.memoryScopeId).toBe('default');
490
+ expect(conv.threadType).toBe("standard");
491
+ expect(conv.memoryScopeId).toBe("default");
389
492
  });
390
493
  });
391
494
 
392
- describe('conversation metadata read helpers', () => {
495
+ describe("conversation metadata read helpers", () => {
393
496
  beforeEach(() => {
394
497
  const db = getDb();
395
498
  db.run(`DELETE FROM messages`);
396
499
  db.run(`DELETE FROM conversations`);
397
500
  });
398
501
 
399
- test('getConversationThreadType returns standard for standard conversation', () => {
400
- const conv = createConversation('test');
401
- expect(getConversationThreadType(conv.id)).toBe('standard');
502
+ test("getConversationThreadType returns standard for standard conversation", () => {
503
+ const conv = createConversation("test");
504
+ expect(getConversationThreadType(conv.id)).toBe("standard");
402
505
  });
403
506
 
404
- test('getConversationThreadType returns private for private conversation', () => {
405
- const conv = createConversation({ threadType: 'private' });
406
- expect(getConversationThreadType(conv.id)).toBe('private');
507
+ test("getConversationThreadType returns private for private conversation", () => {
508
+ const conv = createConversation({ threadType: "private" });
509
+ expect(getConversationThreadType(conv.id)).toBe("private");
407
510
  });
408
511
 
409
- test('getConversationThreadType returns standard for missing conversation', () => {
410
- expect(getConversationThreadType('nonexistent-id')).toBe('standard');
512
+ test("getConversationThreadType returns standard for missing conversation", () => {
513
+ expect(getConversationThreadType("nonexistent-id")).toBe("standard");
411
514
  });
412
515
 
413
- test('getConversationMemoryScopeId returns default for standard conversation', () => {
414
- const conv = createConversation('test');
415
- expect(getConversationMemoryScopeId(conv.id)).toBe('default');
516
+ test("getConversationMemoryScopeId returns default for standard conversation", () => {
517
+ const conv = createConversation("test");
518
+ expect(getConversationMemoryScopeId(conv.id)).toBe("default");
416
519
  });
417
520
 
418
- test('getConversationMemoryScopeId returns private scope for private conversation', () => {
419
- const conv = createConversation({ threadType: 'private' });
521
+ test("getConversationMemoryScopeId returns private scope for private conversation", () => {
522
+ const conv = createConversation({ threadType: "private" });
420
523
  expect(getConversationMemoryScopeId(conv.id)).toBe(`private:${conv.id}`);
421
524
  });
422
525
 
423
- test('getConversationMemoryScopeId returns default for missing conversation', () => {
424
- expect(getConversationMemoryScopeId('nonexistent-id')).toBe('default');
526
+ test("getConversationMemoryScopeId returns default for missing conversation", () => {
527
+ expect(getConversationMemoryScopeId("nonexistent-id")).toBe("default");
425
528
  });
426
529
  });
427
530
 
@@ -429,42 +532,42 @@ describe('conversation metadata read helpers', () => {
429
532
  // Baseline: attachment reuse across threads
430
533
  // ---------------------------------------------------------------------------
431
534
 
432
- describe('attachment reuse across thread lifecycles', () => {
535
+ describe("attachment reuse across thread lifecycles", () => {
433
536
  beforeEach(() => {
434
537
  const db = getDb();
435
- db.run('DELETE FROM message_attachments');
436
- db.run('DELETE FROM attachments');
437
- db.run('DELETE FROM messages');
438
- db.run('DELETE FROM conversations');
538
+ db.run("DELETE FROM message_attachments");
539
+ db.run("DELETE FROM attachments");
540
+ db.run("DELETE FROM messages");
541
+ db.run("DELETE FROM conversations");
439
542
  });
440
543
 
441
- test('attachment uploaded in conversation A is retrievable by ID without any conversation reference', async () => {
442
- const convA = createConversation('Thread A');
443
- const msgA = await addMessage(convA.id, 'assistant', 'Here is a file');
444
- const stored = uploadAttachment('report.pdf', 'application/pdf', 'JVBER');
544
+ test("attachment uploaded in conversation A is retrievable by ID without any conversation reference", async () => {
545
+ const convA = createConversation("Thread A");
546
+ const msgA = await addMessage(convA.id, "assistant", "Here is a file");
547
+ const stored = uploadAttachment("report.pdf", "application/pdf", "JVBER");
445
548
  linkAttachmentToMessage(msgA.id, stored.id, 0);
446
549
 
447
550
  // Create a completely separate conversation
448
- const convB = createConversation('Thread B');
449
- await addMessage(convB.id, 'user', 'hello');
551
+ const convB = createConversation("Thread B");
552
+ await addMessage(convB.id, "user", "hello");
450
553
 
451
554
  // The attachment is retrievable by ID regardless of which conversation is active.
452
555
  const fetched = getAttachmentById(stored.id);
453
556
  expect(fetched).not.toBeNull();
454
557
  expect(fetched!.id).toBe(stored.id);
455
- expect(fetched!.originalFilename).toBe('report.pdf');
456
- expect(fetched!.dataBase64).toBe('JVBER');
558
+ expect(fetched!.originalFilename).toBe("report.pdf");
559
+ expect(fetched!.dataBase64).toBe("JVBER");
457
560
  });
458
561
 
459
- test('attachment can be linked to messages in different conversations', async () => {
460
- const convA = createConversation('Thread A');
461
- const convB = createConversation('Thread B');
562
+ test("attachment can be linked to messages in different conversations", async () => {
563
+ const convA = createConversation("Thread A");
564
+ const convB = createConversation("Thread B");
462
565
 
463
- const msgA = await addMessage(convA.id, 'assistant', 'Original file');
464
- const msgB = await addMessage(convB.id, 'assistant', 'Reused file');
566
+ const msgA = await addMessage(convA.id, "assistant", "Original file");
567
+ const msgB = await addMessage(convB.id, "assistant", "Reused file");
465
568
 
466
569
  // Upload once, link to both conversations
467
- const stored = uploadAttachment('shared.png', 'image/png', 'iVBORw0K');
570
+ const stored = uploadAttachment("shared.png", "image/png", "iVBORw0K");
468
571
  linkAttachmentToMessage(msgA.id, stored.id, 0);
469
572
  linkAttachmentToMessage(msgB.id, stored.id, 0);
470
573
 
@@ -478,18 +581,18 @@ describe('attachment reuse across thread lifecycles', () => {
478
581
  expect(linkedB[0].id).toBe(stored.id);
479
582
  });
480
583
 
481
- test('deleting conversation A does not orphan attachment reused in conversation B', async () => {
482
- const convA = createConversation('Thread A');
483
- const convB = createConversation('Thread B');
584
+ test("deleting conversation A does not orphan attachment reused in conversation B", async () => {
585
+ const convA = createConversation("Thread A");
586
+ const convB = createConversation("Thread B");
484
587
 
485
588
  // deleteLastExchange deletes from the last user message onward,
486
589
  // so we need a user message before the assistant message that carries the attachment.
487
- await addMessage(convA.id, 'user', 'Please generate a chart');
488
- const msgA = await addMessage(convA.id, 'assistant', 'Original');
489
- await addMessage(convB.id, 'user', 'Show me the chart');
490
- const msgB = await addMessage(convB.id, 'assistant', 'Reused');
590
+ await addMessage(convA.id, "user", "Please generate a chart");
591
+ const msgA = await addMessage(convA.id, "assistant", "Original");
592
+ await addMessage(convB.id, "user", "Show me the chart");
593
+ const msgB = await addMessage(convB.id, "assistant", "Reused");
491
594
 
492
- const stored = uploadAttachment('chart.png', 'image/png', 'AAAA');
595
+ const stored = uploadAttachment("chart.png", "image/png", "AAAA");
493
596
  linkAttachmentToMessage(msgA.id, stored.id, 0);
494
597
  linkAttachmentToMessage(msgB.id, stored.id, 0);
495
598
 
@@ -506,16 +609,16 @@ describe('attachment reuse across thread lifecycles', () => {
506
609
  expect(linkedB[0].id).toBe(stored.id);
507
610
  });
508
611
 
509
- test('content-hash dedup works across conversations', async () => {
510
- const convA = createConversation('Thread A');
511
- const convB = createConversation('Thread B');
612
+ test("content-hash dedup works across conversations", async () => {
613
+ const convA = createConversation("Thread A");
614
+ const convB = createConversation("Thread B");
512
615
 
513
- await addMessage(convA.id, 'user', 'upload in A');
514
- await addMessage(convB.id, 'user', 'upload in B');
616
+ await addMessage(convA.id, "user", "upload in A");
617
+ await addMessage(convB.id, "user", "upload in B");
515
618
 
516
619
  // Same content uploaded in two different conversation contexts
517
- const first = uploadAttachment('photo.png', 'image/png', 'DEDUPCROSS');
518
- const second = uploadAttachment('photo.png', 'image/png', 'DEDUPCROSS');
620
+ const first = uploadAttachment("photo.png", "image/png", "DEDUPCROSS");
621
+ const second = uploadAttachment("photo.png", "image/png", "DEDUPCROSS");
519
622
 
520
623
  // Dedup returns the same attachment row
521
624
  expect(second.id).toBe(first.id);
@@ -526,37 +629,58 @@ describe('attachment reuse across thread lifecycles', () => {
526
629
  // Baseline: no private-thread visibility boundary for attachments
527
630
  // ---------------------------------------------------------------------------
528
631
 
529
- describe('no private-thread attachment visibility boundary', () => {
632
+ describe("no private-thread attachment visibility boundary", () => {
530
633
  beforeEach(() => {
531
634
  const db = getDb();
532
- db.run('DELETE FROM message_attachments');
533
- db.run('DELETE FROM attachments');
534
- db.run('DELETE FROM messages');
535
- db.run('DELETE FROM conversations');
536
- });
537
-
538
- test('attachment from a private thread is visible via getAttachmentById (no thread scoping)', async () => {
539
- const privateConv = createConversation({ title: 'Secret', threadType: 'private' });
540
- expect(privateConv.threadType).toBe('private');
541
-
542
- const msg = await addMessage(privateConv.id, 'assistant', 'Private content');
543
- const stored = uploadAttachment('secret.pdf', 'application/pdf', 'JVBER');
635
+ db.run("DELETE FROM message_attachments");
636
+ db.run("DELETE FROM attachments");
637
+ db.run("DELETE FROM messages");
638
+ db.run("DELETE FROM conversations");
639
+ });
640
+
641
+ test("attachment from a private thread is visible via getAttachmentById (no thread scoping)", async () => {
642
+ const privateConv = createConversation({
643
+ title: "Secret",
644
+ threadType: "private",
645
+ });
646
+ expect(privateConv.threadType).toBe("private");
647
+
648
+ const msg = await addMessage(
649
+ privateConv.id,
650
+ "assistant",
651
+ "Private content",
652
+ );
653
+ const stored = uploadAttachment("secret.pdf", "application/pdf", "JVBER");
544
654
  linkAttachmentToMessage(msg.id, stored.id, 0);
545
655
 
546
656
  // Attachment is globally visible by ID — no thread-type filter exists
547
657
  const fetched = getAttachmentById(stored.id);
548
658
  expect(fetched).not.toBeNull();
549
- expect(fetched!.originalFilename).toBe('secret.pdf');
550
- });
551
-
552
- test('attachment from private thread can be linked to a standard thread message', async () => {
553
- const privateConv = createConversation({ title: 'Private', threadType: 'private' });
554
- const standardConv = createConversation({ title: 'Standard', threadType: 'standard' });
555
-
556
- const privateMsg = await addMessage(privateConv.id, 'assistant', 'Private file');
557
- const standardMsg = await addMessage(standardConv.id, 'assistant', 'Reusing private file');
659
+ expect(fetched!.originalFilename).toBe("secret.pdf");
660
+ });
661
+
662
+ test("attachment from private thread can be linked to a standard thread message", async () => {
663
+ const privateConv = createConversation({
664
+ title: "Private",
665
+ threadType: "private",
666
+ });
667
+ const standardConv = createConversation({
668
+ title: "Standard",
669
+ threadType: "standard",
670
+ });
671
+
672
+ const privateMsg = await addMessage(
673
+ privateConv.id,
674
+ "assistant",
675
+ "Private file",
676
+ );
677
+ const standardMsg = await addMessage(
678
+ standardConv.id,
679
+ "assistant",
680
+ "Reusing private file",
681
+ );
558
682
 
559
- const stored = uploadAttachment('private-doc.png', 'image/png', 'PRIVDATA');
683
+ const stored = uploadAttachment("private-doc.png", "image/png", "PRIVDATA");
560
684
  linkAttachmentToMessage(privateMsg.id, stored.id, 0);
561
685
  linkAttachmentToMessage(standardMsg.id, stored.id, 0);
562
686
 
@@ -569,10 +693,13 @@ describe('no private-thread attachment visibility boundary', () => {
569
693
  expect(linkedStandard[0].id).toBe(stored.id);
570
694
  });
571
695
 
572
- test('getAttachmentsForMessage returns private thread attachments', async () => {
573
- const privateConv = createConversation({ title: 'Private', threadType: 'private' });
574
- const msg = await addMessage(privateConv.id, 'assistant', 'Private media');
575
- const stored = uploadAttachment('photo.jpg', 'image/jpeg', 'AAAA');
696
+ test("getAttachmentsForMessage returns private thread attachments", async () => {
697
+ const privateConv = createConversation({
698
+ title: "Private",
699
+ threadType: "private",
700
+ });
701
+ const msg = await addMessage(privateConv.id, "assistant", "Private media");
702
+ const stored = uploadAttachment("photo.jpg", "image/jpeg", "AAAA");
576
703
  linkAttachmentToMessage(msg.id, stored.id, 0);
577
704
 
578
705
  const linked = getAttachmentsForMessage(msg.id);
@@ -580,34 +707,62 @@ describe('no private-thread attachment visibility boundary', () => {
580
707
  expect(linked[0].id).toBe(stored.id);
581
708
  });
582
709
 
583
- test('content-hash dedup works across private and standard threads', () => {
584
- createConversation({ title: 'Private', threadType: 'private' });
585
- createConversation({ title: 'Standard', threadType: 'standard' });
710
+ test("content-hash dedup works across private and standard threads", () => {
711
+ createConversation({ title: "Private", threadType: "private" });
712
+ createConversation({ title: "Standard", threadType: "standard" });
586
713
 
587
714
  // Same content uploaded in private and standard contexts
588
- const fromPrivate = uploadAttachment('file.png', 'image/png', 'CROSSTHREAD');
589
- const fromStandard = uploadAttachment('file.png', 'image/png', 'CROSSTHREAD');
715
+ const fromPrivate = uploadAttachment(
716
+ "file.png",
717
+ "image/png",
718
+ "CROSSTHREAD",
719
+ );
720
+ const fromStandard = uploadAttachment(
721
+ "file.png",
722
+ "image/png",
723
+ "CROSSTHREAD",
724
+ );
590
725
 
591
726
  // Dedup returns the same row — no thread-type isolation
592
727
  expect(fromStandard.id).toBe(fromPrivate.id);
593
728
  });
594
729
 
595
- test('clearAll removes attachments from both private and standard threads', async () => {
596
- const privateConv = createConversation({ title: 'Private', threadType: 'private' });
597
- const standardConv = createConversation({ title: 'Standard', threadType: 'standard' });
598
-
599
- const privateMsg = await addMessage(privateConv.id, 'assistant', 'Private file');
600
- const standardMsg = await addMessage(standardConv.id, 'assistant', 'Standard file');
730
+ test("clearAll removes attachments from both private and standard threads", async () => {
731
+ const privateConv = createConversation({
732
+ title: "Private",
733
+ threadType: "private",
734
+ });
735
+ const standardConv = createConversation({
736
+ title: "Standard",
737
+ threadType: "standard",
738
+ });
739
+
740
+ const privateMsg = await addMessage(
741
+ privateConv.id,
742
+ "assistant",
743
+ "Private file",
744
+ );
745
+ const standardMsg = await addMessage(
746
+ standardConv.id,
747
+ "assistant",
748
+ "Standard file",
749
+ );
601
750
 
602
- const att1 = uploadAttachment('private.png', 'image/png', 'PRIV');
603
- const att2 = uploadAttachment('standard.png', 'image/png', 'STD');
751
+ const att1 = uploadAttachment("private.png", "image/png", "PRIV");
752
+ const att2 = uploadAttachment("standard.png", "image/png", "STD");
604
753
  linkAttachmentToMessage(privateMsg.id, att1.id, 0);
605
754
  linkAttachmentToMessage(standardMsg.id, att2.id, 0);
606
755
 
607
756
  clearAll();
608
757
 
609
- const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
610
- const attachmentCount = raw.query('SELECT COUNT(*) AS c FROM attachments').get() as { c: number };
758
+ const raw = (
759
+ getDb() as unknown as {
760
+ $client: import("bun:sqlite").Database;
761
+ }
762
+ ).$client;
763
+ const attachmentCount = raw
764
+ .query("SELECT COUNT(*) AS c FROM attachments")
765
+ .get() as { c: number };
611
766
  expect(attachmentCount.c).toBe(0);
612
767
  });
613
768
  });