@vellumai/assistant 0.11.1 → 0.11.2-staging.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 (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -3,17 +3,18 @@
3
3
  */
4
4
 
5
5
  import { existsSync, readFileSync } from "node:fs";
6
- import { availableParallelism, cpus, totalmem } from "node:os";
6
+ import { totalmem } from "node:os";
7
7
 
8
8
  import { z } from "zod";
9
9
 
10
- import { getCpuLimit, getIsPlatform } from "../../config/env-registry.js";
10
+ import { getIsPlatform } from "../../config/env-registry.js";
11
11
  import { getDbMigrationReadiness } from "../../daemon/daemon-readiness.js";
12
12
  import { parseIdentityFields } from "../../daemon/handlers/identity.js";
13
13
  import { getProfilerRuntimeStatus } from "../../daemon/profiler-run-store.js";
14
14
  import { getMaxRollbackVersion } from "../../persistence/migrations/run-migrations.js";
15
15
  import { migrationSteps } from "../../persistence/steps.js";
16
16
  import { getCesClient } from "../../security/secure-keys.js";
17
+ import { getContainerCpuCores } from "../../util/cgroup-cpu.js";
17
18
  import {
18
19
  getContainerMemoryLimitBytes,
19
20
  getContainerMemoryUsageBytes,
@@ -92,97 +93,6 @@ interface CpuInfo {
92
93
  maxCores: number;
93
94
  }
94
95
 
95
- /**
96
- * Parse a Kubernetes-style CPU string (e.g. "2000m", "1", "500m") into
97
- * fractional cores. Returns null if the value is not a recognized format.
98
- */
99
- function parseK8sCpuCores(value: string): number | null {
100
- const trimmed = value.trim();
101
- const milliMatch = trimmed.match(/^(\d+)m$/);
102
- if (milliMatch) {
103
- const millis = parseInt(milliMatch[1], 10);
104
- return millis > 0 ? millis / 1000 : null;
105
- }
106
- if (/^\d+(\.\d+)?$/.test(trimmed)) {
107
- const num = parseFloat(trimmed);
108
- return !isNaN(num) && num > 0 ? num : null;
109
- }
110
- return null;
111
- }
112
-
113
- /**
114
- * Read the container's CPU core limit.
115
- *
116
- * Resolution order:
117
- * 1. VELLUM_CPU_LIMIT env var (K8s resource format, e.g. "2000m" or "2").
118
- * In platform mode the container runs under gVisor where cgroup files may
119
- * report the node's CPU count rather than the sandbox limit.
120
- * 2. cgroups v2 cpu.max (quota / period → fractional cores).
121
- * 3. cgroups v1 cpu.cfs_quota_us / cpu.cfs_period_us.
122
- * 4. os.cpus().length as last resort.
123
- */
124
- function getContainerCpuCores(): number {
125
- // 1. Prefer the explicit env var set by the platform StatefulSet template.
126
- try {
127
- const envLimit = getCpuLimit();
128
- if (envLimit) {
129
- const parsed = parseK8sCpuCores(envLimit);
130
- if (parsed !== null) {
131
- return parsed;
132
- }
133
- }
134
- } catch {
135
- /* env var parsing failed – fall through */
136
- }
137
-
138
- // 2. Try cgroups v2: /sys/fs/cgroup/cpu.max contains "$MAX $PERIOD".
139
- try {
140
- const raw = readFileSync("/sys/fs/cgroup/cpu.max", "utf-8").trim();
141
- if (!raw.startsWith("max")) {
142
- const parts = raw.split(/\s+/);
143
- const quota = parseInt(parts[0], 10);
144
- const period = parseInt(parts[1], 10);
145
- if (!isNaN(quota) && !isNaN(period) && period > 0 && quota > 0) {
146
- const cores = quota / period;
147
- // Sanity check: if the value looks like the node's full CPU count
148
- // and we're on a platform pod, it's likely gVisor leaking the host value.
149
- if (cores < cpus().length * 0.9 || !getIsPlatform()) {
150
- return cores;
151
- }
152
- }
153
- }
154
- } catch {
155
- /* not available */
156
- }
157
-
158
- // 3. Try cgroups v1.
159
- try {
160
- const quota = parseInt(
161
- readFileSync("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", "utf-8").trim(),
162
- 10,
163
- );
164
- const period = parseInt(
165
- readFileSync("/sys/fs/cgroup/cpu/cpu.cfs_period_us", "utf-8").trim(),
166
- 10,
167
- );
168
- if (!isNaN(quota) && !isNaN(period) && period > 0 && quota > 0) {
169
- const cores = quota / period;
170
- if (cores < cpus().length * 0.9 || !getIsPlatform()) {
171
- return cores;
172
- }
173
- }
174
- } catch {
175
- /* not available */
176
- }
177
-
178
- // 4. Fall back to the visible CPU count; 0 when even that syscall fails.
179
- try {
180
- return cpus().length || availableParallelism();
181
- } catch {
182
- return 0;
183
- }
184
- }
185
-
186
96
  /**
187
97
  * Read the container's CPU usage from cgroup accounting files.
188
98
  *
@@ -8,8 +8,13 @@
8
8
  */
9
9
  import { describe, expect, test } from "bun:test";
10
10
 
11
+ import type { AdmissionPolicy } from "@vellumai/gateway-client";
12
+
11
13
  import type { AdmissionPolicyInput } from "./admission-policy.js";
12
- import { enforceAdmissionPolicy } from "./admission-policy.js";
14
+ import {
15
+ enforceAdmissionPolicy,
16
+ trustedContactPromotionClearsFloor,
17
+ } from "./admission-policy.js";
13
18
 
14
19
  function makeInput(
15
20
  overrides: Partial<AdmissionPolicyInput>,
@@ -176,3 +181,28 @@ describe("enforceAdmissionPolicy — rank vs floor", () => {
176
181
  expect(result.admitted).toBe(true);
177
182
  });
178
183
  });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Guardian approval reach: which floors a trusted-contact promotion clears
187
+ // ---------------------------------------------------------------------------
188
+
189
+ describe("trustedContactPromotionClearsFloor", () => {
190
+ const clears: AdmissionPolicy[] = [
191
+ "trusted_contacts",
192
+ "any_contact",
193
+ "strangers",
194
+ ];
195
+ const doesNotClear: AdmissionPolicy[] = ["guardian_only", "no_one"];
196
+
197
+ for (const policy of clears) {
198
+ test(`${policy} is cleared by a trusted-contact promotion`, () => {
199
+ expect(trustedContactPromotionClearsFloor(policy)).toBe(true);
200
+ });
201
+ }
202
+
203
+ for (const policy of doesNotClear) {
204
+ test(`${policy} is not cleared by a trusted-contact promotion`, () => {
205
+ expect(trustedContactPromotionClearsFloor(policy)).toBe(false);
206
+ });
207
+ }
208
+ });
@@ -102,6 +102,24 @@ const POLICIES_THAT_COULD_UPGRADE: ReadonlySet<AdmissionPolicy> = new Set([
102
102
  "strangers",
103
103
  ]);
104
104
 
105
+ /**
106
+ * Whether promoting a caller to a trusted contact would lift them past this
107
+ * floor.
108
+ *
109
+ * A guardian approving an access request activates the requester as a trusted
110
+ * contact (directly for voice, per `guardian-request-resolvers.ts`; via the
111
+ * minted verification code for text), so approval only helps on floors at or
112
+ * below the trusted-contact rank. `guardian_only` and `no_one` sit above it:
113
+ * an approved caller would still be below the floor, so callers denied by
114
+ * those floors must not be routed into an approval flow that cannot admit
115
+ * them.
116
+ */
117
+ export function trustedContactPromotionClearsFloor(
118
+ policy: AdmissionPolicy,
119
+ ): boolean {
120
+ return ADMISSION_FLOOR[policy] <= TRUST_CLASS_RANK.trusted_contact;
121
+ }
122
+
105
123
  /**
106
124
  * Enforce the admission policy floor against the resolved trust class.
107
125
  *
@@ -39,7 +39,10 @@ import {
39
39
  updateConnection,
40
40
  } from "../../providers/inference/connections.js";
41
41
  import { PROVIDER_CATALOG } from "../../providers/model-catalog.js";
42
- import { isVellumManagedConnection } from "../../providers/vellum-model-routing.js";
42
+ import {
43
+ isVellumManagedConnection,
44
+ VELLUM_MANAGED_PROVIDER,
45
+ } from "../../providers/vellum-model-routing.js";
43
46
  import { credentialKey } from "../../security/credential-key.js";
44
47
  import { deleteSecureKeyAsync } from "../../security/secure-keys.js";
45
48
  import {
@@ -192,6 +195,39 @@ function deriveConnectionAuth(provider: string, credential: unknown): Auth {
192
195
  return derived;
193
196
  }
194
197
 
198
+ /**
199
+ * Platform auth and `provider: "vellum"` record the same fact (the managed
200
+ * route) in two columns, and derivation always sets them together. Only an
201
+ * explicit `auth` object can split them, producing a row dispatch bills to
202
+ * the platform while every provider-keyed check reads it as BYOK, or the
203
+ * reverse.
204
+ */
205
+ function assertAuthMatchesProvider(provider: string, auth: Auth): void {
206
+ const managedAuth = auth.type === "platform";
207
+ const managedProvider = provider === VELLUM_MANAGED_PROVIDER;
208
+ if (managedAuth === managedProvider) {
209
+ return;
210
+ }
211
+ throw new BadRequestError(
212
+ managedAuth
213
+ ? `Auth type "platform" is only valid for provider "${VELLUM_MANAGED_PROVIDER}", not "${provider}". Vellum-managed routing is selected by the provider, so omit "auth" to derive it.`
214
+ : `Provider "${VELLUM_MANAGED_PROVIDER}" is always platform-authenticated; "${auth.type}" auth is not valid for it. Omit "auth" to derive it, or name a real provider for key-based auth.`,
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Stable form of an auth object for equality checks. Auth values are flat, so
220
+ * sorting the entries is enough to make two encodings of the same auth
221
+ * compare equal regardless of key order.
222
+ */
223
+ function authFingerprint(auth: Auth): string {
224
+ return JSON.stringify(
225
+ Object.fromEntries(
226
+ Object.entries(auth).sort(([a], [b]) => a.localeCompare(b)),
227
+ ),
228
+ );
229
+ }
230
+
195
231
  // ---------------------------------------------------------------------------
196
232
  // Handlers
197
233
  // ---------------------------------------------------------------------------
@@ -300,6 +336,9 @@ async function handleCreateConnection({ body = {} }: RouteHandlerArgs) {
300
336
  if (!authResult.success) {
301
337
  throw new BadRequestError(`Invalid auth: ${authResult.error.message}`);
302
338
  }
339
+ if (auth !== undefined) {
340
+ assertAuthMatchesProvider(providerResult.data, authResult.data);
341
+ }
303
342
 
304
343
  const labelRaw = body.label;
305
344
  if (
@@ -395,6 +434,16 @@ async function handleUpdateConnection({
395
434
  if (!authResult.success) {
396
435
  throw new BadRequestError(`Invalid auth: ${authResult.error.message}`);
397
436
  }
437
+ // The pairing is enforced on an actual auth change, not on the field being
438
+ // present: the web editor and the CLI both resend the stored auth on every
439
+ // edit, so a row whose columns already disagree stays relabelable and
440
+ // re-pointable.
441
+ if (
442
+ body.auth !== undefined &&
443
+ authFingerprint(authResult.data) !== authFingerprint(existing.auth)
444
+ ) {
445
+ assertAuthMatchesProvider(existing.provider, authResult.data);
446
+ }
398
447
 
399
448
  const labelRaw = body.label;
400
449
  if (
@@ -643,7 +692,7 @@ export const ROUTES: RouteDefinition[] = [
643
692
  },
644
693
  summary: "Create a provider connection",
645
694
  description:
646
- "Create a new named provider connection. When auth is omitted it is derived from the provider (keyless providers get none, vellum gets platform, everything else needs credential for api_key auth). Fails with 409 if a connection with this name already exists.",
695
+ "Create a new named provider connection. When auth is omitted it is derived from the provider (keyless providers get none, vellum gets platform, everything else needs credential for api_key auth). An explicit auth object must agree with the provider; platform auth belongs to vellum and only to vellum. Fails with 409 if a connection with this name already exists.",
647
696
  tags: ["inference"],
648
697
  requestBody: z.object({
649
698
  name: z.string().min(1),
@@ -672,7 +721,7 @@ export const ROUTES: RouteDefinition[] = [
672
721
  },
673
722
  summary: "Update a provider connection",
674
723
  description:
675
- "Update an existing connection. Cannot rename or change the provider. Omitting auth keeps the stored auth; passing credential alone rotates the key via provider-derived api_key auth. For the Vellum-managed connection (vellum) the auth is locked to platform; label remains editable.",
724
+ "Update an existing connection. Cannot rename or change the provider. Omitting auth keeps the stored auth; passing credential alone rotates the key via provider-derived api_key auth. An explicit auth object must agree with the connection's provider; platform auth belongs to vellum and only to vellum. For the Vellum-managed connection (vellum) the auth is locked to platform; label remains editable.",
676
725
  tags: ["inference"],
677
726
  pathParams: [{ name: "name", description: "Connection name" }],
678
727
  requestBody: z.object({
@@ -116,7 +116,10 @@ async function handleEmitSignal({ body = {} }: RouteHandlerArgs) {
116
116
  const EditNotificationParams = z
117
117
  .object({
118
118
  id: z.string().min(1).describe("Feed item id (notif:<uuid>) or bare uuid"),
119
- title: z.string().optional(),
119
+ title: z
120
+ .string()
121
+ .optional()
122
+ .describe("New title. An empty value is ignored, never cleared."),
120
123
  body: z.string().optional(),
121
124
  urgency: UrgencySchema.optional(),
122
125
  status: z.enum(["new", "seen", "acted_on", "dismissed"]).optional(),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Platform route handlers for the shared HTTP/IPC route table.
3
3
  *
4
- * Serves eight operations:
4
+ * Serves ten operations:
5
5
  * - platform_status (GET platform/status): aggregates platform context,
6
6
  * credentials, assistant ID, and webhook secret. (Velay tunnel status
7
7
  * lives on the gateway — see gateway_status.)
@@ -18,6 +18,10 @@
18
18
  * - platform_subscription (GET platform/subscription): fetches the org's
19
19
  * current plan, subscription status, and entitlements.
20
20
  * - platform_plans (GET platform/plans): fetches the plan catalog with pricing.
21
+ * - platform_invoices_list (GET platform/invoices): fetches one
22
+ * cursor-paginated page of the org's Stripe invoice history.
23
+ * - platform_invoices_by_id_get (GET platform/invoices/:id): pages through the
24
+ * invoice list and returns a single invoice by Stripe invoice ID.
21
25
  */
22
26
 
23
27
  import { z } from "zod";
@@ -38,6 +42,7 @@ import { ACTOR_PRINCIPALS, LOCAL_PRINCIPALS } from "../auth/route-policy.js";
38
42
  import {
39
43
  BadRequestError,
40
44
  InternalError,
45
+ NotFoundError,
41
46
  UnprocessableEntityError,
42
47
  } from "./errors.js";
43
48
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
@@ -126,6 +131,28 @@ const PlatformCreditsResponseSchema = z.object({
126
131
  });
127
132
  type PlatformCreditsResponse = z.infer<typeof PlatformCreditsResponseSchema>;
128
133
 
134
+ const PlatformInvoiceSchema = z.object({
135
+ id: z.string(),
136
+ number: z.string().nullable(),
137
+ status: z.string().nullable(),
138
+ currency: z.string(),
139
+ amount_due: z.number(),
140
+ amount_paid: z.number(),
141
+ amount_remaining: z.number(),
142
+ created: z.number(),
143
+ hosted_invoice_url: z.string().nullable(),
144
+ invoice_pdf: z.string().nullable(),
145
+ });
146
+ type PlatformInvoice = z.infer<typeof PlatformInvoiceSchema>;
147
+
148
+ const PlatformInvoicesListResponseSchema = z.object({
149
+ invoices: z.array(PlatformInvoiceSchema),
150
+ has_more: z.boolean(),
151
+ });
152
+ type PlatformInvoicesListResponse = z.infer<
153
+ typeof PlatformInvoicesListResponseSchema
154
+ >;
155
+
129
156
  const SubscriptionPackageSchema = z.object({
130
157
  key: z.string(),
131
158
  name: z.string(),
@@ -383,15 +410,47 @@ async function handlePlatformCredits(
383
410
  };
384
411
  }
385
412
 
413
+ /** Default per-request timeout for platform fetches. */
414
+ const PLATFORM_FETCH_TIMEOUT_MS = 10_000;
415
+
416
+ interface FetchPlatformJsonOptions {
417
+ /**
418
+ * Abort signal from the caller's request. Combined with the per-request
419
+ * timeout so whichever fires first cancels the fetch.
420
+ */
421
+ signal?: AbortSignal;
422
+ /**
423
+ * Per-request timeout override in milliseconds. Defaults to
424
+ * PLATFORM_FETCH_TIMEOUT_MS; callers with an aggregate deadline (the
425
+ * invoices_get cursor walk) pass the remaining budget instead.
426
+ */
427
+ timeoutMs?: number;
428
+ /**
429
+ * When set, an upstream HTTP 400 is surfaced as a BadRequestError combining
430
+ * the response detail with this hint, so caller-correctable input errors
431
+ * (e.g. an invalid pagination cursor) keep their 400 status. When unset, a
432
+ * 400 falls through to the generic InternalError path.
433
+ */
434
+ badRequestHint?: string;
435
+ /**
436
+ * When set, an upstream HTTP 404 resolves to this value instead of the
437
+ * generic InternalError path, for resources where the platform signals
438
+ * "nothing here" with a 404 (e.g. the invoice list for an organization
439
+ * without invoice history). When unset, a 404 stays an InternalError.
440
+ */
441
+ notFoundValue?: unknown;
442
+ }
443
+
386
444
  /**
387
445
  * GET a JSON resource from the platform using the assistant's stored platform
388
446
  * credentials. Throws UnprocessableEntityError when credentials are missing and
389
447
  * InternalError on transport / non-2xx failures; `label` names the resource in
390
- * those error messages.
448
+ * those error messages. See FetchPlatformJsonOptions for per-call overrides.
391
449
  */
392
450
  async function fetchPlatformJson(
393
451
  path: string,
394
452
  label: string,
453
+ options?: FetchPlatformJsonOptions,
395
454
  ): Promise<unknown> {
396
455
  const context = await resolvePlatformCallbackRegistrationContext();
397
456
 
@@ -402,6 +461,9 @@ async function fetchPlatformJson(
402
461
  }
403
462
 
404
463
  const url = `${context.platformBaseUrl}${path}`;
464
+ const timeout = AbortSignal.timeout(
465
+ options?.timeoutMs ?? PLATFORM_FETCH_TIMEOUT_MS,
466
+ );
405
467
  let response: Response;
406
468
  try {
407
469
  response = await fetch(url, {
@@ -410,7 +472,9 @@ async function fetchPlatformJson(
410
472
  Authorization: context.authHeader,
411
473
  Accept: "application/json",
412
474
  },
413
- signal: AbortSignal.timeout(10_000),
475
+ signal: options?.signal
476
+ ? AbortSignal.any([options.signal, timeout])
477
+ : timeout,
414
478
  });
415
479
  } catch (err) {
416
480
  throw new InternalError(
@@ -419,7 +483,16 @@ async function fetchPlatformJson(
419
483
  }
420
484
 
421
485
  if (!response.ok) {
486
+ if (response.status === 404 && options?.notFoundValue !== undefined) {
487
+ return options.notFoundValue;
488
+ }
422
489
  const detail = await response.text().catch(() => "");
490
+ if (response.status === 400 && options?.badRequestHint) {
491
+ throw new BadRequestError(
492
+ `Platform rejected the ${label} request${detail ? `: ${detail}` : ""}. ` +
493
+ options.badRequestHint,
494
+ );
495
+ }
423
496
  throw new InternalError(
424
497
  `Failed to fetch ${label} (HTTP ${response.status}): ${detail}`,
425
498
  );
@@ -485,6 +558,129 @@ async function handlePlatformPlans(
485
558
  return { plans: data.plans ?? [] };
486
559
  }
487
560
 
561
+ /**
562
+ * Fetch one cursor-paginated page of the org's Stripe invoice history from
563
+ * the platform. Pass the previous page's last invoice id as `startingAfter`
564
+ * to fetch the next (older) page.
565
+ */
566
+ async function fetchPlatformInvoicesPage(
567
+ startingAfter: string | undefined,
568
+ options?: FetchPlatformJsonOptions,
569
+ ): Promise<PlatformInvoicesListResponse> {
570
+ let path = "/v1/organizations/billing/invoices/";
571
+ if (startingAfter) {
572
+ path += `?${new URLSearchParams({ starting_after: startingAfter })}`;
573
+ }
574
+ return (await fetchPlatformJson(path, "invoices", {
575
+ ...options,
576
+ // The platform returns 404 for an organization without invoice history;
577
+ // the web client treats that as an empty page
578
+ // (clients/web/src/domains/settings/components/invoices-table.tsx) and
579
+ // the daemon matches it.
580
+ notFoundValue: { invoices: [], has_more: false },
581
+ })) as PlatformInvoicesListResponse;
582
+ }
583
+
584
+ async function handlePlatformInvoicesList(
585
+ args: RouteHandlerArgs,
586
+ ): Promise<PlatformInvoicesListResponse> {
587
+ const startingAfter = args.queryParams?.starting_after;
588
+ return await fetchPlatformInvoicesPage(startingAfter, {
589
+ signal: args.abortSignal,
590
+ // The platform returns 400 for an invalid or expired starting_after
591
+ // cursor. Only a caller-supplied cursor is caller-correctable, so the
592
+ // hint (and the BadRequestError it triggers) applies just when one was
593
+ // passed; a 400 on a cursor-less request stays an InternalError.
594
+ badRequestHint: startingAfter
595
+ ? "If you passed starting_after, use the id of the last invoice from the previous page."
596
+ : undefined,
597
+ });
598
+ }
599
+
600
+ /**
601
+ * Runaway guard for the invoices_get cursor walk: 25 pages is 2,500
602
+ * invoices at the platform's 100-per-page size.
603
+ */
604
+ export const MAX_INVOICE_PAGES = 25;
605
+
606
+ /**
607
+ * Aggregate wall-clock deadline for the invoices_get cursor walk, matching
608
+ * the gateway IPC client's 30s request timeout. Guards against the walk
609
+ * running long after the caller has given up, since a gateway IPC timeout
610
+ * does not abort the request signal.
611
+ */
612
+ export const INVOICE_WALK_DEADLINE_MS = 30_000;
613
+
614
+ function invoiceWalkTimeoutError(id: string): InternalError {
615
+ return new InternalError(
616
+ `Invoice lookup for "${id}" timed out after ` +
617
+ `${INVOICE_WALK_DEADLINE_MS / 1000} seconds while paging invoice history`,
618
+ );
619
+ }
620
+
621
+ async function handlePlatformInvoiceGet(
622
+ args: RouteHandlerArgs,
623
+ ): Promise<PlatformInvoice> {
624
+ const id = args.pathParams?.id;
625
+ if (!id) {
626
+ throw new BadRequestError("invoice id is required");
627
+ }
628
+
629
+ // There is no per-invoice platform endpoint, so walk the paginated list
630
+ // until the invoice turns up or the cursor is exhausted.
631
+ const walkStartedAt = Date.now();
632
+ let cursor: string | undefined;
633
+ for (let pages = 0; pages < MAX_INVOICE_PAGES; pages++) {
634
+ // Stop walking pages once the caller has gone away (gateway IPC timeout,
635
+ // disconnected HTTP client). In-flight fetches are cancelled via the
636
+ // signal passed to fetchPlatformInvoicesPage.
637
+ if (args.abortSignal?.aborted) {
638
+ throw new InternalError(
639
+ `Invoice lookup for "${id}" aborted: caller disconnected`,
640
+ );
641
+ }
642
+ const remainingMs = walkStartedAt + INVOICE_WALK_DEADLINE_MS - Date.now();
643
+ if (remainingMs <= 0) {
644
+ throw invoiceWalkTimeoutError(id);
645
+ }
646
+ // Bound the page fetch by the remaining aggregate budget so a fetch
647
+ // started just under the deadline cannot run the walk past it.
648
+ let page: PlatformInvoicesListResponse;
649
+ try {
650
+ // No badRequestHint: the cursor is internally generated, so an
651
+ // upstream 400 here is an internal failure, not a caller input error.
652
+ page = await fetchPlatformInvoicesPage(cursor, {
653
+ signal: args.abortSignal,
654
+ timeoutMs: Math.min(PLATFORM_FETCH_TIMEOUT_MS, remainingMs),
655
+ });
656
+ } catch (err) {
657
+ // A fetch cancelled by the deadline remainder should read as the
658
+ // walk's aggregate timeout, not a generic network failure.
659
+ if (
660
+ !args.abortSignal?.aborted &&
661
+ Date.now() - walkStartedAt >= INVOICE_WALK_DEADLINE_MS
662
+ ) {
663
+ throw invoiceWalkTimeoutError(id);
664
+ }
665
+ throw err;
666
+ }
667
+ const match = page.invoices.find((invoice) => invoice.id === id);
668
+ if (match) {
669
+ return match;
670
+ }
671
+ if (!page.has_more || page.invoices.length === 0) {
672
+ throw new NotFoundError(
673
+ `Invoice "${id}" not found. Run 'assistant platform invoices list' to see available invoices.`,
674
+ );
675
+ }
676
+ cursor = page.invoices.at(-1)!.id;
677
+ }
678
+
679
+ throw new InternalError(
680
+ `Invoice "${id}" not found in the first ${MAX_INVOICE_PAGES} pages of invoice history`,
681
+ );
682
+ }
683
+
488
684
  // ---------------------------------------------------------------------------
489
685
  // Route definitions
490
686
  // ---------------------------------------------------------------------------
@@ -611,4 +807,43 @@ export const ROUTES: RouteDefinition[] = [
611
807
  handler: handlePlatformPlans,
612
808
  responseBody: PlatformPlansResponseSchema,
613
809
  },
810
+ {
811
+ operationId: "platform_invoices_list",
812
+ endpoint: "platform/invoices",
813
+ method: "GET",
814
+ policy: {
815
+ requiredScopes: ["settings.read"],
816
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
817
+ },
818
+ summary: "List one page of the organization's Stripe invoices",
819
+ description:
820
+ "Fetches one page of the org's Stripe invoice history (newest first) from the platform billing invoices endpoint. Amounts are in the currency's minor units. When has_more is true, pass the last invoice's id as starting_after to fetch the next page.",
821
+ tags: ["platform"],
822
+ queryParams: [
823
+ {
824
+ name: "starting_after",
825
+ type: "string",
826
+ required: false,
827
+ description:
828
+ "Cursor: return invoices older than the invoice with this id (from the previous page's last entry).",
829
+ },
830
+ ],
831
+ handler: handlePlatformInvoicesList,
832
+ responseBody: PlatformInvoicesListResponseSchema,
833
+ },
834
+ {
835
+ operationId: "platform_invoices_by_id_get",
836
+ endpoint: "platform/invoices/:id",
837
+ method: "GET",
838
+ policy: {
839
+ requiredScopes: ["settings.read"],
840
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
841
+ },
842
+ summary: "Get a single Stripe invoice by ID",
843
+ description:
844
+ "Pages through the org's invoice list from the platform and returns the invoice matching the given Stripe invoice ID (e.g. in_xxx). 404 if no such invoice.",
845
+ tags: ["platform"],
846
+ handler: handlePlatformInvoiceGet,
847
+ responseBody: PlatformInvoiceSchema,
848
+ },
614
849
  ];
@@ -1,5 +1,4 @@
1
1
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
2
- import { getConfig } from "../../../config/loader.js";
3
2
  import { RouteError } from "../errors.js";
4
3
 
5
4
  /**
@@ -16,7 +15,7 @@ import { RouteError } from "../errors.js";
16
15
  const PLAYGROUND_DISABLED_CODE = "playground_disabled";
17
16
 
18
17
  function isPlaygroundEnabled(): boolean {
19
- return isAssistantFeatureFlagEnabled("compaction-playground", getConfig());
18
+ return isAssistantFeatureFlagEnabled("compaction-playground");
20
19
  }
21
20
 
22
21
  /**
@@ -4,10 +4,8 @@ import {
4
4
  conversationMetadataSyncTag,
5
5
  SYNC_TAGS,
6
6
  } from "../../daemon/message-types/sync.js";
7
- import {
8
- getSlackConversationInfo,
9
- SlackApiError,
10
- } from "../../messaging/providers/slack/api.js";
7
+ import { getSlackConversationInfo } from "../../messaging/providers/slack/api.js";
8
+ import { SlackApiError } from "../../messaging/providers/slack/web-api-transport.js";
11
9
  import {
12
10
  getBindingByConversation,
13
11
  updateExternalChatName,