@vellumai/assistant 0.5.0 → 0.5.2

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 (347) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -9
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-queue.test.ts +36 -1
  37. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  38. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  39. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  41. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  42. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  43. package/src/__tests__/conversation-store.test.ts +24 -21
  44. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  45. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  46. package/src/__tests__/conversation-title-service.test.ts +137 -0
  47. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  48. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  49. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  51. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  52. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  53. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  54. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  55. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  56. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  57. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  58. package/src/__tests__/diagnostics-export.test.ts +70 -1
  59. package/src/__tests__/filesystem-tools.test.ts +4 -2
  60. package/src/__tests__/first-greeting.test.ts +80 -0
  61. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  63. package/src/__tests__/history-repair.test.ts +103 -10
  64. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  65. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  66. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  67. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  68. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  69. package/src/__tests__/media-generate-image.test.ts +47 -94
  70. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  71. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  72. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  73. package/src/__tests__/migration-export-http.test.ts +3 -1
  74. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  75. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  76. package/src/__tests__/mime-builder.test.ts +3 -2
  77. package/src/__tests__/non-member-access-request.test.ts +12 -1
  78. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  80. package/src/__tests__/oauth-store.test.ts +115 -0
  81. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  82. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  83. package/src/__tests__/recording-handler.test.ts +17 -0
  84. package/src/__tests__/registry.test.ts +3 -8
  85. package/src/__tests__/relay-server.test.ts +1 -1
  86. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  87. package/src/__tests__/schema-transforms.test.ts +165 -5
  88. package/src/__tests__/server-history-render.test.ts +2 -2
  89. package/src/__tests__/skill-feature-flags-integration.test.ts +18 -17
  90. package/src/__tests__/skill-feature-flags.test.ts +13 -13
  91. package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
  92. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  93. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  94. package/src/__tests__/starter-task-flow.test.ts +1 -0
  95. package/src/__tests__/suggestion-routes.test.ts +443 -0
  96. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  97. package/src/__tests__/swarm-recursion.test.ts +1 -0
  98. package/src/__tests__/swarm-tool.test.ts +1 -0
  99. package/src/__tests__/system-prompt.test.ts +8 -0
  100. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  101. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  102. package/src/__tests__/top-level-renderer.test.ts +22 -0
  103. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  104. package/src/__tests__/web-fetch.test.ts +6 -2
  105. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  106. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  107. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  108. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  109. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  110. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  111. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  112. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  113. package/src/agent/attachments.ts +27 -1
  114. package/src/agent/loop.ts +29 -1
  115. package/src/avatar/traits-png-sync.ts +80 -25
  116. package/src/bundler/app-bundler.ts +4 -4
  117. package/src/calls/call-domain.ts +1 -0
  118. package/src/calls/voice-session-bridge.ts +1 -0
  119. package/src/cli/commands/auth.ts +92 -0
  120. package/src/cli/commands/avatar.ts +7 -6
  121. package/src/cli/commands/config.ts +2 -0
  122. package/src/cli/commands/oauth/providers.ts +29 -0
  123. package/src/cli/program.ts +12 -0
  124. package/src/cli.ts +15 -48
  125. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  126. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  127. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  128. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  129. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  130. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  131. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  135. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  136. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  137. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  139. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  140. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  141. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  142. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  143. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  144. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  145. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  146. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  148. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  149. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  150. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  151. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  152. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  153. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  154. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  155. package/src/config/bundled-tool-registry.ts +2 -14
  156. package/src/config/feature-flag-registry.json +16 -0
  157. package/src/config/loader.ts +64 -0
  158. package/src/config/raw-config-utils.ts +30 -0
  159. package/src/config/schema-utils.ts +28 -7
  160. package/src/config/schema.ts +8 -0
  161. package/src/config/schemas/elevenlabs.ts +18 -0
  162. package/src/config/schemas/memory-lifecycle.ts +4 -2
  163. package/src/config/schemas/memory-storage.ts +1 -1
  164. package/src/config/schemas/services.ts +8 -6
  165. package/src/contacts/contact-store.ts +13 -6
  166. package/src/contacts/contacts-write.ts +0 -1
  167. package/src/context/window-manager.ts +13 -2
  168. package/src/daemon/conversation-agent-loop-handlers.ts +46 -42
  169. package/src/daemon/conversation-agent-loop.ts +56 -19
  170. package/src/daemon/conversation-attachments.ts +18 -36
  171. package/src/daemon/conversation-error.ts +2 -1
  172. package/src/daemon/conversation-history.ts +18 -4
  173. package/src/daemon/conversation-lifecycle.ts +39 -15
  174. package/src/daemon/conversation-messaging.ts +70 -26
  175. package/src/daemon/conversation-process.ts +58 -34
  176. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  177. package/src/daemon/conversation-slash.ts +121 -256
  178. package/src/daemon/conversation-surfaces.ts +143 -20
  179. package/src/daemon/conversation-tool-setup.ts +0 -6
  180. package/src/daemon/conversation-workspace.ts +21 -1
  181. package/src/daemon/conversation.ts +51 -29
  182. package/src/daemon/first-greeting.ts +35 -0
  183. package/src/daemon/handlers/config-embeddings.ts +148 -0
  184. package/src/daemon/handlers/config-model.ts +71 -26
  185. package/src/daemon/handlers/conversations.ts +0 -23
  186. package/src/daemon/handlers/recording.ts +26 -21
  187. package/src/daemon/history-repair.ts +28 -8
  188. package/src/daemon/host-cu-proxy.ts +2 -2
  189. package/src/daemon/lifecycle.ts +106 -64
  190. package/src/daemon/message-protocol.ts +3 -0
  191. package/src/daemon/message-types/conversations.ts +19 -0
  192. package/src/daemon/message-types/messages.ts +1 -0
  193. package/src/daemon/message-types/shared.ts +2 -0
  194. package/src/daemon/message-types/surfaces.ts +2 -0
  195. package/src/daemon/message-types/upgrades.ts +23 -0
  196. package/src/daemon/server.ts +83 -12
  197. package/src/daemon/shutdown-handlers.ts +8 -5
  198. package/src/daemon/startup-error.ts +9 -0
  199. package/src/daemon/tool-side-effects.ts +11 -28
  200. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  201. package/src/instrument.ts +0 -4
  202. package/src/media/app-icon-generator.ts +2 -2
  203. package/src/memory/app-git-service.ts +28 -16
  204. package/src/memory/app-store.ts +230 -41
  205. package/src/memory/attachments-store.ts +558 -130
  206. package/src/memory/conversation-attention-store.ts +70 -0
  207. package/src/memory/conversation-crud.ts +442 -3
  208. package/src/memory/conversation-directories.ts +125 -0
  209. package/src/memory/conversation-disk-view.ts +390 -0
  210. package/src/memory/conversation-key-store.ts +17 -5
  211. package/src/memory/conversation-queries.ts +5 -1
  212. package/src/memory/conversation-title-service.ts +21 -49
  213. package/src/memory/db-init.ts +28 -0
  214. package/src/memory/embedding-backend.ts +42 -53
  215. package/src/memory/embedding-gemini.test.ts +4 -4
  216. package/src/memory/embedding-local.ts +1 -3
  217. package/src/memory/embedding-ollama.ts +1 -3
  218. package/src/memory/embedding-openai.ts +1 -3
  219. package/src/memory/indexer.ts +9 -7
  220. package/src/memory/items-extractor.ts +42 -13
  221. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  222. package/src/memory/job-handlers/embedding.test.ts +1 -4
  223. package/src/memory/llm-request-log-store.ts +100 -1
  224. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  225. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  226. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  227. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  228. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  229. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  230. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  231. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  232. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  233. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  234. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  235. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  236. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  237. package/src/memory/migrations/index.ts +7 -0
  238. package/src/memory/migrations/registry.ts +13 -0
  239. package/src/memory/retriever.test.ts +601 -2
  240. package/src/memory/retriever.ts +85 -9
  241. package/src/memory/schema/conversations.ts +6 -0
  242. package/src/memory/schema/infrastructure.ts +13 -7
  243. package/src/memory/schema/oauth.ts +6 -0
  244. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  245. package/src/notifications/copy-composer.ts +26 -0
  246. package/src/notifications/decision-engine.ts +14 -1
  247. package/src/notifications/emit-signal.ts +1 -1
  248. package/src/notifications/signal.ts +36 -0
  249. package/src/oauth/byo-connection.test.ts +1 -45
  250. package/src/oauth/byo-connection.ts +2 -8
  251. package/src/oauth/connect-orchestrator.ts +15 -11
  252. package/src/oauth/connection-resolver.test.ts +191 -0
  253. package/src/oauth/connection-resolver.ts +66 -38
  254. package/src/oauth/connection.ts +0 -1
  255. package/src/oauth/oauth-store.ts +97 -47
  256. package/src/oauth/platform-connection.test.ts +0 -1
  257. package/src/oauth/platform-connection.ts +11 -3
  258. package/src/oauth/seed-providers.ts +78 -3
  259. package/src/oauth/token-persistence.ts +16 -10
  260. package/src/permissions/checker.ts +62 -19
  261. package/src/prompts/system-prompt.ts +2 -0
  262. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  263. package/src/providers/anthropic/client.ts +8 -1
  264. package/src/providers/failover.ts +4 -1
  265. package/src/providers/gemini/client.ts +50 -0
  266. package/src/providers/model-catalog.ts +92 -0
  267. package/src/providers/model-intents.ts +29 -20
  268. package/src/providers/openai/client.ts +49 -0
  269. package/src/providers/types.ts +2 -0
  270. package/src/runtime/access-request-helper.ts +16 -7
  271. package/src/runtime/auth/credential-service.ts +3 -1
  272. package/src/runtime/auth/route-policy.ts +14 -1
  273. package/src/runtime/btw-sidechain.ts +101 -0
  274. package/src/runtime/channel-reply-delivery.ts +17 -1
  275. package/src/runtime/http-router.ts +3 -1
  276. package/src/runtime/http-server.ts +196 -141
  277. package/src/runtime/http-types.ts +1 -0
  278. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  279. package/src/runtime/routes/access-request-decision.ts +41 -0
  280. package/src/runtime/routes/app-management-routes.ts +6 -3
  281. package/src/runtime/routes/app-routes.ts +7 -3
  282. package/src/runtime/routes/approval-routes.ts +1 -0
  283. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  284. package/src/runtime/routes/attachment-routes.ts +45 -15
  285. package/src/runtime/routes/btw-routes.ts +21 -61
  286. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  287. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  288. package/src/runtime/routes/conversation-routes.ts +222 -28
  289. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  290. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  291. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  292. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  293. package/src/runtime/routes/log-export-routes.ts +3 -0
  294. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  295. package/src/runtime/routes/memory-item-routes.ts +4 -0
  296. package/src/runtime/routes/migration-routes.ts +4 -1
  297. package/src/runtime/routes/oauth-apps.ts +291 -0
  298. package/src/runtime/routes/secret-routes.ts +28 -1
  299. package/src/runtime/routes/settings-routes.ts +14 -0
  300. package/src/runtime/routes/trace-event-routes.ts +4 -1
  301. package/src/schedule/schedule-store.ts +9 -21
  302. package/src/security/secure-keys.ts +21 -0
  303. package/src/signals/bash.ts +1 -1
  304. package/src/swarm/backend-claude-code.ts +3 -6
  305. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  306. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  307. package/src/tools/AGENTS.md +6 -10
  308. package/src/tools/apps/executors.ts +17 -232
  309. package/src/tools/claude-code/claude-code.ts +2 -3
  310. package/src/tools/credentials/vault.ts +7 -12
  311. package/src/tools/host-filesystem/read.ts +13 -10
  312. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  313. package/src/tools/schedule/list.ts +2 -7
  314. package/src/tools/schema-transforms.ts +5 -0
  315. package/src/tools/shared/filesystem/format-diff.ts +4 -21
  316. package/src/tools/skills/execute.ts +1 -1
  317. package/src/tools/tool-manifest.ts +0 -6
  318. package/src/tools/ui-surface/definitions.ts +2 -2
  319. package/src/util/device-id.ts +28 -5
  320. package/src/util/platform.ts +6 -0
  321. package/src/util/pricing.ts +1 -0
  322. package/src/util/retry.ts +1 -3
  323. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  324. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  325. package/src/workspace/migrations/006-services-config.ts +5 -0
  326. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  327. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  328. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  329. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  330. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  331. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  332. package/src/workspace/migrations/registry.ts +10 -0
  333. package/src/workspace/top-level-renderer.ts +12 -0
  334. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  335. package/src/__tests__/asset-search-tool.test.ts +0 -536
  336. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  337. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  338. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  339. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  340. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  341. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  342. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  343. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  344. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  345. package/src/daemon/media-visibility-policy.ts +0 -59
  346. package/src/tools/assets/materialize.ts +0 -248
  347. package/src/tools/assets/search.ts +0 -400
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
1
+ import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
@@ -15,6 +15,8 @@ mock.module("../util/platform.js", () => ({
15
15
  getLogPath: () => join(testDir, "test.log"),
16
16
  ensureDataDir: () => {},
17
17
  getRootDir: () => testDir,
18
+ getWorkspaceDir: () => join(testDir, "workspace"),
19
+ getConversationsDir: () => join(testDir, "workspace", "conversations"),
18
20
  }));
19
21
 
20
22
  mock.module("../util/logger.js", () => ({
@@ -40,8 +42,10 @@ import {
40
42
  deleteAttachment,
41
43
  deleteOrphanAttachments,
42
44
  getAttachmentById,
45
+ getAttachmentContent,
43
46
  getAttachmentsByIds,
44
47
  getAttachmentsForMessage,
48
+ getFilePathForAttachment,
45
49
  isValidBase64,
46
50
  linkAttachmentToMessage,
47
51
  MAX_UPLOAD_BYTES,
@@ -49,7 +53,8 @@ import {
49
53
  validateAttachmentUpload,
50
54
  } from "../memory/attachments-store.js";
51
55
  import { addMessage, createConversation } from "../memory/conversation-crud.js";
52
- import { getDb, initializeDb, resetDb } from "../memory/db.js";
56
+ import { getConversationDirPath } from "../memory/conversation-disk-view.js";
57
+ import { getDb, initializeDb, rawGet, rawRun, resetDb } from "../memory/db.js";
53
58
 
54
59
  initializeDb();
55
60
 
@@ -70,8 +75,24 @@ function resetTables() {
70
75
  db.run("DELETE FROM conversations");
71
76
  }
72
77
 
78
+ function getConversationTimestamp(createdAt: number): string {
79
+ return new Date(createdAt).toISOString().replace(/:/g, "-");
80
+ }
81
+
82
+ function getLegacyConversationDirPath(
83
+ conversationId: string,
84
+ createdAt: number,
85
+ ): string {
86
+ return join(
87
+ testDir,
88
+ "workspace",
89
+ "conversations",
90
+ `${conversationId}_${getConversationTimestamp(createdAt)}`,
91
+ );
92
+ }
93
+
73
94
  // ---------------------------------------------------------------------------
74
- // uploadAttachment
95
+ // uploadAttachment — stages until linked
75
96
  // ---------------------------------------------------------------------------
76
97
 
77
98
  describe("uploadAttachment", () => {
@@ -88,6 +109,29 @@ describe("uploadAttachment", () => {
88
109
  expect(stored.createdAt).toBeGreaterThan(0);
89
110
  });
90
111
 
112
+ test("keeps uploads staged until linked", () => {
113
+ const stored = uploadAttachment("small.txt", "text/plain", "aGVsbG8=");
114
+ const filePath = getFilePathForAttachment(stored.id);
115
+
116
+ expect(filePath).toBeNull();
117
+ expect(getAttachmentContent(stored.id)?.toString()).toBe("hello");
118
+ });
119
+
120
+ test("stores base64 in the DB row until linked", () => {
121
+ const stored = uploadAttachment("test.txt", "text/plain", "dGVzdA==");
122
+
123
+ // Staged uploads keep the payload inline until they are attached to a message.
124
+ const rawRow = rawGet<{ data_base64: string }>(
125
+ "SELECT data_base64 FROM attachments WHERE id = ?",
126
+ stored.id,
127
+ );
128
+ expect(rawRow!.data_base64).toBe("dGVzdA==");
129
+
130
+ const row = getAttachmentById(stored.id, { hydrateFileData: true });
131
+ expect(row).not.toBeNull();
132
+ expect(row!.dataBase64).toBe("dGVzdA==");
133
+ });
134
+
91
135
  test("classifies image MIME as image kind", () => {
92
136
  const stored = uploadAttachment("pic.jpg", "image/jpeg", "AAAA");
93
137
  expect(stored.kind).toBe("image");
@@ -105,12 +149,12 @@ describe("uploadAttachment", () => {
105
149
  });
106
150
 
107
151
  test("computes sizeBytes from base64 correctly", () => {
108
- // "hello" = "aGVsbG8=" (8 chars, 1 pad 5 bytes)
152
+ // "hello" = "aGVsbG8=" (8 chars, 1 pad -> 5 bytes)
109
153
  const stored = uploadAttachment("hello.txt", "text/plain", "aGVsbG8=");
110
154
  expect(stored.sizeBytes).toBe(5);
111
155
  });
112
156
 
113
- test("deduplicates by content hash", () => {
157
+ test("does not deduplicate identical uploads before linking", () => {
114
158
  const first = uploadAttachment(
115
159
  "photo.png",
116
160
  "image/png",
@@ -121,10 +165,10 @@ describe("uploadAttachment", () => {
121
165
  "image/png",
122
166
  "iVBORw0KGgoAAAANSUh",
123
167
  );
124
- expect(second.id).toBe(first.id);
168
+ expect(second.id).not.toBe(first.id);
125
169
  });
126
170
 
127
- test("deduplicates even when filenames differ", () => {
171
+ test("does not deduplicate identical content when filenames differ", () => {
128
172
  const first = uploadAttachment(
129
173
  "original.png",
130
174
  "image/png",
@@ -135,7 +179,7 @@ describe("uploadAttachment", () => {
135
179
  "image/png",
136
180
  "DUPECONTENT123",
137
181
  );
138
- expect(second.id).toBe(first.id);
182
+ expect(second.id).not.toBe(first.id);
139
183
  });
140
184
 
141
185
  test("does not deduplicate different content", () => {
@@ -146,7 +190,7 @@ describe("uploadAttachment", () => {
146
190
 
147
191
  test("rejects payloads exceeding MAX_UPLOAD_BYTES", () => {
148
192
  // Build a base64 string that decodes to just over the limit.
149
- // 4 base64 chars 3 bytes, so we need ceil((MAX_UPLOAD_BYTES+1)/3)*4 chars.
193
+ // 4 base64 chars -> 3 bytes, so we need ceil((MAX_UPLOAD_BYTES+1)/3)*4 chars.
150
194
  const oversizedLength = Math.ceil((MAX_UPLOAD_BYTES + 1) / 3) * 4;
151
195
  const oversizedData = "A".repeat(oversizedLength);
152
196
 
@@ -162,7 +206,7 @@ describe("uploadAttachment", () => {
162
206
  });
163
207
 
164
208
  test("accepts base64 with non-standard padding/length", () => {
165
- // Lenient on length only character set is validated
209
+ // Lenient on length -- only character set is validated
166
210
  expect(() => uploadAttachment("ok.txt", "text/plain", "AAA")).not.toThrow();
167
211
  });
168
212
 
@@ -178,6 +222,41 @@ describe("uploadAttachment", () => {
178
222
  });
179
223
  });
180
224
 
225
+ // ---------------------------------------------------------------------------
226
+ // getAttachmentContent — staged inline or materialized on disk
227
+ // ---------------------------------------------------------------------------
228
+
229
+ describe("getAttachmentContent", () => {
230
+ beforeEach(resetTables);
231
+
232
+ test("returns staged content before the attachment is linked", () => {
233
+ const stored = uploadAttachment("hello.txt", "text/plain", "aGVsbG8=");
234
+ const content = getAttachmentContent(stored.id);
235
+
236
+ expect(content).not.toBeNull();
237
+ expect(content!.toString()).toBe("hello");
238
+ });
239
+
240
+ test("returns null for nonexistent attachment", () => {
241
+ const content = getAttachmentContent("no-such-id");
242
+ expect(content).toBeNull();
243
+ });
244
+
245
+ test("returns null when a materialized on-disk file is missing (ENOENT)", async () => {
246
+ const conv = createConversation();
247
+ const msg = await addMessage(conv.id, "assistant", "File");
248
+ const stored = uploadAttachment("test.txt", "text/plain", "dGVzdA==");
249
+ linkAttachmentToMessage(msg.id, stored.id, 0);
250
+ const filePath = getFilePathForAttachment(stored.id);
251
+
252
+ // Remove the file to simulate ENOENT
253
+ rmSync(filePath!);
254
+
255
+ const content = getAttachmentContent(stored.id);
256
+ expect(content).toBeNull();
257
+ });
258
+ });
259
+
181
260
  // ---------------------------------------------------------------------------
182
261
  // isValidBase64
183
262
  // ---------------------------------------------------------------------------
@@ -219,6 +298,23 @@ describe("deleteAttachment", () => {
219
298
  expect(fetched).toBeNull();
220
299
  });
221
300
 
301
+ test("cleans up on-disk file when deleting", async () => {
302
+ const conv = createConversation();
303
+ const msg = await addMessage(conv.id, "assistant", "cleanup");
304
+ const stored = uploadAttachment("cleanup.txt", "text/plain", "dGVzdA==");
305
+ linkAttachmentToMessage(msg.id, stored.id, 0);
306
+ const filePath = getFilePathForAttachment(stored.id);
307
+ expect(existsSync(filePath!)).toBe(true);
308
+
309
+ rawRun(
310
+ "DELETE FROM message_attachments WHERE attachment_id = ?",
311
+ stored.id,
312
+ );
313
+
314
+ deleteAttachment(stored.id);
315
+ expect(existsSync(filePath!)).toBe(false);
316
+ });
317
+
222
318
  test("returns not_found for nonexistent attachment", () => {
223
319
  const result = deleteAttachment("nonexistent-id");
224
320
  expect(result).toBe("not_found");
@@ -229,13 +325,9 @@ describe("deleteAttachment", () => {
229
325
  const msg1 = await addMessage(conv.id, "user", "First upload");
230
326
  const msg2 = await addMessage(conv.id, "user", "Duplicate upload");
231
327
 
232
- // Dedup: both uploads return the same attachment row
233
328
  const first = uploadAttachment("photo.png", "image/png", "SHAREDCONTENT1");
234
- const second = uploadAttachment("photo.png", "image/png", "SHAREDCONTENT1");
235
- expect(second.id).toBe(first.id);
236
-
237
329
  linkAttachmentToMessage(msg1.id, first.id, 0);
238
- linkAttachmentToMessage(msg2.id, second.id, 0);
330
+ linkAttachmentToMessage(msg2.id, first.id, 0);
239
331
 
240
332
  // Delete should return still_referenced and NOT remove the attachment row
241
333
  const result = deleteAttachment(first.id);
@@ -254,7 +346,7 @@ describe("deleteAttachment", () => {
254
346
 
255
347
  test("deletes attachment when no messages reference it", () => {
256
348
  const stored = uploadAttachment("lonely.txt", "text/plain", "UNREFERENCED");
257
- // No linkAttachmentToMessage call zero references
349
+ // No linkAttachmentToMessage call -- zero references
258
350
  const result = deleteAttachment(stored.id);
259
351
  expect(result).toBe("deleted");
260
352
 
@@ -270,11 +362,13 @@ describe("deleteAttachment", () => {
270
362
  describe("getAttachmentsByIds", () => {
271
363
  beforeEach(resetTables);
272
364
 
273
- test("returns matching attachments with data", () => {
365
+ test("returns matching attachments with hydrated dataBase64", () => {
274
366
  const a = uploadAttachment("a.txt", "text/plain", "AAAA");
275
367
  const b = uploadAttachment("b.txt", "text/plain", "BBBB");
276
368
 
277
- const results = getAttachmentsByIds([a.id, b.id]);
369
+ const results = getAttachmentsByIds([a.id, b.id], {
370
+ hydrateFileData: true,
371
+ });
278
372
  expect(results).toHaveLength(2);
279
373
  expect(results[0].dataBase64).toBe("AAAA");
280
374
  expect(results[1].dataBase64).toBe("BBBB");
@@ -299,9 +393,9 @@ describe("getAttachmentsByIds", () => {
299
393
  describe("getAttachmentById", () => {
300
394
  beforeEach(resetTables);
301
395
 
302
- test("returns attachment with data when found", () => {
396
+ test("returns attachment with hydrated dataBase64 when found", () => {
303
397
  const stored = uploadAttachment("report.pdf", "application/pdf", "JVBER");
304
- const result = getAttachmentById(stored.id);
398
+ const result = getAttachmentById(stored.id, { hydrateFileData: true });
305
399
 
306
400
  expect(result).not.toBeNull();
307
401
  expect(result!.id).toBe(stored.id);
@@ -334,6 +428,43 @@ describe("linkAttachmentToMessage + getAttachmentsForMessage", () => {
334
428
  expect(linked[0].id).toBe(stored.id);
335
429
  expect(linked[0].originalFilename).toBe("chart.png");
336
430
  expect(linked[0].dataBase64).toBe("iVBORw0K");
431
+ expect(getFilePathForAttachment(stored.id)).toContain("/conversations/");
432
+ });
433
+
434
+ test("uses timestamp-first conversation directory and does not recreate a legacy sibling", async () => {
435
+ const conv = createConversation();
436
+ const msg = await addMessage(conv.id, "assistant", "Disk view repaired");
437
+ const canonicalDir = getConversationDirPath(conv.id, conv.createdAt);
438
+ const legacyDir = getLegacyConversationDirPath(conv.id, conv.createdAt);
439
+ rmSync(legacyDir, { recursive: true, force: true });
440
+
441
+ const stored = uploadAttachment("repaired.png", "image/png", "iVBORw0K");
442
+ linkAttachmentToMessage(msg.id, stored.id, 0);
443
+
444
+ const filePath = getFilePathForAttachment(stored.id);
445
+ expect(filePath).not.toBeNull();
446
+ expect(filePath!).toContain(join(canonicalDir, "attachments"));
447
+ expect(existsSync(filePath!)).toBe(true);
448
+ expect(existsSync(legacyDir)).toBe(false);
449
+ });
450
+
451
+ test("reuses an existing legacy conversation directory when timestamp-first is absent", async () => {
452
+ const conv = createConversation();
453
+ const msg = await addMessage(conv.id, "assistant", "Legacy path");
454
+ const canonicalDir = getConversationDirPath(conv.id, conv.createdAt);
455
+ const legacyDir = getLegacyConversationDirPath(conv.id, conv.createdAt);
456
+
457
+ rmSync(canonicalDir, { recursive: true, force: true });
458
+ mkdirSync(legacyDir, { recursive: true });
459
+
460
+ const stored = uploadAttachment("legacy.png", "image/png", "iVBORw0K");
461
+ linkAttachmentToMessage(msg.id, stored.id, 0);
462
+
463
+ const filePath = getFilePathForAttachment(stored.id);
464
+ expect(filePath).not.toBeNull();
465
+ expect(filePath!).toContain(join(legacyDir, "attachments"));
466
+ expect(existsSync(filePath!)).toBe(true);
467
+ expect(existsSync(canonicalDir)).toBe(false);
337
468
  });
338
469
 
339
470
  test("returns attachments in position order", async () => {
@@ -375,6 +506,23 @@ describe("deleteOrphanAttachments", () => {
375
506
  expect(removed).toBe(1);
376
507
  });
377
508
 
509
+ test("cleans up on-disk files when removing orphaned materialized attachments", async () => {
510
+ const conv = createConversation();
511
+ const msg = await addMessage(conv.id, "assistant", "Orphan me");
512
+ const stored = uploadAttachment("orphan.txt", "text/plain", "ZGF0YQ==");
513
+ linkAttachmentToMessage(msg.id, stored.id, 0);
514
+ const filePath = getFilePathForAttachment(stored.id);
515
+ expect(existsSync(filePath!)).toBe(true);
516
+
517
+ rawRun(
518
+ "DELETE FROM message_attachments WHERE attachment_id = ?",
519
+ stored.id,
520
+ );
521
+
522
+ deleteOrphanAttachments([stored.id]);
523
+ expect(existsSync(filePath!)).toBe(false);
524
+ });
525
+
378
526
  test("preserves attachments that are still linked", async () => {
379
527
  const conv = createConversation();
380
528
  const msg = await addMessage(conv.id, "assistant", "With attachment");
@@ -500,7 +648,7 @@ describe("validateAttachmentUpload", () => {
500
648
  });
501
649
 
502
650
  test("handles filenames without extensions", () => {
503
- // No extension only MIME check applies
651
+ // No extension -- only MIME check applies
504
652
  expect(validateAttachmentUpload("Makefile", "text/plain").ok).toBe(true);
505
653
  expect(validateAttachmentUpload("Makefile", "application/x-evil").ok).toBe(
506
654
  false,
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { attachmentsToContentBlocks } from "../agent/attachments.js";
3
+ import {
4
+ attachmentsToContentBlocks,
5
+ enrichMessageWithSourcePaths,
6
+ } from "../agent/attachments.js";
4
7
  import { createUserMessage } from "../agent/message-types.js";
5
8
 
6
9
  // ---------------------------------------------------------------------------
@@ -141,3 +144,114 @@ describe("createUserMessage with image attachments", () => {
141
144
  expect(imageBlock.source.type).toBe("base64");
142
145
  });
143
146
  });
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // enrichMessageWithSourcePaths
150
+ // ---------------------------------------------------------------------------
151
+
152
+ describe("enrichMessageWithSourcePaths", () => {
153
+ test("appends a source path annotation for images with filePath", () => {
154
+ const attachments = [
155
+ {
156
+ filename: "photo.jpg",
157
+ mimeType: "image/jpeg",
158
+ data: "base64img",
159
+ filePath: "/Users/me/Desktop/photo.jpg",
160
+ },
161
+ ];
162
+ const original = createUserMessage("what is this?", attachments);
163
+ const enriched = enrichMessageWithSourcePaths(original, attachments);
164
+
165
+ expect(enriched).not.toBe(original);
166
+ // Original has text + image = 2 blocks; enriched adds 1 annotation = 3
167
+ expect(enriched.content).toHaveLength(3);
168
+ const annotation = enriched.content[2] as { type: "text"; text: string };
169
+ expect(annotation.type).toBe("text");
170
+ expect(annotation.text).toBe(
171
+ "[Attached image source: /Users/me/Desktop/photo.jpg]",
172
+ );
173
+ });
174
+
175
+ test("returns the original message (same reference) when no images have filePath", () => {
176
+ const attachments = [
177
+ {
178
+ filename: "photo.jpg",
179
+ mimeType: "image/jpeg",
180
+ data: "base64img",
181
+ },
182
+ ];
183
+ const original = createUserMessage("what is this?", attachments);
184
+ const result = enrichMessageWithSourcePaths(original, attachments);
185
+
186
+ expect(result).toBe(original);
187
+ });
188
+
189
+ test("skips non-image attachments with filePath", () => {
190
+ const attachments = [
191
+ {
192
+ filename: "doc.pdf",
193
+ mimeType: "application/pdf",
194
+ data: "pdfdata",
195
+ filePath: "/Users/me/Documents/doc.pdf",
196
+ },
197
+ ];
198
+ const original = createUserMessage("review this", attachments);
199
+ const result = enrichMessageWithSourcePaths(original, attachments);
200
+
201
+ // Non-image attachments are not annotated, so we get back the same ref
202
+ expect(result).toBe(original);
203
+ });
204
+
205
+ test("handles multiple images with file paths", () => {
206
+ const attachments = [
207
+ {
208
+ filename: "a.jpg",
209
+ mimeType: "image/jpeg",
210
+ data: "img1",
211
+ filePath: "/path/to/a.jpg",
212
+ },
213
+ {
214
+ filename: "b.png",
215
+ mimeType: "image/png",
216
+ data: "img2",
217
+ filePath: "/path/to/b.png",
218
+ },
219
+ ];
220
+ const original = createUserMessage("compare", attachments);
221
+ const enriched = enrichMessageWithSourcePaths(original, attachments);
222
+
223
+ expect(enriched).not.toBe(original);
224
+ // text + 2 images + 1 annotation = 4
225
+ expect(enriched.content).toHaveLength(4);
226
+ const annotation = enriched.content[3] as { type: "text"; text: string };
227
+ expect(annotation.type).toBe("text");
228
+ expect(annotation.text).toBe(
229
+ "[Attached image source: /path/to/a.jpg]\n[Attached image source: /path/to/b.png]",
230
+ );
231
+ });
232
+
233
+ test("only annotates images that have filePath, skips those without", () => {
234
+ const attachments = [
235
+ {
236
+ filename: "a.jpg",
237
+ mimeType: "image/jpeg",
238
+ data: "img1",
239
+ filePath: "/path/to/a.jpg",
240
+ },
241
+ {
242
+ filename: "b.png",
243
+ mimeType: "image/png",
244
+ data: "img2",
245
+ // no filePath — e.g. pasted screenshot
246
+ },
247
+ ];
248
+ const original = createUserMessage("compare", attachments);
249
+ const enriched = enrichMessageWithSourcePaths(original, attachments);
250
+
251
+ expect(enriched).not.toBe(original);
252
+ // text + 2 images + 1 annotation (only for a.jpg) = 4
253
+ expect(enriched.content).toHaveLength(4);
254
+ const annotation = enriched.content[3] as { type: "text"; text: string };
255
+ expect(annotation.text).toBe("[Attached image source: /path/to/a.jpg]");
256
+ });
257
+ });
@@ -348,6 +348,7 @@ describe("POST /v1/btw", () => {
348
348
 
349
349
  // Options: tool_choice must be "none"
350
350
  expect(options!.config!.tool_choice).toEqual({ type: "none" });
351
+ expect(options!.config!.modelIntent).toBe("latency-optimized");
351
352
  });
352
353
 
353
354
  // -- No persistence --
@@ -813,6 +813,44 @@ describe("canonical-guardian-store", () => {
813
813
  );
814
814
  });
815
815
 
816
+ test("expireAllPendingCanonicalRequests expires persistent kinds with past expiresAt", () => {
817
+ const expiredAccess = createCanonicalGuardianRequest({
818
+ kind: "access_request",
819
+ sourceType: "channel",
820
+ conversationId: "conv-bulk-persist-expired-1",
821
+ guardianPrincipalId: TEST_PRINCIPAL,
822
+ expiresAt: Date.now() - 10_000,
823
+ });
824
+ const expiredGrant = createCanonicalGuardianRequest({
825
+ kind: "tool_grant_request",
826
+ sourceType: "channel",
827
+ conversationId: "conv-bulk-persist-expired-2",
828
+ guardianPrincipalId: TEST_PRINCIPAL,
829
+ expiresAt: Date.now() - 10_000,
830
+ });
831
+ // Persistent kind with future expiresAt should NOT be expired
832
+ const futureAccess = createCanonicalGuardianRequest({
833
+ kind: "access_request",
834
+ sourceType: "channel",
835
+ conversationId: "conv-bulk-persist-expired-3",
836
+ guardianPrincipalId: TEST_PRINCIPAL,
837
+ expiresAt: Date.now() + 60_000,
838
+ });
839
+
840
+ const count = expireAllPendingCanonicalRequests();
841
+ expect(count).toBe(2);
842
+
843
+ expect(getCanonicalGuardianRequest(expiredAccess.id)!.status).toBe(
844
+ "expired",
845
+ );
846
+ expect(getCanonicalGuardianRequest(expiredGrant.id)!.status).toBe(
847
+ "expired",
848
+ );
849
+ expect(getCanonicalGuardianRequest(futureAccess.id)!.status).toBe(
850
+ "pending",
851
+ );
852
+ });
853
+
816
854
  test("expireAllPendingCanonicalRequests does not affect already-resolved requests", () => {
817
855
  const approved = createCanonicalGuardianRequest({
818
856
  kind: "tool_approval",
@@ -365,6 +365,61 @@ describe("channel-reply-delivery", () => {
365
365
  expect(deliveryCalls[0].payload.user).toBeUndefined();
366
366
  });
367
367
 
368
+ it("suppresses delivery when the only text segment is <no_response/>", async () => {
369
+ await deliverRenderedReplyViaCallback({
370
+ callbackUrl: "http://gateway/deliver/slack",
371
+ chatId: "chat-silent",
372
+ textSegments: ["<no_response/>"],
373
+ fallbackText: "Fallback text",
374
+ interSegmentDelayMs: 0,
375
+ });
376
+
377
+ expect(deliveryCalls).toHaveLength(0);
378
+ });
379
+
380
+ it("suppresses attachment delivery when <no_response/> is present", async () => {
381
+ await deliverRenderedReplyViaCallback({
382
+ callbackUrl: "http://gateway/deliver/slack",
383
+ chatId: "chat-silent-att",
384
+ textSegments: ["<no_response/>"],
385
+ attachments: [
386
+ {
387
+ id: "att-no-resp",
388
+ filename: "secret.txt",
389
+ mimeType: "text/plain",
390
+ sizeBytes: 10,
391
+ kind: "uploaded",
392
+ },
393
+ ],
394
+ interSegmentDelayMs: 0,
395
+ });
396
+
397
+ expect(deliveryCalls).toHaveLength(0);
398
+ });
399
+
400
+ it("suppresses delivery for <no_response/> with surrounding whitespace", async () => {
401
+ await deliverRenderedReplyViaCallback({
402
+ callbackUrl: "http://gateway/deliver/slack",
403
+ chatId: "chat-silent-ws",
404
+ textSegments: [" <no_response/> "],
405
+ interSegmentDelayMs: 0,
406
+ });
407
+
408
+ expect(deliveryCalls).toHaveLength(0);
409
+ });
410
+
411
+ it("delivers other segments when <no_response/> is mixed with real text", async () => {
412
+ await deliverRenderedReplyViaCallback({
413
+ callbackUrl: "http://gateway/deliver/slack",
414
+ chatId: "chat-mixed",
415
+ textSegments: ["<no_response/>", "Real response."],
416
+ interSegmentDelayMs: 0,
417
+ });
418
+
419
+ expect(deliveryCalls).toHaveLength(1);
420
+ expect(deliveryCalls[0].payload.text).toBe("Real response.");
421
+ });
422
+
368
423
  it("passes startFromSegment through deliverReplyViaCallback options", async () => {
369
424
  conversationMessages.push(
370
425
  { id: "msg-u", role: "user", content: "hi" },
@@ -294,6 +294,28 @@ describe("Permission Checker", () => {
294
294
  );
295
295
  });
296
296
 
297
+ test("git --no-pager log is low risk (boolean global flag before subcommand)", async () => {
298
+ expect(
299
+ await classifyRisk("bash", { command: "git --no-pager log" }),
300
+ ).toBe(RiskLevel.Low);
301
+ });
302
+
303
+ test("git -C /some/path status is low risk (value-taking flag before subcommand)", async () => {
304
+ expect(
305
+ await classifyRisk("bash", {
306
+ command: "git -C /some/path status",
307
+ }),
308
+ ).toBe(RiskLevel.Low);
309
+ });
310
+
311
+ test("git -c core.editor=vim diff is low risk (value-taking -c flag before subcommand)", async () => {
312
+ expect(
313
+ await classifyRisk("bash", {
314
+ command: "git -c core.editor=vim diff",
315
+ }),
316
+ ).toBe(RiskLevel.Low);
317
+ });
318
+
297
319
  test("echo is low risk", async () => {
298
320
  expect(await classifyRisk("bash", { command: "echo hello" })).toBe(
299
321
  RiskLevel.Low,
@@ -381,6 +403,38 @@ describe("Permission Checker", () => {
381
403
  ).toBe(RiskLevel.Medium);
382
404
  });
383
405
 
406
+ test("git -C status commit is medium risk (value-taking flag with dir named like a subcommand)", async () => {
407
+ expect(
408
+ await classifyRisk("bash", {
409
+ command: "git -C status commit",
410
+ }),
411
+ ).toBe(RiskLevel.Medium);
412
+ });
413
+
414
+ test("git -C /path push is medium risk (value-taking flag before mutating subcommand)", async () => {
415
+ expect(
416
+ await classifyRisk("bash", {
417
+ command: "git -C /path push",
418
+ }),
419
+ ).toBe(RiskLevel.Medium);
420
+ });
421
+
422
+ test("git --git-dir /path/to/.git push is medium risk", async () => {
423
+ expect(
424
+ await classifyRisk("bash", {
425
+ command: "git --git-dir /path/to/.git push",
426
+ }),
427
+ ).toBe(RiskLevel.Medium);
428
+ });
429
+
430
+ test("git --no-pager push is medium risk (boolean flag before mutating subcommand)", async () => {
431
+ expect(
432
+ await classifyRisk("bash", {
433
+ command: "git --no-pager push",
434
+ }),
435
+ ).toBe(RiskLevel.Medium);
436
+ });
437
+
384
438
  test("opaque construct (eval) is medium risk", async () => {
385
439
  expect(await classifyRisk("bash", { command: 'eval "ls"' })).toBe(
386
440
  RiskLevel.Medium,
@@ -56,6 +56,8 @@ mock.module("../config/loader.js", () => ({
56
56
  mock.module("../security/secure-keys.js", () => ({
57
57
  getSecureKeyAsync: async (name: string) =>
58
58
  name === "anthropic" ? "fake-anthropic-key" : null,
59
+ getProviderKeyAsync: async (provider: string) =>
60
+ provider === "anthropic" ? "fake-anthropic-key" : undefined,
59
61
  }));
60
62
 
61
63
  // ---------------------------------------------------------------------------
@@ -40,6 +40,8 @@ mock.module("../config/loader.js", () => ({
40
40
  mock.module("../security/secure-keys.js", () => ({
41
41
  getSecureKeyAsync: async (name: string) =>
42
42
  name === "anthropic" ? "fake-anthropic-key" : null,
43
+ getProviderKeyAsync: async (provider: string) =>
44
+ provider === "anthropic" ? "fake-anthropic-key" : undefined,
43
45
  }));
44
46
 
45
47
  import { claudeCodeTool } from "../tools/claude-code/claude-code.js";
@@ -149,7 +149,8 @@ describe("Compaction benchmark", () => {
149
149
  );
150
150
  // Target is maxInputTokens * (targetBudgetRatio - summaryBudgetRatio)
151
151
  const targetTokens = Math.floor(
152
- config.maxInputTokens * (config.targetBudgetRatio - config.summaryBudgetRatio),
152
+ config.maxInputTokens *
153
+ (config.targetBudgetRatio - config.summaryBudgetRatio),
153
154
  );
154
155
  expect(result.estimatedInputTokens).toBeLessThanOrEqual(targetTokens);
155
156
  });