@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,489 @@
1
+ /**
2
+ * admin/src/agent-cron-runs.ts
3
+ * AgentCronRunService — create and list AgentCronRun records.
4
+ *
5
+ * Each run records one cron invocation: whether it was skipped (pre-check
6
+ * returned false), the outcome, and any error.
7
+ */
8
+
9
+ import type {
10
+ AgentCronRun,
11
+ Prisma,
12
+ PrismaClient,
13
+ } from "../prisma/client/client.ts";
14
+ import { NotFoundError } from "./errors.ts";
15
+
16
+ export type { AgentCronRun };
17
+
18
+ /** An AgentCronRun with its per-model token/cost breakdown rows attached. */
19
+ type AgentCronRunWithModelBreakdown = Prisma.AgentCronRunGetPayload<{
20
+ include: { modelBreakdown: true };
21
+ }>;
22
+
23
+ /**
24
+ * An AgentCronRun with its per-model token/cost breakdown rows and its
25
+ * owning cron's id/name/schedule attached — used by cross-cron listings
26
+ * (e.g. listForAgent) so callers don't need N+1 cron lookups.
27
+ */
28
+ type AgentCronRunWithCron = Prisma.AgentCronRunGetPayload<{
29
+ include: {
30
+ modelBreakdown: true;
31
+ cron: { select: { id: true; name: true; schedule: true } };
32
+ phaseCron: { select: { id: true; name: true } };
33
+ };
34
+ }>;
35
+
36
+ interface CreateAgentCronRunInput {
37
+ startedAt: Date;
38
+ completedAt?: Date | null;
39
+ skipped?: boolean;
40
+ skipReason?: string | null;
41
+ outcome?: string | null;
42
+ error?: string | null;
43
+ /** Child AgentCronJob id (FK) of the pipeline phase this run served (dev-task/review/patch/deploy). Null for legacy five-job crons or runs with no phase attribution. */
44
+ phaseId?: string | null;
45
+ /** Work item type this run was dispatched against ("task" | "pr"). Null when the tick had no dispatch. */
46
+ itemType?: string | null;
47
+ /** Work item id this run was dispatched against (e.g. "WLS-2.2" or "acme/x#123"). Null when the tick had no dispatch. */
48
+ itemId?: string | null;
49
+ /** Slash-command variant this dispatch ran ("default" | "alternate"). Null when the run had no variant attribution. */
50
+ commandVariant?: string | null;
51
+ }
52
+
53
+ export interface ModelBreakdownEntry {
54
+ model: string;
55
+ inputTokens: number;
56
+ outputTokens: number;
57
+ cacheReadTokens: number;
58
+ cacheCreationTokens: number;
59
+ costUsd: number;
60
+ }
61
+
62
+ /**
63
+ * First-turn context baseline reported by the agent: the usage of the run's
64
+ * first assistant message, whose input + cacheCreation + cacheRead is the
65
+ * full always-loaded context independent of cache warmth
66
+ * (computed agent-side from the Claude CLI stream).
67
+ */
68
+ interface ContextBaselineEntry {
69
+ model: string;
70
+ contextTokens: number;
71
+ inputTokens: number;
72
+ cacheCreationTokens: number;
73
+ cacheReadTokens: number;
74
+ }
75
+
76
+ /** Per-skill / per-subagent token attribution row reported by the agent. */
77
+ interface SkillUsageEntry {
78
+ kind: string;
79
+ name: string;
80
+ invocations: number;
81
+ turns: number;
82
+ inputTokens: number;
83
+ outputTokens: number;
84
+ cacheReadTokens: number;
85
+ cacheCreationTokens: number;
86
+ invokeContextDelta?: number | null;
87
+ }
88
+
89
+ export interface PatchAgentCronRunInput {
90
+ completedAt?: Date | null;
91
+ outcome?: string | null;
92
+ error?: string | null;
93
+ skipped?: boolean;
94
+ skipReason?: string | null;
95
+ inputTokens?: number | null;
96
+ outputTokens?: number | null;
97
+ cacheReadTokens?: number | null;
98
+ cacheCreationTokens?: number | null;
99
+ /** Agent session id this run was executed under. Null clears it. */
100
+ sessionId?: string | null;
101
+ /**
102
+ * Most recent debounced progress-push (recordProgress()) timestamp,
103
+ * sourced from the agent's injected Clock. Null clears it. Overwritten on
104
+ * every progress push — always the latest, not a history.
105
+ */
106
+ lastHeartbeatAt?: Date | null;
107
+ modelBreakdown?: ModelBreakdownEntry[];
108
+ /** Spread onto the baseline* columns. */
109
+ contextBaseline?: ContextBaselineEntry;
110
+ turns?: number | null;
111
+ toolCalls?: number | null;
112
+ contextFingerprint?: string | null;
113
+ pluginVersion?: string | null;
114
+ claudeCodeVersion?: string | null;
115
+ /** Upserted per [cronRunId, kind, name], like modelBreakdown per model. */
116
+ skillUsage?: SkillUsageEntry[];
117
+ }
118
+
119
+ interface ListAgentCronRunsOptions {
120
+ limit?: number;
121
+ offset?: number;
122
+ /** Narrow to runs dispatched against this work item (e.g. "WLS-2.2" or "acme/x#123"). */
123
+ itemId?: string;
124
+ /** Narrow to runs dispatched by this phase cron (a child AgentCronJob id). */
125
+ phaseId?: string;
126
+ }
127
+
128
+ interface AgentCronRunList {
129
+ items: AgentCronRunWithModelBreakdown[];
130
+ total: number;
131
+ limit: number;
132
+ offset: number;
133
+ }
134
+
135
+ interface ListForAgentOptions {
136
+ /** Narrow to a single cron. Still scoped to agentId — a cronId belonging
137
+ * to a different agent yields an empty result, not an error. */
138
+ cronId?: string;
139
+ /**
140
+ * "skipped" filters WHERE skipped = true regardless of the outcome column
141
+ * (mirrors the cronRunOutcomeLabel display convention: skipped is a
142
+ * boolean flag, not an outcome column value). Any other value filters
143
+ * WHERE skipped = false AND outcome = value. Omitted means no filter.
144
+ */
145
+ outcome?: string;
146
+ limit?: number;
147
+ offset?: number;
148
+ }
149
+
150
+ interface AgentCronRunListForAgent {
151
+ items: AgentCronRunWithCron[];
152
+ total: number;
153
+ limit: number;
154
+ offset: number;
155
+ }
156
+
157
+ /**
158
+ * Filter options for `listAcrossAgents()`. Mirrors `ListForAgentOptions` —
159
+ * the agent scoping itself comes from the `agentIds` array argument, not
160
+ * from an option here.
161
+ */
162
+ interface ListAcrossAgentsOptions {
163
+ /** Narrow to a single cron, still scoped to the given agentIds. */
164
+ cronId?: string;
165
+ /**
166
+ * "skipped" filters WHERE skipped = true regardless of the outcome column
167
+ * (same semantics as ListForAgentOptions.outcome). Any other value filters
168
+ * WHERE skipped = false AND outcome = value. Omitted means no filter.
169
+ */
170
+ outcome?: string;
171
+ limit?: number;
172
+ offset?: number;
173
+ }
174
+
175
+ export class AgentCronRunService {
176
+ constructor(private prisma: PrismaClient) {}
177
+
178
+ /**
179
+ * Create a new run record for a cron job.
180
+ * Validates that the cronId exists and belongs to agentId.
181
+ * Throws NotFoundError if the cronId doesn't exist or is not owned by agentId.
182
+ */
183
+ async create(
184
+ cronId: string,
185
+ agentId: string,
186
+ input: CreateAgentCronRunInput,
187
+ ): Promise<AgentCronRun> {
188
+ // Validate ownership
189
+ const job = await this.prisma.agentCronJob.findUnique({
190
+ where: { id: cronId },
191
+ });
192
+ if (!job || job.agentId !== agentId) {
193
+ throw new NotFoundError(`cron job ${cronId} not found`);
194
+ }
195
+
196
+ return this.prisma.agentCronRun.create({
197
+ data: {
198
+ cronId,
199
+ agentId,
200
+ startedAt: input.startedAt,
201
+ completedAt: input.completedAt ?? null,
202
+ skipped: input.skipped ?? false,
203
+ skipReason: input.skipReason ?? null,
204
+ outcome: input.outcome ?? null,
205
+ error: input.error ?? null,
206
+ phaseId: input.phaseId ?? null,
207
+ itemType: input.itemType ?? null,
208
+ itemId: input.itemId ?? null,
209
+ commandVariant: input.commandVariant ?? null,
210
+ },
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Update an existing run record with completion data and/or token fields.
216
+ * Validates that the runId exists, belongs to agentId, and belongs to cronId.
217
+ * Throws NotFoundError if the run doesn't exist, is not owned by agentId, or
218
+ * does not belong to the given cronId (prevents cross-cron access).
219
+ */
220
+ async patch(
221
+ runId: string,
222
+ agentId: string,
223
+ cronId: string,
224
+ input: PatchAgentCronRunInput,
225
+ ): Promise<AgentCronRunWithModelBreakdown> {
226
+ const run = await this.prisma.agentCronRun.findUnique({
227
+ where: { id: runId },
228
+ });
229
+ if (!run || run.agentId !== agentId) {
230
+ throw new NotFoundError(`cron run ${runId} not found`);
231
+ }
232
+ if (run.cronId !== cronId) {
233
+ throw new NotFoundError(`cron run ${runId} not found`);
234
+ }
235
+
236
+ // The top-level token fields (inputTokens/outputTokens/…) are still accepted
237
+ // on the input for backward compatibility with older agent builds, but they
238
+ // are no longer persisted: those columns were dropped and all token
239
+ // accounting now flows through AgentCronRunModelBreakdown (see modelBreakdown
240
+ // below). We simply ignore them here.
241
+ // TODO(CTT-cleanup): remove top-level token field acceptance once all agent
242
+ // builds send modelBreakdown only (see agent/src/runtime/cron-handler.ts's
243
+ // buildTokenPayload()).
244
+ const runData = {
245
+ ...(input.completedAt !== undefined && {
246
+ completedAt: input.completedAt,
247
+ }),
248
+ ...(input.outcome !== undefined && { outcome: input.outcome }),
249
+ ...(input.error !== undefined && { error: input.error }),
250
+ ...(input.skipped !== undefined && { skipped: input.skipped }),
251
+ ...(input.skipReason !== undefined && { skipReason: input.skipReason }),
252
+ ...(input.sessionId !== undefined && { sessionId: input.sessionId }),
253
+ ...(input.lastHeartbeatAt !== undefined && {
254
+ lastHeartbeatAt: input.lastHeartbeatAt,
255
+ }),
256
+ ...(input.contextBaseline !== undefined && {
257
+ baselineModel: input.contextBaseline.model,
258
+ baselineContextTokens: input.contextBaseline.contextTokens,
259
+ baselineInputTokens: input.contextBaseline.inputTokens,
260
+ baselineCacheCreationTokens: input.contextBaseline.cacheCreationTokens,
261
+ baselineCacheReadTokens: input.contextBaseline.cacheReadTokens,
262
+ }),
263
+ ...(input.turns !== undefined && { turns: input.turns }),
264
+ ...(input.toolCalls !== undefined && { toolCalls: input.toolCalls }),
265
+ ...(input.contextFingerprint !== undefined && {
266
+ contextFingerprint: input.contextFingerprint,
267
+ }),
268
+ ...(input.pluginVersion !== undefined && {
269
+ pluginVersion: input.pluginVersion,
270
+ }),
271
+ ...(input.claudeCodeVersion !== undefined && {
272
+ claudeCodeVersion: input.claudeCodeVersion,
273
+ }),
274
+ };
275
+
276
+ const breakdownUpserts = (input.modelBreakdown ?? []).map((entry) =>
277
+ this.prisma.agentCronRunModelBreakdown.upsert({
278
+ where: {
279
+ cronRunId_model: { cronRunId: runId, model: entry.model },
280
+ },
281
+ create: {
282
+ cronRunId: runId,
283
+ model: entry.model,
284
+ inputTokens: entry.inputTokens,
285
+ outputTokens: entry.outputTokens,
286
+ cacheReadTokens: entry.cacheReadTokens,
287
+ cacheCreationTokens: entry.cacheCreationTokens,
288
+ costUsd: entry.costUsd,
289
+ },
290
+ update: {
291
+ inputTokens: entry.inputTokens,
292
+ outputTokens: entry.outputTokens,
293
+ cacheReadTokens: entry.cacheReadTokens,
294
+ cacheCreationTokens: entry.cacheCreationTokens,
295
+ costUsd: entry.costUsd,
296
+ },
297
+ }),
298
+ );
299
+ // Per-skill attribution rows follow the same upsert-per-key pattern as
300
+ // the model breakdown (unique on [cronRunId, kind, name]).
301
+ const skillUsageUpserts = (input.skillUsage ?? []).map((entry) => {
302
+ const fields = {
303
+ invocations: entry.invocations,
304
+ turns: entry.turns,
305
+ inputTokens: entry.inputTokens,
306
+ outputTokens: entry.outputTokens,
307
+ cacheReadTokens: entry.cacheReadTokens,
308
+ cacheCreationTokens: entry.cacheCreationTokens,
309
+ invokeContextDelta: entry.invokeContextDelta ?? null,
310
+ };
311
+ return this.prisma.agentCronRunSkillUsage.upsert({
312
+ where: {
313
+ cronRunId_kind_name: {
314
+ cronRunId: runId,
315
+ kind: entry.kind,
316
+ name: entry.name,
317
+ },
318
+ },
319
+ create: {
320
+ cronRunId: runId,
321
+ kind: entry.kind,
322
+ name: entry.name,
323
+ ...fields,
324
+ },
325
+ update: fields,
326
+ });
327
+ });
328
+
329
+ if (breakdownUpserts.length > 0 || skillUsageUpserts.length > 0) {
330
+ // Wrap the run update and all child upserts in a single transaction
331
+ // so the writes are atomic — partial breakdown/attribution rows are
332
+ // never visible. The run update (with its modelBreakdown include) must
333
+ // be the LAST statement in the array: Prisma's array-form $transaction
334
+ // issues statements in order, so an earlier include would return
335
+ // breakdown rows as they stood before this call's upserts ran.
336
+ const results = await this.prisma.$transaction([
337
+ ...breakdownUpserts,
338
+ ...skillUsageUpserts,
339
+ this.prisma.agentCronRun.update({
340
+ where: { id: runId },
341
+ data: runData,
342
+ include: { modelBreakdown: true },
343
+ }),
344
+ ]);
345
+ const updatedRun = results[
346
+ results.length - 1
347
+ ] as AgentCronRunWithModelBreakdown;
348
+ return updatedRun;
349
+ }
350
+
351
+ return this.prisma.agentCronRun.update({
352
+ where: { id: runId },
353
+ data: runData,
354
+ include: { modelBreakdown: true },
355
+ });
356
+ }
357
+
358
+ /**
359
+ * List all runs for a cron job, sorted descending by startedAt.
360
+ * Validates that the cronId exists and belongs to agentId.
361
+ * Throws NotFoundError if the cronId doesn't exist or is not owned by agentId.
362
+ * Defaults to limit=20, offset=0.
363
+ */
364
+ async list(
365
+ cronId: string,
366
+ agentId: string,
367
+ opts?: ListAgentCronRunsOptions,
368
+ ): Promise<AgentCronRunList> {
369
+ const limit = opts?.limit ?? 20;
370
+ const offset = opts?.offset ?? 0;
371
+
372
+ // Validate ownership
373
+ const job = await this.prisma.agentCronJob.findUnique({
374
+ where: { id: cronId },
375
+ });
376
+ if (!job || job.agentId !== agentId) {
377
+ throw new NotFoundError(`cron job ${cronId} not found`);
378
+ }
379
+
380
+ const where: Prisma.AgentCronRunWhereInput = { cronId, agentId };
381
+ if (opts?.itemId) {
382
+ where.itemId = opts.itemId;
383
+ }
384
+ if (opts?.phaseId) {
385
+ where.phaseId = opts.phaseId;
386
+ }
387
+
388
+ const [items, total] = await this.prisma.$transaction([
389
+ this.prisma.agentCronRun.findMany({
390
+ where,
391
+ orderBy: { startedAt: "desc" },
392
+ take: limit,
393
+ skip: offset,
394
+ include: { modelBreakdown: true },
395
+ }),
396
+ this.prisma.agentCronRun.count({ where }),
397
+ ]);
398
+
399
+ return { items, total, limit, offset };
400
+ }
401
+
402
+ /**
403
+ * List all runs for an agent across every cron it owns, sorted descending
404
+ * by startedAt. Unlike `.list()`, this method is agent-scoped by
405
+ * construction and does not validate a specific cronId's ownership — if
406
+ * opts.cronId is given but belongs to a different agent, the WHERE clause
407
+ * (agentId AND cronId) simply matches no rows.
408
+ * Defaults to limit=20, offset=0.
409
+ */
410
+ async listForAgent(
411
+ agentId: string,
412
+ opts?: ListForAgentOptions,
413
+ ): Promise<AgentCronRunListForAgent> {
414
+ const limit = opts?.limit ?? 20;
415
+ const offset = opts?.offset ?? 0;
416
+
417
+ const where: Prisma.AgentCronRunWhereInput = { agentId };
418
+ if (opts?.cronId) {
419
+ where.cronId = opts.cronId;
420
+ }
421
+ if (opts?.outcome === "skipped") {
422
+ where.skipped = true;
423
+ } else if (opts?.outcome !== undefined) {
424
+ where.skipped = false;
425
+ where.outcome = opts.outcome;
426
+ }
427
+
428
+ const [items, total] = await this.prisma.$transaction([
429
+ this.prisma.agentCronRun.findMany({
430
+ where,
431
+ orderBy: { startedAt: "desc" },
432
+ take: limit,
433
+ skip: offset,
434
+ include: {
435
+ modelBreakdown: true,
436
+ cron: { select: { id: true, name: true, schedule: true } },
437
+ phaseCron: { select: { id: true, name: true } },
438
+ },
439
+ }),
440
+ this.prisma.agentCronRun.count({ where }),
441
+ ]);
442
+
443
+ return { items, total, limit, offset };
444
+ }
445
+
446
+ /**
447
+ * List runs across a set of agents, sorted descending by startedAt.
448
+ * Unlike `.listForAgent()`, this is scoped to a list of agent ids rather
449
+ * than a single one — used by fleet-wide views (e.g. an org dashboard)
450
+ * that need a globally-ordered feed across many agents' cron history.
451
+ * An empty agentIds array yields an empty result, not an error.
452
+ * Defaults to limit=20, offset=0.
453
+ */
454
+ async listAcrossAgents(
455
+ agentIds: string[],
456
+ opts?: ListAcrossAgentsOptions,
457
+ ): Promise<AgentCronRunListForAgent> {
458
+ const limit = opts?.limit ?? 20;
459
+ const offset = opts?.offset ?? 0;
460
+
461
+ const where: Prisma.AgentCronRunWhereInput = { agentId: { in: agentIds } };
462
+ if (opts?.cronId) {
463
+ where.cronId = opts.cronId;
464
+ }
465
+ if (opts?.outcome === "skipped") {
466
+ where.skipped = true;
467
+ } else if (opts?.outcome !== undefined) {
468
+ where.skipped = false;
469
+ where.outcome = opts.outcome;
470
+ }
471
+
472
+ const [items, total] = await this.prisma.$transaction([
473
+ this.prisma.agentCronRun.findMany({
474
+ where,
475
+ orderBy: { startedAt: "desc" },
476
+ take: limit,
477
+ skip: offset,
478
+ include: {
479
+ modelBreakdown: true,
480
+ cron: { select: { id: true, name: true, schedule: true } },
481
+ phaseCron: { select: { id: true, name: true } },
482
+ },
483
+ }),
484
+ this.prisma.agentCronRun.count({ where }),
485
+ ]);
486
+
487
+ return { items, total, limit, offset };
488
+ }
489
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * admin/src/agent-deletion-checklist.ts
3
+ *
4
+ * Pure builder for the operator-facing "manual steps" checklist surfaced when
5
+ * an agent is deleted. Several agent secrets (GH_TOKEN, LINEAR_API_KEY,
6
+ * STRIPE_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY, ...) are
7
+ * hand-pasted into AgentEnv with no stored ID/owner metadata, so no automated
8
+ * revocation is possible on agent delete. Rather than silently dropping them,
9
+ * this module turns the agent's `AgentEnv` rows into a reminder checklist for
10
+ * the operator to work through by hand.
11
+ *
12
+ * Uses the existing `AgentEnv.secret` boolean (checked by default in the "add
13
+ * env var" admin UI form) — no schema changes needed.
14
+ *
15
+ * This function is PURE — no fs, no network, no Prisma, no clock. It only
16
+ * shapes plain objects from plain objects, so it's unit-tested in isolation
17
+ * without a DB. See ./agent-manifest.ts for the same "pure builder" pattern.
18
+ */
19
+
20
+ /** A single operator-facing reminder produced for one AgentEnv row. */
21
+ export interface ManualStep {
22
+ /** The AgentEnv key this reminder is about (e.g. "GH_TOKEN"). */
23
+ key: string;
24
+ /** Human-readable instructions for the operator. */
25
+ message: string;
26
+ }
27
+
28
+ /**
29
+ * Per-agent Anthropic identity pieces still present after a failed removal.
30
+ * A null/absent piece is already gone and gets no entry.
31
+ */
32
+ export interface LeftoverIdentity {
33
+ anthropicFederationRuleId?: string | null;
34
+ anthropicServiceAccountId?: string | null;
35
+ k8sServiceAccountName?: string | null;
36
+ }
37
+
38
+ /**
39
+ * Keys handled by the Slack app deletion step (ADC-2.1 / ADC-3.1) — excluded
40
+ * entirely from this checklist so operators aren't told to do the same
41
+ * cleanup twice.
42
+ */
43
+ const SLACK_APP_MANAGED_KEYS = new Set([
44
+ "SLACK_APP_ID",
45
+ "SLACK_SIGNING_SECRET",
46
+ "SLACK_BOT_TOKEN",
47
+ ]);
48
+
49
+ /**
50
+ * Named/standard keys with specific revocation instructions. Exact-match on
51
+ * key name — a differently-cased key (e.g. "gh_token") falls through to the
52
+ * generic custom-secret message rather than being matched here, since
53
+ * AgentEnv keys are conventionally upper-snake-case and we'd rather surface
54
+ * an unexpected casing than silently apply the wrong instructions.
55
+ */
56
+ const NAMED_KEY_INSTRUCTIONS: Record<string, string> = {
57
+ GH_TOKEN:
58
+ "Revoke this GitHub personal access token at https://github.com/settings/tokens (or the fine-grained PAT settings page).",
59
+ ANTHROPIC_API_KEY: "Revoke/rotate this key in the Anthropic console.",
60
+ CLAUDE_CODE_OAUTH_TOKEN:
61
+ "Revoke this token via `claude auth logout` or the Anthropic console.",
62
+ };
63
+
64
+ /**
65
+ * Build the manual-steps checklist for the given `AgentEnv` rows.
66
+ *
67
+ * Bucketing:
68
+ * - Named/standard keys (GH_TOKEN, ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN)
69
+ * get a specific instruction.
70
+ * - Slack-app-managed keys (SLACK_APP_ID, SLACK_SIGNING_SECRET,
71
+ * SLACK_BOT_TOKEN) are excluded — handled by the Slack app deletion step.
72
+ * - Any other row with `secret: true` gets a generic "verify manually"
73
+ * reminder, naming the key.
74
+ * - Rows with `secret: false` never produce an entry, regardless of key name.
75
+ *
76
+ * Order-preserving and non-deduplicating: one `ManualStep` per matching input
77
+ * row, in input order (duplicate keys produce duplicate entries).
78
+ *
79
+ * `leftoverIdentity` (optional) appends one entry per per-agent Anthropic
80
+ * identity piece still present after a failed removal, in removal order:
81
+ * federation rule, Anthropic service account, Kubernetes ServiceAccount.
82
+ */
83
+ export function buildManualStepsChecklist(
84
+ envRows: { key: string; secret: boolean }[],
85
+ leftoverIdentity?: LeftoverIdentity,
86
+ ): ManualStep[] {
87
+ const steps: ManualStep[] = [];
88
+
89
+ for (const row of envRows) {
90
+ if (!row.secret) continue;
91
+ if (SLACK_APP_MANAGED_KEYS.has(row.key)) continue;
92
+
93
+ const namedInstruction = NAMED_KEY_INSTRUCTIONS[row.key];
94
+ if (namedInstruction !== undefined) {
95
+ steps.push({ key: row.key, message: namedInstruction });
96
+ continue;
97
+ }
98
+
99
+ steps.push({
100
+ key: row.key,
101
+ message: `Custom secret '${row.key}' was added manually and has no automated revocation — verify whether it needs to be revoked at the source.`,
102
+ });
103
+ }
104
+
105
+ // Identity pieces the automated removal left behind, in removal order. The
106
+ // rule goes first: Anthropic refuses to archive a service account while a
107
+ // live rule references it.
108
+ if (leftoverIdentity?.anthropicFederationRuleId) {
109
+ steps.push({
110
+ key: "anthropicFederationRuleId",
111
+ message: `Anthropic federation rule ${leftoverIdentity.anthropicFederationRuleId} was not archived automatically — archive it in the Anthropic console first (a service account cannot be archived while a live rule references it).`,
112
+ });
113
+ }
114
+ if (leftoverIdentity?.anthropicServiceAccountId) {
115
+ steps.push({
116
+ key: "anthropicServiceAccountId",
117
+ message: `Anthropic service account ${leftoverIdentity.anthropicServiceAccountId} was not archived automatically — archive it in the Anthropic console once its federation rule is archived.`,
118
+ });
119
+ }
120
+ if (leftoverIdentity?.k8sServiceAccountName) {
121
+ steps.push({
122
+ key: "k8sServiceAccountName",
123
+ message: `Kubernetes ServiceAccount ${leftoverIdentity.k8sServiceAccountName} was not deleted automatically — delete it from the agent namespace with kubectl.`,
124
+ });
125
+ }
126
+
127
+ return steps;
128
+ }