@paperclipai/server 0.2.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 (317) hide show
  1. package/dist/adapters/codex-models.d.ts +4 -0
  2. package/dist/adapters/codex-models.d.ts.map +1 -0
  3. package/dist/adapters/codex-models.js +98 -0
  4. package/dist/adapters/codex-models.js.map +1 -0
  5. package/dist/adapters/http/execute.d.ts +3 -0
  6. package/dist/adapters/http/execute.d.ts.map +1 -0
  7. package/dist/adapters/http/execute.js +39 -0
  8. package/dist/adapters/http/execute.js.map +1 -0
  9. package/dist/adapters/http/index.d.ts +3 -0
  10. package/dist/adapters/http/index.d.ts.map +1 -0
  11. package/dist/adapters/http/index.js +20 -0
  12. package/dist/adapters/http/index.js.map +1 -0
  13. package/dist/adapters/http/test.d.ts +3 -0
  14. package/dist/adapters/http/test.d.ts.map +1 -0
  15. package/dist/adapters/http/test.js +106 -0
  16. package/dist/adapters/http/test.js.map +1 -0
  17. package/dist/adapters/index.d.ts +4 -0
  18. package/dist/adapters/index.d.ts.map +1 -0
  19. package/dist/adapters/index.js +3 -0
  20. package/dist/adapters/index.js.map +1 -0
  21. package/dist/adapters/process/execute.d.ts +3 -0
  22. package/dist/adapters/process/execute.d.ts.map +1 -0
  23. package/dist/adapters/process/execute.js +63 -0
  24. package/dist/adapters/process/execute.js.map +1 -0
  25. package/dist/adapters/process/index.d.ts +3 -0
  26. package/dist/adapters/process/index.d.ts.map +1 -0
  27. package/dist/adapters/process/index.js +23 -0
  28. package/dist/adapters/process/index.js.map +1 -0
  29. package/dist/adapters/process/test.d.ts +3 -0
  30. package/dist/adapters/process/test.d.ts.map +1 -0
  31. package/dist/adapters/process/test.js +77 -0
  32. package/dist/adapters/process/test.js.map +1 -0
  33. package/dist/adapters/registry.d.ts +9 -0
  34. package/dist/adapters/registry.d.ts.map +1 -0
  35. package/dist/adapters/registry.js +63 -0
  36. package/dist/adapters/registry.js.map +1 -0
  37. package/dist/adapters/types.d.ts +2 -0
  38. package/dist/adapters/types.d.ts.map +1 -0
  39. package/dist/adapters/types.js +2 -0
  40. package/dist/adapters/types.js.map +1 -0
  41. package/dist/adapters/utils.d.ts +10 -0
  42. package/dist/adapters/utils.d.ts.map +1 -0
  43. package/dist/adapters/utils.js +14 -0
  44. package/dist/adapters/utils.js.map +1 -0
  45. package/dist/agent-auth-jwt.d.ts +14 -0
  46. package/dist/agent-auth-jwt.d.ts.map +1 -0
  47. package/dist/agent-auth-jwt.js +117 -0
  48. package/dist/agent-auth-jwt.js.map +1 -0
  49. package/dist/app.d.ts +20 -0
  50. package/dist/app.d.ts.map +1 -0
  51. package/dist/app.js +127 -0
  52. package/dist/app.js.map +1 -0
  53. package/dist/auth/better-auth.d.ts +23 -0
  54. package/dist/auth/better-auth.d.ts.map +1 -0
  55. package/dist/auth/better-auth.js +80 -0
  56. package/dist/auth/better-auth.js.map +1 -0
  57. package/dist/board-claim.d.ts +23 -0
  58. package/dist/board-claim.d.ts.map +1 -0
  59. package/dist/board-claim.js +115 -0
  60. package/dist/board-claim.js.map +1 -0
  61. package/dist/config-file.d.ts +3 -0
  62. package/dist/config-file.d.ts.map +1 -0
  63. package/dist/config-file.js +16 -0
  64. package/dist/config-file.js.map +1 -0
  65. package/dist/config.d.ts +33 -0
  66. package/dist/config.d.ts.map +1 -0
  67. package/dist/config.js +114 -0
  68. package/dist/config.js.map +1 -0
  69. package/dist/errors.d.ts +12 -0
  70. package/dist/errors.d.ts.map +1 -0
  71. package/dist/errors.js +28 -0
  72. package/dist/errors.js.map +1 -0
  73. package/dist/home-paths.d.ts +11 -0
  74. package/dist/home-paths.d.ts.map +1 -0
  75. package/dist/home-paths.js +54 -0
  76. package/dist/home-paths.js.map +1 -0
  77. package/dist/index.d.ts +2 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +439 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/middleware/auth.d.ts +12 -0
  82. package/dist/middleware/auth.d.ts.map +1 -0
  83. package/dist/middleware/auth.js +124 -0
  84. package/dist/middleware/auth.js.map +1 -0
  85. package/dist/middleware/board-mutation-guard.d.ts +3 -0
  86. package/dist/middleware/board-mutation-guard.d.ts.map +1 -0
  87. package/dist/middleware/board-mutation-guard.js +60 -0
  88. package/dist/middleware/board-mutation-guard.js.map +1 -0
  89. package/dist/middleware/error-handler.d.ts +3 -0
  90. package/dist/middleware/error-handler.d.ts.map +1 -0
  91. package/dist/middleware/error-handler.js +22 -0
  92. package/dist/middleware/error-handler.js.map +1 -0
  93. package/dist/middleware/index.d.ts +4 -0
  94. package/dist/middleware/index.d.ts.map +1 -0
  95. package/dist/middleware/index.js +4 -0
  96. package/dist/middleware/index.js.map +1 -0
  97. package/dist/middleware/logger.d.ts +4 -0
  98. package/dist/middleware/logger.d.ts.map +1 -0
  99. package/dist/middleware/logger.js +37 -0
  100. package/dist/middleware/logger.js.map +1 -0
  101. package/dist/middleware/private-hostname-guard.d.ts +11 -0
  102. package/dist/middleware/private-hostname-guard.d.ts.map +1 -0
  103. package/dist/middleware/private-hostname-guard.js +78 -0
  104. package/dist/middleware/private-hostname-guard.js.map +1 -0
  105. package/dist/middleware/validate.d.ts +4 -0
  106. package/dist/middleware/validate.d.ts.map +1 -0
  107. package/dist/middleware/validate.js +7 -0
  108. package/dist/middleware/validate.js.map +1 -0
  109. package/dist/paths.d.ts +3 -0
  110. package/dist/paths.d.ts.map +1 -0
  111. package/dist/paths.js +31 -0
  112. package/dist/paths.js.map +1 -0
  113. package/dist/realtime/live-events-ws.d.ts +10 -0
  114. package/dist/realtime/live-events-ws.d.ts.map +1 -0
  115. package/dist/realtime/live-events-ws.js +185 -0
  116. package/dist/realtime/live-events-ws.js.map +1 -0
  117. package/dist/redaction.d.ts +4 -0
  118. package/dist/redaction.d.ts.map +1 -0
  119. package/dist/redaction.js +63 -0
  120. package/dist/redaction.js.map +1 -0
  121. package/dist/routes/access.d.ts +9 -0
  122. package/dist/routes/access.d.ts.map +1 -0
  123. package/dist/routes/access.js +887 -0
  124. package/dist/routes/access.js.map +1 -0
  125. package/dist/routes/activity.d.ts +3 -0
  126. package/dist/routes/activity.d.ts.map +1 -0
  127. package/dist/routes/activity.js +87 -0
  128. package/dist/routes/activity.js.map +1 -0
  129. package/dist/routes/agents.d.ts +3 -0
  130. package/dist/routes/agents.d.ts.map +1 -0
  131. package/dist/routes/agents.js +1132 -0
  132. package/dist/routes/agents.js.map +1 -0
  133. package/dist/routes/approvals.d.ts +3 -0
  134. package/dist/routes/approvals.d.ts.map +1 -0
  135. package/dist/routes/approvals.js +271 -0
  136. package/dist/routes/approvals.js.map +1 -0
  137. package/dist/routes/assets.d.ts +4 -0
  138. package/dist/routes/assets.d.ts.map +1 -0
  139. package/dist/routes/assets.js +138 -0
  140. package/dist/routes/assets.js.map +1 -0
  141. package/dist/routes/authz.d.ts +15 -0
  142. package/dist/routes/authz.d.ts.map +1 -0
  143. package/dist/routes/authz.js +40 -0
  144. package/dist/routes/authz.js.map +1 -0
  145. package/dist/routes/companies.d.ts +3 -0
  146. package/dist/routes/companies.d.ts.map +1 -0
  147. package/dist/routes/companies.js +159 -0
  148. package/dist/routes/companies.js.map +1 -0
  149. package/dist/routes/costs.d.ts +3 -0
  150. package/dist/routes/costs.d.ts.map +1 -0
  151. package/dist/routes/costs.js +113 -0
  152. package/dist/routes/costs.js.map +1 -0
  153. package/dist/routes/dashboard.d.ts +3 -0
  154. package/dist/routes/dashboard.d.ts.map +1 -0
  155. package/dist/routes/dashboard.js +15 -0
  156. package/dist/routes/dashboard.js.map +1 -0
  157. package/dist/routes/goals.d.ts +3 -0
  158. package/dist/routes/goals.d.ts.map +1 -0
  159. package/dist/routes/goals.js +95 -0
  160. package/dist/routes/goals.js.map +1 -0
  161. package/dist/routes/health.d.ts +9 -0
  162. package/dist/routes/health.d.ts.map +1 -0
  163. package/dist/routes/health.js +38 -0
  164. package/dist/routes/health.js.map +1 -0
  165. package/dist/routes/index.d.ts +15 -0
  166. package/dist/routes/index.d.ts.map +1 -0
  167. package/dist/routes/index.js +15 -0
  168. package/dist/routes/index.js.map +1 -0
  169. package/dist/routes/issues.d.ts +4 -0
  170. package/dist/routes/issues.d.ts.map +1 -0
  171. package/dist/routes/issues.js +973 -0
  172. package/dist/routes/issues.js.map +1 -0
  173. package/dist/routes/llms.d.ts +3 -0
  174. package/dist/routes/llms.d.ts.map +1 -0
  175. package/dist/routes/llms.js +78 -0
  176. package/dist/routes/llms.js.map +1 -0
  177. package/dist/routes/projects.d.ts +3 -0
  178. package/dist/routes/projects.d.ts.map +1 -0
  179. package/dist/routes/projects.js +253 -0
  180. package/dist/routes/projects.js.map +1 -0
  181. package/dist/routes/secrets.d.ts +3 -0
  182. package/dist/routes/secrets.d.ts.map +1 -0
  183. package/dist/routes/secrets.js +128 -0
  184. package/dist/routes/secrets.js.map +1 -0
  185. package/dist/routes/sidebar-badges.d.ts +3 -0
  186. package/dist/routes/sidebar-badges.d.ts.map +1 -0
  187. package/dist/routes/sidebar-badges.js +47 -0
  188. package/dist/routes/sidebar-badges.js.map +1 -0
  189. package/dist/secrets/external-stub-providers.d.ts +5 -0
  190. package/dist/secrets/external-stub-providers.d.ts.map +1 -0
  191. package/dist/secrets/external-stub-providers.js +21 -0
  192. package/dist/secrets/external-stub-providers.js.map +1 -0
  193. package/dist/secrets/local-encrypted-provider.d.ts +3 -0
  194. package/dist/secrets/local-encrypted-provider.d.ts.map +1 -0
  195. package/dist/secrets/local-encrypted-provider.js +116 -0
  196. package/dist/secrets/local-encrypted-provider.js.map +1 -0
  197. package/dist/secrets/provider-registry.d.ts +5 -0
  198. package/dist/secrets/provider-registry.d.ts.map +1 -0
  199. package/dist/secrets/provider-registry.js +20 -0
  200. package/dist/secrets/provider-registry.js.map +1 -0
  201. package/dist/secrets/types.d.ts +21 -0
  202. package/dist/secrets/types.d.ts.map +1 -0
  203. package/dist/secrets/types.js +2 -0
  204. package/dist/secrets/types.js.map +1 -0
  205. package/dist/services/access.d.ts +81 -0
  206. package/dist/services/access.d.ts.map +1 -0
  207. package/dist/services/access.js +187 -0
  208. package/dist/services/access.js.map +1 -0
  209. package/dist/services/activity-log.d.ts +14 -0
  210. package/dist/services/activity-log.d.ts.map +1 -0
  211. package/dist/services/activity-log.js +32 -0
  212. package/dist/services/activity-log.js.map +1 -0
  213. package/dist/services/activity.d.ts +764 -0
  214. package/dist/services/activity.d.ts.map +1 -0
  215. package/dist/services/activity.js +105 -0
  216. package/dist/services/activity.js.map +1 -0
  217. package/dist/services/agent-permissions.d.ts +6 -0
  218. package/dist/services/agent-permissions.d.ts.map +1 -0
  219. package/dist/services/agent-permissions.js +18 -0
  220. package/dist/services/agent-permissions.js.map +1 -0
  221. package/dist/services/agents.d.ts +1494 -0
  222. package/dist/services/agents.d.ts.map +1 -0
  223. package/dist/services/agents.js +454 -0
  224. package/dist/services/agents.js.map +1 -0
  225. package/dist/services/approvals.d.ts +540 -0
  226. package/dist/services/approvals.d.ts.map +1 -0
  227. package/dist/services/approvals.js +173 -0
  228. package/dist/services/approvals.js.map +1 -0
  229. package/dist/services/assets.d.ts +33 -0
  230. package/dist/services/assets.d.ts.map +1 -0
  231. package/dist/services/assets.js +17 -0
  232. package/dist/services/assets.js.map +1 -0
  233. package/dist/services/companies.d.ts +503 -0
  234. package/dist/services/companies.d.ts.map +1 -0
  235. package/dist/services/companies.js +120 -0
  236. package/dist/services/companies.js.map +1 -0
  237. package/dist/services/company-portability.d.ts +8 -0
  238. package/dist/services/company-portability.d.ts.map +1 -0
  239. package/dist/services/company-portability.js +851 -0
  240. package/dist/services/company-portability.js.map +1 -0
  241. package/dist/services/costs.d.ts +50 -0
  242. package/dist/services/costs.d.ts.map +1 -0
  243. package/dist/services/costs.js +166 -0
  244. package/dist/services/costs.js.map +1 -0
  245. package/dist/services/dashboard.d.ts +21 -0
  246. package/dist/services/dashboard.d.ts.map +1 -0
  247. package/dist/services/dashboard.js +96 -0
  248. package/dist/services/dashboard.js.map +1 -0
  249. package/dist/services/goals.d.ts +407 -0
  250. package/dist/services/goals.d.ts.map +1 -0
  251. package/dist/services/goals.js +29 -0
  252. package/dist/services/goals.js.map +1 -0
  253. package/dist/services/heartbeat.d.ts +1666 -0
  254. package/dist/services/heartbeat.d.ts.map +1 -0
  255. package/dist/services/heartbeat.js +1752 -0
  256. package/dist/services/heartbeat.js.map +1 -0
  257. package/dist/services/index.d.ts +20 -0
  258. package/dist/services/index.d.ts.map +1 -0
  259. package/dist/services/index.js +20 -0
  260. package/dist/services/index.js.map +1 -0
  261. package/dist/services/issue-approvals.d.ts +56 -0
  262. package/dist/services/issue-approvals.d.ts.map +1 -0
  263. package/dist/services/issue-approvals.js +153 -0
  264. package/dist/services/issue-approvals.js.map +1 -0
  265. package/dist/services/issues.d.ts +756 -0
  266. package/dist/services/issues.d.ts.map +1 -0
  267. package/dist/services/issues.js +917 -0
  268. package/dist/services/issues.js.map +1 -0
  269. package/dist/services/live-events.d.ts +12 -0
  270. package/dist/services/live-events.d.ts.map +1 -0
  271. package/dist/services/live-events.js +24 -0
  272. package/dist/services/live-events.js.map +1 -0
  273. package/dist/services/projects.d.ts +66 -0
  274. package/dist/services/projects.d.ts.map +1 -0
  275. package/dist/services/projects.js +472 -0
  276. package/dist/services/projects.js.map +1 -0
  277. package/dist/services/run-log-store.d.ts +34 -0
  278. package/dist/services/run-log-store.d.ts.map +1 -0
  279. package/dist/services/run-log-store.js +112 -0
  280. package/dist/services/run-log-store.js.map +1 -0
  281. package/dist/services/secrets.d.ts +506 -0
  282. package/dist/services/secrets.d.ts.map +1 -0
  283. package/dist/services/secrets.js +284 -0
  284. package/dist/services/secrets.js.map +1 -0
  285. package/dist/services/sidebar-badges.d.ts +9 -0
  286. package/dist/services/sidebar-badges.d.ts.map +1 -0
  287. package/dist/services/sidebar-badges.js +33 -0
  288. package/dist/services/sidebar-badges.js.map +1 -0
  289. package/dist/startup-banner.d.ts +27 -0
  290. package/dist/startup-banner.d.ts.map +1 -0
  291. package/dist/startup-banner.js +112 -0
  292. package/dist/startup-banner.js.map +1 -0
  293. package/dist/storage/index.d.ts +6 -0
  294. package/dist/storage/index.d.ts.map +1 -0
  295. package/dist/storage/index.js +29 -0
  296. package/dist/storage/index.js.map +1 -0
  297. package/dist/storage/local-disk-provider.d.ts +3 -0
  298. package/dist/storage/local-disk-provider.d.ts.map +1 -0
  299. package/dist/storage/local-disk-provider.js +79 -0
  300. package/dist/storage/local-disk-provider.js.map +1 -0
  301. package/dist/storage/provider-registry.d.ts +4 -0
  302. package/dist/storage/provider-registry.d.ts.map +1 -0
  303. package/dist/storage/provider-registry.js +15 -0
  304. package/dist/storage/provider-registry.js.map +1 -0
  305. package/dist/storage/s3-provider.d.ts +11 -0
  306. package/dist/storage/s3-provider.d.ts.map +1 -0
  307. package/dist/storage/s3-provider.js +123 -0
  308. package/dist/storage/s3-provider.js.map +1 -0
  309. package/dist/storage/service.d.ts +3 -0
  310. package/dist/storage/service.d.ts.map +1 -0
  311. package/dist/storage/service.js +120 -0
  312. package/dist/storage/service.js.map +1 -0
  313. package/dist/storage/types.d.ts +55 -0
  314. package/dist/storage/types.d.ts.map +1 -0
  315. package/dist/storage/types.js +2 -0
  316. package/dist/storage/types.js.map +1 -0
  317. package/package.json +62 -0
@@ -0,0 +1,1752 @@
1
+ import fs from "node:fs/promises";
2
+ import { and, asc, desc, eq, gt, inArray, sql } from "drizzle-orm";
3
+ import { agents, agentRuntimeState, agentTaskSessions, agentWakeupRequests, heartbeatRunEvents, heartbeatRuns, costEvents, issues, projectWorkspaces, } from "@paperclipai/db";
4
+ import { conflict, notFound } from "../errors.js";
5
+ import { logger } from "../middleware/logger.js";
6
+ import { publishLiveEvent } from "./live-events.js";
7
+ import { getRunLogStore } from "./run-log-store.js";
8
+ import { getServerAdapter, runningProcesses } from "../adapters/index.js";
9
+ import { createLocalAgentJwt } from "../agent-auth-jwt.js";
10
+ import { parseObject, asBoolean, asNumber, appendWithCap, MAX_EXCERPT_BYTES } from "../adapters/utils.js";
11
+ import { secretService } from "./secrets.js";
12
+ import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
13
+ const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
14
+ const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1;
15
+ const HEARTBEAT_MAX_CONCURRENT_RUNS_MAX = 10;
16
+ const DEFERRED_WAKE_CONTEXT_KEY = "_paperclipWakeContext";
17
+ const startLocksByAgent = new Map();
18
+ const REPO_ONLY_CWD_SENTINEL = "/__paperclip_repo_only__";
19
+ function appendExcerpt(prev, chunk) {
20
+ return appendWithCap(prev, chunk, MAX_EXCERPT_BYTES);
21
+ }
22
+ function normalizeMaxConcurrentRuns(value) {
23
+ const parsed = Math.floor(asNumber(value, HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT));
24
+ if (!Number.isFinite(parsed))
25
+ return HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT;
26
+ return Math.max(HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT, Math.min(HEARTBEAT_MAX_CONCURRENT_RUNS_MAX, parsed));
27
+ }
28
+ async function withAgentStartLock(agentId, fn) {
29
+ const previous = startLocksByAgent.get(agentId) ?? Promise.resolve();
30
+ const run = previous.then(fn);
31
+ const marker = run.then(() => undefined, () => undefined);
32
+ startLocksByAgent.set(agentId, marker);
33
+ try {
34
+ return await run;
35
+ }
36
+ finally {
37
+ if (startLocksByAgent.get(agentId) === marker) {
38
+ startLocksByAgent.delete(agentId);
39
+ }
40
+ }
41
+ }
42
+ function readNonEmptyString(value) {
43
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
44
+ }
45
+ function parseIssueAssigneeAdapterOverrides(raw) {
46
+ const parsed = parseObject(raw);
47
+ const parsedAdapterConfig = parseObject(parsed.adapterConfig);
48
+ const adapterConfig = Object.keys(parsedAdapterConfig).length > 0 ? parsedAdapterConfig : null;
49
+ const useProjectWorkspace = typeof parsed.useProjectWorkspace === "boolean"
50
+ ? parsed.useProjectWorkspace
51
+ : null;
52
+ if (!adapterConfig && useProjectWorkspace === null)
53
+ return null;
54
+ return {
55
+ adapterConfig,
56
+ useProjectWorkspace,
57
+ };
58
+ }
59
+ function deriveTaskKey(contextSnapshot, payload) {
60
+ return (readNonEmptyString(contextSnapshot?.taskKey) ??
61
+ readNonEmptyString(contextSnapshot?.taskId) ??
62
+ readNonEmptyString(contextSnapshot?.issueId) ??
63
+ readNonEmptyString(payload?.taskKey) ??
64
+ readNonEmptyString(payload?.taskId) ??
65
+ readNonEmptyString(payload?.issueId) ??
66
+ null);
67
+ }
68
+ function deriveCommentId(contextSnapshot, payload) {
69
+ return (readNonEmptyString(contextSnapshot?.wakeCommentId) ??
70
+ readNonEmptyString(contextSnapshot?.commentId) ??
71
+ readNonEmptyString(payload?.commentId) ??
72
+ null);
73
+ }
74
+ function enrichWakeContextSnapshot(input) {
75
+ const { contextSnapshot, reason, source, triggerDetail, payload } = input;
76
+ const issueIdFromPayload = readNonEmptyString(payload?.["issueId"]);
77
+ const commentIdFromPayload = readNonEmptyString(payload?.["commentId"]);
78
+ const taskKey = deriveTaskKey(contextSnapshot, payload);
79
+ const wakeCommentId = deriveCommentId(contextSnapshot, payload);
80
+ if (!readNonEmptyString(contextSnapshot["wakeReason"]) && reason) {
81
+ contextSnapshot.wakeReason = reason;
82
+ }
83
+ if (!readNonEmptyString(contextSnapshot["issueId"]) && issueIdFromPayload) {
84
+ contextSnapshot.issueId = issueIdFromPayload;
85
+ }
86
+ if (!readNonEmptyString(contextSnapshot["taskId"]) && issueIdFromPayload) {
87
+ contextSnapshot.taskId = issueIdFromPayload;
88
+ }
89
+ if (!readNonEmptyString(contextSnapshot["taskKey"]) && taskKey) {
90
+ contextSnapshot.taskKey = taskKey;
91
+ }
92
+ if (!readNonEmptyString(contextSnapshot["commentId"]) && commentIdFromPayload) {
93
+ contextSnapshot.commentId = commentIdFromPayload;
94
+ }
95
+ if (!readNonEmptyString(contextSnapshot["wakeCommentId"]) && wakeCommentId) {
96
+ contextSnapshot.wakeCommentId = wakeCommentId;
97
+ }
98
+ if (!readNonEmptyString(contextSnapshot["wakeSource"]) && source) {
99
+ contextSnapshot.wakeSource = source;
100
+ }
101
+ if (!readNonEmptyString(contextSnapshot["wakeTriggerDetail"]) && triggerDetail) {
102
+ contextSnapshot.wakeTriggerDetail = triggerDetail;
103
+ }
104
+ return {
105
+ contextSnapshot,
106
+ issueIdFromPayload,
107
+ commentIdFromPayload,
108
+ taskKey,
109
+ wakeCommentId,
110
+ };
111
+ }
112
+ function mergeCoalescedContextSnapshot(existingRaw, incoming) {
113
+ const existing = parseObject(existingRaw);
114
+ const merged = {
115
+ ...existing,
116
+ ...incoming,
117
+ };
118
+ const commentId = deriveCommentId(incoming, null);
119
+ if (commentId) {
120
+ merged.commentId = commentId;
121
+ merged.wakeCommentId = commentId;
122
+ }
123
+ return merged;
124
+ }
125
+ function runTaskKey(run) {
126
+ return deriveTaskKey(run.contextSnapshot, null);
127
+ }
128
+ function isSameTaskScope(left, right) {
129
+ return (left ?? null) === (right ?? null);
130
+ }
131
+ function truncateDisplayId(value, max = 128) {
132
+ if (!value)
133
+ return null;
134
+ return value.length > max ? value.slice(0, max) : value;
135
+ }
136
+ function normalizeAgentNameKey(value) {
137
+ if (typeof value !== "string")
138
+ return null;
139
+ const normalized = value.trim().toLowerCase();
140
+ return normalized.length > 0 ? normalized : null;
141
+ }
142
+ const defaultSessionCodec = {
143
+ deserialize(raw) {
144
+ const asObj = parseObject(raw);
145
+ if (Object.keys(asObj).length > 0)
146
+ return asObj;
147
+ const sessionId = readNonEmptyString(raw?.sessionId);
148
+ if (sessionId)
149
+ return { sessionId };
150
+ return null;
151
+ },
152
+ serialize(params) {
153
+ if (!params || Object.keys(params).length === 0)
154
+ return null;
155
+ return params;
156
+ },
157
+ getDisplayId(params) {
158
+ return readNonEmptyString(params?.sessionId);
159
+ },
160
+ };
161
+ function getAdapterSessionCodec(adapterType) {
162
+ const adapter = getServerAdapter(adapterType);
163
+ return adapter.sessionCodec ?? defaultSessionCodec;
164
+ }
165
+ function normalizeSessionParams(params) {
166
+ if (!params)
167
+ return null;
168
+ return Object.keys(params).length > 0 ? params : null;
169
+ }
170
+ function resolveNextSessionState(input) {
171
+ const { codec, adapterResult, previousParams, previousDisplayId, previousLegacySessionId } = input;
172
+ if (adapterResult.clearSession) {
173
+ return {
174
+ params: null,
175
+ displayId: null,
176
+ legacySessionId: null,
177
+ };
178
+ }
179
+ const explicitParams = adapterResult.sessionParams;
180
+ const hasExplicitParams = adapterResult.sessionParams !== undefined;
181
+ const hasExplicitSessionId = adapterResult.sessionId !== undefined;
182
+ const explicitSessionId = readNonEmptyString(adapterResult.sessionId);
183
+ const hasExplicitDisplay = adapterResult.sessionDisplayId !== undefined;
184
+ const explicitDisplayId = readNonEmptyString(adapterResult.sessionDisplayId);
185
+ const shouldUsePrevious = !hasExplicitParams && !hasExplicitSessionId && !hasExplicitDisplay;
186
+ const candidateParams = hasExplicitParams
187
+ ? explicitParams
188
+ : hasExplicitSessionId
189
+ ? (explicitSessionId ? { sessionId: explicitSessionId } : null)
190
+ : previousParams;
191
+ const serialized = normalizeSessionParams(codec.serialize(normalizeSessionParams(candidateParams) ?? null));
192
+ const deserialized = normalizeSessionParams(codec.deserialize(serialized));
193
+ const displayId = truncateDisplayId(explicitDisplayId ??
194
+ (codec.getDisplayId ? codec.getDisplayId(deserialized) : null) ??
195
+ readNonEmptyString(deserialized?.sessionId) ??
196
+ (shouldUsePrevious ? previousDisplayId : null) ??
197
+ explicitSessionId ??
198
+ (shouldUsePrevious ? previousLegacySessionId : null));
199
+ const legacySessionId = explicitSessionId ??
200
+ readNonEmptyString(deserialized?.sessionId) ??
201
+ displayId ??
202
+ (shouldUsePrevious ? previousLegacySessionId : null);
203
+ return {
204
+ params: serialized,
205
+ displayId,
206
+ legacySessionId,
207
+ };
208
+ }
209
+ export function heartbeatService(db) {
210
+ const runLogStore = getRunLogStore();
211
+ const secretsSvc = secretService(db);
212
+ async function getAgent(agentId) {
213
+ return db
214
+ .select()
215
+ .from(agents)
216
+ .where(eq(agents.id, agentId))
217
+ .then((rows) => rows[0] ?? null);
218
+ }
219
+ async function getRun(runId) {
220
+ return db
221
+ .select()
222
+ .from(heartbeatRuns)
223
+ .where(eq(heartbeatRuns.id, runId))
224
+ .then((rows) => rows[0] ?? null);
225
+ }
226
+ async function getRuntimeState(agentId) {
227
+ return db
228
+ .select()
229
+ .from(agentRuntimeState)
230
+ .where(eq(agentRuntimeState.agentId, agentId))
231
+ .then((rows) => rows[0] ?? null);
232
+ }
233
+ async function getTaskSession(companyId, agentId, adapterType, taskKey) {
234
+ return db
235
+ .select()
236
+ .from(agentTaskSessions)
237
+ .where(and(eq(agentTaskSessions.companyId, companyId), eq(agentTaskSessions.agentId, agentId), eq(agentTaskSessions.adapterType, adapterType), eq(agentTaskSessions.taskKey, taskKey)))
238
+ .then((rows) => rows[0] ?? null);
239
+ }
240
+ async function resolveSessionBeforeForWakeup(agent, taskKey) {
241
+ if (taskKey) {
242
+ const codec = getAdapterSessionCodec(agent.adapterType);
243
+ const existingTaskSession = await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey);
244
+ const parsedParams = normalizeSessionParams(codec.deserialize(existingTaskSession?.sessionParamsJson ?? null));
245
+ return truncateDisplayId(existingTaskSession?.sessionDisplayId ??
246
+ (codec.getDisplayId ? codec.getDisplayId(parsedParams) : null) ??
247
+ readNonEmptyString(parsedParams?.sessionId));
248
+ }
249
+ const runtimeForRun = await getRuntimeState(agent.id);
250
+ return runtimeForRun?.sessionId ?? null;
251
+ }
252
+ async function resolveWorkspaceForRun(agent, context, previousSessionParams, opts) {
253
+ const issueId = readNonEmptyString(context.issueId);
254
+ const contextProjectId = readNonEmptyString(context.projectId);
255
+ const issueProjectId = issueId
256
+ ? await db
257
+ .select({ projectId: issues.projectId })
258
+ .from(issues)
259
+ .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
260
+ .then((rows) => rows[0]?.projectId ?? null)
261
+ : null;
262
+ const resolvedProjectId = issueProjectId ?? contextProjectId;
263
+ const useProjectWorkspace = opts?.useProjectWorkspace !== false;
264
+ const workspaceProjectId = useProjectWorkspace ? resolvedProjectId : null;
265
+ const projectWorkspaceRows = workspaceProjectId
266
+ ? await db
267
+ .select()
268
+ .from(projectWorkspaces)
269
+ .where(and(eq(projectWorkspaces.companyId, agent.companyId), eq(projectWorkspaces.projectId, workspaceProjectId)))
270
+ .orderBy(asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
271
+ : [];
272
+ const workspaceHints = projectWorkspaceRows.map((workspace) => ({
273
+ workspaceId: workspace.id,
274
+ cwd: readNonEmptyString(workspace.cwd),
275
+ repoUrl: readNonEmptyString(workspace.repoUrl),
276
+ repoRef: readNonEmptyString(workspace.repoRef),
277
+ }));
278
+ if (projectWorkspaceRows.length > 0) {
279
+ for (const workspace of projectWorkspaceRows) {
280
+ const projectCwd = readNonEmptyString(workspace.cwd);
281
+ if (!projectCwd || projectCwd === REPO_ONLY_CWD_SENTINEL) {
282
+ continue;
283
+ }
284
+ const projectCwdExists = await fs
285
+ .stat(projectCwd)
286
+ .then((stats) => stats.isDirectory())
287
+ .catch(() => false);
288
+ if (projectCwdExists) {
289
+ return {
290
+ cwd: projectCwd,
291
+ source: "project_primary",
292
+ projectId: resolvedProjectId,
293
+ workspaceId: workspace.id,
294
+ repoUrl: workspace.repoUrl,
295
+ repoRef: workspace.repoRef,
296
+ workspaceHints,
297
+ };
298
+ }
299
+ }
300
+ const fallbackCwd = resolveDefaultAgentWorkspaceDir(agent.id);
301
+ await fs.mkdir(fallbackCwd, { recursive: true });
302
+ return {
303
+ cwd: fallbackCwd,
304
+ source: "project_primary",
305
+ projectId: resolvedProjectId,
306
+ workspaceId: projectWorkspaceRows[0]?.id ?? null,
307
+ repoUrl: projectWorkspaceRows[0]?.repoUrl ?? null,
308
+ repoRef: projectWorkspaceRows[0]?.repoRef ?? null,
309
+ workspaceHints,
310
+ };
311
+ }
312
+ const sessionCwd = readNonEmptyString(previousSessionParams?.cwd);
313
+ if (sessionCwd) {
314
+ const sessionCwdExists = await fs
315
+ .stat(sessionCwd)
316
+ .then((stats) => stats.isDirectory())
317
+ .catch(() => false);
318
+ if (sessionCwdExists) {
319
+ return {
320
+ cwd: sessionCwd,
321
+ source: "task_session",
322
+ projectId: resolvedProjectId,
323
+ workspaceId: readNonEmptyString(previousSessionParams?.workspaceId),
324
+ repoUrl: readNonEmptyString(previousSessionParams?.repoUrl),
325
+ repoRef: readNonEmptyString(previousSessionParams?.repoRef),
326
+ workspaceHints,
327
+ };
328
+ }
329
+ }
330
+ const cwd = resolveDefaultAgentWorkspaceDir(agent.id);
331
+ await fs.mkdir(cwd, { recursive: true });
332
+ return {
333
+ cwd,
334
+ source: "agent_home",
335
+ projectId: resolvedProjectId,
336
+ workspaceId: null,
337
+ repoUrl: null,
338
+ repoRef: null,
339
+ workspaceHints,
340
+ };
341
+ }
342
+ async function upsertTaskSession(input) {
343
+ const existing = await getTaskSession(input.companyId, input.agentId, input.adapterType, input.taskKey);
344
+ if (existing) {
345
+ return db
346
+ .update(agentTaskSessions)
347
+ .set({
348
+ sessionParamsJson: input.sessionParamsJson,
349
+ sessionDisplayId: input.sessionDisplayId,
350
+ lastRunId: input.lastRunId,
351
+ lastError: input.lastError,
352
+ updatedAt: new Date(),
353
+ })
354
+ .where(eq(agentTaskSessions.id, existing.id))
355
+ .returning()
356
+ .then((rows) => rows[0] ?? null);
357
+ }
358
+ return db
359
+ .insert(agentTaskSessions)
360
+ .values({
361
+ companyId: input.companyId,
362
+ agentId: input.agentId,
363
+ adapterType: input.adapterType,
364
+ taskKey: input.taskKey,
365
+ sessionParamsJson: input.sessionParamsJson,
366
+ sessionDisplayId: input.sessionDisplayId,
367
+ lastRunId: input.lastRunId,
368
+ lastError: input.lastError,
369
+ })
370
+ .returning()
371
+ .then((rows) => rows[0] ?? null);
372
+ }
373
+ async function clearTaskSessions(companyId, agentId, opts) {
374
+ const conditions = [
375
+ eq(agentTaskSessions.companyId, companyId),
376
+ eq(agentTaskSessions.agentId, agentId),
377
+ ];
378
+ if (opts?.taskKey) {
379
+ conditions.push(eq(agentTaskSessions.taskKey, opts.taskKey));
380
+ }
381
+ if (opts?.adapterType) {
382
+ conditions.push(eq(agentTaskSessions.adapterType, opts.adapterType));
383
+ }
384
+ return db
385
+ .delete(agentTaskSessions)
386
+ .where(and(...conditions))
387
+ .returning()
388
+ .then((rows) => rows.length);
389
+ }
390
+ async function ensureRuntimeState(agent) {
391
+ const existing = await getRuntimeState(agent.id);
392
+ if (existing)
393
+ return existing;
394
+ return db
395
+ .insert(agentRuntimeState)
396
+ .values({
397
+ agentId: agent.id,
398
+ companyId: agent.companyId,
399
+ adapterType: agent.adapterType,
400
+ stateJson: {},
401
+ })
402
+ .returning()
403
+ .then((rows) => rows[0]);
404
+ }
405
+ async function setRunStatus(runId, status, patch) {
406
+ const updated = await db
407
+ .update(heartbeatRuns)
408
+ .set({ status, ...patch, updatedAt: new Date() })
409
+ .where(eq(heartbeatRuns.id, runId))
410
+ .returning()
411
+ .then((rows) => rows[0] ?? null);
412
+ if (updated) {
413
+ publishLiveEvent({
414
+ companyId: updated.companyId,
415
+ type: "heartbeat.run.status",
416
+ payload: {
417
+ runId: updated.id,
418
+ agentId: updated.agentId,
419
+ status: updated.status,
420
+ invocationSource: updated.invocationSource,
421
+ triggerDetail: updated.triggerDetail,
422
+ error: updated.error ?? null,
423
+ errorCode: updated.errorCode ?? null,
424
+ startedAt: updated.startedAt ? new Date(updated.startedAt).toISOString() : null,
425
+ finishedAt: updated.finishedAt ? new Date(updated.finishedAt).toISOString() : null,
426
+ },
427
+ });
428
+ }
429
+ return updated;
430
+ }
431
+ async function setWakeupStatus(wakeupRequestId, status, patch) {
432
+ if (!wakeupRequestId)
433
+ return;
434
+ await db
435
+ .update(agentWakeupRequests)
436
+ .set({ status, ...patch, updatedAt: new Date() })
437
+ .where(eq(agentWakeupRequests.id, wakeupRequestId));
438
+ }
439
+ async function appendRunEvent(run, seq, event) {
440
+ await db.insert(heartbeatRunEvents).values({
441
+ companyId: run.companyId,
442
+ runId: run.id,
443
+ agentId: run.agentId,
444
+ seq,
445
+ eventType: event.eventType,
446
+ stream: event.stream,
447
+ level: event.level,
448
+ color: event.color,
449
+ message: event.message,
450
+ payload: event.payload,
451
+ });
452
+ publishLiveEvent({
453
+ companyId: run.companyId,
454
+ type: "heartbeat.run.event",
455
+ payload: {
456
+ runId: run.id,
457
+ agentId: run.agentId,
458
+ seq,
459
+ eventType: event.eventType,
460
+ stream: event.stream ?? null,
461
+ level: event.level ?? null,
462
+ color: event.color ?? null,
463
+ message: event.message ?? null,
464
+ payload: event.payload ?? null,
465
+ },
466
+ });
467
+ }
468
+ function parseHeartbeatPolicy(agent) {
469
+ const runtimeConfig = parseObject(agent.runtimeConfig);
470
+ const heartbeat = parseObject(runtimeConfig.heartbeat);
471
+ return {
472
+ enabled: asBoolean(heartbeat.enabled, true),
473
+ intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)),
474
+ wakeOnDemand: asBoolean(heartbeat.wakeOnDemand ?? heartbeat.wakeOnAssignment ?? heartbeat.wakeOnOnDemand ?? heartbeat.wakeOnAutomation, true),
475
+ maxConcurrentRuns: normalizeMaxConcurrentRuns(heartbeat.maxConcurrentRuns),
476
+ };
477
+ }
478
+ async function countRunningRunsForAgent(agentId) {
479
+ const [{ count }] = await db
480
+ .select({ count: sql `count(*)` })
481
+ .from(heartbeatRuns)
482
+ .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.status, "running")));
483
+ return Number(count ?? 0);
484
+ }
485
+ async function claimQueuedRun(run) {
486
+ if (run.status !== "queued")
487
+ return run;
488
+ const claimedAt = new Date();
489
+ const claimed = await db
490
+ .update(heartbeatRuns)
491
+ .set({
492
+ status: "running",
493
+ startedAt: run.startedAt ?? claimedAt,
494
+ updatedAt: claimedAt,
495
+ })
496
+ .where(and(eq(heartbeatRuns.id, run.id), eq(heartbeatRuns.status, "queued")))
497
+ .returning()
498
+ .then((rows) => rows[0] ?? null);
499
+ if (!claimed)
500
+ return null;
501
+ publishLiveEvent({
502
+ companyId: claimed.companyId,
503
+ type: "heartbeat.run.status",
504
+ payload: {
505
+ runId: claimed.id,
506
+ agentId: claimed.agentId,
507
+ status: claimed.status,
508
+ invocationSource: claimed.invocationSource,
509
+ triggerDetail: claimed.triggerDetail,
510
+ error: claimed.error ?? null,
511
+ errorCode: claimed.errorCode ?? null,
512
+ startedAt: claimed.startedAt ? new Date(claimed.startedAt).toISOString() : null,
513
+ finishedAt: claimed.finishedAt ? new Date(claimed.finishedAt).toISOString() : null,
514
+ },
515
+ });
516
+ await setWakeupStatus(claimed.wakeupRequestId, "claimed", { claimedAt });
517
+ return claimed;
518
+ }
519
+ async function finalizeAgentStatus(agentId, outcome) {
520
+ const existing = await getAgent(agentId);
521
+ if (!existing)
522
+ return;
523
+ if (existing.status === "paused" || existing.status === "terminated") {
524
+ return;
525
+ }
526
+ const runningCount = await countRunningRunsForAgent(agentId);
527
+ const nextStatus = runningCount > 0
528
+ ? "running"
529
+ : outcome === "succeeded" || outcome === "cancelled"
530
+ ? "idle"
531
+ : "error";
532
+ const updated = await db
533
+ .update(agents)
534
+ .set({
535
+ status: nextStatus,
536
+ lastHeartbeatAt: new Date(),
537
+ updatedAt: new Date(),
538
+ })
539
+ .where(eq(agents.id, agentId))
540
+ .returning()
541
+ .then((rows) => rows[0] ?? null);
542
+ if (updated) {
543
+ publishLiveEvent({
544
+ companyId: updated.companyId,
545
+ type: "agent.status",
546
+ payload: {
547
+ agentId: updated.id,
548
+ status: updated.status,
549
+ lastHeartbeatAt: updated.lastHeartbeatAt
550
+ ? new Date(updated.lastHeartbeatAt).toISOString()
551
+ : null,
552
+ outcome,
553
+ },
554
+ });
555
+ }
556
+ }
557
+ async function reapOrphanedRuns(opts) {
558
+ const staleThresholdMs = opts?.staleThresholdMs ?? 0;
559
+ const now = new Date();
560
+ // Find all runs in "queued" or "running" state
561
+ const activeRuns = await db
562
+ .select()
563
+ .from(heartbeatRuns)
564
+ .where(inArray(heartbeatRuns.status, ["queued", "running"]));
565
+ const reaped = [];
566
+ for (const run of activeRuns) {
567
+ if (runningProcesses.has(run.id))
568
+ continue;
569
+ // Apply staleness threshold to avoid false positives
570
+ if (staleThresholdMs > 0) {
571
+ const refTime = run.updatedAt ? new Date(run.updatedAt).getTime() : 0;
572
+ if (now.getTime() - refTime < staleThresholdMs)
573
+ continue;
574
+ }
575
+ await setRunStatus(run.id, "failed", {
576
+ error: "Process lost -- server may have restarted",
577
+ errorCode: "process_lost",
578
+ finishedAt: now,
579
+ });
580
+ await setWakeupStatus(run.wakeupRequestId, "failed", {
581
+ finishedAt: now,
582
+ error: "Process lost -- server may have restarted",
583
+ });
584
+ const updatedRun = await getRun(run.id);
585
+ if (updatedRun) {
586
+ await appendRunEvent(updatedRun, 1, {
587
+ eventType: "lifecycle",
588
+ stream: "system",
589
+ level: "error",
590
+ message: "Process lost -- server may have restarted",
591
+ });
592
+ await releaseIssueExecutionAndPromote(updatedRun);
593
+ }
594
+ await finalizeAgentStatus(run.agentId, "failed");
595
+ await startNextQueuedRunForAgent(run.agentId);
596
+ runningProcesses.delete(run.id);
597
+ reaped.push(run.id);
598
+ }
599
+ if (reaped.length > 0) {
600
+ logger.warn({ reapedCount: reaped.length, runIds: reaped }, "reaped orphaned heartbeat runs");
601
+ }
602
+ return { reaped: reaped.length, runIds: reaped };
603
+ }
604
+ async function updateRuntimeState(agent, run, result, session) {
605
+ await ensureRuntimeState(agent);
606
+ const usage = result.usage;
607
+ const inputTokens = usage?.inputTokens ?? 0;
608
+ const outputTokens = usage?.outputTokens ?? 0;
609
+ const cachedInputTokens = usage?.cachedInputTokens ?? 0;
610
+ const additionalCostCents = Math.max(0, Math.round((result.costUsd ?? 0) * 100));
611
+ const hasTokenUsage = inputTokens > 0 || outputTokens > 0 || cachedInputTokens > 0;
612
+ await db
613
+ .update(agentRuntimeState)
614
+ .set({
615
+ adapterType: agent.adapterType,
616
+ sessionId: session.legacySessionId,
617
+ lastRunId: run.id,
618
+ lastRunStatus: run.status,
619
+ lastError: result.errorMessage ?? null,
620
+ totalInputTokens: sql `${agentRuntimeState.totalInputTokens} + ${inputTokens}`,
621
+ totalOutputTokens: sql `${agentRuntimeState.totalOutputTokens} + ${outputTokens}`,
622
+ totalCachedInputTokens: sql `${agentRuntimeState.totalCachedInputTokens} + ${cachedInputTokens}`,
623
+ totalCostCents: sql `${agentRuntimeState.totalCostCents} + ${additionalCostCents}`,
624
+ updatedAt: new Date(),
625
+ })
626
+ .where(eq(agentRuntimeState.agentId, agent.id));
627
+ if (additionalCostCents > 0 || hasTokenUsage) {
628
+ await db.insert(costEvents).values({
629
+ companyId: agent.companyId,
630
+ agentId: agent.id,
631
+ provider: result.provider ?? "unknown",
632
+ model: result.model ?? "unknown",
633
+ inputTokens,
634
+ outputTokens,
635
+ costCents: additionalCostCents,
636
+ occurredAt: new Date(),
637
+ });
638
+ }
639
+ if (additionalCostCents > 0) {
640
+ await db
641
+ .update(agents)
642
+ .set({
643
+ spentMonthlyCents: sql `${agents.spentMonthlyCents} + ${additionalCostCents}`,
644
+ updatedAt: new Date(),
645
+ })
646
+ .where(eq(agents.id, agent.id));
647
+ }
648
+ }
649
+ async function startNextQueuedRunForAgent(agentId) {
650
+ return withAgentStartLock(agentId, async () => {
651
+ const agent = await getAgent(agentId);
652
+ if (!agent)
653
+ return [];
654
+ const policy = parseHeartbeatPolicy(agent);
655
+ const runningCount = await countRunningRunsForAgent(agentId);
656
+ const availableSlots = Math.max(0, policy.maxConcurrentRuns - runningCount);
657
+ if (availableSlots <= 0)
658
+ return [];
659
+ const queuedRuns = await db
660
+ .select()
661
+ .from(heartbeatRuns)
662
+ .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.status, "queued")))
663
+ .orderBy(asc(heartbeatRuns.createdAt))
664
+ .limit(availableSlots);
665
+ if (queuedRuns.length === 0)
666
+ return [];
667
+ const claimedRuns = [];
668
+ for (const queuedRun of queuedRuns) {
669
+ const claimed = await claimQueuedRun(queuedRun);
670
+ if (claimed)
671
+ claimedRuns.push(claimed);
672
+ }
673
+ if (claimedRuns.length === 0)
674
+ return [];
675
+ for (const claimedRun of claimedRuns) {
676
+ void executeRun(claimedRun.id).catch((err) => {
677
+ logger.error({ err, runId: claimedRun.id }, "queued heartbeat execution failed");
678
+ });
679
+ }
680
+ return claimedRuns;
681
+ });
682
+ }
683
+ async function executeRun(runId) {
684
+ let run = await getRun(runId);
685
+ if (!run)
686
+ return;
687
+ if (run.status !== "queued" && run.status !== "running")
688
+ return;
689
+ if (run.status === "queued") {
690
+ const claimed = await claimQueuedRun(run);
691
+ if (!claimed) {
692
+ // Another worker has already claimed or finalized this run.
693
+ return;
694
+ }
695
+ run = claimed;
696
+ }
697
+ const agent = await getAgent(run.agentId);
698
+ if (!agent) {
699
+ await setRunStatus(runId, "failed", {
700
+ error: "Agent not found",
701
+ errorCode: "agent_not_found",
702
+ finishedAt: new Date(),
703
+ });
704
+ await setWakeupStatus(run.wakeupRequestId, "failed", {
705
+ finishedAt: new Date(),
706
+ error: "Agent not found",
707
+ });
708
+ const failedRun = await getRun(runId);
709
+ if (failedRun)
710
+ await releaseIssueExecutionAndPromote(failedRun);
711
+ return;
712
+ }
713
+ const runtime = await ensureRuntimeState(agent);
714
+ const context = parseObject(run.contextSnapshot);
715
+ const taskKey = deriveTaskKey(context, null);
716
+ const sessionCodec = getAdapterSessionCodec(agent.adapterType);
717
+ const issueId = readNonEmptyString(context.issueId);
718
+ const issueAssigneeConfig = issueId
719
+ ? await db
720
+ .select({
721
+ assigneeAgentId: issues.assigneeAgentId,
722
+ assigneeAdapterOverrides: issues.assigneeAdapterOverrides,
723
+ })
724
+ .from(issues)
725
+ .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
726
+ .then((rows) => rows[0] ?? null)
727
+ : null;
728
+ const issueAssigneeOverrides = issueAssigneeConfig && issueAssigneeConfig.assigneeAgentId === agent.id
729
+ ? parseIssueAssigneeAdapterOverrides(issueAssigneeConfig.assigneeAdapterOverrides)
730
+ : null;
731
+ const taskSession = taskKey
732
+ ? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey)
733
+ : null;
734
+ const previousSessionParams = normalizeSessionParams(sessionCodec.deserialize(taskSession?.sessionParamsJson ?? null));
735
+ const resolvedWorkspace = await resolveWorkspaceForRun(agent, context, previousSessionParams, { useProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null });
736
+ context.paperclipWorkspace = {
737
+ cwd: resolvedWorkspace.cwd,
738
+ source: resolvedWorkspace.source,
739
+ projectId: resolvedWorkspace.projectId,
740
+ workspaceId: resolvedWorkspace.workspaceId,
741
+ repoUrl: resolvedWorkspace.repoUrl,
742
+ repoRef: resolvedWorkspace.repoRef,
743
+ };
744
+ context.paperclipWorkspaces = resolvedWorkspace.workspaceHints;
745
+ if (resolvedWorkspace.projectId && !readNonEmptyString(context.projectId)) {
746
+ context.projectId = resolvedWorkspace.projectId;
747
+ }
748
+ const runtimeSessionFallback = taskKey ? null : runtime.sessionId;
749
+ const previousSessionDisplayId = truncateDisplayId(taskSession?.sessionDisplayId ??
750
+ (sessionCodec.getDisplayId ? sessionCodec.getDisplayId(previousSessionParams) : null) ??
751
+ readNonEmptyString(previousSessionParams?.sessionId) ??
752
+ runtimeSessionFallback);
753
+ const runtimeForAdapter = {
754
+ sessionId: readNonEmptyString(previousSessionParams?.sessionId) ?? runtimeSessionFallback,
755
+ sessionParams: previousSessionParams,
756
+ sessionDisplayId: previousSessionDisplayId,
757
+ taskKey,
758
+ };
759
+ let seq = 1;
760
+ let handle = null;
761
+ let stdoutExcerpt = "";
762
+ let stderrExcerpt = "";
763
+ try {
764
+ const startedAt = run.startedAt ?? new Date();
765
+ const runningWithSession = await db
766
+ .update(heartbeatRuns)
767
+ .set({
768
+ startedAt,
769
+ sessionIdBefore: runtimeForAdapter.sessionDisplayId ?? runtimeForAdapter.sessionId,
770
+ updatedAt: new Date(),
771
+ })
772
+ .where(eq(heartbeatRuns.id, run.id))
773
+ .returning()
774
+ .then((rows) => rows[0] ?? null);
775
+ if (runningWithSession)
776
+ run = runningWithSession;
777
+ const runningAgent = await db
778
+ .update(agents)
779
+ .set({ status: "running", updatedAt: new Date() })
780
+ .where(eq(agents.id, agent.id))
781
+ .returning()
782
+ .then((rows) => rows[0] ?? null);
783
+ if (runningAgent) {
784
+ publishLiveEvent({
785
+ companyId: runningAgent.companyId,
786
+ type: "agent.status",
787
+ payload: {
788
+ agentId: runningAgent.id,
789
+ status: runningAgent.status,
790
+ outcome: "running",
791
+ },
792
+ });
793
+ }
794
+ const currentRun = run;
795
+ await appendRunEvent(currentRun, seq++, {
796
+ eventType: "lifecycle",
797
+ stream: "system",
798
+ level: "info",
799
+ message: "run started",
800
+ });
801
+ handle = await runLogStore.begin({
802
+ companyId: run.companyId,
803
+ agentId: run.agentId,
804
+ runId,
805
+ });
806
+ await db
807
+ .update(heartbeatRuns)
808
+ .set({
809
+ logStore: handle.store,
810
+ logRef: handle.logRef,
811
+ updatedAt: new Date(),
812
+ })
813
+ .where(eq(heartbeatRuns.id, runId));
814
+ const onLog = async (stream, chunk) => {
815
+ if (stream === "stdout")
816
+ stdoutExcerpt = appendExcerpt(stdoutExcerpt, chunk);
817
+ if (stream === "stderr")
818
+ stderrExcerpt = appendExcerpt(stderrExcerpt, chunk);
819
+ if (handle) {
820
+ await runLogStore.append(handle, {
821
+ stream,
822
+ chunk,
823
+ ts: new Date().toISOString(),
824
+ });
825
+ }
826
+ const payloadChunk = chunk.length > MAX_LIVE_LOG_CHUNK_BYTES
827
+ ? chunk.slice(chunk.length - MAX_LIVE_LOG_CHUNK_BYTES)
828
+ : chunk;
829
+ publishLiveEvent({
830
+ companyId: run.companyId,
831
+ type: "heartbeat.run.log",
832
+ payload: {
833
+ runId: run.id,
834
+ agentId: run.agentId,
835
+ stream,
836
+ chunk: payloadChunk,
837
+ truncated: payloadChunk.length !== chunk.length,
838
+ },
839
+ });
840
+ };
841
+ const config = parseObject(agent.adapterConfig);
842
+ const mergedConfig = issueAssigneeOverrides?.adapterConfig
843
+ ? { ...config, ...issueAssigneeOverrides.adapterConfig }
844
+ : config;
845
+ const resolvedConfig = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, mergedConfig);
846
+ const onAdapterMeta = async (meta) => {
847
+ await appendRunEvent(currentRun, seq++, {
848
+ eventType: "adapter.invoke",
849
+ stream: "system",
850
+ level: "info",
851
+ message: "adapter invocation",
852
+ payload: meta,
853
+ });
854
+ };
855
+ const adapter = getServerAdapter(agent.adapterType);
856
+ const authToken = adapter.supportsLocalAgentJwt
857
+ ? createLocalAgentJwt(agent.id, agent.companyId, agent.adapterType, run.id)
858
+ : null;
859
+ if (adapter.supportsLocalAgentJwt && !authToken) {
860
+ logger.warn({
861
+ companyId: agent.companyId,
862
+ agentId: agent.id,
863
+ runId: run.id,
864
+ adapterType: agent.adapterType,
865
+ }, "local agent jwt secret missing or invalid; running without injected PAPERCLIP_API_KEY");
866
+ }
867
+ const adapterResult = await adapter.execute({
868
+ runId: run.id,
869
+ agent,
870
+ runtime: runtimeForAdapter,
871
+ config: resolvedConfig,
872
+ context,
873
+ onLog,
874
+ onMeta: onAdapterMeta,
875
+ authToken: authToken ?? undefined,
876
+ });
877
+ const nextSessionState = resolveNextSessionState({
878
+ codec: sessionCodec,
879
+ adapterResult,
880
+ previousParams: previousSessionParams,
881
+ previousDisplayId: runtimeForAdapter.sessionDisplayId,
882
+ previousLegacySessionId: runtimeForAdapter.sessionId,
883
+ });
884
+ let outcome;
885
+ const latestRun = await getRun(run.id);
886
+ if (latestRun?.status === "cancelled") {
887
+ outcome = "cancelled";
888
+ }
889
+ else if (adapterResult.timedOut) {
890
+ outcome = "timed_out";
891
+ }
892
+ else if ((adapterResult.exitCode ?? 0) === 0 && !adapterResult.errorMessage) {
893
+ outcome = "succeeded";
894
+ }
895
+ else {
896
+ outcome = "failed";
897
+ }
898
+ let logSummary = null;
899
+ if (handle) {
900
+ logSummary = await runLogStore.finalize(handle);
901
+ }
902
+ const status = outcome === "succeeded"
903
+ ? "succeeded"
904
+ : outcome === "cancelled"
905
+ ? "cancelled"
906
+ : outcome === "timed_out"
907
+ ? "timed_out"
908
+ : "failed";
909
+ const usageJson = adapterResult.usage || adapterResult.costUsd != null
910
+ ? {
911
+ ...(adapterResult.usage ?? {}),
912
+ ...(adapterResult.costUsd != null ? { costUsd: adapterResult.costUsd } : {}),
913
+ ...(adapterResult.billingType ? { billingType: adapterResult.billingType } : {}),
914
+ }
915
+ : null;
916
+ await setRunStatus(run.id, status, {
917
+ finishedAt: new Date(),
918
+ error: outcome === "succeeded"
919
+ ? null
920
+ : adapterResult.errorMessage ?? (outcome === "timed_out" ? "Timed out" : "Adapter failed"),
921
+ errorCode: outcome === "timed_out"
922
+ ? "timeout"
923
+ : outcome === "cancelled"
924
+ ? "cancelled"
925
+ : outcome === "failed"
926
+ ? (adapterResult.errorCode ?? "adapter_failed")
927
+ : null,
928
+ exitCode: adapterResult.exitCode,
929
+ signal: adapterResult.signal,
930
+ usageJson,
931
+ resultJson: adapterResult.resultJson ?? null,
932
+ sessionIdAfter: nextSessionState.displayId ?? nextSessionState.legacySessionId,
933
+ stdoutExcerpt,
934
+ stderrExcerpt,
935
+ logBytes: logSummary?.bytes,
936
+ logSha256: logSummary?.sha256,
937
+ logCompressed: logSummary?.compressed ?? false,
938
+ });
939
+ await setWakeupStatus(run.wakeupRequestId, outcome === "succeeded" ? "completed" : status, {
940
+ finishedAt: new Date(),
941
+ error: adapterResult.errorMessage ?? null,
942
+ });
943
+ const finalizedRun = await getRun(run.id);
944
+ if (finalizedRun) {
945
+ await appendRunEvent(finalizedRun, seq++, {
946
+ eventType: "lifecycle",
947
+ stream: "system",
948
+ level: outcome === "succeeded" ? "info" : "error",
949
+ message: `run ${outcome}`,
950
+ payload: {
951
+ status,
952
+ exitCode: adapterResult.exitCode,
953
+ },
954
+ });
955
+ await releaseIssueExecutionAndPromote(finalizedRun);
956
+ }
957
+ if (finalizedRun) {
958
+ await updateRuntimeState(agent, finalizedRun, adapterResult, {
959
+ legacySessionId: nextSessionState.legacySessionId,
960
+ });
961
+ if (taskKey) {
962
+ if (adapterResult.clearSession || (!nextSessionState.params && !nextSessionState.displayId)) {
963
+ await clearTaskSessions(agent.companyId, agent.id, {
964
+ taskKey,
965
+ adapterType: agent.adapterType,
966
+ });
967
+ }
968
+ else {
969
+ await upsertTaskSession({
970
+ companyId: agent.companyId,
971
+ agentId: agent.id,
972
+ adapterType: agent.adapterType,
973
+ taskKey,
974
+ sessionParamsJson: nextSessionState.params,
975
+ sessionDisplayId: nextSessionState.displayId,
976
+ lastRunId: finalizedRun.id,
977
+ lastError: outcome === "succeeded" ? null : (adapterResult.errorMessage ?? "run_failed"),
978
+ });
979
+ }
980
+ }
981
+ }
982
+ await finalizeAgentStatus(agent.id, outcome);
983
+ }
984
+ catch (err) {
985
+ const message = err instanceof Error ? err.message : "Unknown adapter failure";
986
+ logger.error({ err, runId }, "heartbeat execution failed");
987
+ let logSummary = null;
988
+ if (handle) {
989
+ try {
990
+ logSummary = await runLogStore.finalize(handle);
991
+ }
992
+ catch (finalizeErr) {
993
+ logger.warn({ err: finalizeErr, runId }, "failed to finalize run log after error");
994
+ }
995
+ }
996
+ const failedRun = await setRunStatus(run.id, "failed", {
997
+ error: message,
998
+ errorCode: "adapter_failed",
999
+ finishedAt: new Date(),
1000
+ stdoutExcerpt,
1001
+ stderrExcerpt,
1002
+ logBytes: logSummary?.bytes,
1003
+ logSha256: logSummary?.sha256,
1004
+ logCompressed: logSummary?.compressed ?? false,
1005
+ });
1006
+ await setWakeupStatus(run.wakeupRequestId, "failed", {
1007
+ finishedAt: new Date(),
1008
+ error: message,
1009
+ });
1010
+ if (failedRun) {
1011
+ await appendRunEvent(failedRun, seq++, {
1012
+ eventType: "error",
1013
+ stream: "system",
1014
+ level: "error",
1015
+ message,
1016
+ });
1017
+ await releaseIssueExecutionAndPromote(failedRun);
1018
+ await updateRuntimeState(agent, failedRun, {
1019
+ exitCode: null,
1020
+ signal: null,
1021
+ timedOut: false,
1022
+ errorMessage: message,
1023
+ }, {
1024
+ legacySessionId: runtimeForAdapter.sessionId,
1025
+ });
1026
+ if (taskKey && (previousSessionParams || previousSessionDisplayId || taskSession)) {
1027
+ await upsertTaskSession({
1028
+ companyId: agent.companyId,
1029
+ agentId: agent.id,
1030
+ adapterType: agent.adapterType,
1031
+ taskKey,
1032
+ sessionParamsJson: previousSessionParams,
1033
+ sessionDisplayId: previousSessionDisplayId,
1034
+ lastRunId: failedRun.id,
1035
+ lastError: message,
1036
+ });
1037
+ }
1038
+ }
1039
+ await finalizeAgentStatus(agent.id, "failed");
1040
+ }
1041
+ finally {
1042
+ await startNextQueuedRunForAgent(agent.id);
1043
+ }
1044
+ }
1045
+ async function releaseIssueExecutionAndPromote(run) {
1046
+ const promotedRun = await db.transaction(async (tx) => {
1047
+ await tx.execute(sql `select id from issues where company_id = ${run.companyId} and execution_run_id = ${run.id} for update`);
1048
+ const issue = await tx
1049
+ .select({
1050
+ id: issues.id,
1051
+ companyId: issues.companyId,
1052
+ })
1053
+ .from(issues)
1054
+ .where(and(eq(issues.companyId, run.companyId), eq(issues.executionRunId, run.id)))
1055
+ .then((rows) => rows[0] ?? null);
1056
+ if (!issue)
1057
+ return;
1058
+ await tx
1059
+ .update(issues)
1060
+ .set({
1061
+ executionRunId: null,
1062
+ executionAgentNameKey: null,
1063
+ executionLockedAt: null,
1064
+ updatedAt: new Date(),
1065
+ })
1066
+ .where(eq(issues.id, issue.id));
1067
+ while (true) {
1068
+ const deferred = await tx
1069
+ .select()
1070
+ .from(agentWakeupRequests)
1071
+ .where(and(eq(agentWakeupRequests.companyId, issue.companyId), eq(agentWakeupRequests.status, "deferred_issue_execution"), sql `${agentWakeupRequests.payload} ->> 'issueId' = ${issue.id}`))
1072
+ .orderBy(asc(agentWakeupRequests.requestedAt))
1073
+ .limit(1)
1074
+ .then((rows) => rows[0] ?? null);
1075
+ if (!deferred)
1076
+ return null;
1077
+ const deferredAgent = await tx
1078
+ .select()
1079
+ .from(agents)
1080
+ .where(eq(agents.id, deferred.agentId))
1081
+ .then((rows) => rows[0] ?? null);
1082
+ if (!deferredAgent ||
1083
+ deferredAgent.companyId !== issue.companyId ||
1084
+ deferredAgent.status === "paused" ||
1085
+ deferredAgent.status === "terminated" ||
1086
+ deferredAgent.status === "pending_approval") {
1087
+ await tx
1088
+ .update(agentWakeupRequests)
1089
+ .set({
1090
+ status: "failed",
1091
+ finishedAt: new Date(),
1092
+ error: "Deferred wake could not be promoted: agent is not invokable",
1093
+ updatedAt: new Date(),
1094
+ })
1095
+ .where(eq(agentWakeupRequests.id, deferred.id));
1096
+ continue;
1097
+ }
1098
+ const deferredPayload = parseObject(deferred.payload);
1099
+ const deferredContextSeed = parseObject(deferredPayload[DEFERRED_WAKE_CONTEXT_KEY]);
1100
+ const promotedContextSeed = { ...deferredContextSeed };
1101
+ const promotedReason = readNonEmptyString(deferred.reason) ?? "issue_execution_promoted";
1102
+ const promotedSource = readNonEmptyString(deferred.source) ?? "automation";
1103
+ const promotedTriggerDetail = readNonEmptyString(deferred.triggerDetail) ?? null;
1104
+ const promotedPayload = deferredPayload;
1105
+ delete promotedPayload[DEFERRED_WAKE_CONTEXT_KEY];
1106
+ const { contextSnapshot: promotedContextSnapshot, taskKey: promotedTaskKey, } = enrichWakeContextSnapshot({
1107
+ contextSnapshot: promotedContextSeed,
1108
+ reason: promotedReason,
1109
+ source: promotedSource,
1110
+ triggerDetail: promotedTriggerDetail,
1111
+ payload: promotedPayload,
1112
+ });
1113
+ const sessionBefore = await resolveSessionBeforeForWakeup(deferredAgent, promotedTaskKey);
1114
+ const now = new Date();
1115
+ const newRun = await tx
1116
+ .insert(heartbeatRuns)
1117
+ .values({
1118
+ companyId: deferredAgent.companyId,
1119
+ agentId: deferredAgent.id,
1120
+ invocationSource: promotedSource,
1121
+ triggerDetail: promotedTriggerDetail,
1122
+ status: "queued",
1123
+ wakeupRequestId: deferred.id,
1124
+ contextSnapshot: promotedContextSnapshot,
1125
+ sessionIdBefore: sessionBefore,
1126
+ })
1127
+ .returning()
1128
+ .then((rows) => rows[0]);
1129
+ await tx
1130
+ .update(agentWakeupRequests)
1131
+ .set({
1132
+ status: "queued",
1133
+ reason: "issue_execution_promoted",
1134
+ runId: newRun.id,
1135
+ claimedAt: null,
1136
+ finishedAt: null,
1137
+ error: null,
1138
+ updatedAt: now,
1139
+ })
1140
+ .where(eq(agentWakeupRequests.id, deferred.id));
1141
+ await tx
1142
+ .update(issues)
1143
+ .set({
1144
+ executionRunId: newRun.id,
1145
+ executionAgentNameKey: normalizeAgentNameKey(deferredAgent.name),
1146
+ executionLockedAt: now,
1147
+ updatedAt: now,
1148
+ })
1149
+ .where(eq(issues.id, issue.id));
1150
+ return newRun;
1151
+ }
1152
+ });
1153
+ if (!promotedRun)
1154
+ return;
1155
+ publishLiveEvent({
1156
+ companyId: promotedRun.companyId,
1157
+ type: "heartbeat.run.queued",
1158
+ payload: {
1159
+ runId: promotedRun.id,
1160
+ agentId: promotedRun.agentId,
1161
+ invocationSource: promotedRun.invocationSource,
1162
+ triggerDetail: promotedRun.triggerDetail,
1163
+ wakeupRequestId: promotedRun.wakeupRequestId,
1164
+ },
1165
+ });
1166
+ await startNextQueuedRunForAgent(promotedRun.agentId);
1167
+ }
1168
+ async function enqueueWakeup(agentId, opts = {}) {
1169
+ const source = opts.source ?? "on_demand";
1170
+ const triggerDetail = opts.triggerDetail ?? null;
1171
+ const contextSnapshot = { ...(opts.contextSnapshot ?? {}) };
1172
+ const reason = opts.reason ?? null;
1173
+ const payload = opts.payload ?? null;
1174
+ const { contextSnapshot: enrichedContextSnapshot, issueIdFromPayload, taskKey, wakeCommentId, } = enrichWakeContextSnapshot({
1175
+ contextSnapshot,
1176
+ reason,
1177
+ source,
1178
+ triggerDetail,
1179
+ payload,
1180
+ });
1181
+ const issueId = readNonEmptyString(enrichedContextSnapshot.issueId) ?? issueIdFromPayload;
1182
+ const agent = await getAgent(agentId);
1183
+ if (!agent)
1184
+ throw notFound("Agent not found");
1185
+ if (agent.status === "paused" ||
1186
+ agent.status === "terminated" ||
1187
+ agent.status === "pending_approval") {
1188
+ throw conflict("Agent is not invokable in its current state", { status: agent.status });
1189
+ }
1190
+ const policy = parseHeartbeatPolicy(agent);
1191
+ const writeSkippedRequest = async (reason) => {
1192
+ await db.insert(agentWakeupRequests).values({
1193
+ companyId: agent.companyId,
1194
+ agentId,
1195
+ source,
1196
+ triggerDetail,
1197
+ reason,
1198
+ payload,
1199
+ status: "skipped",
1200
+ requestedByActorType: opts.requestedByActorType ?? null,
1201
+ requestedByActorId: opts.requestedByActorId ?? null,
1202
+ idempotencyKey: opts.idempotencyKey ?? null,
1203
+ finishedAt: new Date(),
1204
+ });
1205
+ };
1206
+ if (source === "timer" && !policy.enabled) {
1207
+ await writeSkippedRequest("heartbeat.disabled");
1208
+ return null;
1209
+ }
1210
+ if (source !== "timer" && !policy.wakeOnDemand) {
1211
+ await writeSkippedRequest("heartbeat.wakeOnDemand.disabled");
1212
+ return null;
1213
+ }
1214
+ const bypassIssueExecutionLock = reason === "issue_comment_mentioned" ||
1215
+ readNonEmptyString(enrichedContextSnapshot.wakeReason) === "issue_comment_mentioned";
1216
+ if (issueId && !bypassIssueExecutionLock) {
1217
+ const agentNameKey = normalizeAgentNameKey(agent.name);
1218
+ const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
1219
+ const outcome = await db.transaction(async (tx) => {
1220
+ await tx.execute(sql `select id from issues where id = ${issueId} and company_id = ${agent.companyId} for update`);
1221
+ const issue = await tx
1222
+ .select({
1223
+ id: issues.id,
1224
+ companyId: issues.companyId,
1225
+ executionRunId: issues.executionRunId,
1226
+ executionAgentNameKey: issues.executionAgentNameKey,
1227
+ })
1228
+ .from(issues)
1229
+ .where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)))
1230
+ .then((rows) => rows[0] ?? null);
1231
+ if (!issue) {
1232
+ await tx.insert(agentWakeupRequests).values({
1233
+ companyId: agent.companyId,
1234
+ agentId,
1235
+ source,
1236
+ triggerDetail,
1237
+ reason: "issue_execution_issue_not_found",
1238
+ payload,
1239
+ status: "skipped",
1240
+ requestedByActorType: opts.requestedByActorType ?? null,
1241
+ requestedByActorId: opts.requestedByActorId ?? null,
1242
+ idempotencyKey: opts.idempotencyKey ?? null,
1243
+ finishedAt: new Date(),
1244
+ });
1245
+ return { kind: "skipped" };
1246
+ }
1247
+ let activeExecutionRun = issue.executionRunId
1248
+ ? await tx
1249
+ .select()
1250
+ .from(heartbeatRuns)
1251
+ .where(eq(heartbeatRuns.id, issue.executionRunId))
1252
+ .then((rows) => rows[0] ?? null)
1253
+ : null;
1254
+ if (activeExecutionRun && activeExecutionRun.status !== "queued" && activeExecutionRun.status !== "running") {
1255
+ activeExecutionRun = null;
1256
+ }
1257
+ if (!activeExecutionRun && issue.executionRunId) {
1258
+ await tx
1259
+ .update(issues)
1260
+ .set({
1261
+ executionRunId: null,
1262
+ executionAgentNameKey: null,
1263
+ executionLockedAt: null,
1264
+ updatedAt: new Date(),
1265
+ })
1266
+ .where(eq(issues.id, issue.id));
1267
+ }
1268
+ if (!activeExecutionRun) {
1269
+ const legacyRun = await tx
1270
+ .select()
1271
+ .from(heartbeatRuns)
1272
+ .where(and(eq(heartbeatRuns.companyId, issue.companyId), inArray(heartbeatRuns.status, ["queued", "running"]), sql `${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issue.id}`))
1273
+ .orderBy(sql `case when ${heartbeatRuns.status} = 'running' then 0 else 1 end`, asc(heartbeatRuns.createdAt))
1274
+ .limit(1)
1275
+ .then((rows) => rows[0] ?? null);
1276
+ if (legacyRun) {
1277
+ activeExecutionRun = legacyRun;
1278
+ const legacyAgent = await tx
1279
+ .select({ name: agents.name })
1280
+ .from(agents)
1281
+ .where(eq(agents.id, legacyRun.agentId))
1282
+ .then((rows) => rows[0] ?? null);
1283
+ await tx
1284
+ .update(issues)
1285
+ .set({
1286
+ executionRunId: legacyRun.id,
1287
+ executionAgentNameKey: normalizeAgentNameKey(legacyAgent?.name),
1288
+ executionLockedAt: new Date(),
1289
+ updatedAt: new Date(),
1290
+ })
1291
+ .where(eq(issues.id, issue.id));
1292
+ }
1293
+ }
1294
+ if (activeExecutionRun) {
1295
+ const executionAgent = await tx
1296
+ .select({ name: agents.name })
1297
+ .from(agents)
1298
+ .where(eq(agents.id, activeExecutionRun.agentId))
1299
+ .then((rows) => rows[0] ?? null);
1300
+ const executionAgentNameKey = normalizeAgentNameKey(issue.executionAgentNameKey) ??
1301
+ normalizeAgentNameKey(executionAgent?.name);
1302
+ const isSameExecutionAgent = Boolean(executionAgentNameKey) && executionAgentNameKey === agentNameKey;
1303
+ const shouldQueueFollowupForCommentWake = Boolean(wakeCommentId) &&
1304
+ activeExecutionRun.status === "running" &&
1305
+ isSameExecutionAgent;
1306
+ if (isSameExecutionAgent && !shouldQueueFollowupForCommentWake) {
1307
+ const mergedContextSnapshot = mergeCoalescedContextSnapshot(activeExecutionRun.contextSnapshot, enrichedContextSnapshot);
1308
+ const mergedRun = await tx
1309
+ .update(heartbeatRuns)
1310
+ .set({
1311
+ contextSnapshot: mergedContextSnapshot,
1312
+ updatedAt: new Date(),
1313
+ })
1314
+ .where(eq(heartbeatRuns.id, activeExecutionRun.id))
1315
+ .returning()
1316
+ .then((rows) => rows[0] ?? activeExecutionRun);
1317
+ await tx.insert(agentWakeupRequests).values({
1318
+ companyId: agent.companyId,
1319
+ agentId,
1320
+ source,
1321
+ triggerDetail,
1322
+ reason: "issue_execution_same_name",
1323
+ payload,
1324
+ status: "coalesced",
1325
+ coalescedCount: 1,
1326
+ requestedByActorType: opts.requestedByActorType ?? null,
1327
+ requestedByActorId: opts.requestedByActorId ?? null,
1328
+ idempotencyKey: opts.idempotencyKey ?? null,
1329
+ runId: mergedRun.id,
1330
+ finishedAt: new Date(),
1331
+ });
1332
+ return { kind: "coalesced", run: mergedRun };
1333
+ }
1334
+ const deferredPayload = {
1335
+ ...(payload ?? {}),
1336
+ issueId,
1337
+ [DEFERRED_WAKE_CONTEXT_KEY]: enrichedContextSnapshot,
1338
+ };
1339
+ const existingDeferred = await tx
1340
+ .select()
1341
+ .from(agentWakeupRequests)
1342
+ .where(and(eq(agentWakeupRequests.companyId, agent.companyId), eq(agentWakeupRequests.agentId, agentId), eq(agentWakeupRequests.status, "deferred_issue_execution"), sql `${agentWakeupRequests.payload} ->> 'issueId' = ${issue.id}`))
1343
+ .orderBy(asc(agentWakeupRequests.requestedAt))
1344
+ .limit(1)
1345
+ .then((rows) => rows[0] ?? null);
1346
+ if (existingDeferred) {
1347
+ const existingDeferredPayload = parseObject(existingDeferred.payload);
1348
+ const existingDeferredContext = parseObject(existingDeferredPayload[DEFERRED_WAKE_CONTEXT_KEY]);
1349
+ const mergedDeferredContext = mergeCoalescedContextSnapshot(existingDeferredContext, enrichedContextSnapshot);
1350
+ const mergedDeferredPayload = {
1351
+ ...existingDeferredPayload,
1352
+ ...(payload ?? {}),
1353
+ issueId,
1354
+ [DEFERRED_WAKE_CONTEXT_KEY]: mergedDeferredContext,
1355
+ };
1356
+ await tx
1357
+ .update(agentWakeupRequests)
1358
+ .set({
1359
+ payload: mergedDeferredPayload,
1360
+ coalescedCount: (existingDeferred.coalescedCount ?? 0) + 1,
1361
+ updatedAt: new Date(),
1362
+ })
1363
+ .where(eq(agentWakeupRequests.id, existingDeferred.id));
1364
+ return { kind: "deferred" };
1365
+ }
1366
+ await tx.insert(agentWakeupRequests).values({
1367
+ companyId: agent.companyId,
1368
+ agentId,
1369
+ source,
1370
+ triggerDetail,
1371
+ reason: "issue_execution_deferred",
1372
+ payload: deferredPayload,
1373
+ status: "deferred_issue_execution",
1374
+ requestedByActorType: opts.requestedByActorType ?? null,
1375
+ requestedByActorId: opts.requestedByActorId ?? null,
1376
+ idempotencyKey: opts.idempotencyKey ?? null,
1377
+ });
1378
+ return { kind: "deferred" };
1379
+ }
1380
+ const wakeupRequest = await tx
1381
+ .insert(agentWakeupRequests)
1382
+ .values({
1383
+ companyId: agent.companyId,
1384
+ agentId,
1385
+ source,
1386
+ triggerDetail,
1387
+ reason,
1388
+ payload,
1389
+ status: "queued",
1390
+ requestedByActorType: opts.requestedByActorType ?? null,
1391
+ requestedByActorId: opts.requestedByActorId ?? null,
1392
+ idempotencyKey: opts.idempotencyKey ?? null,
1393
+ })
1394
+ .returning()
1395
+ .then((rows) => rows[0]);
1396
+ const newRun = await tx
1397
+ .insert(heartbeatRuns)
1398
+ .values({
1399
+ companyId: agent.companyId,
1400
+ agentId,
1401
+ invocationSource: source,
1402
+ triggerDetail,
1403
+ status: "queued",
1404
+ wakeupRequestId: wakeupRequest.id,
1405
+ contextSnapshot: enrichedContextSnapshot,
1406
+ sessionIdBefore: sessionBefore,
1407
+ })
1408
+ .returning()
1409
+ .then((rows) => rows[0]);
1410
+ await tx
1411
+ .update(agentWakeupRequests)
1412
+ .set({
1413
+ runId: newRun.id,
1414
+ updatedAt: new Date(),
1415
+ })
1416
+ .where(eq(agentWakeupRequests.id, wakeupRequest.id));
1417
+ await tx
1418
+ .update(issues)
1419
+ .set({
1420
+ executionRunId: newRun.id,
1421
+ executionAgentNameKey: agentNameKey,
1422
+ executionLockedAt: new Date(),
1423
+ updatedAt: new Date(),
1424
+ })
1425
+ .where(eq(issues.id, issue.id));
1426
+ return { kind: "queued", run: newRun };
1427
+ });
1428
+ if (outcome.kind === "deferred" || outcome.kind === "skipped")
1429
+ return null;
1430
+ if (outcome.kind === "coalesced")
1431
+ return outcome.run;
1432
+ const newRun = outcome.run;
1433
+ publishLiveEvent({
1434
+ companyId: newRun.companyId,
1435
+ type: "heartbeat.run.queued",
1436
+ payload: {
1437
+ runId: newRun.id,
1438
+ agentId: newRun.agentId,
1439
+ invocationSource: newRun.invocationSource,
1440
+ triggerDetail: newRun.triggerDetail,
1441
+ wakeupRequestId: newRun.wakeupRequestId,
1442
+ },
1443
+ });
1444
+ await startNextQueuedRunForAgent(agent.id);
1445
+ return newRun;
1446
+ }
1447
+ const activeRuns = await db
1448
+ .select()
1449
+ .from(heartbeatRuns)
1450
+ .where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, ["queued", "running"])))
1451
+ .orderBy(desc(heartbeatRuns.createdAt));
1452
+ const sameScopeQueuedRun = activeRuns.find((candidate) => candidate.status === "queued" && isSameTaskScope(runTaskKey(candidate), taskKey));
1453
+ const sameScopeRunningRun = activeRuns.find((candidate) => candidate.status === "running" && isSameTaskScope(runTaskKey(candidate), taskKey));
1454
+ const shouldQueueFollowupForCommentWake = Boolean(wakeCommentId) && Boolean(sameScopeRunningRun) && !sameScopeQueuedRun;
1455
+ const coalescedTargetRun = sameScopeQueuedRun ??
1456
+ (shouldQueueFollowupForCommentWake ? null : sameScopeRunningRun ?? null);
1457
+ if (coalescedTargetRun) {
1458
+ const mergedContextSnapshot = mergeCoalescedContextSnapshot(coalescedTargetRun.contextSnapshot, contextSnapshot);
1459
+ const mergedRun = await db
1460
+ .update(heartbeatRuns)
1461
+ .set({
1462
+ contextSnapshot: mergedContextSnapshot,
1463
+ updatedAt: new Date(),
1464
+ })
1465
+ .where(eq(heartbeatRuns.id, coalescedTargetRun.id))
1466
+ .returning()
1467
+ .then((rows) => rows[0] ?? coalescedTargetRun);
1468
+ await db.insert(agentWakeupRequests).values({
1469
+ companyId: agent.companyId,
1470
+ agentId,
1471
+ source,
1472
+ triggerDetail,
1473
+ reason,
1474
+ payload,
1475
+ status: "coalesced",
1476
+ coalescedCount: 1,
1477
+ requestedByActorType: opts.requestedByActorType ?? null,
1478
+ requestedByActorId: opts.requestedByActorId ?? null,
1479
+ idempotencyKey: opts.idempotencyKey ?? null,
1480
+ runId: mergedRun.id,
1481
+ finishedAt: new Date(),
1482
+ });
1483
+ return mergedRun;
1484
+ }
1485
+ const wakeupRequest = await db
1486
+ .insert(agentWakeupRequests)
1487
+ .values({
1488
+ companyId: agent.companyId,
1489
+ agentId,
1490
+ source,
1491
+ triggerDetail,
1492
+ reason,
1493
+ payload,
1494
+ status: "queued",
1495
+ requestedByActorType: opts.requestedByActorType ?? null,
1496
+ requestedByActorId: opts.requestedByActorId ?? null,
1497
+ idempotencyKey: opts.idempotencyKey ?? null,
1498
+ })
1499
+ .returning()
1500
+ .then((rows) => rows[0]);
1501
+ const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
1502
+ const newRun = await db
1503
+ .insert(heartbeatRuns)
1504
+ .values({
1505
+ companyId: agent.companyId,
1506
+ agentId,
1507
+ invocationSource: source,
1508
+ triggerDetail,
1509
+ status: "queued",
1510
+ wakeupRequestId: wakeupRequest.id,
1511
+ contextSnapshot: enrichedContextSnapshot,
1512
+ sessionIdBefore: sessionBefore,
1513
+ })
1514
+ .returning()
1515
+ .then((rows) => rows[0]);
1516
+ await db
1517
+ .update(agentWakeupRequests)
1518
+ .set({
1519
+ runId: newRun.id,
1520
+ updatedAt: new Date(),
1521
+ })
1522
+ .where(eq(agentWakeupRequests.id, wakeupRequest.id));
1523
+ publishLiveEvent({
1524
+ companyId: newRun.companyId,
1525
+ type: "heartbeat.run.queued",
1526
+ payload: {
1527
+ runId: newRun.id,
1528
+ agentId: newRun.agentId,
1529
+ invocationSource: newRun.invocationSource,
1530
+ triggerDetail: newRun.triggerDetail,
1531
+ wakeupRequestId: newRun.wakeupRequestId,
1532
+ },
1533
+ });
1534
+ await startNextQueuedRunForAgent(agent.id);
1535
+ return newRun;
1536
+ }
1537
+ return {
1538
+ list: (companyId, agentId, limit) => {
1539
+ const query = db
1540
+ .select()
1541
+ .from(heartbeatRuns)
1542
+ .where(agentId
1543
+ ? and(eq(heartbeatRuns.companyId, companyId), eq(heartbeatRuns.agentId, agentId))
1544
+ : eq(heartbeatRuns.companyId, companyId))
1545
+ .orderBy(desc(heartbeatRuns.createdAt));
1546
+ if (limit) {
1547
+ return query.limit(limit);
1548
+ }
1549
+ return query;
1550
+ },
1551
+ getRun,
1552
+ getRuntimeState: async (agentId) => {
1553
+ const state = await getRuntimeState(agentId);
1554
+ const agent = await getAgent(agentId);
1555
+ if (!agent)
1556
+ return null;
1557
+ const ensured = state ?? (await ensureRuntimeState(agent));
1558
+ const latestTaskSession = await db
1559
+ .select()
1560
+ .from(agentTaskSessions)
1561
+ .where(and(eq(agentTaskSessions.companyId, agent.companyId), eq(agentTaskSessions.agentId, agent.id)))
1562
+ .orderBy(desc(agentTaskSessions.updatedAt))
1563
+ .limit(1)
1564
+ .then((rows) => rows[0] ?? null);
1565
+ return {
1566
+ ...ensured,
1567
+ sessionDisplayId: latestTaskSession?.sessionDisplayId ?? ensured.sessionId,
1568
+ sessionParamsJson: latestTaskSession?.sessionParamsJson ?? null,
1569
+ };
1570
+ },
1571
+ listTaskSessions: async (agentId) => {
1572
+ const agent = await getAgent(agentId);
1573
+ if (!agent)
1574
+ throw notFound("Agent not found");
1575
+ return db
1576
+ .select()
1577
+ .from(agentTaskSessions)
1578
+ .where(and(eq(agentTaskSessions.companyId, agent.companyId), eq(agentTaskSessions.agentId, agentId)))
1579
+ .orderBy(desc(agentTaskSessions.updatedAt), desc(agentTaskSessions.createdAt));
1580
+ },
1581
+ resetRuntimeSession: async (agentId, opts) => {
1582
+ const agent = await getAgent(agentId);
1583
+ if (!agent)
1584
+ throw notFound("Agent not found");
1585
+ await ensureRuntimeState(agent);
1586
+ const taskKey = readNonEmptyString(opts?.taskKey);
1587
+ const clearedTaskSessions = await clearTaskSessions(agent.companyId, agent.id, taskKey ? { taskKey, adapterType: agent.adapterType } : undefined);
1588
+ const runtimePatch = {
1589
+ sessionId: null,
1590
+ lastError: null,
1591
+ updatedAt: new Date(),
1592
+ };
1593
+ if (!taskKey) {
1594
+ runtimePatch.stateJson = {};
1595
+ }
1596
+ const updated = await db
1597
+ .update(agentRuntimeState)
1598
+ .set(runtimePatch)
1599
+ .where(eq(agentRuntimeState.agentId, agentId))
1600
+ .returning()
1601
+ .then((rows) => rows[0] ?? null);
1602
+ if (!updated)
1603
+ return null;
1604
+ return {
1605
+ ...updated,
1606
+ sessionDisplayId: null,
1607
+ sessionParamsJson: null,
1608
+ clearedTaskSessions,
1609
+ };
1610
+ },
1611
+ listEvents: (runId, afterSeq = 0, limit = 200) => db
1612
+ .select()
1613
+ .from(heartbeatRunEvents)
1614
+ .where(and(eq(heartbeatRunEvents.runId, runId), gt(heartbeatRunEvents.seq, afterSeq)))
1615
+ .orderBy(asc(heartbeatRunEvents.seq))
1616
+ .limit(Math.max(1, Math.min(limit, 1000))),
1617
+ readLog: async (runId, opts) => {
1618
+ const run = await getRun(runId);
1619
+ if (!run)
1620
+ throw notFound("Heartbeat run not found");
1621
+ if (!run.logStore || !run.logRef)
1622
+ throw notFound("Run log not found");
1623
+ const result = await runLogStore.read({
1624
+ store: run.logStore,
1625
+ logRef: run.logRef,
1626
+ }, opts);
1627
+ return {
1628
+ runId,
1629
+ store: run.logStore,
1630
+ logRef: run.logRef,
1631
+ ...result,
1632
+ };
1633
+ },
1634
+ invoke: async (agentId, source = "on_demand", contextSnapshot = {}, triggerDetail = "manual", actor) => enqueueWakeup(agentId, {
1635
+ source,
1636
+ triggerDetail,
1637
+ contextSnapshot,
1638
+ requestedByActorType: actor?.actorType,
1639
+ requestedByActorId: actor?.actorId ?? null,
1640
+ }),
1641
+ wakeup: enqueueWakeup,
1642
+ reapOrphanedRuns,
1643
+ tickTimers: async (now = new Date()) => {
1644
+ const allAgents = await db.select().from(agents);
1645
+ let checked = 0;
1646
+ let enqueued = 0;
1647
+ let skipped = 0;
1648
+ for (const agent of allAgents) {
1649
+ if (agent.status === "paused" || agent.status === "terminated")
1650
+ continue;
1651
+ const policy = parseHeartbeatPolicy(agent);
1652
+ if (!policy.enabled || policy.intervalSec <= 0)
1653
+ continue;
1654
+ checked += 1;
1655
+ const baseline = new Date(agent.lastHeartbeatAt ?? agent.createdAt).getTime();
1656
+ const elapsedMs = now.getTime() - baseline;
1657
+ if (elapsedMs < policy.intervalSec * 1000)
1658
+ continue;
1659
+ const run = await enqueueWakeup(agent.id, {
1660
+ source: "timer",
1661
+ triggerDetail: "system",
1662
+ reason: "heartbeat_timer",
1663
+ requestedByActorType: "system",
1664
+ requestedByActorId: "heartbeat_scheduler",
1665
+ contextSnapshot: {
1666
+ source: "scheduler",
1667
+ reason: "interval_elapsed",
1668
+ now: now.toISOString(),
1669
+ },
1670
+ });
1671
+ if (run)
1672
+ enqueued += 1;
1673
+ else
1674
+ skipped += 1;
1675
+ }
1676
+ return { checked, enqueued, skipped };
1677
+ },
1678
+ cancelRun: async (runId) => {
1679
+ const run = await getRun(runId);
1680
+ if (!run)
1681
+ throw notFound("Heartbeat run not found");
1682
+ if (run.status !== "running" && run.status !== "queued")
1683
+ return run;
1684
+ const running = runningProcesses.get(run.id);
1685
+ if (running) {
1686
+ running.child.kill("SIGTERM");
1687
+ const graceMs = Math.max(1, running.graceSec) * 1000;
1688
+ setTimeout(() => {
1689
+ if (!running.child.killed) {
1690
+ running.child.kill("SIGKILL");
1691
+ }
1692
+ }, graceMs);
1693
+ }
1694
+ const cancelled = await setRunStatus(run.id, "cancelled", {
1695
+ finishedAt: new Date(),
1696
+ error: "Cancelled by control plane",
1697
+ errorCode: "cancelled",
1698
+ });
1699
+ await setWakeupStatus(run.wakeupRequestId, "cancelled", {
1700
+ finishedAt: new Date(),
1701
+ error: "Cancelled by control plane",
1702
+ });
1703
+ if (cancelled) {
1704
+ await appendRunEvent(cancelled, 1, {
1705
+ eventType: "lifecycle",
1706
+ stream: "system",
1707
+ level: "warn",
1708
+ message: "run cancelled",
1709
+ });
1710
+ await releaseIssueExecutionAndPromote(cancelled);
1711
+ }
1712
+ runningProcesses.delete(run.id);
1713
+ await finalizeAgentStatus(run.agentId, "cancelled");
1714
+ await startNextQueuedRunForAgent(run.agentId);
1715
+ return cancelled;
1716
+ },
1717
+ cancelActiveForAgent: async (agentId) => {
1718
+ const runs = await db
1719
+ .select()
1720
+ .from(heartbeatRuns)
1721
+ .where(and(eq(heartbeatRuns.agentId, agentId), inArray(heartbeatRuns.status, ["queued", "running"])));
1722
+ for (const run of runs) {
1723
+ await setRunStatus(run.id, "cancelled", {
1724
+ finishedAt: new Date(),
1725
+ error: "Cancelled due to agent pause",
1726
+ errorCode: "cancelled",
1727
+ });
1728
+ await setWakeupStatus(run.wakeupRequestId, "cancelled", {
1729
+ finishedAt: new Date(),
1730
+ error: "Cancelled due to agent pause",
1731
+ });
1732
+ const running = runningProcesses.get(run.id);
1733
+ if (running) {
1734
+ running.child.kill("SIGTERM");
1735
+ runningProcesses.delete(run.id);
1736
+ }
1737
+ await releaseIssueExecutionAndPromote(run);
1738
+ }
1739
+ return runs.length;
1740
+ },
1741
+ getActiveRunForAgent: async (agentId) => {
1742
+ const [run] = await db
1743
+ .select()
1744
+ .from(heartbeatRuns)
1745
+ .where(and(eq(heartbeatRuns.agentId, agentId), eq(heartbeatRuns.status, "running")))
1746
+ .orderBy(desc(heartbeatRuns.startedAt))
1747
+ .limit(1);
1748
+ return run ?? null;
1749
+ },
1750
+ };
1751
+ }
1752
+ //# sourceMappingURL=heartbeat.js.map