@app-vitals/agent-admin 0.0.1 → 0.1.1

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 (325) hide show
  1. package/Dockerfile +43 -0
  2. package/README.md +72 -2
  3. package/e2e/agents-page.e2e.ts +338 -0
  4. package/e2e/chat-degraded.e2e.ts +153 -0
  5. package/e2e/chat-page.e2e.ts +169 -0
  6. package/e2e/chat-progress.e2e.ts +206 -0
  7. package/e2e/chat-thread-page.e2e.ts +444 -0
  8. package/e2e/login-page.e2e.ts +148 -0
  9. package/e2e/test-server.ts +427 -0
  10. package/openapi.json +6098 -0
  11. package/openapi.platform.json +5553 -0
  12. package/package.json +62 -4
  13. package/playwright.config.ts +25 -0
  14. package/prisma/client/browser.ts +134 -0
  15. package/prisma/client/client.ts +158 -0
  16. package/prisma/client/commonInputTypes.ts +578 -0
  17. package/prisma/client/enums.ts +15 -0
  18. package/prisma/client/internal/class.ts +424 -0
  19. package/prisma/client/internal/prismaNamespace.ts +2898 -0
  20. package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
  21. package/prisma/client/models/Account.ts +1774 -0
  22. package/prisma/client/models/AccountInvite.ts +1386 -0
  23. package/prisma/client/models/AccountMember.ts +1351 -0
  24. package/prisma/client/models/Agent.ts +4554 -0
  25. package/prisma/client/models/AgentAsk.ts +1373 -0
  26. package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
  27. package/prisma/client/models/AgentCronJob.ts +2416 -0
  28. package/prisma/client/models/AgentCronRun.ts +2858 -0
  29. package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
  30. package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
  31. package/prisma/client/models/AgentEnv.ts +1421 -0
  32. package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
  33. package/prisma/client/models/AgentMember.ts +1316 -0
  34. package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
  35. package/prisma/client/models/AgentPlugin.ts +1421 -0
  36. package/prisma/client/models/AgentToken.ts +1380 -0
  37. package/prisma/client/models/AgentTool.ts +1351 -0
  38. package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
  39. package/prisma/client/models/ChatThreadWatch.ts +1183 -0
  40. package/prisma/client/models/PushSubscription.ts +1233 -0
  41. package/prisma/client/models/SessionAlertState.ts +1187 -0
  42. package/prisma/client/models/SessionFollow.ts +1187 -0
  43. package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
  44. package/prisma/client/models.ts +34 -0
  45. package/prisma/migrations/20260608000000_init/migration.sql +112 -0
  46. package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
  47. package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
  48. package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
  49. package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
  50. package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
  51. package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
  52. package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
  53. package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
  54. package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
  55. package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
  56. package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
  57. package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
  58. package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
  59. package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
  60. package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
  61. package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
  62. package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
  63. package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
  64. package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
  65. package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
  66. package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
  67. package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
  68. package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
  69. package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
  70. package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
  71. package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
  72. package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
  73. package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
  74. package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
  75. package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
  76. package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
  77. package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
  78. package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
  79. package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
  80. package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
  81. package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
  82. package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
  83. package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
  84. package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
  85. package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
  86. package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
  87. package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
  88. package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
  89. package/prisma/migrations/migration_lock.toml +3 -0
  90. package/prisma/schema/coding.prisma +85 -0
  91. package/prisma/schema/schema.prisma +506 -0
  92. package/prisma.config.ts +34 -0
  93. package/pwa-assets/icons/apple-touch-icon.png +0 -0
  94. package/pwa-assets/icons/favicon-32.png +0 -0
  95. package/pwa-assets/icons/icon-192.png +0 -0
  96. package/pwa-assets/icons/icon-512.png +0 -0
  97. package/pwa-assets/icons/icon-maskable-192.png +0 -0
  98. package/pwa-assets/icons/icon-maskable-512.png +0 -0
  99. package/src/account-agent-create.ts +42 -0
  100. package/src/account-agent-create.unit.test.ts +28 -0
  101. package/src/account-created-notifier.ts +71 -0
  102. package/src/account-created-notifier.unit.test.ts +70 -0
  103. package/src/account-email.ts +9 -0
  104. package/src/account-email.unit.test.ts +21 -0
  105. package/src/account-invites.ts +70 -0
  106. package/src/account-lifecycle.integration.test.ts +329 -0
  107. package/src/account-lifecycle.ts +206 -0
  108. package/src/account-lifecycle.unit.test.ts +108 -0
  109. package/src/account-members.ts +128 -0
  110. package/src/account-onboarding.integration.test.ts +101 -0
  111. package/src/account-onboarding.ts +95 -0
  112. package/src/account-trial-expiry-sweeper.ts +85 -0
  113. package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
  114. package/src/account-trial-warning-sweeper.ts +216 -0
  115. package/src/accounts-api.integration.test.ts +196 -0
  116. package/src/accounts-api.smoke.test.ts +292 -0
  117. package/src/accounts-api.ts +289 -0
  118. package/src/accounts-api.unit.test.ts +93 -0
  119. package/src/accounts.integration.test.ts +178 -0
  120. package/src/accounts.ts +208 -0
  121. package/src/admin-boot-config.ts +574 -0
  122. package/src/admin-boot-config.unit.test.ts +28 -0
  123. package/src/admin-boot.boot-order.unit.test.ts +91 -0
  124. package/src/admin-boot.ts +690 -0
  125. package/src/admin-dev-login.smoke.test.ts +317 -0
  126. package/src/admin-extension.ts +432 -0
  127. package/src/admin-extension.unit.test.ts +532 -0
  128. package/src/admin-local-agent.smoke.test.ts +1643 -0
  129. package/src/admin-spec.smoke.test.ts +96 -0
  130. package/src/admin-spec.ts +186 -0
  131. package/src/admin-ui-account-nav.ts +20 -0
  132. package/src/admin-ui-account-pages.ts +131 -0
  133. package/src/admin-ui-account-pages.unit.test.ts +112 -0
  134. package/src/admin-ui-account.smoke.test.ts +286 -0
  135. package/src/admin-ui-account.ts +185 -0
  136. package/src/admin-ui-accounts-pages.ts +202 -0
  137. package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
  138. package/src/admin-ui-accounts.smoke.test.ts +434 -0
  139. package/src/admin-ui-accounts.ts +266 -0
  140. package/src/admin-ui-layout.ts +60 -0
  141. package/src/admin-ui-layout.unit.test.ts +140 -0
  142. package/src/admin-ui-pages.ts +3489 -0
  143. package/src/admin-ui-pages.unit.test.ts +4355 -0
  144. package/src/admin-ui-push-test.smoke.test.ts +125 -0
  145. package/src/admin-ui-push-test.ts +102 -0
  146. package/src/admin-ui-styles.ts +656 -0
  147. package/src/admin-ui-styles.unit.test.ts +103 -0
  148. package/src/admin-ui.fixtures.ts +457 -0
  149. package/src/admin-ui.smoke.test.ts +7227 -0
  150. package/src/admin-ui.ts +3824 -0
  151. package/src/agent-account-assignment.ts +49 -0
  152. package/src/agent-account-assignment.unit.test.ts +99 -0
  153. package/src/agent-anthropic-identity.integration.test.ts +99 -0
  154. package/src/agent-anthropic-identity.ts +78 -0
  155. package/src/agent-anthropic-identity.unit.test.ts +77 -0
  156. package/src/agent-ask-config.ts +59 -0
  157. package/src/agent-ask-config.unit.test.ts +70 -0
  158. package/src/agent-ask.integration.test.ts +175 -0
  159. package/src/agent-ask.smoke.test.ts +457 -0
  160. package/src/agent-ask.ts +378 -0
  161. package/src/agent-chat-tokens.integration.test.ts +420 -0
  162. package/src/agent-chat-tokens.smoke.test.ts +551 -0
  163. package/src/agent-chat-tokens.ts +259 -0
  164. package/src/agent-cron-jobs.integration.test.ts +582 -0
  165. package/src/agent-cron-jobs.ts +615 -0
  166. package/src/agent-cron-jobs.unit.test.ts +420 -0
  167. package/src/agent-cron-run-stats.integration.test.ts +1551 -0
  168. package/src/agent-cron-run-stats.smoke.test.ts +953 -0
  169. package/src/agent-cron-run-stats.ts +752 -0
  170. package/src/agent-cron-runs.integration.test.ts +1491 -0
  171. package/src/agent-cron-runs.ts +489 -0
  172. package/src/agent-deletion-checklist.ts +128 -0
  173. package/src/agent-deletion-checklist.unit.test.ts +201 -0
  174. package/src/agent-deletion-per-agent.integration.test.ts +323 -0
  175. package/src/agent-deletion.integration.test.ts +323 -0
  176. package/src/agent-deletion.ts +329 -0
  177. package/src/agent-deletion.unit.test.ts +412 -0
  178. package/src/agent-envs.integration.test.ts +323 -0
  179. package/src/agent-envs.ts +244 -0
  180. package/src/agent-github-installations.integration.test.ts +115 -0
  181. package/src/agent-github-installations.ts +52 -0
  182. package/src/agent-manifest.ts +685 -0
  183. package/src/agent-manifest.unit.test.ts +870 -0
  184. package/src/agent-members.ts +69 -0
  185. package/src/agent-members.unit.test.ts +250 -0
  186. package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
  187. package/src/agent-plugins.integration.test.ts +157 -0
  188. package/src/agent-plugins.ts +95 -0
  189. package/src/agent-plugins.unit.test.ts +43 -0
  190. package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
  191. package/src/agent-provisioner.integration.test.ts +1245 -0
  192. package/src/agent-provisioner.ts +1650 -0
  193. package/src/agent-provisioner.unit.test.ts +1852 -0
  194. package/src/agent-tokens.integration.test.ts +150 -0
  195. package/src/agent-tokens.ts +112 -0
  196. package/src/agent-tools.integration.test.ts +129 -0
  197. package/src/agent-tools.ts +111 -0
  198. package/src/agents-api.integration.test.ts +705 -0
  199. package/src/agents-api.sentry.smoke.test.ts +478 -0
  200. package/src/agents-api.smoke.test.ts +5986 -0
  201. package/src/agents-api.ts +2826 -0
  202. package/src/agents.integration.test.ts +512 -0
  203. package/src/agents.ts +1097 -0
  204. package/src/agents.unit.test.ts +1736 -0
  205. package/src/anthropic-credential-guard.ts +44 -0
  206. package/src/anthropic-credential-guard.unit.test.ts +88 -0
  207. package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
  208. package/src/anthropic-federated-token-provider.ts +249 -0
  209. package/src/anthropic-identity-client.fixtures.ts +237 -0
  210. package/src/anthropic-identity-client.integration.test.ts +342 -0
  211. package/src/anthropic-identity-client.ts +473 -0
  212. package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
  213. package/src/anthropic-identity-name.integration.test.ts +83 -0
  214. package/src/anthropic-identity-name.ts +163 -0
  215. package/src/anthropic-identity-name.unit.test.ts +345 -0
  216. package/src/anthropic-openapi-contract.fixtures.ts +192 -0
  217. package/src/api-auth.ts +383 -0
  218. package/src/api-auth.unit.test.ts +837 -0
  219. package/src/api.smoke.test.ts +1225 -0
  220. package/src/api.ts +376 -0
  221. package/src/attachment-validation.ts +124 -0
  222. package/src/attachment-validation.unit.test.ts +218 -0
  223. package/src/caller-scope.ts +131 -0
  224. package/src/caller-scope.unit.test.ts +116 -0
  225. package/src/chat-credential-config.ts +22 -0
  226. package/src/chat-credentials-logging.unit.test.ts +92 -0
  227. package/src/chat-markers.ts +114 -0
  228. package/src/chat-markers.unit.test.ts +116 -0
  229. package/src/chat-service-provisioning-client.integration.test.ts +412 -0
  230. package/src/chat-service-provisioning-client.ts +221 -0
  231. package/src/chat.smoke.test.ts +1875 -0
  232. package/src/clock.ts +47 -0
  233. package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
  234. package/src/coding-task-store-provisioning-client.ts +156 -0
  235. package/src/crypto.ts +83 -0
  236. package/src/dev-auth-guard.ts +29 -0
  237. package/src/dev-auth-guard.unit.test.ts +51 -0
  238. package/src/errors.ts +77 -0
  239. package/src/errors.unit.test.ts +52 -0
  240. package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
  241. package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
  242. package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
  243. package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
  244. package/src/fixtures/github-app-manifest-cassette.json +44 -0
  245. package/src/fixtures/google-auth-cassette.json +36 -0
  246. package/src/fixtures/http-chat-client-cassette.json +226 -0
  247. package/src/fixtures/k8s-cassette.json +470 -0
  248. package/src/fixtures/okta-auth-cassette.json +36 -0
  249. package/src/fixtures/slack-provision-cassette.json +12 -0
  250. package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
  251. package/src/fixtures/task-store-provisioning-cassette.json +85 -0
  252. package/src/github-app-provisioning-client.integration.test.ts +149 -0
  253. package/src/github-app-provisioning-client.ts +161 -0
  254. package/src/github-app-provisioning-client.unit.test.ts +114 -0
  255. package/src/github-provisioning-service.ts +569 -0
  256. package/src/github-provisioning-service.unit.test.ts +123 -0
  257. package/src/google-auth-client.integration.test.ts +195 -0
  258. package/src/google-auth-client.ts +121 -0
  259. package/src/http-chat-client.integration.test.ts +554 -0
  260. package/src/http-chat-client.ts +533 -0
  261. package/src/http-chat-client.unit.test.ts +119 -0
  262. package/src/import-boundary.unit.test.ts +240 -0
  263. package/src/kubernetes-client.integration.test.ts +697 -0
  264. package/src/kubernetes-client.ts +1027 -0
  265. package/src/kubernetes-client.unit.test.ts +726 -0
  266. package/src/main.smoke.test.ts +91 -0
  267. package/src/main.ts +21 -0
  268. package/src/main.unit.test.ts +801 -0
  269. package/src/okta-auth-client.integration.test.ts +263 -0
  270. package/src/okta-auth-client.ts +152 -0
  271. package/src/openapi-schemas.ts +1310 -0
  272. package/src/openapi-schemas.unit.test.ts +348 -0
  273. package/src/per-agent-identity.fixtures.ts +305 -0
  274. package/src/platform-doc-route.ts +40 -0
  275. package/src/prisma-client.integration.test.ts +57 -0
  276. package/src/prisma-client.ts +187 -0
  277. package/src/prisma-client.unit.test.ts +178 -0
  278. package/src/prisma-tx.ts +23 -0
  279. package/src/push-content.ts +177 -0
  280. package/src/push-content.unit.test.ts +213 -0
  281. package/src/push-crypto.ts +135 -0
  282. package/src/push-crypto.unit.test.ts +66 -0
  283. package/src/push-sender.ts +169 -0
  284. package/src/push-sender.unit.test.ts +107 -0
  285. package/src/push-service.ts +332 -0
  286. package/src/push-service.unit.test.ts +679 -0
  287. package/src/push-toggle.ts +135 -0
  288. package/src/push-toggle.unit.test.ts +78 -0
  289. package/src/pwa-https-gating.smoke.test.ts +229 -0
  290. package/src/pwa-icons.unit.test.ts +41 -0
  291. package/src/pwa-routes.smoke.test.ts +276 -0
  292. package/src/pwa.ts +380 -0
  293. package/src/pwa.unit.test.ts +321 -0
  294. package/src/schema.unit.test.ts +122 -0
  295. package/src/self-serve-config.ts +52 -0
  296. package/src/self-serve-config.unit.test.ts +79 -0
  297. package/src/server.smoke.test.ts +273 -0
  298. package/src/server.ts +50 -0
  299. package/src/service-integration.ts +67 -0
  300. package/src/service-integration.unit.test.ts +129 -0
  301. package/src/slack-provisioning-client.integration.test.ts +308 -0
  302. package/src/slack-provisioning-client.ts +411 -0
  303. package/src/slack-provisioning-client.unit.test.ts +80 -0
  304. package/src/slack-provisioning-service.integration.test.ts +637 -0
  305. package/src/slack-provisioning-service.ts +451 -0
  306. package/src/slack-provisioning.integration.test.ts +395 -0
  307. package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
  308. package/src/specialty-manifest-loader.ts +255 -0
  309. package/src/specialty-manifest-loader.unit.test.ts +301 -0
  310. package/src/specialty-registry.schema.unit.test.ts +35 -0
  311. package/src/specialty-registry.ts +236 -0
  312. package/src/specialty-registry.unit.test.ts +272 -0
  313. package/src/startup-policy-reconcile.ts +77 -0
  314. package/src/startup-policy-reconcile.unit.test.ts +251 -0
  315. package/src/theme-tokens.json +12 -0
  316. package/src/token-crypto.ts +60 -0
  317. package/src/token-crypto.unit.test.ts +182 -0
  318. package/src/trial-expiry-sweeper.integration.test.ts +318 -0
  319. package/src/trial-expiry-sweeper.ts +132 -0
  320. package/src/trial-expiry-sweeper.unit.test.ts +391 -0
  321. package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
  322. package/src/trial-expiry-warning-sweeper.ts +447 -0
  323. package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
  324. package/src/voice-contract.unit.test.ts +102 -0
  325. package/tsconfig.json +9 -0
@@ -0,0 +1,533 @@
1
+ /**
2
+ * admin/src/http-chat-client.ts
3
+ * Chat service client for the admin service.
4
+ *
5
+ * Mirrors the HttpChatServiceProvisioningClient pattern for consistency.
6
+ * All methods call the chat service's thread + message API using an admin token.
7
+ */
8
+
9
+ // ─── Types (inline — no cross-package coupling) ───────────────────────────────
10
+
11
+ export interface ChatThread {
12
+ id: string;
13
+ agentId: string;
14
+ title: string | null;
15
+ memberId: string | null;
16
+ /**
17
+ * Agent-to-agent ask threads (COS-31.1): `kind: "ask"`, the asking agent's
18
+ * id and the ask depth. Optional so threads from a chat service that
19
+ * predates these columns deserialize cleanly.
20
+ */
21
+ kind?: string | null;
22
+ askFromAgentId?: string | null;
23
+ askDepth?: number | null;
24
+ createdAt: string;
25
+ updatedAt: string;
26
+ }
27
+
28
+ export interface MessageTokens {
29
+ input_tokens: number;
30
+ output_tokens: number;
31
+ cache_read_input_tokens?: number;
32
+ cache_creation_input_tokens?: number;
33
+ }
34
+
35
+ export interface ChatMessage {
36
+ id: string;
37
+ threadId: string;
38
+ role: string;
39
+ body: string;
40
+ createdAt: string;
41
+ claimedBy: string | null;
42
+ claimed?: boolean;
43
+ claimedAt?: string | null;
44
+ heartbeatAt?: string | null;
45
+ repliedAt: string | null;
46
+ tokens: MessageTokens | null;
47
+ costUsd: number | null;
48
+ errorKind?: string | null;
49
+ attachmentFilename: string | null;
50
+ attachmentSize: number | null;
51
+ /**
52
+ * Live-progress columns (added chat-side in CFB-2.2). The agent's heartbeat
53
+ * reports a coarse `progressPhase` (one of lib/progress-phases.ts's closed
54
+ * set, or null before the first phase is reported) and bumps `progressSeq`
55
+ * every time it makes forward progress. `cancelRequestedAt` is set when a
56
+ * cancel has been requested for the in-flight reply. All optional so older
57
+ * fixtures / responses that predate these columns deserialize cleanly.
58
+ */
59
+ progressPhase?: string | null;
60
+ progressSeq?: number;
61
+ cancelRequestedAt?: string | null;
62
+ }
63
+
64
+ export interface ThreadStats {
65
+ messageCount: number;
66
+ totalInputTokens: number;
67
+ totalOutputTokens: number;
68
+ totalCostUsd: number;
69
+ }
70
+
71
+ /** Optional file attachment carried alongside a created message. */
72
+ interface MessageAttachment {
73
+ filename: string;
74
+ size: number;
75
+ bytes: Uint8Array;
76
+ }
77
+
78
+ export interface ListThreadsResult {
79
+ threads: ChatThread[];
80
+ total: number;
81
+ limit: number;
82
+ offset: number;
83
+ }
84
+
85
+ interface ListMessagesResult {
86
+ messages: ChatMessage[];
87
+ total: number;
88
+ limit: number;
89
+ offset: number;
90
+ }
91
+
92
+ export interface ListThreadsOptions {
93
+ limit?: number;
94
+ offset?: number;
95
+ }
96
+
97
+ interface ListMessagesOptions {
98
+ limit?: number;
99
+ offset?: number;
100
+ /**
101
+ * Return only messages ordered *after* the message with this id. Applied as
102
+ * a read-side filter over the full ordered result (the chat service itself
103
+ * only understands limit/offset), so incremental polls stay cheap without
104
+ * changing the chat service HTTP API.
105
+ */
106
+ since?: string;
107
+ }
108
+
109
+ interface CreateThreadOptions {
110
+ title?: string;
111
+ memberId?: string;
112
+ /** Thread kind; "ask" marks an agent-to-agent ask thread (COS-31.1). */
113
+ kind?: "ask";
114
+ /** Id of the agent that asked (ask threads only). */
115
+ askFromAgentId?: string;
116
+ /** Ask chain depth (ask threads only; admin always sends 1). */
117
+ askDepth?: number;
118
+ }
119
+
120
+ interface UpdateThreadOptions {
121
+ title?: string;
122
+ }
123
+
124
+ // ─── Interface ────────────────────────────────────────────────────────────────
125
+
126
+ export interface ChatClient {
127
+ listThreads(
128
+ agentId?: string,
129
+ opts?: ListThreadsOptions,
130
+ ): Promise<ListThreadsResult>;
131
+
132
+ getThread(id: string): Promise<ChatThread>;
133
+
134
+ createThread(
135
+ agentId: string,
136
+ opts?: CreateThreadOptions,
137
+ ): Promise<ChatThread>;
138
+
139
+ updateThread(id: string, data: UpdateThreadOptions): Promise<ChatThread>;
140
+
141
+ deleteThread(id: string): Promise<void>;
142
+
143
+ listMessages(
144
+ threadId: string,
145
+ opts?: ListMessagesOptions,
146
+ ): Promise<ListMessagesResult>;
147
+
148
+ createMessage(
149
+ threadId: string,
150
+ role: string,
151
+ body: string,
152
+ attachment?: MessageAttachment,
153
+ ): Promise<ChatMessage>;
154
+
155
+ getThreadStats(threadId: string): Promise<ThreadStats>;
156
+
157
+ /**
158
+ * Fetch a message's attachment bytes + filename, for proxying playback/
159
+ * download back to the browser (VM-3.1) — the browser never holds the
160
+ * chat service's admin bearer token, so it cannot call the chat service's
161
+ * GET /:id/attachment directly. Returns null when the message has no
162
+ * attachment (e.g. a user-authored attachment already cleared by the
163
+ * ephemeral-retention policy — see chat/src/routes/messages.ts).
164
+ */
165
+ getAttachment(
166
+ threadId: string,
167
+ messageId: string,
168
+ ): Promise<{ bytes: Uint8Array; filename: string } | null>;
169
+ }
170
+
171
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
172
+
173
+ /**
174
+ * Return only the messages ordered *after* the message whose id is `since`,
175
+ * assuming `messages` is in chronological order. If `since` isn't found (e.g.
176
+ * the client's last-seen id was trimmed away), returns the full list so the
177
+ * caller re-syncs rather than silently dropping everything.
178
+ */
179
+ export function filterSince(
180
+ messages: ChatMessage[],
181
+ since: string,
182
+ ): ChatMessage[] {
183
+ const idx = messages.findIndex((m) => m.id === since);
184
+ return idx === -1 ? messages : messages.slice(idx + 1);
185
+ }
186
+
187
+ /**
188
+ * Project a chat-service message onto the explicit field set the admin's
189
+ * messages.json poll response carries, plus any extras the caller computes
190
+ * (e.g. the server-rendered `bubbleHtml`). Never spread the upstream object:
191
+ * the chat service once leaked its raw `attachmentBytes` column, and passing
192
+ * that through JSON-expanded a few MB of audio into ~36 MB per poll.
193
+ */
194
+ export function toPolledMessage<Extra extends Record<string, unknown>>(
195
+ m: ChatMessage,
196
+ extra: Extra,
197
+ ): ChatMessage & Extra {
198
+ return {
199
+ id: m.id,
200
+ threadId: m.threadId,
201
+ role: m.role,
202
+ body: m.body,
203
+ createdAt: m.createdAt,
204
+ claimedBy: m.claimedBy,
205
+ claimed: m.claimed,
206
+ claimedAt: m.claimedAt,
207
+ heartbeatAt: m.heartbeatAt,
208
+ repliedAt: m.repliedAt,
209
+ tokens: m.tokens,
210
+ costUsd: m.costUsd,
211
+ errorKind: m.errorKind,
212
+ attachmentFilename: m.attachmentFilename,
213
+ attachmentSize: m.attachmentSize,
214
+ progressPhase: m.progressPhase,
215
+ progressSeq: m.progressSeq,
216
+ cancelRequestedAt: m.cancelRequestedAt,
217
+ ...extra,
218
+ };
219
+ }
220
+
221
+ // ─── Http implementation ──────────────────────────────────────────────────────
222
+
223
+ export class HttpChatClient implements ChatClient {
224
+ private readonly fetchFn: typeof fetch;
225
+
226
+ constructor(
227
+ private readonly baseUrl: string,
228
+ private readonly adminToken: string,
229
+ opts?: { fetchFn?: typeof fetch },
230
+ ) {
231
+ this.fetchFn = opts?.fetchFn ?? fetch;
232
+ }
233
+
234
+ private authHeaders(): Record<string, string> {
235
+ return {
236
+ Authorization: `Bearer ${this.adminToken}`,
237
+ "Content-Type": "application/json",
238
+ };
239
+ }
240
+
241
+ async listThreads(
242
+ agentId?: string,
243
+ opts?: ListThreadsOptions,
244
+ ): Promise<ListThreadsResult> {
245
+ const params = new URLSearchParams();
246
+ if (agentId) params.set("agentId", agentId);
247
+ if (opts?.limit !== undefined) params.set("limit", String(opts.limit));
248
+ if (opts?.offset !== undefined) params.set("offset", String(opts.offset));
249
+ const qs = params.size > 0 ? `?${params}` : "";
250
+ const res = await this.fetchFn(`${this.baseUrl}/threads${qs}`, {
251
+ headers: this.authHeaders(),
252
+ });
253
+ if (!res.ok) {
254
+ throw new Error(
255
+ `chat-service GET /threads failed: ${res.status} ${res.statusText}`,
256
+ );
257
+ }
258
+ return res.json() as Promise<ListThreadsResult>;
259
+ }
260
+
261
+ async getThread(id: string): Promise<ChatThread> {
262
+ const res = await this.fetchFn(`${this.baseUrl}/threads/${id}`, {
263
+ headers: this.authHeaders(),
264
+ });
265
+ if (!res.ok) {
266
+ throw new Error(
267
+ `chat-service GET /threads/${id} failed: ${res.status} ${res.statusText}`,
268
+ );
269
+ }
270
+ return res.json() as Promise<ChatThread>;
271
+ }
272
+
273
+ async createThread(
274
+ agentId: string,
275
+ opts?: CreateThreadOptions,
276
+ ): Promise<ChatThread> {
277
+ const res = await this.fetchFn(`${this.baseUrl}/threads`, {
278
+ method: "POST",
279
+ headers: this.authHeaders(),
280
+ body: JSON.stringify({
281
+ agentId,
282
+ ...(opts?.title ? { title: opts.title } : {}),
283
+ ...(opts?.memberId ? { memberId: opts.memberId } : {}),
284
+ ...(opts?.kind ? { kind: opts.kind } : {}),
285
+ ...(opts?.askFromAgentId
286
+ ? { askFromAgentId: opts.askFromAgentId }
287
+ : {}),
288
+ ...(opts?.askDepth !== undefined ? { askDepth: opts.askDepth } : {}),
289
+ }),
290
+ });
291
+ if (!res.ok) {
292
+ throw new Error(
293
+ `chat-service POST /threads failed: ${res.status} ${res.statusText}`,
294
+ );
295
+ }
296
+ return res.json() as Promise<ChatThread>;
297
+ }
298
+
299
+ async updateThread(
300
+ id: string,
301
+ data: UpdateThreadOptions,
302
+ ): Promise<ChatThread> {
303
+ const res = await this.fetchFn(`${this.baseUrl}/threads/${id}`, {
304
+ method: "PATCH",
305
+ headers: this.authHeaders(),
306
+ body: JSON.stringify(data),
307
+ });
308
+ if (!res.ok) {
309
+ throw new Error(
310
+ `chat-service PATCH /threads/${id} failed: ${res.status} ${res.statusText}`,
311
+ );
312
+ }
313
+ return res.json() as Promise<ChatThread>;
314
+ }
315
+
316
+ async deleteThread(id: string): Promise<void> {
317
+ const res = await this.fetchFn(`${this.baseUrl}/threads/${id}`, {
318
+ method: "DELETE",
319
+ headers: this.authHeaders(),
320
+ });
321
+ if (!res.ok) {
322
+ throw new Error(
323
+ `chat-service DELETE /threads/${id} failed: ${res.status} ${res.statusText}`,
324
+ );
325
+ }
326
+ }
327
+
328
+ async listMessages(
329
+ threadId: string,
330
+ opts?: ListMessagesOptions,
331
+ ): Promise<ListMessagesResult> {
332
+ const params = new URLSearchParams();
333
+ if (opts?.limit !== undefined) params.set("limit", String(opts.limit));
334
+ if (opts?.offset !== undefined) params.set("offset", String(opts.offset));
335
+ const qs = params.size > 0 ? `?${params}` : "";
336
+ const res = await this.fetchFn(
337
+ `${this.baseUrl}/threads/${threadId}/messages${qs}`,
338
+ {
339
+ headers: this.authHeaders(),
340
+ },
341
+ );
342
+ if (!res.ok) {
343
+ throw new Error(
344
+ `chat-service GET /threads/${threadId}/messages failed: ${res.status} ${res.statusText}`,
345
+ );
346
+ }
347
+ const result = (await res.json()) as ListMessagesResult;
348
+ if (opts?.since) {
349
+ return { ...result, messages: filterSince(result.messages, opts.since) };
350
+ }
351
+ return result;
352
+ }
353
+
354
+ async createMessage(
355
+ threadId: string,
356
+ role: string,
357
+ body: string,
358
+ attachment?: MessageAttachment,
359
+ ): Promise<ChatMessage> {
360
+ const payload: Record<string, unknown> = { role, body };
361
+ if (attachment) {
362
+ payload.attachmentBytes = Buffer.from(attachment.bytes).toString(
363
+ "base64",
364
+ );
365
+ payload.attachmentFilename = attachment.filename;
366
+ payload.attachmentSize = attachment.size;
367
+ }
368
+ const res = await this.fetchFn(
369
+ `${this.baseUrl}/threads/${threadId}/messages`,
370
+ {
371
+ method: "POST",
372
+ headers: this.authHeaders(),
373
+ body: JSON.stringify(payload),
374
+ },
375
+ );
376
+ if (!res.ok) {
377
+ throw new Error(
378
+ `chat-service POST /threads/${threadId}/messages failed: ${res.status} ${res.statusText}`,
379
+ );
380
+ }
381
+ return res.json() as Promise<ChatMessage>;
382
+ }
383
+
384
+ async getThreadStats(threadId: string): Promise<ThreadStats> {
385
+ const res = await this.fetchFn(
386
+ `${this.baseUrl}/threads/${threadId}/stats`,
387
+ {
388
+ headers: this.authHeaders(),
389
+ },
390
+ );
391
+ if (!res.ok) {
392
+ throw new Error(
393
+ `chat-service GET /threads/${threadId}/stats failed: ${res.status} ${res.statusText}`,
394
+ );
395
+ }
396
+ return res.json() as Promise<ThreadStats>;
397
+ }
398
+
399
+ async getAttachment(
400
+ threadId: string,
401
+ messageId: string,
402
+ ): Promise<{ bytes: Uint8Array; filename: string } | null> {
403
+ const res = await this.fetchFn(
404
+ `${this.baseUrl}/threads/${threadId}/messages/${messageId}/attachment`,
405
+ { headers: { Authorization: `Bearer ${this.adminToken}` } },
406
+ );
407
+ if (res.status === 404) return null;
408
+ if (!res.ok) {
409
+ throw new Error(
410
+ `chat-service GET /threads/${threadId}/messages/${messageId}/attachment failed: ${res.status} ${res.statusText}`,
411
+ );
412
+ }
413
+ const filename = parseAttachmentFilename(
414
+ res.headers.get("content-disposition"),
415
+ );
416
+ const bytes = new Uint8Array(await res.arrayBuffer());
417
+ return { bytes, filename };
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Extract the filename from a `Content-Disposition: attachment;
423
+ * filename="x"` header (as sent by chat/src/routes/messages.ts's
424
+ * getAttachmentRoute). Falls back to a generic name if the header is
425
+ * missing or unparsable.
426
+ */
427
+ function parseAttachmentFilename(header: string | null): string {
428
+ if (!header) return "attachment";
429
+ const match = header.match(/filename="?([^";]+)"?/);
430
+ return match?.[1] ?? "attachment";
431
+ }
432
+
433
+ // ─── Noop implementation ──────────────────────────────────────────────────────
434
+
435
+ export class NoopChatClient implements ChatClient {
436
+ async listThreads(
437
+ _agentId?: string,
438
+ _opts?: ListThreadsOptions,
439
+ ): Promise<ListThreadsResult> {
440
+ return { threads: [], total: 0, limit: 50, offset: 0 };
441
+ }
442
+
443
+ async getThread(_id: string): Promise<ChatThread> {
444
+ return {
445
+ id: "",
446
+ agentId: "",
447
+ title: null,
448
+ memberId: null,
449
+ createdAt: new Date().toISOString(),
450
+ updatedAt: new Date().toISOString(),
451
+ };
452
+ }
453
+
454
+ async createThread(
455
+ _agentId: string,
456
+ _opts?: CreateThreadOptions,
457
+ ): Promise<ChatThread> {
458
+ return {
459
+ id: "",
460
+ agentId: _agentId,
461
+ title: null,
462
+ memberId: null,
463
+ createdAt: new Date().toISOString(),
464
+ updatedAt: new Date().toISOString(),
465
+ };
466
+ }
467
+
468
+ async updateThread(
469
+ _id: string,
470
+ data: UpdateThreadOptions,
471
+ ): Promise<ChatThread> {
472
+ return {
473
+ id: _id,
474
+ agentId: "",
475
+ title: data.title ?? null,
476
+ memberId: null,
477
+ createdAt: new Date().toISOString(),
478
+ updatedAt: new Date().toISOString(),
479
+ };
480
+ }
481
+
482
+ async deleteThread(_id: string): Promise<void> {
483
+ // noop
484
+ }
485
+
486
+ async listMessages(
487
+ _threadId: string,
488
+ _opts?: ListMessagesOptions,
489
+ ): Promise<ListMessagesResult> {
490
+ return { messages: [], total: 0, limit: 50, offset: 0 };
491
+ }
492
+
493
+ async createMessage(
494
+ threadId: string,
495
+ role: string,
496
+ body: string,
497
+ attachment?: MessageAttachment,
498
+ ): Promise<ChatMessage> {
499
+ return {
500
+ id: "",
501
+ threadId,
502
+ role,
503
+ body,
504
+ createdAt: new Date().toISOString(),
505
+ claimedBy: null,
506
+ repliedAt: null,
507
+ tokens: null,
508
+ costUsd: null,
509
+ errorKind: null,
510
+ attachmentFilename: attachment?.filename ?? null,
511
+ attachmentSize: attachment?.size ?? null,
512
+ progressPhase: null,
513
+ progressSeq: 0,
514
+ cancelRequestedAt: null,
515
+ };
516
+ }
517
+
518
+ async getThreadStats(_threadId: string): Promise<ThreadStats> {
519
+ return {
520
+ messageCount: 0,
521
+ totalInputTokens: 0,
522
+ totalOutputTokens: 0,
523
+ totalCostUsd: 0,
524
+ };
525
+ }
526
+
527
+ async getAttachment(
528
+ _threadId: string,
529
+ _messageId: string,
530
+ ): Promise<{ bytes: Uint8Array; filename: string } | null> {
531
+ return null;
532
+ }
533
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * admin/src/http-chat-client.unit.test.ts
3
+ * Pure unit tests for the read-side helpers in http-chat-client.ts.
4
+ */
5
+
6
+ import { describe, expect, test } from "bun:test";
7
+ import { readFileSync } from "node:fs";
8
+ import {
9
+ type ChatMessage,
10
+ filterSince,
11
+ HttpChatClient,
12
+ } from "./http-chat-client.ts";
13
+
14
+ const cassette: Record<string, { status: number; body: unknown }> = JSON.parse(
15
+ readFileSync(
16
+ new URL("./fixtures/http-chat-client-cassette.json", import.meta.url)
17
+ .pathname,
18
+ "utf-8",
19
+ ),
20
+ );
21
+
22
+ /** HttpChatClient over an injected fetchFn replaying one cassette entry. */
23
+ function cassetteClient(key: string) {
24
+ const entry = cassette[key];
25
+ if (!entry) throw new Error(`cassette key not found: ${key}`);
26
+ const bodies: unknown[] = [];
27
+ const fetchFn = (async (_input: RequestInfo | URL, init?: RequestInit) => {
28
+ bodies.push(JSON.parse(String(init?.body ?? "{}")));
29
+ return new Response(JSON.stringify(entry.body), {
30
+ status: entry.status,
31
+ headers: { "Content-Type": "application/json" },
32
+ });
33
+ }) as typeof fetch;
34
+ return {
35
+ client: new HttpChatClient("https://chat.example.com", "admin-token", {
36
+ fetchFn,
37
+ }),
38
+ bodies,
39
+ };
40
+ }
41
+
42
+ function msg(id: string): ChatMessage {
43
+ return {
44
+ id,
45
+ threadId: "t",
46
+ role: "user",
47
+ body: id,
48
+ createdAt: "2024-01-01T00:00:00.000Z",
49
+ claimedBy: null,
50
+ repliedAt: null,
51
+ tokens: null,
52
+ costUsd: null,
53
+ errorKind: null,
54
+ attachmentFilename: null,
55
+ attachmentSize: null,
56
+ };
57
+ }
58
+
59
+ describe("filterSince (CFB-2.3 incremental ?since polling)", () => {
60
+ const messages = [msg("a"), msg("b"), msg("c"), msg("d")];
61
+
62
+ test("returns only messages after the given id", () => {
63
+ expect(filterSince(messages, "b").map((m) => m.id)).toEqual(["c", "d"]);
64
+ });
65
+
66
+ test("returns empty when since is the last message", () => {
67
+ expect(filterSince(messages, "d")).toEqual([]);
68
+ });
69
+
70
+ test("returns the full list when the since id is not found (re-sync)", () => {
71
+ expect(filterSince(messages, "zzz").map((m) => m.id)).toEqual([
72
+ "a",
73
+ "b",
74
+ "c",
75
+ "d",
76
+ ]);
77
+ });
78
+
79
+ test("returns everything after the first when since is the first id", () => {
80
+ expect(filterSince(messages, "a").map((m) => m.id)).toEqual([
81
+ "b",
82
+ "c",
83
+ "d",
84
+ ]);
85
+ });
86
+ });
87
+
88
+ describe("HttpChatClient.createThread ask fields (COS-31.1)", () => {
89
+ test("sends kind, askFromAgentId and askDepth when given", async () => {
90
+ const { client, bodies } = cassetteClient("createThread_ask_success");
91
+ const thread = await client.createThread("agent-123", {
92
+ title: "ask from agent-456 - 2026-01-03T00:00:00.000Z",
93
+ kind: "ask",
94
+ askFromAgentId: "agent-456",
95
+ askDepth: 1,
96
+ });
97
+ expect(bodies).toEqual([
98
+ {
99
+ agentId: "agent-123",
100
+ title: "ask from agent-456 - 2026-01-03T00:00:00.000Z",
101
+ kind: "ask",
102
+ askFromAgentId: "agent-456",
103
+ askDepth: 1,
104
+ },
105
+ ]);
106
+ expect(thread).toMatchObject({
107
+ id: "thread_ask",
108
+ kind: "ask",
109
+ askFromAgentId: "agent-456",
110
+ askDepth: 1,
111
+ });
112
+ });
113
+
114
+ test("omits the ask fields when not given", async () => {
115
+ const { client, bodies } = cassetteClient("createThread_minimal_success");
116
+ await client.createThread("agent-123", { title: "t" });
117
+ expect(bodies).toEqual([{ agentId: "agent-123", title: "t" }]);
118
+ });
119
+ });