@vellumai/assistant 0.7.2 → 0.7.3

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 +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -0,0 +1,379 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { DiskUsageInfo } from "../util/disk-usage.js";
4
+
5
+ let diskSample: DiskUsageInfo | null = null;
6
+ const eventSubscribers = new Set<(event: unknown) => void>();
7
+
8
+ mock.module("../config/loader.js", () => ({
9
+ getConfig: () => ({}),
10
+ }));
11
+
12
+ mock.module("../util/disk-usage.js", () => ({
13
+ getDiskUsageInfo: () => diskSample,
14
+ }));
15
+
16
+ mock.module("../runtime/assistant-event.js", () => ({
17
+ buildAssistantEvent: (message: unknown, conversationId?: string) => ({
18
+ id: "event-test",
19
+ type: "message",
20
+ timestamp: new Date().toISOString(),
21
+ conversationId,
22
+ message,
23
+ }),
24
+ }));
25
+
26
+ mock.module("../runtime/assistant-event-hub.js", () => ({
27
+ AssistantEventHub: class {},
28
+ broadcastMessage: () => {},
29
+ capabilityForMessageType: () => undefined,
30
+ assistantEventHub: {
31
+ publish: async (event: unknown) => {
32
+ for (const callback of eventSubscribers) callback(event);
33
+ },
34
+ subscribe: ({ callback }: { callback: (event: unknown) => void }) => {
35
+ eventSubscribers.add(callback);
36
+ return {
37
+ dispose: () => {
38
+ eventSubscribers.delete(callback);
39
+ },
40
+ };
41
+ },
42
+ },
43
+ }));
44
+
45
+ const { _setOverridesForTesting } =
46
+ await import("../config/assistant-feature-flags.js");
47
+ const {
48
+ DISK_PRESSURE_OVERRIDE_CONFIRMATION,
49
+ DISK_PRESSURE_THRESHOLD_PERCENT,
50
+ __resetDiskPressureGuardForTests,
51
+ evaluateDiskPressureNow,
52
+ } = await import("../daemon/disk-pressure-guard.js");
53
+ const { assistantEventHub } = await import("../runtime/assistant-event-hub.js");
54
+ const { getPolicy } = await import("../runtime/auth/route-policy.js");
55
+ const { RouteError } = await import("../runtime/routes/errors.js");
56
+ const { ROUTES } = await import("../runtime/routes/disk-pressure-routes.js");
57
+
58
+ type DiskPressureRouteResult = {
59
+ status: {
60
+ enabled: boolean;
61
+ state: string;
62
+ locked: boolean;
63
+ acknowledged: boolean;
64
+ overrideActive: boolean;
65
+ effectivelyLocked: boolean;
66
+ lockId: string | null;
67
+ usagePercent: number | null;
68
+ thresholdPercent: number;
69
+ path: string | null;
70
+ lastCheckedAt: string | null;
71
+ blockedCapabilities: string[];
72
+ error: string | null;
73
+ };
74
+ };
75
+
76
+ function setFeatureFlag(enabled: boolean): void {
77
+ _setOverridesForTesting({ "safe-storage-limits": enabled });
78
+ }
79
+
80
+ function setDiskUsage(usedMb: number, totalMb = 100): void {
81
+ diskSample = {
82
+ path: "/workspace",
83
+ totalMb,
84
+ usedMb,
85
+ freeMb: Math.max(0, totalMb - usedMb),
86
+ };
87
+ }
88
+
89
+ function getRoute(endpoint: string, method: string) {
90
+ const route = ROUTES.find(
91
+ (r) => r.endpoint === endpoint && r.method === method,
92
+ );
93
+ if (!route) throw new Error(`${method} ${endpoint} route not registered`);
94
+ return route;
95
+ }
96
+
97
+ async function callRoute(
98
+ endpoint: string,
99
+ method: string,
100
+ body?: Record<string, unknown>,
101
+ ): Promise<DiskPressureRouteResult> {
102
+ return (await getRoute(endpoint, method).handler({
103
+ body,
104
+ })) as DiskPressureRouteResult;
105
+ }
106
+
107
+ function expectRouteError(
108
+ error: unknown,
109
+ code: string,
110
+ statusCode: number,
111
+ ): void {
112
+ expect(error).toBeInstanceOf(RouteError);
113
+ const routeError = error as { code: string; statusCode: number };
114
+ expect(routeError.code).toBe(code);
115
+ expect(routeError.statusCode).toBe(statusCode);
116
+ }
117
+
118
+ async function expectRouteRejects(
119
+ endpoint: string,
120
+ method: string,
121
+ body: Record<string, unknown> | undefined,
122
+ code: string,
123
+ statusCode: number,
124
+ ): Promise<void> {
125
+ try {
126
+ await callRoute(endpoint, method, body);
127
+ throw new Error("Expected route to reject");
128
+ } catch (error) {
129
+ expectRouteError(error, code, statusCode);
130
+ }
131
+ }
132
+
133
+ async function flushPublishedEvents(): Promise<void> {
134
+ await Promise.resolve();
135
+ await Promise.resolve();
136
+ }
137
+
138
+ beforeEach(() => {
139
+ __resetDiskPressureGuardForTests();
140
+ setFeatureFlag(true);
141
+ setDiskUsage(10);
142
+ });
143
+
144
+ afterEach(() => {
145
+ __resetDiskPressureGuardForTests();
146
+ _setOverridesForTesting({});
147
+ diskSample = null;
148
+ eventSubscribers.clear();
149
+ });
150
+
151
+ describe("disk pressure routes", () => {
152
+ test("registers routes and route auth policies", () => {
153
+ for (const [endpoint, method] of [
154
+ ["disk-pressure/status", "GET"],
155
+ ["disk-pressure/acknowledge", "POST"],
156
+ ["disk-pressure/override", "POST"],
157
+ ] as const) {
158
+ expect(
159
+ ROUTES.some(
160
+ (route) => route.endpoint === endpoint && route.method === method,
161
+ ),
162
+ ).toBe(true);
163
+ expect(getPolicy(endpoint)).toBeDefined();
164
+ }
165
+ });
166
+
167
+ test("returns disabled status without error when the feature flag is off", async () => {
168
+ setFeatureFlag(false);
169
+ setDiskUsage(99);
170
+
171
+ const result = await callRoute("disk-pressure/status", "GET");
172
+
173
+ expect(result.status).toMatchObject({
174
+ enabled: false,
175
+ state: "disabled",
176
+ locked: false,
177
+ acknowledged: false,
178
+ overrideActive: false,
179
+ effectivelyLocked: false,
180
+ lockId: null,
181
+ usagePercent: null,
182
+ path: null,
183
+ lastCheckedAt: null,
184
+ blockedCapabilities: [],
185
+ error: null,
186
+ });
187
+ expect(result.status.thresholdPercent).toBe(
188
+ DISK_PRESSURE_THRESHOLD_PERCENT,
189
+ );
190
+ });
191
+
192
+ test("returns the full status shape for an active lock", async () => {
193
+ setDiskUsage(99);
194
+ evaluateDiskPressureNow();
195
+
196
+ const result = await callRoute("disk-pressure/status", "GET");
197
+
198
+ expect(result.status.enabled).toBe(true);
199
+ expect(result.status.state).toBe("critical");
200
+ expect(result.status.locked).toBe(true);
201
+ expect(result.status.acknowledged).toBe(false);
202
+ expect(result.status.overrideActive).toBe(false);
203
+ expect(result.status.effectivelyLocked).toBe(true);
204
+ expect(result.status.lockId).toBeTruthy();
205
+ expect(result.status.usagePercent).toBe(99);
206
+ expect(result.status.thresholdPercent).toBe(
207
+ DISK_PRESSURE_THRESHOLD_PERCENT,
208
+ );
209
+ expect(result.status.path).toBe("/workspace");
210
+ expect(result.status.lastCheckedAt).toBeTruthy();
211
+ expect(result.status.blockedCapabilities).toEqual([
212
+ "agent-turns",
213
+ "background-work",
214
+ "remote-ingress",
215
+ ]);
216
+ expect(result.status.error).toBeNull();
217
+ });
218
+
219
+ test("returns cached status without sampling or emitting a read-path status event", async () => {
220
+ const events: unknown[] = [];
221
+ const subscription = assistantEventHub.subscribe({
222
+ type: "process",
223
+ callback: (event) => {
224
+ if (event.message.type === "disk_pressure_status_changed") {
225
+ events.push(event.message);
226
+ }
227
+ },
228
+ });
229
+
230
+ try {
231
+ setDiskUsage(99);
232
+
233
+ const result = await callRoute("disk-pressure/status", "GET");
234
+ await flushPublishedEvents();
235
+
236
+ expect(result.status.state).toBe("ok");
237
+ expect(result.status.locked).toBe(false);
238
+ expect(result.status.usagePercent).toBeNull();
239
+ expect(events).toEqual([]);
240
+ } finally {
241
+ subscription.dispose();
242
+ }
243
+ });
244
+
245
+ test("acknowledges an active lock without overriding it", async () => {
246
+ setDiskUsage(99);
247
+ evaluateDiskPressureNow();
248
+
249
+ const result = await callRoute("disk-pressure/acknowledge", "POST");
250
+
251
+ expect(result.status.acknowledged).toBe(true);
252
+ expect(result.status.overrideActive).toBe(false);
253
+ expect(result.status.effectivelyLocked).toBe(true);
254
+ });
255
+
256
+ test("overrides an active lock only after the confirmation phrase", async () => {
257
+ setDiskUsage(99);
258
+ evaluateDiskPressureNow();
259
+
260
+ const result = await callRoute("disk-pressure/override", "POST", {
261
+ confirmation: DISK_PRESSURE_OVERRIDE_CONFIRMATION,
262
+ });
263
+
264
+ expect(result.status.locked).toBe(true);
265
+ expect(result.status.overrideActive).toBe(true);
266
+ expect(result.status.effectivelyLocked).toBe(false);
267
+ });
268
+
269
+ test("rejects an invalid override phrase with INVALID_CONFIRMATION", async () => {
270
+ setDiskUsage(99);
271
+ evaluateDiskPressureNow();
272
+
273
+ await expectRouteRejects(
274
+ "disk-pressure/override",
275
+ "POST",
276
+ { confirmation: "I accept the risks" },
277
+ "INVALID_CONFIRMATION",
278
+ 400,
279
+ );
280
+ });
281
+
282
+ test("rejects acknowledgement and override when no lock is active", async () => {
283
+ setDiskUsage(10);
284
+ evaluateDiskPressureNow();
285
+
286
+ await expectRouteRejects(
287
+ "disk-pressure/acknowledge",
288
+ "POST",
289
+ undefined,
290
+ "NOT_LOCKED",
291
+ 409,
292
+ );
293
+ await expectRouteRejects(
294
+ "disk-pressure/override",
295
+ "POST",
296
+ { confirmation: DISK_PRESSURE_OVERRIDE_CONFIRMATION },
297
+ "NOT_LOCKED",
298
+ 409,
299
+ );
300
+ });
301
+
302
+ test("rejects repeated acknowledgement", async () => {
303
+ setDiskUsage(99);
304
+ evaluateDiskPressureNow();
305
+ await callRoute("disk-pressure/acknowledge", "POST");
306
+
307
+ await expectRouteRejects(
308
+ "disk-pressure/acknowledge",
309
+ "POST",
310
+ undefined,
311
+ "ALREADY_ACKNOWLEDGED",
312
+ 409,
313
+ );
314
+ });
315
+
316
+ test("rejects repeated override", async () => {
317
+ setDiskUsage(99);
318
+ evaluateDiskPressureNow();
319
+ await callRoute("disk-pressure/override", "POST", {
320
+ confirmation: DISK_PRESSURE_OVERRIDE_CONFIRMATION,
321
+ });
322
+
323
+ await expectRouteRejects(
324
+ "disk-pressure/override",
325
+ "POST",
326
+ { confirmation: DISK_PRESSURE_OVERRIDE_CONFIRMATION },
327
+ "ALREADY_OVERRIDDEN",
328
+ 409,
329
+ );
330
+ });
331
+
332
+ test("emits typed status-change events for lock, acknowledgement, override, usage, and unlock changes", async () => {
333
+ const events: unknown[] = [];
334
+ const subscription = assistantEventHub.subscribe({
335
+ type: "process",
336
+ callback: (event) => {
337
+ if (event.message.type === "disk_pressure_status_changed") {
338
+ events.push(event.message);
339
+ }
340
+ },
341
+ });
342
+
343
+ try {
344
+ setDiskUsage(99);
345
+ evaluateDiskPressureNow();
346
+ await callRoute("disk-pressure/acknowledge", "POST");
347
+ await callRoute("disk-pressure/override", "POST", {
348
+ confirmation: DISK_PRESSURE_OVERRIDE_CONFIRMATION,
349
+ });
350
+ setDiskUsage(98);
351
+ evaluateDiskPressureNow();
352
+ setDiskUsage(10);
353
+ evaluateDiskPressureNow();
354
+ await flushPublishedEvents();
355
+ } finally {
356
+ subscription.dispose();
357
+ }
358
+
359
+ const statuses = events.map(
360
+ (message) =>
361
+ (message as { status: DiskPressureRouteResult["status"] }).status,
362
+ );
363
+ expect(statuses.map((status) => status.state)).toEqual([
364
+ "critical",
365
+ "critical",
366
+ "critical",
367
+ "critical",
368
+ "ok",
369
+ ]);
370
+ expect(statuses[0].enabled).toBe(true);
371
+ expect(statuses[1].acknowledged).toBe(true);
372
+ expect(statuses[1].overrideActive).toBe(false);
373
+ expect(statuses[2].overrideActive).toBe(true);
374
+ expect(statuses[2].effectivelyLocked).toBe(false);
375
+ expect(statuses[3].usagePercent).toBe(98);
376
+ expect(statuses[4].locked).toBe(false);
377
+ expect(statuses[4].lockId).toBeNull();
378
+ });
379
+ });
@@ -0,0 +1,277 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { SkillProjectionCache } from "../daemon/conversation-skill-tools.js";
4
+ import type { SkillProjectionContext } from "../daemon/conversation-tool-setup.js";
5
+ import type { Message, ToolDefinition } from "../providers/types.js";
6
+ import type { DiskUsageInfo } from "../util/disk-usage.js";
7
+
8
+ let diskSample: DiskUsageInfo | null = null;
9
+
10
+ const mockConfig = {
11
+ timeouts: {
12
+ shellDefaultTimeoutSec: 120,
13
+ shellMaxTimeoutSec: 600,
14
+ permissionTimeoutSec: 300,
15
+ },
16
+ sandbox: {
17
+ enabled: false,
18
+ backend: "native" as const,
19
+ docker: {
20
+ image: "vellum-sandbox:latest",
21
+ cpus: 1,
22
+ memoryMb: 512,
23
+ pidsLimit: 256,
24
+ network: "none" as const,
25
+ },
26
+ },
27
+ permissions: { mode: "workspace" as const },
28
+ };
29
+
30
+ mock.module("../config/loader.js", () => ({
31
+ getConfig: () => mockConfig,
32
+ getConfigReadOnly: () => mockConfig,
33
+ loadConfig: () => mockConfig,
34
+ applyNestedDefaults: () => mockConfig,
35
+ deepMergeOverwrite: (_base: unknown, override: unknown) => override,
36
+ invalidateConfigCache: () => undefined,
37
+ loadRawConfig: () => ({}),
38
+ saveRawConfig: () => undefined,
39
+ getNestedValue: () => undefined,
40
+ setNestedValue: () => undefined,
41
+ mergeDefaultWorkspaceConfig: (config: unknown) => config,
42
+ API_KEY_PROVIDERS: [] as const,
43
+ _appendQuarantineBulletin: () => undefined,
44
+ }));
45
+
46
+ mock.module("../daemon/conversation-skill-tools.js", () => ({
47
+ projectSkillTools: mock((_history: Message[], _opts: unknown) => ({
48
+ allowedToolNames: new Set<string>(),
49
+ toolDefinitions: [],
50
+ })),
51
+ resetSkillToolProjection: () => undefined,
52
+ }));
53
+
54
+ mock.module("../runtime/agent-wake.js", () => ({
55
+ wakeAgentForOpportunity: async () => undefined,
56
+ }));
57
+
58
+ mock.module("../runtime/assistant-event.js", () => ({
59
+ buildAssistantEvent: (message: unknown, conversationId?: string) => ({
60
+ id: "event-test",
61
+ type: "message",
62
+ timestamp: new Date().toISOString(),
63
+ conversationId,
64
+ message,
65
+ }),
66
+ }));
67
+
68
+ mock.module("../runtime/assistant-event-hub.js", () => ({
69
+ AssistantEventHub: class {},
70
+ broadcastMessage: () => {},
71
+ capabilityForMessageType: () => undefined,
72
+ assistantEventHub: {
73
+ publish: async () => undefined,
74
+ listClientsByCapability: () => [],
75
+ },
76
+ }));
77
+
78
+ mock.module("../util/disk-usage.js", () => ({
79
+ getDiskUsageInfo: () => diskSample,
80
+ }));
81
+
82
+ const { _setOverridesForTesting } =
83
+ await import("../config/assistant-feature-flags.js");
84
+ const {
85
+ DISK_PRESSURE_THRESHOLD_PERCENT,
86
+ __resetDiskPressureGuardForTests,
87
+ evaluateDiskPressureNow,
88
+ } = await import("../daemon/disk-pressure-guard.js");
89
+ const { createResolveToolsCallback } =
90
+ await import("../daemon/conversation-tool-setup.js");
91
+ const { ToolApprovalHandler } =
92
+ await import("../tools/tool-approval-handler.js");
93
+ const {
94
+ _clearRegistryForTesting,
95
+ listBackgroundTools,
96
+ registerBackgroundTool,
97
+ } = await import("../tools/background-tool-registry.js");
98
+ const { shellTool } = await import("../tools/terminal/shell.js");
99
+ const { hostShellTool } = await import("../tools/host-terminal/host-shell.js");
100
+
101
+ function makeToolDef(name: string): ToolDefinition {
102
+ return { name, description: `${name} tool`, input_schema: {} };
103
+ }
104
+
105
+ function makeProjectionCtx(
106
+ overrides: Partial<SkillProjectionContext> = {},
107
+ ): SkillProjectionContext {
108
+ return {
109
+ skillProjectionState: new Map(),
110
+ skillProjectionCache: {} as SkillProjectionCache,
111
+ coreToolNames: new Set(),
112
+ toolsDisabledDepth: 0,
113
+ ...overrides,
114
+ };
115
+ }
116
+
117
+ function setDiskUsage(usedMb: number, totalMb = 100): void {
118
+ diskSample = {
119
+ path: "/workspace",
120
+ totalMb,
121
+ usedMb,
122
+ freeMb: Math.max(0, totalMb - usedMb),
123
+ };
124
+ }
125
+
126
+ beforeEach(() => {
127
+ _clearRegistryForTesting();
128
+ __resetDiskPressureGuardForTests();
129
+ _setOverridesForTesting({ "safe-storage-limits": true });
130
+ setDiskUsage(10);
131
+ });
132
+
133
+ afterEach(() => {
134
+ _clearRegistryForTesting();
135
+ __resetDiskPressureGuardForTests();
136
+ _setOverridesForTesting({});
137
+ diskSample = null;
138
+ });
139
+
140
+ describe("disk pressure cleanup tool restrictions", () => {
141
+ test("cleanup mode hides non-allowlisted tools and restores normal tools after the turn", () => {
142
+ const toolDefs = [
143
+ makeToolDef("bash"),
144
+ makeToolDef("host_bash"),
145
+ makeToolDef("file_read"),
146
+ makeToolDef("file_write"),
147
+ makeToolDef("skill_execute"),
148
+ makeToolDef("web_fetch"),
149
+ ];
150
+ const ctx = makeProjectionCtx({ diskPressureCleanupModeActive: true });
151
+ const resolve = createResolveToolsCallback(toolDefs, ctx)!;
152
+
153
+ const cleanupTools = resolve([]);
154
+ const cleanupNames = cleanupTools.map((tool) => tool.name).sort();
155
+
156
+ expect(cleanupNames).toEqual(["bash", "file_read", "host_bash"]);
157
+ expect(ctx.allowedToolNames?.has("bash")).toBe(true);
158
+ expect(ctx.allowedToolNames?.has("file_write")).toBe(false);
159
+ expect(ctx.allowedToolNames?.has("skill_execute")).toBe(false);
160
+
161
+ ctx.diskPressureCleanupModeActive = false;
162
+ const normalTools = resolve([]);
163
+ const normalNames = normalTools.map((tool) => tool.name);
164
+
165
+ expect(normalNames).toContain("file_write");
166
+ expect(normalNames).toContain("skill_execute");
167
+ expect(normalNames).toContain("web_fetch");
168
+ expect(ctx.allowedToolNames?.has("file_write")).toBe(true);
169
+ });
170
+
171
+ test("executor fallback rejects non-cleanup tools even if stale allowlist includes them", async () => {
172
+ const handler = new ToolApprovalHandler();
173
+ const result = await handler.checkPreExecutionGates(
174
+ "file_write",
175
+ { path: "large.log", content: "data" },
176
+ {
177
+ workingDir: "/workspace",
178
+ conversationId: "conv-cleanup",
179
+ trustClass: "guardian",
180
+ allowedToolNames: new Set(["file_write"]),
181
+ diskPressureCleanupModeActive: true,
182
+ },
183
+ "sandbox",
184
+ "low",
185
+ Date.now(),
186
+ () => undefined,
187
+ );
188
+
189
+ expect(result.allowed).toBe(false);
190
+ if (result.allowed) {
191
+ throw new Error("Expected disk pressure cleanup gate to reject tool");
192
+ }
193
+ expect(result.result.content).toContain(
194
+ "not available during disk pressure cleanup mode",
195
+ );
196
+ });
197
+
198
+ test("locking cancels registered terminal background tools with disk pressure reason", () => {
199
+ const bashCancel = mock((_reason?: string) => undefined);
200
+ const hostCancel = mock((_reason?: string) => undefined);
201
+ const otherCancel = mock((_reason?: string) => undefined);
202
+
203
+ registerBackgroundTool({
204
+ id: "bg-bash",
205
+ toolName: "bash",
206
+ conversationId: "conv-1",
207
+ command: "sleep 100",
208
+ startedAt: 1,
209
+ cancel: bashCancel,
210
+ });
211
+ registerBackgroundTool({
212
+ id: "bg-host",
213
+ toolName: "host_bash",
214
+ conversationId: "conv-1",
215
+ command: "sleep 100",
216
+ startedAt: 2,
217
+ cancel: hostCancel,
218
+ });
219
+ registerBackgroundTool({
220
+ id: "bg-other",
221
+ toolName: "web_fetch",
222
+ conversationId: "conv-1",
223
+ command: "fetch",
224
+ startedAt: 3,
225
+ cancel: otherCancel,
226
+ });
227
+
228
+ setDiskUsage(DISK_PRESSURE_THRESHOLD_PERCENT);
229
+ const status = evaluateDiskPressureNow();
230
+
231
+ expect(status.locked).toBe(true);
232
+ expect(bashCancel).toHaveBeenCalledWith("disk_pressure");
233
+ expect(hostCancel).toHaveBeenCalledWith("disk_pressure");
234
+ expect(otherCancel).not.toHaveBeenCalled();
235
+ expect(listBackgroundTools().map((tool) => tool.id)).toEqual(["bg-other"]);
236
+ });
237
+
238
+ test("background shell modes are blocked during cleanup mode", async () => {
239
+ const shellResult = await shellTool.execute(
240
+ {
241
+ command: "sleep 100",
242
+ activity: "check disk usage",
243
+ background: true,
244
+ },
245
+ {
246
+ workingDir: "/workspace",
247
+ conversationId: "conv-cleanup",
248
+ trustClass: "guardian",
249
+ diskPressureCleanupModeActive: true,
250
+ },
251
+ );
252
+
253
+ expect(shellResult.isError).toBe(true);
254
+ expect(shellResult.content).toContain(
255
+ "background shell commands are not available",
256
+ );
257
+
258
+ const hostResult = await hostShellTool.execute(
259
+ {
260
+ command: "sleep 100",
261
+ activity: "check disk usage",
262
+ background: true,
263
+ },
264
+ {
265
+ workingDir: "/workspace",
266
+ conversationId: "conv-cleanup",
267
+ trustClass: "guardian",
268
+ diskPressureCleanupModeActive: true,
269
+ },
270
+ );
271
+
272
+ expect(hostResult.isError).toBe(true);
273
+ expect(hostResult.content).toContain(
274
+ "background host shell commands are not available",
275
+ );
276
+ });
277
+ });